en.javascript.info/1-js/5-functions-closures/4-closures-module/article.md
Ilya Kantor 87bf53d076 update
2014-11-16 01:40:20 +03:00

13 KiB
Raw Blame History

Модули через замыкания

Приём программирования "модуль" имеет громадное количество вариаций.

Его цель -- скрыть внутренние детали реализации скрипта. В том числе: временные переменные, константы, вспомогательные мини-функции и т.п.

Зачем нужен модуль?

Допустим, мы хотим разработать скрипт, который делает что-то полезное.

В браузере скрипты могут делать много чего -- если бы мы умели работать со страницей, то могли бы сделать так, чтобы все блоки кода красиво расцвечивались, так сделано на этом сайте.

Но, так как пока мы со страницей работать не умеем (скоро научимся), то пусть скрипт просто выводит сообщение:

Файл highlight.js

//+ run
// глобальная переменная нашего скрипта
var message = "Привет";

// функция для вывода этой переменной
function showMessage() {
  alert(message);
}

// выводим сообщение
showMessage();

У этого скрипта есть свои внутренние переменные и функции.

В данном случае это message и showMessage.

Если подключить подобный скрипт к странице "как есть", то возможен конфликт с переменными, которые она использует.

То есть, при подключении к такой странице он её "сломает":

<script>
  var message = "Пожалуйста, нажмите на кнопку";
</script>
<script src="highlight.js"></script>

<button>Кнопка</button>
<script>
  alert(message);
</script>

Будет выведено два раза слово "Привет".

[edit src="highlight-conflict"/]

Если же убрать скрипт highlight.js, то страница будет выводить правильное сообщение.

Проблема возникла потому, что переменная message из скрипта highlight.js перезаписала объявленную на странице.

Приём проектирования "Модуль"

Чтобы проблемы не было, нам всего-то нужно, чтобы у скрипта была своя собственная область видимости, чтобы его переменные не попали на страницу.

Для этого мы завернём всё его содержимое в функцию, которую тут же запустим.

Файл highlight.js, оформленный как модуль:

//+ run
(function() {
  
  // глобальная переменная нашего скрипта
  var message = "Привет";

  // функция для вывода этой переменной
  function showMessage() {
    alert(message);
  }

  // выводим сообщение
  showMessage();

})();

Этот скрипт при подключении к той же странице будет работать корректно.

Будет выводиться "Привет", а затем "Пожалуйста, нажмите на кнопку".

[edit src="highlight-module"/]

Зачем скобки вокруг функции?

В примере выше объявление модуля выглядит так:

//+ run
(function() {
 
  alert("объявляем локальные переменные, функции, работаем");
  // ...

}());

В начале и в конце стоят скобки, так как иначе была бы ошибка.

Вот неверный вариант:

//+ run
function() {
 // будет ошибка
}();

Ошибка при его запуске произойдет потому, что браузер, видя ключевое слово function в основном потоке кода, попытается прочитать Function Declaration, а здесь имени нет.

Впрочем, даже если имя поставить, то работать тоже не будет:

//+ run
function work() {
  // ...
}();  // syntax error

Дело в том, что "на месте" разрешено вызывать только Function Expression.

Общее правило таково:

  • **Если браузер видит `function` в основном потоке кода -- он считает, что это `Function Declaration`.**
  • **Если же `function` идёт в составе более сложного выражения, то он считает, что это `Function Expression`.**

Для этого и нужны скобки -- показать, что у нас Function Expression, который по правилам JavaScript можно вызвать "на месте".

Можно показать это другим способом, например поставив перед функцией оператор:

//+ run
+function() {
  alert('Вызов на месте');
}();

!function() {
  alert('Так тоже будет работать');
}();

Библиотека

Приём "модуль" используется почти во всех современных библиотеках.

Ведь что такое библиотека? Это полезные функции, ради которых её подключают, плюс временные переменные и вспомогательные функции, которые библиотека использует внутри себя.

Посмотрим, к примеру, на библиотеку Lodash, хотя могли бы и jQuery, там почти то же самое.

Если её подключить, то появится функция lodash (она же _), в которую можно обернуть любой объект, так что lodash(obj) -- это обёртка, добавляющая к объекту функциональность.

