Я думал, что понял взаимодействие замыканий и объектов-лексических окружений в языке JavaScript. Однако, при решении задачи «Армия функций» в подразделе 6.3 «Замыкание» оказалось, что понял не до конца и местами неправильно.
В указанной задаче дан следующий кусок кода:
function makeArmy() { let shooters = []; let i = 0; while (i < 10) { let shooter = function() { // функция shooter должна выводить alert( i ); // порядковый номер стрелка }; shooters.push(shooter); i++; } return shooters; } let army = makeArmy(); army[0](); // выводит 10, а должна выводить 0 army[5](); // выводит 10, а должна выводить 5 // ... у всех стрелков будет номер 10 вместо 0, 1, 2, 3...
Во-первых, требуется объяснить, почему код не работает так, как планировалось. Во-вторых, нужно исправить этот код, чтобы он начал работать так, как планировалось.
Минут за десять я интуитивно сообразил, как исправить код (и мой вариант решения совпал со вторым из двух вариантов решения, предложенных авторами учебника). Однако, я не мог самому себе четко объяснить, почему исправленный код начал работать. Я решил перечитать текст подраздела в четвертый раз и только тогда картинка сложилась. Как оказалось, в первые три прочтения я пропустил неосмысленным следующий текст, цитата:
Для цикла у каждой итерации своё отдельное лексическое окружение.
Думаю, эту фразу авторам учебника стоит выделить жирным.
Как работает обычный цикл (не содержащий вложенных функций) с объектами-лексическими окружениями?
На первой итерации для внутреннего содержимого (ограниченного фигурными скобками
{}
) цикла создается объект-лексическое окружение с хранилищем локальных для цикла переменных и ссылкой на внешний объект-лексическое окружение. На следующей итерации, как я понимаю, локальные переменные из объекта-лексического окружения предыдущей итерации копируются в хранилище нового объекта-лексического окружения, созданного для текущей итерации цикла. И так далее.Как только цикл перешел на очередную итерацию, в программе не остается ссылок на объект-лексическое окружение прошлой итерации, то есть он (объект-лексическое окружение прошлой итерации) становится недостижимым и поэтому автоматически уничтожается сборщиком мусора.
Таким образом, при работе обычного цикла (не содержащего вложенных функций) в памяти большую часть времени присутствует для цикла лишь один объект-лексическое окружение, относящийся к текущей итерации цикла. После окончания работы цикла все объекты-лексические окружения, которые были созданы для итераций цикла, оказываются уничтоженными сборщиком мусора.
Как работает цикл, содержащий вложенную функцию, из приведенного выше кода?
В нем точно так же, как и в обычном цикле, для каждой итерации создается новый объект-лексическое окружение (локальные переменные цикла, как я понимаю, копируются из объекта в объект на протяжении цикла).
Однако, из-за того, что ссылка на объект-лексическое окружение каждой итерации записывается в скрытое свойство
Environment
вложенной в цикл функции, после окончания каждой итерации ее объект-лексическое окружение остается достижимым из программы и поэтому не унитожается сборщиком мусора.Таким образом, при работе вышеприведенного кода в памяти остаются все объекты-лексические окружения всех итераций цикла. Они остаются в памяти и после окончания цикла.
Итак, функция
makeArmy
после своего запуска возвращает в переменную army
массив из десяти функций shooter
(по-русски «стрелок»), каждая из которых имеет в своем скрытом свойстве Environment
ссылку на объект-лексическое окружение соответствующей итерации цикла в функции makeArmy
. У меня получилась такая иллюстрация:
Как видно на рисунке, после окончания работы функции
makeArmy
в памяти будет десять объектов-лексических окружений соответствующих итераций цикла, один объект-лексическое окружение функции makeArmy
(потому что эта функция запускалась только один раз) и один объект-глобальное лексическое окружение. (Я не стал рисовать все десять элементов массива army
и все десять объектов-лексических окружений итераций цикла. Нарисовал только несколько первых и последние элементы массива и объекты-лексические окружения итераций цикла. Кроме этого, я не стал приписывать какое-то определенное значение переменной i
, ограничившись вместо этого многоточием. Перед запуском цикла там хранится значение 0
, затем другие значения до 10
включительно.)Из иллюстрации можно понять, наконец, почему вышеприведенный код не работает, как планировалось. Так как переменная
i
объявлена вне цикла, то ее значение хранится в объекте-лексическом окружении, предназначенном для локальных переменных функции makeArmy
. При запуске функций shooter
из массива army
каждая функция ищет значение переменной i
в цепочке объектов-лексических окружений и находит ее только в лексическом окружении, в котором хранятся локальные переменные функции makeArmy
. А так как к моменту вызова любой функции shooter
из массива цикл уже закончился (работа функции makeArmy
, в которой содержится цикл, ведь, уже закончилась), то переменная i
содержит значение 10
. Это значение и будет выдаваться при запуске любой из функций shooter
, хранящихся в массиве army
.Чтобы заставить код работать так, как планировалось, я добавил внутрь цикла локальную для цикла переменную
j
и присвоил ей значение внешней для цикла переменной i
. Кроме этого, я заменил в тексте функции shooter
переменную i
на переменную j
(исправления отмечены красным шрифтом):function makeArmy() { let shooters = []; let i = 0; while (i < 10) { let j = i; let shooter = function() { // функция shooter должна выводить alert( j ); // порядковый номер стрелка }; shooters.push(shooter); i++; } return shooters; } let army = makeArmy(); army[0](); // выводит 0 army[5](); // выводит 5
Вот как теперь будет выглядеть структура объектов-лексических окружений:

Другой вариант решения, предложенный авторами учебника, состоит в том, чтобы заменить цикл
while
на цикл for
следующим образом:function makeArmy() { let shooters = []; for(let i = 0; i < 10; i++) { let shooter = function() { // функция shooter должна выводить alert( i ); // порядковый номер стрелка }; shooters.push(shooter); } return shooters; } let army = makeArmy(); army[0](); // выводит 0 army[5](); // выводит 5
То есть переменная
i
переместилась внутрь цикла и стала локальной для цикла.В этом случае структура объектов-лексических окружений будет выглядеть так:
