08. Функции. Часть 2. Замыкания
Замыкания
![]() |
Комбинация исполняемого кода и области видимости, в которой этот код исполняется, называется замыканием этого кода. То есть замыкание функции — это ее код плюс объект вызова функции плюс глобальный объект window. |
Пока ничего особенного.
Ничего особенного даже тогда, когда внутри функции расположена вложенная функция. Например:
01 function f()
02 {
03 var y = 0;
04 function g()
05 {
06 y++;
07 return y;
08 }
09 return g();
10 }
11 var z = f(); // Равно 1
12 z = f(); // Конечно, снова 1 (было бы странно, если было бы иначе)
Рассмотрим по шагам, что происходит в 11 строке этого кода.
-
Вычисляется функция f.
-
1.1. Для функции f создается
объект вызова (условно: ов_f). В него входят:
- локальная переменная y;
- локальная переменная g (содержит ссылку на вложенную функцию);
- объект arguments.
- 1.2. Выполняется строка 03: в переменную y заносится число 0.
-
1.3. Выполняется строка
09: вычисляется функция g и вычисленное
значение возвращается в качестве значения функции f.
-
1.3.1. Для функции g создается объект вызова
(условно: ов_g). В него входят:
- объект arguments
- цепочка областей видимости для функции g: ов_g – ов_f – window
- 1.3.2. Выполняется строка 06: значение переменной y в объекте вызова ов_f увеличивается на 1.
- 1.3.3. В строке 07: значение переменной y (то есть число 1) возвращается в качестве результата выполнения функции g.
- 1.3.4. Функция g закончила свою работу, и сборщик мусора уничтожает объект ов_g.
-
1.3.1. Для функции g создается объект вызова
(условно: ов_g). В него входят:
- 1.4. В строке 09 вызов g() вернул значение 1, и это значение возвращается в качестве значения функции f.
- 1.5. Функция f закончила свою работу, и сборщик мусора уничтожает объект ов_f.
-
1.1. Для функции f создается
объект вызова (условно: ов_f). В него входят:
- Переменная z получает значение 1.
В строке 12 функция f вызывается еще раз и, конечно, снова возвращает 1 в качестве результата.
Заметим, что после вызова функции f её объект вызова уничтожен, а ещё раньше был уничтожен объект вызова функции g, и в этом нет ничего удивительного: сборщик мусора берется за метлу всякий раз, когда на объект больше нет ссылок.
А теперь давайте посмотрим на следующий код:
01 function f()
02 {
03 var y = 0;
04 function g()
05 {
06 y++;
07 return y;
08 }
09 return g;
10 }
11 var z = f(); // Переменной z присвоена ссылка на функцию g
12 var w = z(); // Вызывается g, переменная w получает значение 1
13 w = z(); // Вызывается g ещё раз, w получает значение 2
13 w = z(); // А теперь w равно 3
Отличие в строках 01–10 только в одном: функция f возвращает не значение функции g, а ссылку на эту функцию!
Это означает, что теперь можно запускать вложенную функцию g, находясь за пределами её родителя f.
Но на что будет ссылаться z в строке 11 (должна на функцию g), если объект вызова функции f будет уничтожен после завершения её работы? Ведь g — локальная переменная функции f, значит, существует только в её объекте вызова?
И вот здесь вступает в игру важное правило:
Если функция f возвращает ссылку на свою внутреннюю функцию g, то после завершения работы функции f, её объект вызова не уничтожается.
Вот как!
То есть в строке 11 переменная z будет ссылаться на функцию g, расположенную в объекте вызова функции f, который продолжает существовать.
В частности, в объекте ов_f продолжает существовать переменная y (со значением 0).
В строке 12 происходит вызов функции g. Этот вызов увеличивает значение переменной y на 1 и возвращает это новое значение.
В строке 1113 функция g вызывается ещё раз. Значение переменной y становится равным 2, и это значение возвращается.
Аналогично, в строке 1113 переменная y в объекте вызова функции f станет равной 3.
Феномен сохранения объекта вызова функции позволяет говорить о лексической области видимости функции в том случае, если она возвращает ссылку на вложенную функцию.
Обычно область видимости функции является динамической — она создаётся во время вызова функции и уничтожается после окончания её работы. Если функция возвращает ссылку на вложенную функцию, сборщик мусора отдыхает: локальные переменные такой функции продолжают существовать. Можно сказать, что область видимости такой функции фиксируется уже на этапе описания, то есть задается лексикой, поэтому, область видимости получается лексической.
Как уже говорилось, замыканием в языках программирования называется комбинация исполняемого кода и области видимости, в которой этот код исполняется.
Все функции образуют замыкания. Но особый интерес представляют замыкания вложенных функций в том случае, когда ссылки на них передаются за пределы объемлющей функции.
Когда говорят о замыканиях, обычно имеют ввиду именно такие функции.
Замыканием (в узком смысле) называют такую вложенную функцию (вместе с объектом вызова объемлющей функции), ссылка на которую передаётся за пределы объемлющей функции. Замыкания работают не в динамической, а в лексической области видимости — после окончания работы объемлющей функции, её объект вызова продолжает своё существование.
Задача. Написать функцию getId, которая генерировала бы идентификатор (каждый раз разный, без повторов) в формате baseчисло:
getId(); // Равно "base0"
getId(); // Равно "base1"
getId(); // Равно "base2"
...
Самое простое решение имеет вид:
var id = 0;
function getId()
{
return "base"+id++;
}
Плохо, что приходится заводить глобальную переменную id. Глобальные переменные — это всегда плохо, их количество нужно сводить к минимуму. Вспомним, что глобальная переменная становится свойством глобального объекта window, а значит, можно перекрыть глобальной переменной какое-нибудь встроенное свойство window, если имена случайно совпадут.
Кроме того, на гипертекстовой странице, кроме нашего кода, могут использоваться сторонние коды, и не хотелось бы случайно пересечься с чужой переменной.
Давайте напишем решение, в котором определим переменную id, как свойство самой функции, ведь функции — это объекты, и ничто не мешает определять в них наши собственные свойства.
getId.id = 0;
function getId()
{
return "base"+getId.id++;
}
Неплохо! Этот код можно записать более элегантно:
getId = function ()
{
if (!arguments.callee.id) arguments.callee.id = 0;
return arguments.callee.id++;
}
Однако, проблема остается, ведь свойство getId.id доступно за пределами функции getId, значит, случайно может быть испорчено, например, установлено в 0, и тогда функция getId начнет генерировать повторяющиеся идентификаторы.
Проблема полностью решается при помощи замыкания:
getId = function () // Внешняя функция, заданная в виде литерала
{
var id = 0; // Локальная переменная не уничтожается
// после завершения работы внешней функции
return function () { return id++;}; // Возвращаем ссылку на
// вложенную функцию,
// заданную в виде литерала
}(); // Вызов внешней функции
Работает это так. Переменной getId присваивается результат вызова функционального литерала (безымянной функции). Результат этого вызова — ссылка на вложенную функцию, в силу чего образуется замыкание, в котором сохраняется переменная id, и эта переменная недоступна за пределами созданной функции getId.
Проверим:
getId(); // Равно "base0"
getId(); // Равно "base1"
getId(); // Равно "base2"
Заметим, вложенная функция, как и внешняя, задана при помощи литерала. Это означает, что вложенная функция создается где то в памяти, а ссылка на нее записывается в переменную getId. Значит, теперь функциональный литерал function(){return id++} приобретает это имя, и к нему можно обращаться как getId().