Принцип работы вложенных функций или “непонятное замыкание” в JavaScript

IngeniousTom

Сейчас я прохожу самостоятельное обучение на одном из онлайн-ресурсов по 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)?

2 ответа

IngeniousTom

Проблема с переменной возникает из-за замыкания функции на контекст, то есть на внешнюю переменную 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]();

Что в первом, что во втором примерах делается одно и то же - вызывается промежуточная функция с параметром для копирования и разрыва контекста. Только в одном формируется просто целевая функция, а во втором весь объект целиком.


IngeniousTom

Проблема возникает вот здесь:

for (var i = 0; i < 10; i++) {
    var shooter = function() {
      alert( i );
    };
    shooters.push(shooter);
}

А именно при вызовах:

army[0]();
army[5]();

Первое решение простое, ведь при вызове функция возвращала конечное значение цикла(10). Оборачивая в функцию, мы делаем так, чтобы значение i записывалось и возвращалось каждый раз.

А обернув во временную функцию, функция будет выполнятся i раз.

licensed under cc by-sa 3.0 with attribution.