为什么在函数调用中捕获对象的值?

当你点击这个代码时,这个代码应该会popup一个包含图像编号的警报:

for(var i=0; i<10; i++) { $("#img" + i).click( function () { alert(i); } ); } 

你可以看到它不能在http://jsfiddle.net/upFaJ/工作。 我知道这是因为所有的点击处理程序闭包都是指向同一个对象i ,所以每个处理程序在触发时popup“10”。

但是,当我这样做,它工作正常:

 for(var i=0; i<10; i++) { (function (i2) { $("#img" + i2).click( function () { alert(i2); } ); })(i); } 

你可以在http://jsfiddle.net/v4sSD/看到它的工作。

为什么它工作? 记忆中仍然只有一个对象,对吧? 对象总是通过引用传递,而不是被复制,所以自动执行的函数调用应该没有区别。 这两个代码片段的输出应该是相同的。 那么为什么i对象被复制10次? 为什么它工作?

我认为这个版本不起作用是很有趣的:

 for(var i=0; i<10; i++) { (function () { $("#img" + i).click( function () { alert(i); } ); })(); } 

看来,作为一个函数parameter passing的对象使所有的差异。


编辑:好的,所以前面的例子可以通过基元( i )被值传递给函数调用来解释。 但是这个使用真实对象的例子呢?

 for(var i=0; i<5; i++) { var toggler = $("<img/>", { "src": "http://www.famfamfam.com/lab/icons/silk/icons/cross.png" }); toggler.click(function () { toggler.attr("src", "http://www.famfamfam.com/lab/icons/silk/icons/tick.png"); }); $("#container").append(toggler); } 

不工作: http : //jsfiddle.net/Zpwku/

 for(var i=0; i<5; i++) { var toggler = $("<img/>", { "src": "http://www.famfamfam.com/lab/icons/silk/icons/cross.png" }); (function (t) { t.click(function () { t.attr("src", "http://www.famfamfam.com/lab/icons/silk/icons/tick.png"); }); $("#container").append(t); })(toggler); } 

工作: http : //jsfiddle.net/YLSn6/

大部分的答案都是正确的,因为传递一个对象作为一个函数参数会打破闭包,从而允许我们从循环内分配一些东西给函数。 但是我想指出为什么是这样的情况,这不仅仅是封闭的特例。

你看,JavaScript传递函数的方式与其他语言有些不同。 首先,根据天气情况来看,它有两种方式可以做到原始的价值或者客体。 对于原始值, 似乎是按值传递的,而对于它似乎是按照引用传递的。

javascript如何传递函数参数

实际上,javascript的真正解释解释了这两种情况,以及为什么它打破封闭,只使用一种机制。

JavaScript所做的实际上是通过引用副本传递参数。 也就是说,它会创build另一个参数引用并将新的引用传递给该函数。

按价值传递?

假设javascript中的所有variables都是引用。 在其他语言中,当我们说一个variables是一个引用时,我们期望它的行为如下所示:

 var i = 1; function increment (n) { n = n+1 }; increment(i); // we would expect i to be 2 if i is a reference 

但在JavaScript中,情况并非如此:

 console.log(i); // i is still 1 

这是一个经典的价值传递不是吗?

通过参考传递?

但是等等,对于对象来说,这是一个不同的故事:

 var o = {a:1,b:2} function foo (x) { xc = 3; } foo(o); 

如果参数是按值传递的,我们预计o对象将保持不变,但是:

 console.log(o); // outputs {a:1,b:2,c:3} 

这是经典的传递。 所以我们有两种行为取决于我们传递一个基本types或一个对象的天气。

等等,什么?

但等一下,看看这个:

 var o = {a:1,b:2,c:3} function bar (x) { x = {a:2,b:4,c:6} } bar(o); 

现在看看会发生什么:

 console.log(o); // outputs {a:1,b:2,c:3} 

什么! 这不是通过参考! 值是不变的!

这就是为什么我把它称为通过副本参考 。 如果我们这样思考,一切都是有道理的。 当传入一个函数时,我们不需要将原语视为具有特殊行为,因为对象的行为方式相同。 如果我们尝试修改对象,variables指向那么它就像传递参考一样工作,但如果我们试图修改引用本身,那么它就像传递值一样工作。

这也解释了为什么通过传递一个variables作为函数参数来打破闭包。 因为函数调用会创build另一个不受闭包约束的引用,如原始variables。

结语:我撒谎

