Подробнее про замыкания в JavaScript
Замыкание - одно из мощных выразительных средств javascript, которым часто пренебрегают, и даже не советуют употреблять. |
Замыкание - одно из мощных выразительных средств javascript, которым часто пренебрегают, и даже не советуют употреблять.
Действительно, замыкания создать очень легко, даже ненароком, и они могут приводить к проблемам. Но на самом деле они очень удобны, просто нужно понимать, что реально происходит.
Простое описание
Если говорить просто, то замыкания - это внутренние функции. Ведь javascript разрешает создавать функции по ходу выполнения скрипта. И эти функции имеют доступ к переменным внешней функции.
В этом примере создается внутренняя функция func, изнутри которой доступны как локальные переменные, так и переменные внешней функции outer:
function outer() { var outerVar; var func = function() { var innerVar ... x = innerVar + outerVar } return func } |
Когда заканчивает работать функция outer, внутренняя функция func остается жить, ее можно запускать в другом месте кода.
Получается, что при запуске func используется переменная уже отработавшей функции outer, т.е самим фактом своего существования, func замыкает на себя переменные внешней функции (а точнее - всех внешних функций).
Наиболее часто замыкания применяются для назначения функций-обработчиков событий:
function addHideHandler(sourceId, targetId) { var sourceNode = document.getElementById(sourceId) var handler = function() { var targetNode = document.getElementById(targetId) targetNode.style.display = ‘none’ } sourceNode.onclick = handler } |
Эта функция принимает два ID элементов HTML и ставит первому элементу обработчик onclick, который прячет второй элемент.
Т.е,
// при клике на элемент с ID="clickToHide" // будет спрятан элемент с ID="info" addHideHandler("clickToHide", "info") |
Здесь динамически созданный обработчик события handler использует targetId из внешней функции для доступа к элементу.
Подробное описание
..На самом деле происходящее в интерпретаторе Javascript гораздо сложнее и содержит куда больше деталей, чем здесь описано...
..Но чтобы понять и использовать замыкания, достаточно понять внутренний механизм работы функций, хотя бы и в таком, местами упрощенном виде...
[[scope]]
Каждое выполнение функции хранит все переменные в специальном объекте с кодовым именем [[scope]], который нельзя получить в явном виде, но он есть .
Каждый вызов var... - всего лишь создает новое свойство этого объекта, а любое упоминание переменной - первым делом ищется в свойствах этого объекта.
Такова внутренняя структура "области видимости" - обычный объект. Все изменения локальных переменных являются изменениями свойств этого неявного объекта.
Обычно после того, как функция закончила выполнение, ее область видимости [[scope]], т.е весь набор локальных переменных убивается.
Общий поток выполнения выглядит так:
// функция для примера function sum(x,y) { // неявно создался объект [[scope]] ... // в [[scope]] записалось свойство z var z // нашли переменную в [[scope]], [[scope]].z = x+y z = x+y // нашли переменную в [[scope]], return [[scope]].z return z // функция закончилась, // [[scope]] никому больше не нужен и умирает вместе с z } |
Кстати, для кода вне функции(и вообще глобальных переменных) роль объекта-контейнера [[scope]] выполняет объект window.
Область видимости вложенной функции
Когда одна функция создается внутри другой, то для нее создается ссылка на объект с локальными переменными [[scope]] внешней функции.
Именно за счет этого из внутренней функции можно получить переменные внешней функции - через ссылку на ее [[scope]]. Сначала ищем у себя, затем - во внешнем [[scope]] - и так далее по цепочке до самого объекта window.
Замыкание - это когда объект локальных переменных [[scope]] внешней функции остается жить после ее завершения.
Внутренняя функция может обратиться к нему в любой момент и получить переменную внешней функции.
Например, разберем работу функции, которая устанавливает обработчики событий:
function addHideHandler(sourceId, targetId) { // создан объект [[scope]] со свойствами sourceId, targetId // записать в [[scope]] свойство sourceNode var sourceNode = document.getElementById(sourceId) // записать в [[scope]] свойство handler var handler = function() { var targetNode = document.getElementById(targetId) targetNode.style.display = ‘none’ } sourceNode.onclick = handler // функция закончила выполнение // (***) и тут - самое интересное! } |
При запуске функции все происходит стандартно:
- создается [[scope]]
- туда записываются локальные переменные
- внутренняя функция получает ссылку на [[scope]]
Но в самом конце - внутренняя функция присваивается sourceNode.onclick. Внешняя функция закончила свою работу, но внутренняя - может запуститься когда-нибудь потом.
Интерпретатор javascript не проводит анализ - понадобятся ли внутренней функции переменные из внешней, и какие переменные могут быть нужны.
Вместо этого он просто оставляет весь [[scope]] внешней функции в живых.
Чтобы когда внутренняя функция запустится, если она вдруг не найдет какую-либо переменную в своем [[scope]] - она могла обратиться к [[scope]] внешней функции и нашла бы ее там.
Если внешняя функция была создана внутри еще одной (еще более внешней) функции - то в цепочку добавляется еще один консервированный [[scope]] и так - до глобальной области window.
Пример на понимание
В этом примере внешняя функция makeShout() создает внутреннюю shout().
function makeShout() { var phrase = "Превед!" var shout = function() { alert(phrase) } phrase = "Готово!" return shout } shout = makeShout() // что выдаст? shout() |
Функция shout() на правах внутренней функции имеет доступ к переменной phrase. Какое значение она выведет - первое или второе?
Если неочевидно - перед тем, как читать дальше, попробуйте этот пример запустить.
А вот - подробное описание происходящего в недрах javascript:
- Внутри makeShout()
- создается [[scope]]
- В [[scope]] пишется: phrase="Превед!"
- В [[scope]] пишется: shout=..функция..
- shout получает ссылку на [[scope]] внешней функции
- [[scope]].phrase меняется на новое значение "Готово!"
- При запуске shout()
- Создается свой собственный объект [[scope2]]
- Ищется phrase в [[scope2]] - не найден
- Ищется phrase в [[scope]] внешней функции - найдено значение "Готово!"
- alert("Готово!")
То есть, внутренняя функция получает последнее значение внешних переменных.
Пример ошибочного использования
Функция addEvents принимает массив div'ов и ставит каждому вывод своего номера на onclick.
С вопроса "Почему это не работает?" люди обычно начинают изучение замыканий.
function addEvents(divs) { for(var i=0; i<divs.length; i++) { divs[i].innerHTML = i divs[i].onclick = function() { alert(i) } } } |
Для тестового примера сделаем 10 разноцветных нумерованных div'ов с разными цветами:
function makeDivs(parentId) { for (var i=0;i<10;i++) { var j = 9-i var div = document.createElement('div') div.style.backgroundColor = '#'+i+i+j+j+j+i div.className="closure-div" div.style.color = '#'+j+j+i+i+i+j document.getElementById(parentId).appendChild(div) } } |
Кнопка ниже создаст 10 дивов и вызовет для них addEvents
Если Вы покликаете на div'ы - они все выдают одинаковый alert.
Такой глюк возник из-за того, что все функции div[i].onclick получают значение i из одного на всех [[scope]] внешней функции. А это значение ([[scope]].i) на момент активации onclick-обработчика равно 10 (цикл завершился как только i==10).
Чтобы все было в порядке, в таких случаях применяют специальный прием - выделение [[scope]]. Следующая функция работает правильно. В ней все то же самое, кроме div.onclick.
function addEvents2(divs) { for(var i=0; i<divs.length; i++) { divs[i].innerHTML = i divs[i].onclick = function(x) { return function() { alert(x) } }(i) } } |
Теперь все должно быть в порядке - каждый div дает alert на свой номер.
Для присваивания div.onclick запускается временная функция function(x) {..}, принимающая аргумент x и возвращающая обработчик, который берет x из [[scope]] этой временной функции.
Запись
function(x) {..}используется для создания функции, и тут же (i) - для запуска с аргументом i.
Вообще, javascript очень удобный в этом смысле язык. Допускает любые конструкции, например:
alert( function a(){ return [5] }()[0] ) // => выведет 5 |
Временная функция function(x) {..} заканчивает работать тут же, оставляя в своем [[scope]] правильное значение x, равное текущей переменной i цикла.
Когда обработчик активизируется - alert возьмет из [[scope]] ближайшей внешней функциии правильное значение x.
По идее, этих примеров должно хватать для понимания и практического использования замыканий.
« Назад