Замыкания: построчное объяснение примера "Javascript: Good Parts" - PullRequest
11 голосов
/ 01 июня 2010

Я читаю «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.

Ответы [ 4 ]

20 голосов
/ 01 июня 2010

Я думаю, что это очень распространенный источник путаницы для новичков в 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.

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

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

for (i = 0; i < nodes.length; i += 1) {
   nodes[i].onclick = makeOnClickCallback(i);
}
3 голосов
/ 01 июня 2010

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

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

2 голосов
/ 01 июня 2010

В обоих примерах к любому переданному узлу привязан обработчик события 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 - это то, что вы ожидаете - новая переменная, унаследованная от области действия анонимной функции.

Удачи в разгадке.

0 голосов
/ 01 июня 2010

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

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

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

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

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

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

...