Сейчас я прохожу самостоятельное обучение на одном из онлайн-ресурсов по JavaScript. В нем помимо теории есть еще и задачки. Так вот на одной из задач в теме "Область видимости. Замыкания" я столкнулся с полным непониманием решения задачи, которую предлагает автор (свое решение у меня было только одно, и его я рассматривать не буду - ниже задача с кодом и примерами от автора).
Задание:
Следующий код создает массив функций-стрелков shooters. По замыслу, каждый стрелок должен выводить свой номер:
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
Почему все стрелки́ выводят одно и то же? Поправьте код, чтобы стрелки работали как задумано. Предложите несколько вариантов исправления. (Вопрос автора задания)
Предложенные автором варианты решения, которые мне непонятны:
(1) Использовать дополнительную функцию для того, чтобы «поймать» текущее значение 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
Я так и не понял этот вариант решения задания. Что дают в этом варианте эти вторые скобки с i и почему в JavaScript, в отличии от других нормальных языков типа Java x = i хотя имена у параметров разные. Иными словами я не понимаю, как происходит отлавливание i.
(2) Обернуть весь цикл во временную функцию:
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
Этот вариант решения задания еще больше вводит меня в ступор. Где вообще хранится значения i каждого стрелка? Какую роль во всем этом играют вторые скобки с параметром (i)?
Ответ
Проблема с переменной возникает из-за замыкания функции на контекст, то есть на внешнюю переменную i. Это можно легко определить, если вывести доступ к этой переменной во внешний код. Я буду использовать вывод на консоль вместо всплывающего сообщения.
function makeArmy() {
var shooters = [];
for (makeArmy.i = 0; makeArmy.i < 10; makeArmy.i++) {
var shooter = function() {
console.log( makeArmy.i );
};
shooters.push(shooter);
}
return shooters;
}
Теперь наглядна видна зависимость и что функция выводит переменную к которой привязана во время вызова. А к моменту вызова цикл полностью прошел и переменная равна конечному значению 10
var shooters = makeArmy();
a[0](); //выведет 10
makeArmy.i = 20;
a[0](); //выведет 20
Соответственно, для нормального формирования функции надо эту связь разорвать и скопировать текущий номер внутри цикла.
[Решение]
Это можно сделать через дополнительную функцию, которая скопирует значение, я упрощу пример для этого:
function test(){
var i = 0;
result = function(){ console.log(i); };
i = 10;
return result;
}
test()(); //test() возвращает функцию, вторые скобки для мгновенного вызова вернувшейся функции
Нам надо сделать так, что бы переменная не замыкалась, а копировалась. Одним из способов является передать переменную в качестве параметра функции, в таком случае она скопируется, а не замкнется. А внутри мы сформируем нужную нам функцию или объект на эту скопированную переменную.
function test(){
var i = 0;
var makeResult = function(x){
return function(){ console.log(x); };
}
var result = makeResult(i); // вызываем функцию и передаем ей параметр который скопируется внутри и из него сформируется нужная функция.
i = 10;
return result;
}
test()(); //выводит 0
Таким образом мы обошли замыкание на внешний контекст благодаря копированию при вызове с параметром. На самом деле это можно упростить, сразу вызвав функцию makeResult
function test(){
var i = 0;
var result = function(x){
return function(){ console.log(x); };
}(i); //сразу вызываем
i = 10;
return result;
}
test()(); //выводит 0
Тоже самое касается цикла с солдатами, мы сделали промежуточную функцию, которая сформировала нужную и вернула ее по мере увеличения переменной i
function test(){
var results = [];
for(var i = 0; i < 10; i++){
var result = function(x){
return function(){ console.log(x); };
}(i);
results.push(result);
}
return results;
}
test()[5]();
Что в первом, что во втором примерах делается одно и то же - вызывается промежуточная функция с параметром для копирования и разрыва контекста.
Только в одном формируется просто целевая функция, а во втором весь объект целиком.