JavaScript 闭包和块范围的变量在循环内存管理中

JavaScript Closures and Block-Scoped Variables In Loop Memory Management

提问人:mishar 提问时间:8/21/2023 更新时间:8/22/2023 访问量:29

问:

这个问题是几个不同的(我认为是相关的)问题,我在下面给出,但总的来说,我试图理解 David Flanagan 的 O'Reilly 关于 JavaScript 的书中的一段代码,它给出了一个示例,即使用循环来创建意外的闭包:

function constfuncs() {
    let funcs = [];
    for(var i = 0; i < 10; i++) {
        funcs[i] = () => i;
    }
    return funcs;
}

let funcs = constfuncs();
funcs[5]() //10, rather than 5

上面的代码示例旨在使数组的每个元素都是一个函数,该函数可以在没有参数的情况下调用,以返回一个数值等效于其索引的值;但是,它没有成功做到这一点。我想我在很大程度上理解了为什么这是:,因为它是使用 声明的,作用域是函数,而不是该循环的块,因此存储在 中的每个函数使用的函数相同,并在该循环中结束,因此 中返回的十个函数中的任何一个在调用时总是计算为 。funcsfuncsvar ivarconstfuncsforifuncsi10funcs10

在本书的下一页,它指出要解决上述问题,只需进行一行更改:只需使用块范围,因此使用而不是在该循环中,如下所示:ilet ivar ifor

function constfuncs() {
    let funcs = [];
    for(let i = 0; i < 10; i++) {
        funcs[i] = () => i;
    }
    return funcs;
}

let funcs = constfuncs();
funcs[5]() //is now 5, as desired

然而,我不明白的是这如何解决问题。 对我来说,似乎只存在一次,但创建的十个函数中的每一个似乎都可以访问自己的内存版本/副本。如果在循环中声明了一个新的块范围的局部变量,该变量在每次迭代中被分配了值,而不是作为每个函数返回的值,那么也许它对我来说更有意义,如果是在循环中声明的块范围局部变量在每次迭代时被重新声明(如使用新内存);但即便如此,并非如此,但上述内容仍然按预期工作。这让我想到了我的问题:iiforiifuncsi

  1. 如何在 JavaScript 中管理内存中的闭包?我对 JS 和闭包相当陌生,并且习惯于具有堆栈和堆的语言。
  2. 定义闭包的行为是否会导致将内存从堆栈复制到堆,以保存外部局部变量以供该闭包使用?这是因为闭包似乎无视仅存储在堆栈上的局部变量,因为它仍然可以访问它们。
  3. 是否在内存中(每次在不同的内存位置)中为声明它们的外部循环的每次迭代重新创建块范围的局部变量?
  4. 如果可能的话,是到 3.,这是否完全受是否使用闭包的影响(即,在一般情况下,在循环迭代中只在内存中创建一次,但在每次循环迭代中,如果闭包在它的范围内具有该局部块变量)?
  5. 通过“绑定”,我们指的是内存中变量或常量的不同位置吗?正如本书在对上述解决方案的解释中所说的那样,我问道:“因为 和 是块作用域,所以循环的每次迭代都定义了一个独立于所有其他迭代的作用域的作用域,并且这些作用域中的每一个都有自己独立的绑定。letconsti

问题 1-2 是相关的,问题 3-4 也是如此;并且都与上面的例子有关。欢迎任何其他想法来解释为什么上面的第二种方法(与)成功地解决了解决方案。let i

JavaScript 管理 闭包 堆内存 局部变量

评论

0赞 mishar 8/21/2023
我在这里阅读了@slebetman发布的答案,但它只是将闭包描述为将另一个堆栈帧的局部变量存储在“RAM 中的某个地方”的外部变量。
0赞 VLAZ 8/21/2023
使用 for 循环的 let 和 block 范围的解释
0赞 kca 8/22/2023
我建议创建单独的问题,每个问题只关注你的一个问题。(只有当你理解了前一个时,才问下一个可能也是有意义的。如果你一次问多个问题,只知道部分或其中一个答案的人无法回答,所以你需要等待一个知道所有答案的人,并愿意写一个很长的答案(这可能不会发生)。

答:

1赞 kca 8/22/2023 #1

从技术上讲,我不是在回答你们中的任何一个问题,但我可以解释一下你们想知道“这如何解决问题”的部分:

根据定义,在 for 循环初始化中使用 let 声明的变量被视为在循环“body”中声明的变量。这是 for 循环的特殊行为。let

另一方面,声明的变量与定义 for 循环的范围相同(即在循环“body”之外)。var

例子

因此,如果你在for循环中声明外部,你将再次得到错误的值(正如你所期望的那样):let i10

function constfuncs(){
    let funcs = [];
    let i;
    for( i = 0; i < 10; i++ ){
        funcs[ i ] = function(){
            return i;
        };
    }
    return funcs;
}

另一方面,即使你保留了内部循环初始化,但声明了一个带有内部循环体的新变量,你也会得到所需的值:varlet5

function constfuncs(){
    let funcs = [];
    for( var i = 0; i < 10; i++ ){
        let value = i;            // <-- new variable in every iteration
        funcs[ i ] = () => value;
    }
    return funcs;
}

言论

较新的声明是专门为这种行为设计的,因为人们往往会对 的行为感到困惑。letvar

(我假设你想知道:IMO有太多的特殊情况。当你去想它时,让行为更直观,但如果你想理解它,它会更复杂。但它仍然解决了 var 的重要缺陷。