Мастер-классы по Javascript Екатеринбург Ростов-на-Дону Москва Узнать больше...
Содержание (скрыть) Содержание (показать)

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

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

  1. Почему все стрелки́ выводят одно и то же? Поправьте код, чтобы стрелки работали как задумано.
  2. Кстати, каким будет значение 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*/!*

#39
Наверх

Реклама

Нашли опечатку?

Нашли опечатку на сайте? Что-то кажется странным?
Выделите соответствующий текст и нажмите Ctrl+Enter!

Последние Комментарии

Помоги другим!

Помоги другим узнать о хорошей статье!