Подробнее про замыкания в JavaScript


Подробнее про замыкания в 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]], который нельзя получить в явном виде, но он есть Smiling.

Каждый вызов 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
 
    // функция закончила выполнение
    // (***) и тут - самое интересное!
}

При запуске функции все происходит стандартно:

  1. создается [[scope]]
  2. туда записываются локальные переменные
  3. внутренняя функция получает ссылку на [[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:

  1. Внутри makeShout()
    1. создается [[scope]]
    2. В [[scope]] пишется: phrase="Превед!"
    3. В [[scope]] пишется: shout=..функция..
    4. shout получает ссылку на [[scope]] внешней функции
    5. [[scope]].phrase меняется на новое значение "Готово!"
  2. При запуске shout()
    1. Создается свой собственный объект [[scope2]]
    2. Ищется phrase в [[scope2]] - не найден
    3. Ищется phrase в [[scope]] внешней функции - найдено значение "Готово!"
    4. 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.

По идее, этих примеров должно хватать для понимания и практического использования замыканий.

© javascript.ru

Оценить Статью:  
1   2   3   4   5   6   7   8   9   10    

« Назад
SAPE все усложнил?

MainLink - простая и прибыльная продажа ссылок!

Последние поступления:

Размещена 10 августа 2020 года

Я по ТВ видел, что через 10 лет мы будем жить лучше, чем в Германии...
Я не понял, что это они с Германией сделать хотят?!

читать далее…

ТехЗадание на Землю

Размещена 14 марта 2018 года

Пpоект Genesis (из коpпоpативной пеpеписки)

читать далее…

Шпаргалка по работе с Vim

Размещена 05 декабря 2017 года

Vim довольно мощный редактор, но работа с ним не всегда наглядна.
Например если нужно отредактировать какой-то файл например при помощи crontab, без знания специфики работы с viv никак.

читать далее…

Ошибка: Error: Cannot find a valid baseurl for repo

Размещена 13 сентабря 2017 года

Если возникает ошибка на centos 5 вида
YumRepo Error: All mirror URLs are not using ftp, http[s] or file.
Eg. Invalid release/

читать далее…

Linux Optimization

Размещена 30 июля 2012 года

Prelink

читать далее…