Закрытие: объяснение "Линия за строкой" примера "Javascript: Good Parts"?

Я читаю "Javascript: The Good Parts" и полностью озадачен тем, что действительно происходит здесь. Более детальное и/или упрощенное объяснение будет с благодарностью.

// BAD EXAMPLE

// Make a function that assigns event handler functions to an array  of nodes the wrong way.
// When you click on a node, an alert box is supposed to display the ordinal of the node.
// But it always displays the number of nodes instead.

var add_the_handlers = function (nodes) {
    var i;
    for (i = 0; i < nodes.length; i += 1) {
        nodes[i].onclick = function (e) {
            alert(i);
        }
    }
};

// END BAD EXAMPLE

Функция add_the_handlers должна была дать каждому обработчику уникальное число (i). Он терпит неудачу, потому что функции обработчика привязаны к переменной i, а не к значению переменной i во время выполнения функции:

// BETTER EXAMPLE

// Make a function that assigns event handler functions to an array of nodes the right way.
// When you click on a node, an alert box will display the ordinal of the node.

var add_the_handlers = function (nodes) {
    var i;
    for (i = 0; i < nodes.length; i += 1) {
        nodes[i].onclick = function (i) {
            return function (e) {
                alert(i);
            };
        }(i);
    }
};

Теперь, вместо назначения функции onclick, мы определяем функцию и сразу вызываем ее, передавая в i. Эта функция вернет функцию обработчика событий, которая привязана к значению i, которое было передано, а не к i, определенному в add_the_handlers. Возвращаемой функции присваивается onclick.

Ответ 1

Я думаю, что это очень распространенный источник путаницы для новичков в JavaScript. Сначала я бы предложил проверить следующую статью Mozilla Dev для краткого введения в тему закрытия и лексического охвата:

Начните с плохого:

var add_the_handlers = function (nodes) {
// Variable i is declared in the local scope of the add_the_handlers() 
// function. 
   var i;

// Nothing special here. A normal for loop.
   for (i = 0; i < nodes.length; i += 1) {

// Now we are going to assign an anonymous function to the onclick property.
       nodes[i].onclick = function (e) {

// The problem here is that this anonymous function has become a closure. It 
// will be sharing the same local variable environment as the add_the_handlers()
// function. Therefore when the callback is called, the i variable will contain 
// the last value it had when add_the_handlers() last returned. 
           alert(i);
       }
   }

// The for loop ends, and i === nodes.length. The add_the_handlers() maintains
// the value of i even after it returns. This is why when the callback
// function is invoked, it will always alert the value of nodes.length.
};

Мы можем решить эту проблему с большим количеством замыканий, как предложил Крокфорд в "хорошем примере". Закрытие - это особый вид объекта, который объединяет две вещи: функцию и среду, в которой была создана эта функция. В JavaScript среда закрытия состоит из любых локальных переменных, которые были в области видимости в момент создания замыкания:

 // Now we are creating an anonymous closure that creates its own local 
 // environment. I renamed the parameter variable x to make it more clear.
 nodes[i].onclick = function (x) {

     // Variable x will be initialized when this function is called.

     // Return the event callback function.
     return function (e) {
         // We use the local variable from the closure environment, and not the 
         // one held in the scope of the outer function add_the_handlers().
         alert(x);
     };
 }(i); // We invoke the function immediately to initialize its internal 
       // environment that will be captured in the closure, and to receive
       // the callback function which we need to assign to the onclick.

Вместо того, чтобы иметь обратные вызовы для общего доступа к единой среде, функция закрытия создает новую среду для каждого из них. Мы могли бы также использовать функцию factory для создания замыкания, как в следующем примере:

function makeOnClickCallback (x) {
   return function (e) {
      alert(x);
   };
}

for (i = 0; i < nodes.length; i += 1) {
   nodes[i].onclick = makeOnClickCallback(i);
}

Ответ 2

Все о закрытии. В первом примере "i" будет равно "nodes.length" для каждого обработчика события клика, потому что он использует "i" из цикла, который создает обработчики событий. К тому времени, когда вызывается обработчик событий, цикл завершится, поэтому "i" будет равно "nodes.length".

Во втором примере "i" - это параметр (поэтому локальная переменная). Обработчики событий будут использовать значение локальной переменной "i" (параметр).

Ответ 3

В обоих примерах любой переданный node имеет связанный с ним обработчик события onclick (точно так же, как <img src="..." onclick="myhandler()"/>, что является плохой практикой).

Различие заключается в том, что в плохом примере каждое замыкание (функции обработчика событий, то есть) ссылается на ту же самую переменную i из-за их общей родительской области.

В хорошем примере используется анонимная функция, которая сразу выполняется. Эта анонимная функция ссылается на ту же самую переменную i, что и в плохом примере, но поскольку она выполнена и снабжена i в качестве своего первого параметра, значение i присваивается локальной переменной, называемой... eh?... i, в точности - таким образом, переписывая ту, которая определена в родительской области.

Перепишите хороший пример, чтобы все было ясно:

var add_the_handlers = function (nodes) {
    var i;
    for (i = 0; i < nodes.length; i += 1) {
        nodes[i].onclick = function (newvar) {
            return function (e) {
                alert(nevar);
            };
        }(i);
    }
};

Здесь мы заменили i на возвращаемую функцию обработчика событий с помощью newvar, и она по-прежнему работает, потому что newvar - это именно то, что вы ожидаете - новая переменная, унаследованная от области анонимной функции.

Удачи в этом.

Ответ 4

Это связано с закрытием.

Когда вы делаете что-то в плохом примере,

когда вы нажимаете каждый node, вы получите самую последнюю ценность я (т.е. у вас есть 3 узла, независимо от того, какой node вы нажмете, вы получите 2). так как ваше предупреждение (i) привязано к ссылке переменной i, а не значению я в момент, когда он был связан в обработчике событий.

Сделав это лучшим примером, вы связали его с тем, что я на момент, когда он был повторен, поэтому нажатие на node 1 даст вам 0, node 2 даст вам 1 и node 3 даст вам 2.

в основном, вы оцениваете то, что я немедленно, когда оно вызывается в строке} (i), и оно передается параметру e, который теперь содержит значение того, что я в данный момент времени.

Btw... Я думаю, что в верхней части примера есть опечатка... она должна быть предупреждена (e) вместо предупреждения (i).