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