Разбираем механизм замыканий на JS

Добрый день.
Людям, которые приходят в JS из языков С++ | С# и подобных, зачастую, сложно разобраться, понять логику JS. Сегодня я хотел бы попытаться рассказать о такой замечательной вещи как замыкание.
По мере рассказа я приведу различные тестовые задания и продемонстрирую пример их решения.

Сразу же необходимо оговориться. И C++ (С++11) и C# поддерживают механизм замыкания. (вспоминаем анонимные методы и т.п. как яркий пример замыканий). Однако, на мой взгляд, в JS логика веб-приложений очень часто может строится непосредственно на замыканиях.

Итак. Что такое замыкание?
Замыкание, говоря простым языком, это возможность использовать переменные одной функции в контексте другой функции, вложенной в первую.

Самый простой (и понятный) пример данного механизма можно продемонстрировать так:
  1. var firstFunc = function( )
  2. {
  3.    var a = 10 ; //локальная переменная функции firstFunc
  4.    var secondFunc = function( )
  5.    {
  6.       console.log(a); //В данной функции a будет также видна и доступна. Выведется число 10
  7.    }
  8.    secondFunc();
  9. }
  10. firstFunc();

This code was highlighted with code.xnim.ru


Как действует замыкание?

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

Использование замыканий:

Пример 1.
Создание свойств.
Предположим, необходимо создать для объекта несколько свойств, доступ к полям (которые и хранят данные) необходимо закрыть. В таком случае существуют вполне изящный способ:

  1.   var createProp = function( targetObject, propName, propValue, setFunction)
  2. {
  3.    var value = propValue;
  4.    targetObject["get" +propName] = function( ){return value;}
  5.    targetObject["set" +propName] = function( newValue){value = setFunction(newValue);}
  6. }

This code was highlighted with code.xnim.ru


Предлагаю разобрать - что делает данный код:
1) он добавляет два метода для объекта targetObject, которые начинаются с set и get
2) c помощью механизма замыкания данные методы имеют доступ к переменной value верхней (по иерархии) функции.
3) на сеттер определяем функцию, которая проверяет необходимое значение и возвращает необходимый результат в value.

Таким образом, после работы функции createProp мы добаляем два метода (геттера и сеттера) для необходимого нам объекта, оба метода работают с одним и тем же значением value, которое нельзя никак изменить, кроме использования сеттера.
Пример 2.
Задача. Имеется набор кнопок. (для простоты зададим им id = i1,i2 и т.д.) по нажатию на кнопку она должна выводить свой порядковый номер.

Решение. Создадим функцию log следующего содержания:
  1. function log()
  2.    {
  3.       for (var i = 1 ; i < 10 ; i++)
  4.       {
  5.          document.getElementById(''i'' +i).onclick = function( i)
  6.          {
  7.             return function( )
  8.             {
  9.                alert(i);
  10.             }
  11.          }(i);
  12.       }
  13.    }

This code was highlighted with code.xnim.ru


Здесь функция в цикле для 9 кнопок добавляет каждой обработчик события на onclick. Однако, необходимо заметить, как это происходит
1) при присвоении значении вызывается анонимная функция
2) анонимная функция на вход принимает значение i. (без механизма замыкания, с помощью параметра)
3) анонимная функция возвращает другую анонимную функцию, которая выводит значение i посредством замыкания.

Здесь мы ввели функцию прослойку. Это вызвано тем, что удалив эту функцию, мы для всех кнопок получили бы значение 10.
Почему так? давайте приведем функцию log без прослойки и посмотрим на порядок действия:

  1. function log()
  2. {
  3.    for (var i = 1 ; i < 10 ; i++)
  4.    {
  5.       document.getElementById(''i'' +i).onclick = function( )
  6.       {
  7.          alert(i);
  8.       }
  9.    }
  10. }

This code was highlighted with code.xnim.ru


В данном случае мы используем сразу же механизм замыкания. Однако переменная i в цикле дойдет до значения 10 и каждый обработчик будет "видеть" переменную именно с значением 10.
Данная особенность означает, что вложенные функцию работают непосредственно с контекстом внешней функции, а не с копией значений.

Грабли
Стоит помнить, что каждая функция также имеет ссылку на объект, в контексте которого она исполняется. Является неверным считать, что вложенная функция ссылкой this имеет тот же объект, что и внешняя функция.
Приведу пример:

  1. function MyClass()
  2. {
  3.    this.someVariable = 10 ;
  4.    var func = function( )
  5.    {
  6.       console.log(this.someVariable)
  7.    
  8.    }
  9.    func();
  10. }
  11. var a = new MyClass();

This code was highlighted with code.xnim.ru


В данном случае в консоль будет выведено undefined, т.к. this внутри func будет ссылаться на объект Window (глобальный объект, в контексте которого выполняется js код)

Таким образом мы познакомились с механизмом замыкания. Спасибо за внимание.
2013-07-26