Выразительный JavaScript - Марейн Хавербеке
Шрифт:
Интервал:
Закладка:
А зачем мы заключили функцию в круглые скобки? Это связано с глюком синтаксиса JavaScript. Если выражение начинается с ключевого слова function, это функциональное выражение. А если инструкция начинается с function, это объявление функции, которое требует названия, и, так как это не выражение, не может быть вызвано при помощи скобок () после неё. Можно представлять себе заключение в скобки как трюк, чтобы функция принудительно интерпретировалась как выражение.
Объекты в качестве интерфейсов
Представьте, что нам надо добавить ещё одну функцию в наш модуль «день недели». Мы уже не можем возвращать функцию, а должны завернуть две функции в объект.
var weekDay = function() {
var names = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"];
return {
name: function(number) { return names[number]; },
number: function(name) { return names.indexOf(name); }
};
}();
console.log(weekDay.name(weekDay.number("Воскресенье")));
// → Воскресенье
Когда модуль большой, собирать все возвращаемые значения в объект в конце функции неудобно, потому что многие возвращаемые функции будут большими, и вам было бы удобнее их записывать где-то в другом месте, рядом со связанным с ними кодом. Удобно объявить объект (обычно называемый exports) и добавлять к нему свойства каждый раз, когда нам надо что-то экспортировать. В следующем примере функция модуля принимает объект интерфейса как аргумент, позволяя коду снаружи функции создать его и сохранить в переменной. Снаружи функции this ссылается на объект глобальной области видимости.
(function(exports) {
var names = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"];
exports.name = function(number) {
return names[number];
};
exports.number = function(name) {
return names.indexOf(name);
};
})(this.weekDay = {});
console.log(weekDay.name(weekDay.number("Четверг")));
// → Четверг
Отсоединяемся от глобальной области видимости
Такой шаблон часто используется в модулях JavaScript, предназначающихся для браузера. Модуль возьмёт одну глобальную переменную и обернёт свой код в функцию, чтобы у него было своё личное пространство имён. Но с этим шаблоном бывают проблемы, когда много модулей требуют одно и то же имя, или когда вам надо загрузить две версии модуля одновременно.
Подкрутив кое-что, мы можем сделать систему, разрешающую одному модулю обращаться к интерфейсному объекту другого, без выхода в глобальную ОВ. Наша цель – функция require, которая, получая имя модуля, загрузит его файл (с диска или из сети, в зависимости от платформы) и вернёт соответствующее значение с интерфейсом.
Этот подход решает проблемы, упомянутые ранее, и у него есть ещё одно преимущество – зависимости вашей программы становятся явными, и поэтому сложнее случайно вызвать ненужный вам модуль без чёткого его объявления.
Нам понадобятся две вещи. Во-первых, функция readFile, возвращающая содержимое файла в виде строки. В стандартном JavaScript такой функции нет, но разные окружения, такие как браузер или Node.js, предоставляют свои способы доступа к файлам. Пока притворимся, что у нас есть такая функция. Во-вторых, нам нужна возможность выполнить содержимое этой строки как код.
Выполняем данные как код
Есть несколько способов получить данные (строку кода) и выполнить их как часть текущей программы.
Самый очевидный – оператор eval, который выполняет строку кода в текущем окружении. Это плохая идея – он нарушает некоторые свойства окружения, которые обычно у него есть, например изоляция от внешнего мира.
function evalAndReturnX(code) {
eval(code);
return x;
}
console.log(evalAndReturnX("var x = 2"));
// → 2
Способ лучше – использовать конструктор Function. Он принимает два аргумента – строку, содержащую список имён аргументов через запятую, и строку, содержащую тело функции.
var plusOne = new Function("n", "return n + 1;");
console.log(plusOne(4));
// → 5
Это то, что нам надо. Мы обернём код модуля в функцию, и её область видимости станет областью видимости нашего модуля.
Require
Вот минимальная версия функции require:
function require(name) {
var code = new Function("exports", readFile(name));
var exports = {};
code(exports);
return exports;
}
console.log(require("weekDay").name(1));
// → Вторник
Так как конструктор new Function оборачивает код модуля в функцию, нам не надо писать функцию, оборачивающую пространство имён, внутри самого модуля. А так как exports является аргументом функции модуля, модулю не нужно его объявлять. Это убирает много мусора из нашего модуля-примера.
var names = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"];
exports.name = function(number) {
return names[number];
};
exports.number = function(name) {
return names.indexOf(name);
};
При использовании такого шаблона модуль обычно начинается с объявления нескольких переменных, которые загружают модули, от которых он зависит.
var weekDay = require("weekDay");
var today = require("today");
console.log(weekDay.name(today.dayNumber()));
У такого простого варианта require есть недостатки. Во-первых, он загрузит и выполнит модуль каждый раз, когда его грузят через require – если у нескольких модулей есть одинаковые зависимости, или вызов require находится внутри функции, которая вызывается многократно, будет потеряно время и энергия.
Это можно решить, храня уже загруженные модули в объекте, и возвращая существующее значение, когда он грузится несколько раз.
Вторая проблема – модуль не может экспортировать переменную напрямую, только через объект export. К примеру, модулю может потребоваться экспортировать только конструктор объекта, объявленного в нём. Сейчас это невозможно, поскольку require всегда использует объект exports в качестве возвращаемого значения.
Традиционное решение – предоставить модули с другой переменной, module, которая является объектом со свойством exports. Оно изначально указывает на пустой объект, созданный require, но может быть перезаписано другим значением, чтобы экспортировать что-либо ещё.
function require(name) {
if (name in require.cache)
return require.cache[name];
var code = new Function("exports, module", readFile(name));
var exports = {}, module = {exports: exports};
code(exports, module);
require.cache[name] = module.exports;
return module.exports;
}
require.cache = Object.create(null);
Сейчас у нас есть система модулей, использующих одну глобальную переменную require, чтобы позволять модулям искать и использовать друг друга без выхода в глобальную область видимости.
Такой стиль системы модулей называется CommonJS, по имени псевдостандарта, который первым его описал. Он встроен в систему Node.js. Настоящие реализации делают гораздо больше описанного мною. Главное, что у них есть более умный способ перехода от имени модуля к его коду, который разрешает загружать модули по относительному пути к файлу, или же по имени модуля, указывающему на локально установленные модули.
Медленная загрузка модулей
Хотя и возможно использовать стиль CommonJS для браузера, но он не очень подходит для этого. Загрузка файла из Сети происходит медленнее, чем с жёсткого диска. Пока скрипт в браузере работает, на сайте ничего другого не происходит (по причинам, которые станут ясны к 14 главе). Значит, если бы каждый вызов require скачивал что-то с дальнего веб-сервера, страница бы зависла на очень долгое время при загрузке.
Можно обойти это, запуская программу типа Browserify с вашим кодом перед выкладыванием её в веб. Она просмотрит все вызовы require, обработает все зависимости и соберёт нужный код в один большой файл. Веб-сайт просто грузит этот файл и получает все необходимые модули.
Второй вариант – оборачивать код модуля в функцию, чтобы загрузчик модулей сначала грузил зависимости в фоне, а потом вызывал функцию, инициализирующую модуль, после загрузки зависимостей. Этим занимается система AMD (асинхронное определение модулей).
Наша простая программа с зависимостями выглядела бы в AMD так:
define(["weekDay", "today"], function(weekDay, today) {
console.log(weekDay.name(today.dayNumber()));
});
Функция define здесь самая важная. Она принимает массив имён модулей, а затем функцию, принимающую один аргумент для каждой из зависимостей. Она загрузит зависимости (если они не были загружены ранее) в фоне, позволяя странице работать, пока файлы скачиваются. Когда всё загружено, define вызывает данную ему функцию, с интерфейсами этих зависимостей в качестве аргументов.