Паттерн "Модуль" базируется на замыканиях и состоит из двух компонентов: внешняя функция, которая определяет лексическое окружение, и возвращаемый набор внутренних функций, которые имеют доступ к этому окружению.
Определим простейший модуль:
const printer = (function(){ const messages = {greeting: "hello"}; return { print: function(){ console.log(messages.greeting); } } })(); printer.print(); // hello
Здесь определена константа printer, которая представляет результат анонимной функции. Внутри подобной функции определен объект messages с некоторыми данными.
Сама анонимная функция возвращает объект, который определяет функцию print. Возвращаемый объект определяет общедоступый API, через который мы можем обращаться к данным, определенным внутри модуля.
return { print: function(){ console.log(messages.greeting); } }
Такая конструкция позволяет закрыть некоторый набор данных в рамках функции-модуля и опосредовать доступ к ним через определенный API - возвращаемые внутренние функции.
Возвращаемые функции могут быть определены где в другом месте, а не внутри анонимной функции:
const printer = (function(){ const messages = {greeting: "Hello METANIT.COM"}; const printMessage = function(){ console.log(messages.greeting); }; return { print: printMessage // функция printMessage определена вне объекта } })(); printer.print(); // Hello METANIT.COM
Если существует вероятность, что модуль уже определен где-то ранее в коде или во внешних подключаемых файлах, то мы можем использовать следующую конструкцию:
var printer = printer || (function(){ const messages = {greeting: "Hello World"}; return { print: function(){ console.log(messages.greeting); } } })(); printer.print(); // Hello World
Определение var printer = printer || (function(){ ...
присваивает переменной значение некоторого объекта printer, если он существует,
либо присваивает результат вызова анонимной IIFE-функции. Но при таком определении мы не можем использовать ключевые слова let
или const
для определения объекта. Поэтому в данном случае объект определяется с помощью var
.
Рассмотрим чуть более сложный пример:
const calculator = (function(){ const data = { number: 0}; return { sum: function(n){ data.number += n; }, subtract: function(n){ data.number -= n; }, print: function(){ console.log("Result: ", data.number); } } })(); calculator.sum(10); calculator.sum(3); calculator.display(); // Result: 13 calculator.subtract(4); calculator.display(); // Result: 9
Данный модуль представляет примитивный калькулятор, который выполняет три операции: сложение, вычитание и вывод результата.
Все данные инкапсулированы в объекте data, который хранит результат операции. Все операции представлены тремя возвращаемыми функциями: sum, subtract и print. Через эти функции мы можем управлять результатом калькулятора извне.
Через параметры IIFE-функций в модули можно передать какие-нибудь данные, например, другие модули:
var moduleA = moduleA || (function () { const message = "Hello World"; return { printMessage: function() { console.log(message); } } })(); var moduleB = moduleB || (function (moduleA) { return { print: function() { moduleA.printMessage(); } } })(moduleA); moduleB.print();
В данном случае модуль moduleB ожидает получение модуля moduleA. Внутри модуля moduleB идет обращение к функции moduleA.printMessage. Аналогично можно передавать и набор модулей.
При работе с модулями может возникнуть задача расширить его функционал - добавить в него функции или переменные. В этом случае мы можем использовать ряд техник.
// первая техника var localeModule = localeModule || (function(locale){ const enMessage = "Hello World"; locale.printEn = function(){console.log(enMessage);}; return locale; })(localeModule || {}); // вторая техника var localeModule = (function(locale){ const ruMessage = "Привет мир"; locale.printRu = function(){console.log(ruMessage);}; return locale; })(localeModule); localeModule.printEn(); // Hello World localeModule.printRu(); // Привет мир
Для расширения модуля можно применять две техники. Первая техника заключается в том, что если модуль еще не создан, то в качестве параметра передается пустой объект:
var localeModule = localeModule || (function(locale){ const enMessage = "Hello World"; locale.printEn = function(){console.log(enMessage);}; return locale; })(localeModule || {});
Так, в данном случае, если модуля localModule еще не существует, то будет создан объект, в который будет добавлена функция printEn для вывода некоторого сообщения.
Преимущество этой техники состоит в том, что скрипты, которые входят в модуль, могут загружаться асинхронно. Не имеет значения, какой скрипт будет загружен первым, поскольку в случае сомнений модуль будет создан заново.
Вторая техника предполагает, что модуль уже существует:
var localeModule = (function(locale){ const ruMessage = "Привет мир"; locale.printRu = function(){console.log(ruMessage);}; return locale; })(localeModule);
Здесь мы уверены, что уже есть объект localModule, и также добавляем к нему новую функцию - printRu. В обоих случаях модуль возвращает в качестве результата расширенный новой функциональность объект из параметра.
Но независимо от того, какой тип расширения модуля применяется, у них есть один общий недостаток: функции, определенные в одном файле исходного кода для модуля, не имеют доступа к частным переменным и константам, определенным в другом файле исходного кода для того же модуля. Например, метод printRu не имеет доступа к константе enMessage.