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

时间:2012-11-22 05:29:43

标签: javascript

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

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

您可以看到它无效http://jsfiddle.net/upFaJ/。我知道这是因为所有的click-handler闭包都引用了同一个对象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次呢?它为什么有效?

我觉得这个版本doesn't work很有意思:

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/

7 个答案:

答案 0 :(得分:7)

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

你知道,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

我放弃了。对此没有任何意义。这就是它的方式。

答案 1 :(得分:3)

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

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

您希望这会提醒不同的值,对吗?因为您将当前值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相同的特征,您可以立即调用它。通过函数参数传递的行为会破坏对i变量的任何引用。

答案 2 :(得分:2)

upvoted deceze的答案,但我想我会尝试一个更简单的解释。闭包的原因是javascript中的变量是函数作用域。闭包创建一个新范围,并通过将i的值作为参数传递,您将在新范围中定义局部变量i。如果没有闭包,您定义的所有点击处理程序都使用相同的i在同一范围内。您的上一个代码段无法工作的原因是因为没有本地i,因此所有点击处理程序都会查找最近的父上下文i已定义。< / p>

我认为可能令你困惑的另一件事是这条评论

  

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

对于对象,但不是原始值(例如,数字)。这就是为什么可以定义新的本地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);
}

答案 3 :(得分:0)

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

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

所以,这个:

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

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

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

答案 4 :(得分:0)

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

更清楚,请看一下这个例子,

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

}

alert(i)

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

您可以看到我在循环后添加了alert,它会显示值为alert的show 10框。

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

答案 5 :(得分:0)

运行下一个示例:

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

i++;

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

答案 6 :(得分: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的闭包相同(不是浅复制),正在为循环的每次迭代更新。
  3. #2非常令人惊讶。这会警告“5”,例如:

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