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