Javascript.RU

Замыкания

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

Каждый вызов 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() { // (1)
    var phrase = "Превед!"  // (2)

    var shout = function() {  // (3,4)
        alert(phrase) 
    }
    
    phrase = "Готово!"  // (5)

    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("Готово!")

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

Замыкание позволяет создать функцию суммирования, которая работает вот так:

sum(a)(b) = a+b

// например
sum(1)(3) = 4

Да, именно так: скобки - не опечатки.

А вот и сама функция sum:

function sum(a) {
  return function(b) {
    return a+b
  }
}

Функция 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 очень удобный в этом смысле язык. Допускает любые конструкции, например, вместо последовательных вызовов:

var f = function(a) { return [0, a, 2*a] }
var t = f(1)
var result = t[2] // 2

можно в одну строчку создать и тут же вызвать функцию и тут же получить 2й элемент массива:

var result = function(a){ return [0,a,2*a] }(1)[2]

Временная функция function(x) {..} заканчивает работать тут же, оставляя в своем [[scope]] правильное значение x, равное текущей переменной i цикла.

Когда обработчик активизируется - alert возьмет из [[scope]] ближайшей внешней функциии правильное значение x.

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

Также про замыкания можно почитать, например в cтатье http://www.jibbering.com/faq/faq_notes/closures.html

Конечно, разобрать происходящее во всех деталях позволит стандарт языка ECMA-262.


Автор: Гость (не зарегистрирован), дата: 14 июля, 2008 - 04:09
#permalink

На моем браузере вывод "makeDivs + addEvents" и "makeDivs + новая функция" отображаются одинаково.


Автор: Илья Кантор, дата: 14 июля, 2008 - 20:08
#permalink

Да, makeDivs должны отображаться одинаково. Но работают по-разному.


Автор: Sirius (не зарегистрирован), дата: 14 августа, 2008 - 19:11
#permalink

Для общего развития очень даже интересно.
Сам занимаюсь сбором информации по теме web-программирования.
Кстати, у меня есть тоже немало интересных на мой взгляд статей и примеров по javaScript
Все-таки без JavaScript вэб был бы очень скучным


Автор: Гость (не зарегистрирован), дата: 17 сентября, 2008 - 17:27
#permalink

Расскажите плз. про последниий пример поподробнее, я ничего не понял
>>alert( function a(){ return [5] }()[0] ) // => выведет 5
и чуть выше тоже


Автор: Илья Кантор, дата: 18 сентября, 2008 - 10:57
#permalink

Ок, спасибо. Сделал эту часть подробнее.


Автор: twolf, дата: 10 октября, 2008 - 16:53
#permalink

Отличная статья. спасибо


Автор: blade-mscl, дата: 13 октября, 2008 - 23:01
#permalink

спасибо. была проблема. разобрался.


Автор: Павел Мартемьянов (не зарегистрирован), дата: 15 ноября, 2008 - 22:21
#permalink

Спасибо за статью.
Возник вопрос по работе с локальными переменными в таких случаях в отладчике Firebug.

В этом примере

var xx = 20;
(function(){
var xx = 10;
func = function(){
alert(xx);
}
})();
func();

отладчик Firebug при вызове функции func()
показывает что xx = 20, хотя alert() выводит правильное значение 10.

Как следить за переменной из [[scope]] функции func ??


Автор: Илья Кантор, дата: 24 ноября, 2008 - 13:50
#permalink

В вашем коде нет явного вызова отладчика командой debugger. Вставьте ее, чтобы понимать точное место вызова в коде.


Автор: Тамара (не зарегистрирован), дата: 24 ноября, 2008 - 01:17
#permalink

Спасибо за столь подробное объянение


Автор: grang (не зарегистрирован), дата: 7 декабря, 2008 - 12:27
#permalink

Отличная статья, спасибо огромное!


Автор: Гаврила (не зарегистрирован), дата: 7 декабря, 2008 - 12:29
#permalink

Молодцы спасибо за статью!


Автор: flaer (не зарегистрирован), дата: 7 декабря, 2008 - 12:31
#permalink

Спс... очень признателен!


Автор: zaremba (не зарегистрирован), дата: 14 декабря, 2008 - 02:49
#permalink

Не лишне было бы упомянуть, что замыкания можно организовать и с использованием неанонимных внутренних функций.


Автор: Dicot (не зарегистрирован), дата: 14 января, 2009 - 12:53
#permalink

Очень похоже, что [[scope]].[[prototype]] вложенной функции == [[scope]] внешней. Это действительно так, или я ошибаюсь?


Автор: Илья Кантор, дата: 20 января, 2009 - 06:53
#permalink

Речь идет просто об иерархии, в чем-то аналогичной наследованию через прототипы.

Однако, насколько я понимаю - из стандарта (http://javascript.ru/ecma/part10#a-10.1.4) не следует, что у [[scope]] есть свойство [[prototype]]. Просто сказано, что [[scope]]'ы формируют иерархию..


Автор: maicroft (не зарегистрирован), дата: 15 февраля, 2009 - 17:48
#permalink

Спасибо. Очень интересно и понятно!


Автор: Гость (не зарегистрирован), дата: 26 февраля, 2009 - 17:28
#permalink

Спасибо за столь подробное объянение


Автор: Гость (не зарегистрирован), дата: 21 апреля, 2009 - 23:58
#permalink

Отлично!


Автор: Гость (не зарегистрирован), дата: 4 июня, 2009 - 16:55
#permalink

хорошая статья) первый раз вижу такое на русском языке)


Автор: dzyanis, дата: 8 июня, 2009 - 18:20
#permalink

