Javascript闭包异常

3
这个脚本的输出结果是:[[1, 2, 3, 4], 10] [[91, 92, 93, 94], 10] [[8888, 8888, 8888, 8888], 10] [['one', 'two', 'three', 'four'], 10]。但是,如果我取消注释hogs = [2, 4, 6, 8],输出结果是:[[1, 2, 3, 4], 10] [[1, 2, 3, 4], 10] [[1, 2, 3, 4], 10] [[1, 2, 3, 4], 10]。如果我把它注释掉,但是取消注释hogs = [333,444,555,666],输出结果是:[[1, 2, 3, 4], 10] [[91, 92, 93, 94], 10] [[8888, 8888, 8888, 8888], 10] [[8888, 8888, 8888, 8888], 10]。我想要充分理解简单的JavaScript闭包,以便能够预测它们的行为。除了分析引擎规范之外,这是否可能?我还想知道为什么用一个语句重新定义整个数组与逐个重新赋值数组的元素相比,具有如此不同的副作用。我害怕过于创新,因为我不知道如何预测副作用,而且我在文献中找不到线索。
var x = 10;
var hogs = [1, 2, 3, 4];
var array5 = (function () {
    var ar = [];
    var z = x;
    var w = hogs;
    function g() {
        ar.push(w);
        ar.push(z);
        return ar;
    } return g;
})()();

console.log(array5);

//hogs = [2, 4, 6, 8];  Uncommenting this prevents array5 from changing.     
x = 40;
hogs[0] = 91;
hogs[1] = 92;
hogs[2] = 93;
hogs[3] = 94;
console.log(array5);

hogs[0] = 8888;
hogs[1] = 8888;The output from this script is:
hogs[2] = 8888;
hogs[3] = 8888;
console.log(array5);

// hogs = [333, 444, 555, 666]; Un-commenting this prevents.. 

hogs[0] = 'one';
hogs[1] = 'two';
hogs[2] = 'three';
hogs[3] = 'four';
x = 40;
console.log(array5);

我认为你并没有重复,当你写下“你不是在说‘告诉变量w引用hogs引用的任何对象’。你所说的是‘告诉变量w引用hogs当前时刻引用的任何对象’”时,你优雅地将整个事情概括了。你很好心地花时间详细解释了这一点。也许你同情那些简单地想象变量值搜索沿着作用域链向外进行直到必要时到达Object对象的语言学习者,而在这种情况下,封闭函数中的值被忽略,直到全局范围中的值被新定义所覆盖。知道改变数组中的值可以保持数组本身不变,但具有一些新的或更改的值分配;这与重新定义数组,使变量指向完全不同的东西非常非常不同。我脑海中浮现出一些静止的内存中的对象,而其他对象则消失并在内存中的其他位置重新出现;这与“该睡觉了”的押韵。对于可能对此感兴趣的其他人来说,所有对象似乎存在类似的情况,如下面的代码所示:
enter code here
var x = 10;
var hogs = {'a': 1, 'b': 2, 'c' : 3, 'd' : 4};
var obj = {};
obj.h = hogs;

var ob5 = (function (arg) {
    function g() {
        var w = arg;
        return w;
    } return g;
})(hogs)();

console.log(ob5);

//hogs = [2, 4, 6, 8];  Uncommenting this prevents array5 from  changing.
x = 40;
hogs['a'] = 91;
hogs['b'] = 92;
hogs['c'] = 93;
hogs['d'] = 94;
console.log(ob5);

hogs.a = 8888;
hogs.b = 8888;
hogs.c = 8888;
hogs.d = 8888;
console.log(ob5);

hogs = {'a':333, 'b':444, 'c':555, 'd':666}; // Prevents further changes.

hogs.a = 'one';
hogs.b = 'two';
hogs.c = 'three';
hogs.d = 'four';
x = 40;
console.log(ob5);

4
我认为这与闭包并没有太大关系。 - Pointy
2个回答

3
闭包返回应用时(被调用时)hogs当前值。如果您稍后更新hogs指向一个新数组,那么调用闭包的结果array5仍然引用旧副本。您的更新仅反映在hogs当前副本中,在您更改它的任何地方都是您的更新将由array5反映的终点。

您可以再次应用闭包,以便获得反映hogs当前值的新值,但您必须给它命名(将其放入变量中)才能再次引用它。


这就是我想到的,也是我预测的。但是除非我包括(解除注释)行:hogs = [2, 4, 6, 8],否则我得不到预测的行为。那会冻结array5在“hogs = [2, 4, 6, 8]”之前的值。否则,array5表现得像对hogs当前值的引用。我不知道该行为如何预测,事后我也无法解释。这对您有意义吗?

是的,这就是您应该期望的行为。数组是可变对象,因此在一个地方更改数组将在您更改它的任何其他地方反映出来。你所做的实际上与这个差不多:

var x = [1, 2, 3];
var y = x;
var x[0] = 4;
// What would you expect the value of y to be here?

