Следующий код создает массив функций-стрелков shooter. По замыслу, каждый стрелок должен выводить свой номер:
function makeArmy() {
var shooters = [];
for(var i=0; i<10; i++) {
var shooter = function() { // функция-стрелок
alert(i); // выводит свой номер
};
shooters.push(shooter);
}
return shooters;
}
var army = makeArmy();
army[0](); // стрелок выводит 10, а должен 0
army[5](); // стрелок выводит 10...
// .. все стрелки выводят 10 вместо 0,1,2...9
- Почему все стрелки́ выводят одно и то же? Поправьте код, чтобы стрелки работали как задумано.
- Кстати, каким будет значение
thisв функции-стрелкеshooter? В примере оно не используется, но все же..?
Вначале разберемся, почему все стрелки выводят одно и то же значение.
В функциях-стрелках shooter отсутствует переменная i. Когда такая функция вызывается, то i она берет из внешнего LexicalEnvironment…
Каким будет значение i?
К моменту вызова army[0](), функция makeArmy уже закончила работу. Цикл завершился, последнее значение было i=10.
В результате все функции shooter получают одно и то же, последнее, значение i=10.
Попробуйте исправить проблему самостоятельно.
Есть несколько способов исправить ситуацию.
Первый способ исправить код - это привязать значение непосредственно к функции-стрелку:
function makeArmy() {
var shooters = [];
for(var i=0; i<10; i++) {
*!*
var shooter = function() {
alert( arguments.callee.i );
};
shooter.i = i;
*/!*
shooters.push(shooter);
}
return shooters;
}
var army = makeArmy();
army[0](); // 0
army[1](); // 1
В этом случае каждая функция хранит в себе свой собственный номер.
Кстати, вот такой вариант работать не будет:
for(var i=0; i<10; i++) {
var shooter = function() {
*!*
alert(shooter.i); // вывести свой номер (не работает!)
// правильный вариант: alert(arguments.callee.i);
*/!*
};
shooter.i = i;
shooters.push(shooter);
}
В этом коде будет всегда выводиться 10, так как alert(shooter.i) при вызове будет искать переменную shooter, а эта переменная меняет значение по ходу цикла, и к моменту вызову она равна последней функции, созданной в цикле.
Именно поэтому нужно обращаться через arguments.callee.
Другое, более продвинутое решение —- использовать дополнительную функцию для того, чтобы «поймать» текущее значение i:
function makeArmy() {
var shooters = [];
for(var i=0; i<10; i++) {
*!*
var shooter = (function(x) {
return function() {
alert( x );
};
})(i);
*/!*
shooters.push(shooter);
}
return shooters;
}
var army = makeArmy();
army[0](); // 0
army[1](); // 1
Посмотрим выделенный фрагмент более внимательно, чтобы понять, что происходит:
var shooter = (function(x) {
return function() {
alert( x );
};
})(i);
Функция shooter создана как результат вызова промежуточного функционального выражения function(x), которое объявляется — и тут же выполняется, получая x = i.
Так как function(x) тут же завершается, то значение x больше не меняется. Оно и будет использовано в возвращаемой функции-стрелке.
Для красоты можно изменить название переменной x на i, суть происходящего при этом не изменится:
var shooter = (function(i) {
return function() {
alert( i );
};
})(i);
Кстати, обратите внимание — скобки вокруг function(i) не нужны, можно и так:
var shooter = function(i) { // *!*без скобок вокруг function(i)*/!*
return function() {
alert( i );
};
}(i);
Скобки добавлены в код для лучшей читаемости, чтобы человек, который просматривает его, не подумал, что var shooter = function, а понял что это вызов «на месте», и присваивается его результат.
Еще один забавный способ - обернуть весь цикл во временную функцию:
function makeArmy() {
var shooters = [];
*!*
for(var i=0; i<10; i++) (function(i) {
var shooter = function() {
alert( i );
};
shooters.push(shooter);
})(i);
*/!*
return shooters;
}
var army = makeArmy();
army[0](); // 0
army[1](); // 1
Вызов (function(i) { ... }) обернут в скобки, чтобы интерпретатор понял, что это Function Expression.
Плюс этого способа - в большей читаемости. Фактически, мы не меняем создание shooter, а просто обертываем итерацию в функцию.
Значение this будет равно объекту army, так как вызов функции идет в объектном синтаксисе, через квадратные скобки. Сравните:
user.sayHi() // this = user user['sayHi']() // this = user army[0]() // *!*this = army*/!*