Паттерн "Модуль" базируется на замыканиях и состоит из двух компонентов: внешняя функция, которая определяет лексическое окружение, и возвращаемый набор внутренних функций, которые имеют доступ к этому окружению.
Определим простейший модуль:
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.