Страницы

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

четверг, 8 ноября 2018 г.

Принцип работы вложенных функций или “непонятное замыкание” в 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)?


Ответ

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

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

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