在闭包上下文中 for 循环内声明的 let 变量的行为
Behavior of let variables declared inside for loop in context of closures
在下面的代码片段中,传递给 setTimeout
的函数与 script
作用域中存在的变量 i
形成一个闭包。因此该函数包含对变量 i
的 引用 。 i
的值在 i
记录到控制台之前更新为 5。输出为:5 5 5 5 5
(带换行符)
script.js
:
let i;
for (i = 0; i < 5; ++i) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}
以上部分我已经很清楚了。 但是当我在for
循环中声明变量i
时,i
的块作用域是for
循环。
for (let i = 0; i < 5; ++i) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}
在这个代码片段中,我希望输出与第一个片段的输出相同,因为闭包包含对变量 i
的 reference 和值在将值记录到控制台之前,reference 应该更新为 5。但实际输出是:0 1 2 3 4
(带换行符)
问题是,为什么第二个片段中的代码会这样?
是因为:
- 对于
for
循环的每次迭代都会在内存中创建变量 i
的新副本,并且之前的副本被垃圾收集?我不确定循环的内存管理是如何工作的。
- 不是存储
i
的引用,而是 i
的 值 存储在闭包中?我不认为是这样。
- 还有什么吗?
请帮忙说明一下。谢谢!
您的问题提到了一些实施细节。然而,这些实施细节在回答您的问题时并不重要。
您确实是对的,for 循环发生了一些特定的行为。让我们在这里看看 ECMAScript 2021 标准:
当评估带有 let
的 for 循环时,首先执行 these steps:
我们对第 4、9 和 10 步特别感兴趣。第 4 步收集在 for 循环中声明的所有 const
和 let
变量。
第 9 步将 perIterationLets
设置为 let
变量(如果有 let 变量),否则设置为空列表。第 10 步然后调用 ForBodyEvaluation 实际 运行 循环:
这里,我们先关注一下3.e,仔细看看CreatePerIterationEnvironment:
仔细遵循调用层次结构,我们注意到 perIterationBindings
是我们之前收集的 let
个变量的列表 perIterationLets
。
在行 e.i、e.ii、e.iii 中,我们现在创建一个新绑定(将其视为变量所在的内存位置)并复制值将上一次迭代中具有相同名称的变量放入其中。这是一个不同的新变量,即使它具有相同的名称!
因此如果循环体内的闭包捕获当前上下文,循环变量将不会改变,因为每个循环体迭代都有自己的当前上下文.
在下面的代码片段中,传递给 setTimeout
的函数与 script
作用域中存在的变量 i
形成一个闭包。因此该函数包含对变量 i
的 引用 。 i
的值在 i
记录到控制台之前更新为 5。输出为:5 5 5 5 5
(带换行符)
script.js
:
let i;
for (i = 0; i < 5; ++i) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}
以上部分我已经很清楚了。 但是当我在for
循环中声明变量i
时,i
的块作用域是for
循环。
for (let i = 0; i < 5; ++i) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}
在这个代码片段中,我希望输出与第一个片段的输出相同,因为闭包包含对变量 i
的 reference 和值在将值记录到控制台之前,reference 应该更新为 5。但实际输出是:0 1 2 3 4
(带换行符)
问题是,为什么第二个片段中的代码会这样?
是因为:
- 对于
for
循环的每次迭代都会在内存中创建变量i
的新副本,并且之前的副本被垃圾收集?我不确定循环的内存管理是如何工作的。 - 不是存储
i
的引用,而是i
的 值 存储在闭包中?我不认为是这样。 - 还有什么吗?
请帮忙说明一下。谢谢!
您的问题提到了一些实施细节。然而,这些实施细节在回答您的问题时并不重要。
您确实是对的,for 循环发生了一些特定的行为。让我们在这里看看 ECMAScript 2021 标准:
当评估带有 let
的 for 循环时,首先执行 these steps:
我们对第 4、9 和 10 步特别感兴趣。第 4 步收集在 for 循环中声明的所有 const
和 let
变量。
第 9 步将 perIterationLets
设置为 let
变量(如果有 let 变量),否则设置为空列表。第 10 步然后调用 ForBodyEvaluation 实际 运行 循环:
这里,我们先关注一下3.e,仔细看看CreatePerIterationEnvironment:
仔细遵循调用层次结构,我们注意到 perIterationBindings
是我们之前收集的 let
个变量的列表 perIterationLets
。
在行 e.i、e.ii、e.iii 中,我们现在创建一个新绑定(将其视为变量所在的内存位置)并复制值将上一次迭代中具有相同名称的变量放入其中。这是一个不同的新变量,即使它具有相同的名称!
因此如果循环体内的闭包捕获当前上下文,循环变量将不会改变,因为每个循环体迭代都有自己的当前上下文.