Армия функций
Следующий код создаёт массив из стрелков (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, и другие сценарии, где такие проблемы реальны.