Вернуться к уроку

Армия функций

важность: 5

Следующий код создает массив функций-стрелков 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

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

Открыть песочницу с тестами для задачи.

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

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

  1. Создаёт пустой массив shooters:

    var shooters = [];
  2. В цикле заполняет массив элементами через shooters.push. При этом каждый элемент массива – это функция, так что в итоге после цикла массив будет таким:

    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. Первый способ исправить код – это привязать значение непосредственно к функции-стрелку:

    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:

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

    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, а просто обертываем итерацию в функцию.

Открыть решение с тестами в песочнице.