В одном из последних выпусков подкаста про Laravel («Мне нравится то, что ты говоришь»), мы говорили о VueJS, и Тэйлор упомянул, что иногда его раздражает процесс передачи больших объектов между JavaScript-фреймворками. Ну вы и сами знаете: определяем группы параметров и методов, затем нам надо втиснуть их в один большой список через запятую, и они становятся набором свойств объекта:
jsVue.doSomethingOrOther({ onething: function () { }, otherThing: function () { }, etcetera: 'etcetera' });
В подкасте я рассказал о своей неугасающей любви к паттерну выявления модулей (Revealing Module Pattern), и пообещал показать пример его применения. Вот и он.
Зачем нужен паттерн выявления модулей?
Впервые я узнал об этом паттерне из книги Эдди Османи Изучаем паттерны проектирования JavaScript.
Давайте рассмотрим небольшой пример, чтобы понять, чем так хорош паттерн выявления модулей. Предположим нам нужен объект Analytics. Мы хотим иметь возможность использовать его из любого места нашего JavaScript, чтобы вызывать Google Analytics, но при этом хотим синтаксис попроще.
jsvar Analytics = {};
Добавим в него два метода — PHPpageView
и PHPaction
.
jsvar Analytics = { pageView: function () { GoogleAnalytics.prepSomethingOrOther(); GoogleAnalytics.pushOrSomething('pageView'); }, action: function (key) { GoogleAnalytics.prepSomethingOrOther(); GoogleAnalytics.pushOrSomething('action', key); } };
Так так, что это у нас тут? Небольшое повторение кода! Если бы у нас были private-методы, мы смогли бы извлечь что-то наподобие метода PHPpushOrSomething()
:
jsvar Analytics = { pushOrSomething: function () { GoogleAnalytics.prepSomethingOrOther(); // Используем function.apply для передачи вызываемых параметров далее GoogleAnalytics.pushOrSomething.apply(this, arguments); }, pageView: function () { this.pushOrSomething('pageView'); }, action: function (key) { this.pushOrSomething('action', key); } };
Выглядит хорошо, но тут есть большая проблема — сейчас наш PHPAnalytics.pushOrSomething()
доступен извне. Вдобавок, пока до этого не дошло, но когда мы начнём создавать из этого объект, мы столкнёмся с проблемой постоянного роста списка через запятую, о которой говорил Тэйлор.
Самовызывающаяся анонимная функция
Петтерн выявления модулей опирается на концепцию самовызывающейся анонимной функции. Давайте сначала посмотрим на анонимную функцию:
jsvar AnalyticsGenerator = function() { return {}; }; var Analytics = AnalyticsGenerator();
Отлично, у нас есть неназванная функция, которая возвращает что-то, ЕСЛИ мы её запустим. Но почему синтаксис такой неудобный, особенно если мы хотим запустить это всего один раз. Вот если бы мы могли вызвать функцию сразу после того, как определили её...
jsvar Analytics = (function() { return {}; })();
Хвала небесам, мы сделали это! Мы просто заключили функцию в круглые скобки, а затем добавили в конец вторую пару круглых скобок, чтобы указать, что она должна быть выполнена. Теперь PHPAnalytics
определена как результат выполнения этой функции, в данном случае это просто пустой объект.
Паттерн выявления модулей
Наконец. Сам паттерн. Посмотрим на предыдущий пример, но уже переписанный согласно паттерну:
jsvar Analytics = (function () { var _pushOrSomething = function () { GoogleAnalytics.prepSomethingOrOther(); // Используем function.apply для передачи вызываемых параметров далее GoogleAnalytics.pushOrSomething.apply(this, arguments); }; var pageView = function () { _pushOrSomething('pageView'); }; var action = function (key) { _pushOrSomething('action', key); }; return { pageView: pageView, action: action }; })();
Обратите внимание, мы обновили вызовы в PHPpageView
и PHPaction
, чтобы обращаться к функции не используя this, а также добавили символ подчёркивания в начало имени «private» функции PHPpushOrSomething()
, просто как напоминание о том, что она должна быть закрытой (private).
В конце, мы определили, что мы хотим возвращать, а всё что не попадает в этот возвращаемый объект будет «private», и это нельзя будет вызвать извне. Это касается и свойств, и вы при необходимости можете выполнять любые процедурные задачи:
jsvar Analytics = (function () { var variableOrWhatever = 42; variableOrWhatever *= 1.0; var _pushOrSomething = function () { GoogleAnalytics.initialize(variableOrWhatever); GoogleAnalytics.somethingOrOther(); // Используем function.apply для передачи вызываемых параметров далее GoogleAnalytics.pushOrSomething.apply(this, arguments); }; var pageView = function () { _pushOrSomething('pageView'); }; var action = function (key) { _pushOrSomething('action', key); }; return { variableOrWhatever: variableOrWhatever, pageView: pageView, action: action }; })();
Если хотите взглянуть на это в действии и посмотреть, что будет при попытке вызова «private» метода, то посмотрите на этот JSBin. Или просто попробуйте выполнить этот код и посмотреть, что происходит при запуске PHPAnalytics._pushOrSomething
(подсказка: вашему браузеру это не понравится).
Нет предела совершенству, ребята. Теперь, когда вам нужно будет сгенерировать JavaScript-объект, у вас всегда будет намного большей возможностей для быстрого создания private-методов и выполнения противного процедурного кода во время его создания.
Дополнения об ES6
Основная причина необходимости таких вещей в том, что JavaScript основан на прототипах, а не на классах и объектах. Это постепенно изменяется, и ES6 привнёс огромные изменения в этот аспект. Если вы пишете на ES6, то будете использовать этот паттерн меньше, но я всё-таки советую держать его в своём арсенале.