Страницы

Поиск по вопросам

суббота, 14 декабря 2019 г.

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

#javascript #функции #замыкания


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


Ответы

Ответ 1



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

Ответ 2



Проблема возникает вот здесь: for (var i = 0; i < 10; i++) { var shooter = function() { alert( i ); }; shooters.push(shooter); } А именно при вызовах: army[0](); army[5](); Первое решение простое, ведь при вызове функция возвращала конечное значение цикла(10). Оборачивая в функцию, мы делаем так, чтобы значение i записывалось и возвращалось каждый раз. А обернув во временную функцию, функция будет выполнятся i раз.

Комментариев нет:

Отправить комментарий