如何在嵌套外层作用域中取消引用JavaScript变量

8

好的,这里有一个问题脚本。

var links = [ 'one', 'two', 'three' ];

for( var i = 0; i < links.length; i++ ) {
    var a = document.createElement( 'div' );
    a.innerHTML = links[i];
    a.onclick = function() { alert( i ) }
    document.body.appendChild( a );
}

这个脚本使用一个数组生成了三个div:one,two和three。
我在每个div上设置了一个(为了简单起见的Dom0)点击处理程序,它会弹出其在数组中位置的索引 - 但实际上并没有!它总是弹出3,即数组的最后一个索引。
这是因为'alert(i)'中的'i'是对外部作用域(在这种情况下是全局)的实时引用,而在循环结束时其值为3。 它需要一种在循环内部取消引用i的方法。
这是其中的一种解决方案,我倾向于使用它。
var links = [ 'one', 'two', 'three' ];

for( var i = 0; i < links.length; i++ ) {
    var a = document.createElement( 'div' );
    a.innerHTML = links[i];
    a.i = i; //set a property of the current element with the current value of i
    a.onclick = function() { alert( this.i ) }
    document.body.appendChild( a );
}

还有其他人做过不同的事情吗?
有没有一种非常聪明的方法来做这件事?
有人知道图书馆是如何做到这一点的吗?


实际上,在您上面的第一个代码示例中,警报显示3而不是2,因为i增加到3会导致循环停止--我知道这有点挑剔,但只是想提一下!不过,问题很好! - Peter Meyer
4个回答

20

你需要使用这个小闭包技巧——创建并执行一个函数来返回你的事件处理函数。

var links = [ 'one', 'two', 'three' ];

for( var i = 0; i < links.length; i++ ) {
    var a = document.createElement( 'div' );
    a.innerHTML = links[i];
    a.onclick = (function(i) { return function() { alert( i ) } })(i);
    document.body.appendChild( a );
}

应该谨慎使用闭包 - 它们可能会干扰垃圾回收器,特别是当作事件处理程序分配时,因为它们很可能会无限期地存在... - Christoph
3
你可以通过创建一个本地作用域变量来实现相同的功能,而无需传递“i”:(function() { var local = i; return function() { alert(local); } })(); 六分之一和彼此相等,但我更喜欢这种方法,因为否则我往往会忘记将变量作为参数传递。 - Grant Wagner
@Christoph,我想知道你为什么这样认为。利用闭包是JavaScript中的基本工具,如果在元素从DOM中移除时正确地删除元素的事件,我从未见过泄漏的证据。 - Prestaul
@Prestaul:看看你的代码:对于每个div元素,你都创建了两个函数对象,它们各自持有对最外层作用域的引用——即其中的变量都无法被收集! - Christoph
@Christoph ;): 最后一句话只适用于原始的JavaScript实现 - 更好的实现可以检查函数体中的eval(),并且只保留对实际引用的变量的引用;) - Christoph
显示剩余4条评论

4

我建议您继续使用自己的解决方案,但可以按以下方式进行修改:

var links = [ 'one', 'two', 'three' ];

function handler() {
    alert( this.i );
}

for( var i = 0; i < links.length; i++ ) {
    var a = document.createElement( 'div' );
    a.innerHTML = links[i];
    a.i = i; //set a property of the current element with the current value of i
    a.onclick = handler;
    document.body.appendChild( a );
}

这样做,仅会创建一个函数对象 - 否则,函数文字将在每个迭代步骤上被评估!

通过闭包的解决方案在性能方面甚至比您的原始代码更糟糕。


为什么要污染全局命名空间,当函数可以声明为内联呢?这样做有什么好处? - Prestaul
@Prestaul:对象越少,内存越少,速度越快。将其放入函数中,全局命名空间将不会被处理程序或链接数组污染。 - some

1

我推荐使用Christoph的方法只使用一个函数,因为它使用更少的资源。

下面是另一种方式,它将值存储在函数上(这是可能的,因为函数是一个对象),并使用argument.callee来获取函数内部的函数引用。在这种情况下,它没有太多意义,但我展示这种技术,因为它在其他方面可能会有用:

var links = [ 'one', 'two', 'three' ];

for( var i = 0; i < links.length; i++ ) {
    var a = document.createElement( 'div' );
    a.innerHTML = links[i];
    a.onclick = function() { alert( arguments.callee.i ) }
    a.onclick.i = i;
    document.body.appendChild( a );
}

当您的函数需要在调用之间存储持久信息时,该技术非常有用。请使用以下内容替换上面的部分:
a.id="div"+i;
a.onclick = function() {
    var me = arguments.callee;
    me.count=(me.count|0) + 1;
    alert( me.i );
}

而且您稍后可以检索它被调用的次数:

for( var i = 0; i < links.length; i++ ){
    alert(document.getElementById("div"+i).onclick.count);
}

它还可以用于在调用之间缓存信息。


0

RoBorg的方法绝对是可行的,但我喜欢略微不同的语法。这两种方法都能实现创建保留'i'的闭包,只是这种语法对我来说更清晰,需要修改现有代码的部分较少:

var links = ['one', 'two', 'three'];

for( var i = 0; i < links.length; i++ ) (function(i) {
    var a = document.createElement( 'div' );
    a.innerHTML = links[i];
    a.onclick = function() { alert( i ) }
    document.body.appendChild( a );
})(i);

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