5

此代码应该在您单击它时弹出带有图像编号的警报:

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

您可以在http://jsfiddle.net/upFaJ/看到它不起作用。我知道这是因为所有的 click-handler 闭包都引用同一个 object i,所以每个处理程序在触发时都会弹出“10”。

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

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

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

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

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

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

似乎将对象作为函数参数传递会产生重大影响。


编辑:好的,所以前面的示例可以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/

4

7 回答 7

8

大多数答案都是正确的,因为将对象作为函数参数传递会破坏闭包,从而允许我们从循环中将事物分配给函数。但我想指出为什么会这样,而且这不仅仅是闭包的特例。

你看,javascript 将参数传递给函数的方式与其他语言有点不同。首先,根据天气,它似乎有两种方法,它是原始值还是对象。对于原始值,它似乎通过值传递,而对于对象,它似乎通过引用传递。

javascript 如何传递函数参数

实际上,对 javascript 所做的真正解释解释了这两种情况,以及它为什么会破坏闭包,只使用一种机制。

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

通过价值传递?

假设 javascript 中的所有变量都是引用。在其他语言中,当我们说变量是引用时,我们希望它的行为如下:

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) {
    x.c = 3;
}
foo(o);

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

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

那是经典的引用传递。所以我们有两种行为,取决于我们传递原始类型或对象的天气。

等等,什么?

但是等一下,看看这个:

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}

什么!这不是通过引用传递!数值不变!

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

这也解释了为什么通过将变量作为函数参数传递会破坏闭包。因为函数调用会创建另一个像原始变量一样不受闭包约束的引用。

后记:我撒谎了

在我们结束之前还有一件事。我之前说过,这统一了原始类型和对象的行为。实际上不,原始类型仍然不同:

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

我放弃。这没有任何意义。就是这样。

于 2012-11-22T08:37:53.207 回答
3

这是因为您正在调用一个函数,并为其传递一个

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

您希望这会提醒不同的值,对吗?因为您正在传递to的当前值ialert

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并立即调用它。通过函数参数传递的行为会破坏对变量的任何引用。i

于 2012-11-22T05:43:48.450 回答
2

赞成 deceze 的回答,但我想我会尝试一个更简单的解释。闭包起作用的原因是 javascript 中的变量是函数作用域的。闭包创建了一个新作用域,通过将iin 的值作为参数传递,您i在新作用域中定义了一个局部变量。如果没有闭包,您定义的所有点击处理程序都在同一范围内,使用相同的i. 您的最后一个代码片段不起作用的原因是因为没有 local 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);
}
于 2012-11-22T05:59:35.403 回答
0

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

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

所以这:

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

为函数的每次单独调用创建一个新变量i2,该变量具有自己的值。i2由于 javascript 中的闭包,每个单独的事件处理程序都会保留每个单独的副本- 从而解决您的问题。

在第三个示例中,没有创建新副本(它们都在循环i中引用相同i的内容),因此它的工作方式与第一个示例相同。for

于 2012-11-22T05:43:05.680 回答
0

代码 1 和代码 3 不起作用,因为i它是一个变量,并且在每个循环中都会更改值。在循环结束时10将分配给i.

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

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

}

alert(i)

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

你可以看到我alert在循环之后放了一个,它会显示带有 value 的显示alert10

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

于 2012-11-22T05:43:56.943 回答
0

运行下一个示例:

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

i++;

您现在会看到,11正在收到警报。因此,您需要i通过将其作为函数参数发送它的值来避免对 的引用。您已经找到了解决方案。

于 2012-11-22T05:47:09.377 回答
0

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

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声明被提升到函数调用的顶部。所有对的引用toggler都指向相同的实际标识符。
  2. 匿名函数中引用的闭包与包含 的闭包相同(不是浅拷贝),toggler循环的每次迭代都会更新闭包。

#2 相当令人惊讶。这会提醒“5”,例如:

var o;
setTimeout(function () { o = {value: 5}; }, 100);
setTimeout(function () { alert(o.value) }, 1000);
于 2013-08-07T02:33:16.347 回答