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

IT技术 javascript
2021-03-11 20:18:56

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

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

您可以在http://jsfiddle.net/upFaJ/看到它不起作用我知道这是因为所有的点击处理程序闭包都指向同一个对象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/

6个回答

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

你看,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

这是一个经典的value传递,不是吗?

通过引用传递?

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

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

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

@bfavaretto:这实际上是有道理的。
2021-04-17 20:18:56
真的很好的解释!尤其是所有奇怪之处的提炼:“如果我们尝试修改变量指向的对象,那么它的工作方式就像按引用传递,但如果我们尝试修改引用本身,那么它的工作方式就像按值传递。”
2021-04-20 20:18:56
补充一点, deceze 说:当您将原始对象作为对象处理时(在其上调用方法,添加属性等),会在它周围创建一个新的临时对象,并在使用后立即丢弃。这就是为什么即使没有该功能,结语中的代码也无法运行的原因。
2021-04-24 20:18:56
确实不错的解释。关于结语:即使没有功能也行不通。你没有说谎。还是你……?o_o
2021-05-09 20:18:56

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

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

@BrianGordon :参考 jQuery 对象示例...在for循环期间toggler每次迭代都会设置变量,创建 5 个不同的 jQuery 对象。但是,因为每个定义都在相同的函数范围内,所以每个新定义都会覆盖最后一个。添加闭包后,您可以维护对每个唯一对象的引用。
2021-04-19 20:18:56
@BrianGordon:我向我的同事解释的方式是,在 javascript 中,参数总是通过copy传递对于原始类型,它是 value副本,对于对象,它是 reference副本是的,我知道,真正的传值与此不同(而真正的传值又是不同的)。所以,它是通过复制传递的(在这个 javascript 疯狂之前被简单地称为通过值传递)
2021-04-24 20:18:56
此外,在第一个和第二个示例中,警报会提醒正确的值,因为 i 仍然是您在发送警报时期望的值。这不是因为函数调用的特殊值传递属性。
2021-04-26 20:18:56
这几乎和我要说的一样。关键问题是何时i取消引用指针:循环迭代或稍后。
2021-05-01 20:18:56
但这不是表明引用而不是值是通过函数参数传递的吗?function toFive (a) { a = 5; } var b = 4; toFive(b);在那之后,b 是 5。所以值 4 没有传递给 toFive,对象引用是。
2021-05-07 20:18:56

赞成 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);
}

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

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

所以这:

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

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

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

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

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

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

}

alert(i)

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

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

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