Кроме того, lodash имеет ряд полезных свойств-функций, например lodash.defaults(object, source) для удобного добавления в объект object значений свойств "по умолчанию", описанных в source.

Выдержка из файла lodash.js для демонстрации того, как организована библиотека:

//+ run
;(function() {
 
*!*
  // порядок не важен, но сначала объявим то, что нужно только внутри библиотеки
  // version, objectTypes, assignDefaults
*/!*
  var version = '2.4.1';  

  var objectTypes = {   
    'function': true,
    'object': true
  };

  function assignDefaults(objectValue, sourceValue) {
    return typeof objectValue == 'undefined' ? sourceValue : objectValue;
  }

*!*
  // а это функция, которая станет lodash.defaults
*/!*
  function defaults(object) {
    if (!object || arguments.length < 2) {
      return object;
    }
    var args = slice(arguments);
    args.push(assignDefaults);
    return assign.apply(null, args);
  }

*!*
  // lodash - основная функция для библиотеки, единственное, что пойдёт наружу
*/!*
  function lodash(value) {
    // ...
  }
  
*!*
  // присвоим ей defaults и другие функции, которые нужно вынести из модуля
*/!*
  lodash.defaults = defaults;
  // lodash... = ...

*!*
  // root - это window в браузере
  // в других окружениях, где window нет, root = this
*/!*
  var root = (objectTypes[typeof window] && window) || this;

  root.lodash = lodash; // в браузере будет window.lodash = lodash

}.call(this)); // this = window в браузере, в Node.JS - по-другому

Внутри внешней функции:

  1. **Происходит что угодно, объявляются свои локальные переменные, функции.**
  2. **В `window` выносится то, что нужно снаружи.**

Технически, мы могли бы вынести в window не только lodash, но и вообще все объекты и функции. На практике, обычно модуль -- это один объект, глобальную область во избежание конфликтов хранят максимально чистой.

[smart header="Зачем точка с запятой в начале?"] Если получится, что несколько JS-файлы объединены в один (и, скорее всего, сжаты минификатором, но это не важно), то без точки с запятой будет ошибка:

//+ run
// a.js, в конце забыта точка с запятой!
*!*
var a = 5
*/!*

// lib.js, библиотека
(function() {
  // ...
})();

Ошибка при запуске будет потому, что JavaScript интерпретирует код как var a = 5(function ...), то есть пытается вызвать число 5 как функцию.

Таковы правила языка, и поэтому рекомендуется явно ставить точку с запятой. В данном случае автор Lodash ставит ; перед функцией, чтобы предупредить эту ошибку. [/smart]

Использование:

<!--+ run -->
<p>Подключим библиотеку</p>
<script src="http://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.js"></script>

<p>Используем <code>_.defaults()</code>.</p>
<script>
var user = { name: 'Вася' };

*!*
_.defaults(user, { name: 'Не указано', employer: 'Не указан' });
*/!*

alert(user.name); // Вася
alert(user.employer); // Не указан
</script>

Экспортирование через return

Можно оформить модуль и чуть по-другому, например передать значение через return:

var lodash = (function() {

  var version;
  function assignDefaults() { ... }

  return {
    defaults: function() {  }
  }

})();

Здесь, кстати, скобки вокруг внешней function() { ... } не обязательны, ведь функция и так объявлена внутри выражения присваивания, а значит -- является Function Expression.

Тем не менее, лучше их ставить, для улучшения читаемости кода, чтобы было сразу видно, что это не простое присвоение функции.

Итого

Модуль при помощи замыканий -- это оборачивание пакета функционала в единую внешнюю функцию, которая тут же выполняется.

Все функции модуля будут иметь доступ к другим переменным и внутренним функциям этого же модуля через замыкание.

Например, defaults из примера выше имеет доступ к assignDefaults.

Но снаружи программист, использующий модуль, может обращаться напрямую только к тем, которые экспортированы.

Благодаря этому будут скрыты внутренние аспекты реализации, которые нужны только разработчику модуля.

Можно придумать и много других вариаций такого подхода. В конце концов, "модуль" -- это всего лишь функция-обёртка для скрытия переменных.