如果您真的想要一个数组的副本,那么您应该使用 slice() 方法进行显式复制:

var array5 = (function () {
    var ar = [];
    var z = x;
    var w = hogs.slice(); // <== Explicit copy!
    function g() {
        ar.push(w);
        ar.push(z);
        return ar;
    } return g;
})()();

非常接近,但我认为你的意思是闭包“使用”当前的 hogs 值,而不是返回。不过你基本上是正确的。 - Adam Jenkins
我可以获取array5的最新值,而无需再次应用闭包,除非取消注释hogs = [2, 4, 6, 8]。如果我取消注释该行,则无论它更改多少次,我始终会获得array 5的原始值。为什么? - user2755778
因为在你到达更改hogs值的行之前,闭包已经将当前hogs值复制到本地变量w中。在变量被复制的时候,它的值是1,2,3,4。在创建IIFE闭包后对全局变量hogs所做的任何更改都不会影响闭包内部的变量w - 它通常会通过引用传递,因为它是一个对象,但是由于你在该闭包内创建了一个本地变量,w保留了它的原始值(即hogs的原始值)。 - Adam Jenkins
这正是我所想的,也是我预测的。但是,除非我包括(取消注释)行:hogs = [2, 4, 6, 8],否则我无法获得预测的行为。这会将array5冻结在“hogs = [2, 4, 6, 8]”之前的值上。否则,array5就像对hogs当前值的引用一样。我不知道如何预测这种行为,事后也无法解释。你觉得有道理吗? - user2755778
@Adam - 不好意思,我指的是返回值。我可能应该说它返回一个数组,并嵌套当前hogs值,但是我只是用“返回”这个词。它确实闭包了hogs,但它最终出现在array5中是因为它被返回了。(这就是我想表达的意思。) - DaoWen
@user2755778 - 是的,那是预期行为。请参考我回答中的编辑部分。 - DaoWen

1

这段话有些啰嗦,但我会用几种不同的方式来解释:

内存中有一个值为 [1,2,3,4] 的数组。访问该数组的方式是使用其名称“hogs”。

闭包中的变量w也指向同一块内存中的数组,但它是通过名称w来指向它的。因此现在有两种访问相同内存中的数组的方式,取决于您所处的作用域——在闭包外部,您可以请求“hogs”,在闭包内部,您可以请求“w”——在任何情况下,您都会得到相同的对象——一个包含[1,2,3,4]的数组。

如果您将“hogs”设置为另一个数组——即[2,4,6,8]——原始数组——即[1,2,3,4]——仍然存在于内存中,但现在无法通过“hogs”进行访问,只能通过在闭包中请求“w”来访问。

因此,当您遇到这行代码时

hogs = [2,4,6,8]

注释掉,然后你就可以

 hogs[0] = 91

你正在通过修改内存中的数组[1,2,3,4]的第一个项,将其更改为91(这两个变量hogs和w都指向同一个数组)。此时,闭包内的变量w和闭包外的变量hogs仍然引用内存中相同的数组
如果取消注释该行,则会重新赋值一个新的数组给变量w,但是hogs仍然引用原数组。
hogs = [2,4,6,8]

然后,猪和w现在引用两个不同的数组 - 猪引用具有值[2,4,6,8]的数组,而w引用具有值[1,2,3,4]的另一个数组。

因此,当你说

hogs[0] = 91

现在,hogs引用的数组看起来像这样:

hogs = [91,4,6,8]

但是变量w并不引用与hogs相同的数组,它仍然包含这些值:
w = [1,2,3,4]

如果您已经理解到这个点,那么只需按原样阅读其余代码。如果现在w引用了一个数组[1,2,3,4],那么通过阅读闭包中的代码,w的值永远不会改变(因为无法从其闭包外部访问它)。因此,您将永远无法更改从array5返回的值 - 这就是为什么您可以一遍又一遍地调用它,同时更改hogs数组的值,但始终会得到相同的对象返回的原因 - 因为 hogs 现在引用了与 w 不同的数组,当您更改 hogs 中的值时,您永远不会更改 w 中的任何值。

这就是我所能做到的最好的。

编辑

我想这里的关键点是说,在以下行:

var w = hogs

你并没有说“告诉变量w引用hogs引用的任何对象”。你说的是“告诉变量w引用hogs当前引用的任何对象”。
因此,当你告诉hogs引用不同的对象(一个数组 - [2,4,6,8])时,w仍然保持对hogs最初引用的对象的引用(一个数组 - 1,2,3,4)。
抱歉重复了。
最后,我想原评论者是正确的——这不是一个闭包概念,而更像是一个变量引用的概念。

在我的第二次编辑中,“Adam”没有出现。 我的意思是说:“Adam,我认为你没有重复; 你优雅地把整件事情概括了……”谢谢。对这些问题感兴趣的人可能会喜欢“http://www.typeof.co/post/immutable-arrays-in-javascript”的博客文章。再次感谢,Adam。 - user2755778

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