Спасибо! На русском аналогов не видел, по моему даже на википедии нет!


Автор: Митяй (не зарегистрирован), дата: 23 июля, 2009 - 07:05
#permalink

Отличная статья, материал не то чтобы сложный, но везде он как-то через назад описан, а тут - ясно и четко.
Кстати, в варианте "Веселой функции"
sum(1)(3) = 4
я бы посоветовал вычитание
sub(7)(8) = -1
гораздо нагляднее, что в какую очередь вызывается.
ИМХО.


Автор: porqz (не зарегистрирован), дата: 4 августа, 2009 - 15:49
#permalink

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


Автор: Леонид Евстигнеев, дата: 4 августа, 2009 - 17:56
#permalink

Подозреваю, что вам поможет слово var.


Автор: Рюкан (не зарегистрирован), дата: 9 октября, 2009 - 14:00
#permalink

Спасибо! очень толково и понятно освящен такой сложный для меня вопрос)


Автор: Девушка (не зарегистрирован), дата: 19 октября, 2009 - 17:41
#permalink

Честно сказать, у меня брызнули слезы из глаз.. Я так долго пыталась понять, как работает Javascript, а тут прочитала - и за 5 минут все улеглось по полочкам. Огромное спасибо!


Автор: Гость (не зарегистрирован), дата: 9 ноября, 2009 - 09:19
#permalink

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


Автор: Shivo (не зарегистрирован), дата: 14 декабря, 2009 - 11:44
#permalink

Спасибо за очень хорошую статью.


Автор: Lexis (не зарегистрирован), дата: 25 декабря, 2009 - 10:31
#permalink

Добрый день. У меня возник следующий вопрос. Что и в каких scope происходит со свойством x в данном примере:

var x = 10;
var f = function() {
  if (x == 10) {
    var x = 20;
  }
  alert(x);
}
f();

Почему в результате я получаю undefined?


Автор: Azadi, дата: 3 января, 2010 - 18:10
#permalink
var x = 10;
var f = function() {
	if (x == 10) {
	x = 20;
	}
return alert(x);
}
f();

Вот это работает как надо, вот только почему не работает при var x=20; мне непонятно... Как вариант, некорректная запись условия. Надо копать мануал.


Автор: B@rmaley.e><e (не зарегистрирован), дата: 3 января, 2010 - 20:16
#permalink

Потому что используя var x Вы объявляете локальную переменную x. Соответственно, она не существует, пока не будет инициализирована. А для ее инициализации должно выполнится условие, т.е. ее значение должно быть = 10.

var x = 10;
var f = function() { // тут x  - локальная, x = undefined
    if (x == 10) { // тут x до сих пор undefined
    var x = 20; // тут x стало бы 20, но эта ветвь никогда не выполнится
    }
return alert(x);
}
f();

Автор: WalterScott, дата: 27 января, 2010 - 15:03
#permalink

Я думаю, x undefined, потому-что в месте вызова alert(x) есть две переменные x и интерпретатор не может определить, к какой из них он должен обратиться.


Автор: Мараторий, дата: 28 января, 2010 - 06:56
#permalink

WalterScott, вы думаете не правильно. Еще раз внимательно прочтите статью и все станет понятно. B@rmaley.e><e все правильно написал выше.
Не забывайте, что интерпретатор проходится по скрипту дважды:
1. сначала собирает данные о локальных переменных (ищет var) и засовывает их в [[scope]]
2. потом начинает исполнять.


Автор: Gozar, дата: 26 февраля, 2010 - 19:01
#permalink

B@rmaley.e>e

Соответственно, она не существует, пока не будет инициализирована

не совсем так, вернее вот так:

При входе в контекст исполнения создаётся свойство VO с именем переменной, и значением undefined

http://javascript.ru/blog/Dmitry-A.-Soshnikov/Tonkosti-ECMA-262-3.-CHast-2.-Obekt-peremennyh.#obekt-peremennyh-v-kontekste-funkcii

Поэтому Ваши слова в виде:

x = undefined

больше отражают суть.


Автор: Yuriy Zaletskyy (не зарегистрирован), дата: 28 января, 2010 - 13:28
#permalink

Как вы отнесетесь, если я сделаю перевод вашей статьи на английский язык с указанием оригинала, т.е. вашей статьи?


Отправить комментарий

Приветствуются комментарии:
  • Полезные.
  • Дополняющие прочитанное.
  • Вопросы по прочитанному. Именно по прочитанному, чтобы ответ на него помог другим разобраться в предмете статьи.
    Для остальных вопросов и обсуждений есть форум.
P.S. Лучшее "спасибо" - не комментарий, как все здорово, а рекомендация или ссылка на статью.
Содержание этого поля является приватным и не предназначено к показу.
  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Разрешены HTML-таги: <strike> <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <u> <i> <b> <pre> <img> <abbr> <blockquote> <h1> <h2> <h3> <h4> <h5> <p> <div> <span> <sub> <sup>
  • Строки и параграфы переносятся автоматически.
  • Текстовые смайлы будут заменены на графические.

Подробнее о форматировании

CAPTCHA
Антиспам
6 + 10 =
Введите результат. Например, для 1+3, введите 4.
 
Текущий раздел
Поиск по сайту
Содержание

Учебник javascript

Основные элементы языка

Сундучок с инструментами

Интерфейсы

Все об AJAX

Оптимизация

Разное

Дерево всех статей

Статьи и мероприятия

Будьте в курсе наших последних новостей!

Ответьте, пожалуйста..
О чем бы вы хотели услышать на конференции по javascript?

На какие темы послушать доклады? Конференция состоится в середине мая.


Последние обсуждения на форуме
Forum
Последние комментарии