en.javascript.info/1-js/5-functions-closures/4-closures-usage/6-make-army/solution.md
2015-07-01 11:40:57 +03:00

6.9 KiB
Raw Blame History

Что происходит в этом коде

Функция makeArmy делает следующее:

  1. Создаёт пустой массив `shooter`:
    var shooters = [];
    
  2. В цикле заполняет массив элементами через `shooter.push`. При этом каждый элемент массива -- это функция, так что в итоге после цикла массив будет таким:
    //+ no-beautify
    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); }
    ];
    

    Этот массив возвращается из функции.

  3. Вызов `army[5]()` -- это получение элемента массива (им будет функция), и тут же -- её запуск.

Почему ошибка

Вначале разберемся, почему все стрелки выводят одно и то же значение.

В функциях-стрелках shooter отсутствует переменная i. Когда такая функция вызывается, то i она берет из внешнего LexicalEnvironment.

Чему же будет равно это значение i?

К моменту вызова army[0](), функция makeArmy уже закончила работу. Цикл завершился, последнее значение было i=10.

В результате все функции shooter получают из внешнего лексического кружения это, одно и то же, последнее, значение i=10.

Попробуйте исправить проблему самостоятельно.

Исправление (3 варианта)

Есть несколько способов исправить ситуацию.

  1. **Первый способ исправить код - это привязать значение непосредственно к функции-стрелку:**
    //+ run
    function makeArmy() {
    
      var shooters = [];
    
      for (var i = 0; i < 10; i++) {
    
    *!*
        var shooter = function me() {
          alert( me.i );
        };
        shooter.i = i;
    */!*
    
        shooters.push(shooter);
      }
    
      return shooters;
    }
    
    var army = makeArmy();
    
    army[0](); // 0
    army[1](); // 1
    

    В этом случае каждая функция хранит в себе свой собственный номер.

    Кстати, обратите внимание на использование Named Function Expression, вот в этом участке:

    ...
    var shooter = function me() {
      alert( me.i );
    };
    ...
    

    Если убрать имя me и оставить обращение через shooter, то работать не будет:

    for (var i = 0; i < 10; i++) {
      var shooter = function() {
    *!*
        alert( shooter.i ); // вывести свой номер (не работает!)
        // потому что откуда функция возьмёт переменную shooter?
        // ..правильно, из внешнего объекта, а там она одна на всех
    */!*
      };
      shooter.i = i;
      shooters.push(shooter);
    }
    

    Вызов alert(shooter.i) при вызове будет искать переменную shooter, а эта переменная меняет значение по ходу цикла, и к моменту вызову она равна последней функции, созданной в цикле.

    Если использовать Named Function Expression, то имя жёстко привязывается к конкретной функции, и поэтому в коде выше me.i возвращает правильный i.

  2. **Другое, более продвинутое решение -- использовать дополнительную функцию для того, чтобы "поймать" текущее значение `i`**:
    //+ run
    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, а понял что это вызов "на месте", и присваивается его результат.

  3. **Еще один забавный способ - обернуть весь цикл во временную функцию**:
    //+ run
    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, а просто обертываем итерацию в функцию.