还有一件事情在我们结束之前。 我之前说过,这样就统一了原始types和对象的行为。 其实不,原始types还是不一样的:

 var i = 1; function bat (n) { n.hello = 'world' }; bat(i); console.log(i.hello); // undefined, i is unchanged 

我放弃。 这没有任何意义。 这只是它的方式。

这是因为你正在调用一个函数 ,传递一个

 for (var i = 0; i < 10; i++) { alert(i); } 

你期望这提醒不同的价值观,对吧? 因为你正在传递我的当前值alert

 function attachClick(val) { $("#img" + val).click( function () { alert(val); } ); } 

有了这个函数,你会期望它提醒任何val传入它,对不对? 在循环中调用时也是可行的:

 for (var i = 0; i < 10; i++) { attachClick(i); } 

这个:

 for (var i = 0; i < 10; i++) { (function (val) { $("#img" + val).click( function () { alert(val); } ); })(i); } 

只是上面的内联声明。 您正在声明一个匿名函数,它具有与上面的attachClick相同的特征,并立即调用它。 通过函数参数传递的行为会中断对ivariables的引用。

upvoted欺骗的答案,但认为我会尝试一个更简单的解释。 闭包的原因是JavaScript中的variables是函数作用域的 。 闭包创build一个新的作用域,并通过将i的值作为parameter passing,您将在新作用域中定义一个局部variablesi 。 没有closures,所有你定义的点击处理程序都在同一个范围内,使用相同的i 。 原因是你最后的代码片段不起作用是因为没有本地i ,所以所有的点击处理程序正在寻找与i定义最近的父上下文。

我认为可能会让你感到困惑的是这个评论

对象总是通过引用传递,而不是被复制,所以自动执行的函数调用应该没有区别。

这对于对象是正确的,但不是原始值(例如数字)。 这就是为什么一个新的本地i可以被定义。 为了演示,如果你做了一些奇怪的事情,比如将数组中的值包装起来,那么闭包就不起作用,因为数组是通过引用传递的。

 // doesn't work for(var i=[0]; i[0]<10; i[0]++) { (function (i2) { $("#img" + i2[0]).click( function () { alert(i2[0]); } ); })(i); } 

在第一个例子中, i只有一个值,它是for循环中使用的for 。 这样,当for循环结束时,所有事件处理程序都会显示i的值,而不是所需的值。

在第二个示例中,安装事件处理程序时的i值被复制到i2函数参数,并且每个函数调用都有一个单独的副本,因此每个事件处理程序都有一个副本。

所以这:

 (function (i2) { $("#img" + i2).click( function () { alert(i2); } ); })(i); 

创build一个新的variablesi2 ,它对于函数的每个单独的调用都有自己的值。 由于JavaScript中的closures,每个单独的事件处理程序都保留了每个单独的i2副本,从而解决了您的问题。

在第三个例子中,我没有创build新的副本(它们都是从for循环引用同一个i ),所以它和第一个例子一样。

代码1和代码3不起作用,因为i是一个variables,并在每个循环中更改值。 循环10的结尾将被分配给i

为了更清楚,看看这个例子,

 for(var i=0; i<10; i++) { } alert(i) 

http://jsfiddle.net/muthkum/t4Ur5/

你可以看到我在循环之后放了一个alert ,它将显示值为10显示alert框。

这是代码1和代码3发生的情况。

运行下一个示例:

 for(var i=0; i<10; i++) { $("#img" + i).click( function () { alert(i); } ); } i++; 

你会看到现在有11被警告。 因此,你需要通过发送它作为函数参数来避免对i的引用。 您已经find解决scheme。

其他答案没有提到的一件事是为什么我在问题中给出的这个例子不起作用:

 for(var i=0; i<5; i++) { var toggler = $("<img/>", { "src": "http://www.famfamfam.com/lab/icons/silk/icons/cross.png" }); toggler.click(function () { toggler.attr("src", "http://www.famfamfam.com/lab/icons/silk/icons/tick.png"); }); $("#container").append(toggler); } 

回想几个月后,我们对JavaScript有了更好的理解,但是它不起作用的原因可以理解为:

  1. var toggler声明被悬挂在函数调用的顶部。 所有对切换器的引用都是相同的实际标识符。
  2. 匿名函数中引用的闭包与包含的toggler函数相同(不是浅的副本),该toggler将在循环的每次迭代中更新。

#2是相当惊人的。 这提醒“5”例如:

 var o; setTimeout(function () { o = {value: 5}; }, 100); setTimeout(function () { alert(o.value) }, 1000);