闭包: "JavaScript: Good Parts" 的示例逐行解释?

11

我正在阅读《JavaScript语言精粹》,但对于这里到底发生了什么感到非常困惑。如果有更详细和/或简化的解释,那将不胜感激。

// 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 值,而不是绑定到在 add_the_handlers 中定义的 i。返回的函数被分配给 onclick。


请查看以下标记的问题:http://stackoverflow.com/questions/tagged/javascript+closures+loops - Christian C. Salvadó
1
你也可以在实时演示中进行尝试 http://jsbin.com/sezisalulede/1/edit?html,js,output - Adriano
4个回答

20

我认为这是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.
};

我们可以像Crockford在"好的例子"中建议的那样,使用更多的闭包来解决这个问题。闭包是一种特殊的对象,它将两个东西组合在一起:一个函数和创建该函数的环境。在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);
}

一个有点相关的问题。在 function(e) 中,"e" 是什么意思?它可以被任何变量替换吗?我曾经认为它代表事件(event),但现在我感到困惑了。 - Matrym
@Matrym:是的,应该有一个参数,当onclick事件被触发时,浏览器会将其传递给回调函数。请查看这篇quirksmode文章,了解不同浏览器中如何处理此问题。 - Daniel Vassallo
如果我们不使用变量会发生什么?我们是否需要传递以便可以链接其他内容? - Matrym
@Matrym:你指的是“e”参数吗?除非你需要访问事件对象,否则不需要该参数。你甚至可以不定义它: return function () { alert(x); }; - Daniel Vassallo
关于GOOD示例的实时演示,可以使用这个小工具 http://jsbin.com/qoyuvecucuju/1/edit?js,output,而BETTER示例则可以使用这个 http://jsbin.com/setaxegehote/1/edit?js,output。 - Adriano
没有什么比逐行详细解释更好,我的疑虑都消除了。 如果我能做到,加100分。 - Alejandro Veltri

3

这涉及到闭包的概念。在第一个例子中,对于每个点击事件处理程序,“i”将等于“nodes.length”,因为它使用了循环中创建事件处理程序的“i”。当事件处理程序被调用时,循环已经结束,所以“i”将等于“nodes.length”。

在第二个例子中,“i”是一个参数(因此是局部变量)。事件处理程序将使用局部变量“i”(即参数)的值。


2
在这两个例子中,传递的每个节点都有一个onclick事件处理程序绑定到它上面(就像<img src="..." onclick="myhandler()"/>一样,不过这种方式并不好)。
不同之处在于,在不好的例子中,由于共同的父范围,每个闭包(即事件处理函数)都引用了完全相同的i变量。
好的例子使用了立即执行的匿名函数。这个匿名函数引用了和不好的例子中完全相同的i变量,但是由于它被执行并提供了i作为其第一个参数,i的值被分配给了一个称为……嗯……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正是你期望的——从匿名函数的作用域继承的新变量。

祝你好运!


"alert(nevar)"中有一个打字错误,正确应该是 "alert(newvar)"。 - Reza

0

这与闭包有关。

当你在坏的例子中执行操作时,

当你点击每个节点时,你将得到最新的i值(即使你有3个节点,无论你点击哪个节点,你都会得到2)。因为你的alert(i)绑定到变量i的引用而不是事件处理程序绑定时i的值。

以更好的方式执行它,你将它绑定到迭代时i的值,所以点击节点1将给你0,节点2将给你1,节点3将给你2。

基本上,你正在评估在}(i)行调用时i的值,并将其传递给参数e,现在e保存了此刻i的值。

顺便说一下...我认为更好的例子部分有一个错别字...应该是alert(e)而不是alert(i)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接