关于闭包:javascript臭名昭著的循环问题?

Javascript infamous Loop issue?

本问题已经有最佳答案,请猛点这里访问。

我有以下代码片段。

1
2
3
4
5
6
7
8
9
10
function addLinks () {
    for (var i=0, link; i<5; i++) {
        link = document.createElement("a");
        link.innerHTML ="Link" + i;
        link.onclick = function () {
            alert(i);
        };
        document.body.appendChild(link);
    }
}

上面的代码用于生成5个链接,并将每个链接与一个警报事件绑定,以显示当前链接ID。但它不起作用。当你点击生成的链接时,它们都会说"链接5"。

但是下面的代码片段是我们的期望。

1
2
3
4
5
6
7
8
9
10
11
12
function addLinks () {
    for (var i=0, link; i<5; i++) {
        link = document.createElement("a");
        link.innerHTML ="Link" + i;
        link.onclick = function (num) {
            return function () {
                alert(num);
            };
        }(i);
        document.body.appendChild(link);
    }
}

以上两个片段都是从这里引用的。正如作者所解释的,似乎闭幕使魔术。

但是它是如何工作的,以及闭包是如何使它工作的,我都无法理解。为什么第一个坏了,第二个坏了?有人能详细解释一下魔法吗?

谢谢。


引用自己对第一个例子的解释:

JavaScript's scopes are function-level, not block-level, and creating a closure just means that the enclosing scope gets added to the lexical environment of the enclosed function.

After the loop terminates, the function-level variable i has the value 5, and that's what the inner function 'sees'.

在第二个示例中,对于每个迭代步骤,外部函数文字将评估一个具有其自身范围和局部变量num的新函数对象,其值设置为i的当前值。由于num从未被修改过,它将在闭包的生命周期中保持不变:下一个迭代步骤不会覆盖旧值,因为函数对象是独立的。

请记住,这种方法非常低效,因为必须为每个链接创建两个新的函数对象。这是不必要的,因为如果使用dom节点进行信息存储,它们很容易共享:

1
2
3
4
5
6
7
8
9
10
11
12
13
function linkListener() {
    alert(this.i);
}

function addLinks () {
    for(var i = 0; i < 5; ++i) {
        var link = document.createElement('a');
        link.appendChild(document.createTextNode('Link ' + i));
        link.i = i;
        link.onclick = linkListener;
        document.body.appendChild(link);
    }
}


我喜欢为胖人写一些简单的解释,因为我很胖,所以这里…

我们在页面上有5个分区,每个分区都有一个ID…第1、第2、第3、第4、第5部分

jquery可以这样做…

1
2
3
for (var i=1; i<=5; i++) {
    $("#div" + i).click ( function() { alert ($(this).index()) } )
}

但真正解决这个问题(并慢慢建立起来)。

步骤1

1
2
3
4
5
for (var i=1; i<=5; i++) {
    $("#div" + i).click (
        // TODO: Write function to handle click event
    )
}

步骤2

1
2
3
4
5
6
7
8
9
10
for (var i=1; i<=5; i++) {
    $("#div" + i).click (
        function(num) {
            // A functions variable values are set WHEN THE FUNCTION IS CALLED!
            // PLEASE UNDERSTAND THIS AND YOU ARE HOME AND DRY (took me 2 years)!
            // Now the click event is expecting a function as a handler so return it
            return function() { alert (num) }
        }(i) // We call the function here, passing in i
    )
}

简单易懂的备选方案

如果你不能理解这一点,那么这应该更容易理解,并具有同样的效果…

1
2
3
4
5
6
7
8
9
10
for (var i=1; i<=5; i++) {

    function clickHandler(num) {    
        $("#div" + i).click (
            function() { alert (num) }
        )
    }
    clickHandler(i);

}

如果您记得函数变量值是在调用函数时设置的,那么这应该很容易理解(但它使用的思想过程与以前完全相同)。


基本上,在第一个示例中,您将onclick处理程序内的i直接绑定到onclick处理程序外的i上。因此,当onclick处理程序外的i发生变化时,onclick处理程序内的i也发生变化。

在第二个示例中,不是将它绑定到onclick处理程序中的num上,而是将它传递到一个函数中,然后该函数将它绑定到onclick处理程序中的num上。当您将它传递给函数时,会复制i的值,而不绑定到num。因此,当i发生变化时,num保持不变。复制是因为javascript中的函数是"闭包",这意味着一旦有东西传递到函数中,它就"关闭"以进行外部修改。


其他人已经解释了正在发生的事情,这里有一个替代的解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
function addLinks () {
  for (var i = 0, link; i < 5; i++) {
    link = document.createElement("a");
    link.innerHTML ="Link" + i;

    with ({ n: i }) {
      link.onclick = function() {
        alert(n);
      };
    }
    document.body.appendChild(link);
  }
}

基本上,可怜的人会放任束缚。


在第一个示例中,只需将此函数绑定到onclick事件:

1
function() {alert(i);};

这意味着在单击事件上,JS应该警告addlink函数i变量的值。由于for循环(),它的值将为5。

在第二个示例中,您生成一个要与另一个函数绑定的函数:

1
2
3
function (num) {
  return function () { alert(num); };
}

这意味着:如果用一个值调用,则返回一个函数来警告输入值。例如,调用function(3)将返回function() { alert(3) };

您在每次迭代时都用值i来调用这个函数,从而为每个链接创建单独的onclick函数。

要点是,在第一个示例中,函数包含变量引用,而在第二个示例中,在外部函数的帮助下,将引用替换为实际值。这称为闭包,大致是因为您将变量的当前值"括"在函数中,而不是保留对它的引用。