javascript 解释for循环的“let”和块作用域

wd2eg0qa  于 2023-02-28  发布在  Java
关注(0)|答案(6)|浏览(163)

我知道let可以防止重复声明,这很好。

let x;
let x; // error!

let声明的变量也可以用在闭包中,这是可以预料到的

let i = 100;
setTimeout(function () { console.log(i) }, i); // '100' after 100 ms

我有点难以理解的是let是如何应用于循环的,这似乎是for循环特有的。

// prints '10' 10 times
for (var i = 0; i < 10; i++) { process.nextTick(_ => console.log(i)) }
// prints '0' through '9'
for (let i = 0; i < 10; i++) { process.nextTick(_ => console.log(i)) }

为什么在这种情况下使用let可以工作?在我的想象中,即使只有一个块是可见的,for实际上为每个迭代创建了一个单独的块,并且let声明是在那个块内部完成的...但是只有一个let声明来初始化值。这只是ES6的语法糖吗?它是如何工作的?
我理解varlet之间的区别,并且已经在上面举例说明了它们,我特别感兴趣的是理解为什么不同的声明会导致使用for循环的不同输出。

kcwpcxri

kcwpcxri1#

这只是ES6的语法糖吗?
不,这不仅仅是语法上的糖衣炮弹,血淋淋的细节隐藏在§ 13.6.3.9 CreatePerIterationEnvironment中。
这是怎么回事?
如果在for语句中使用let关键字,它将检查绑定了哪些名称,然后

  • 用这些名称为a)初始化器表达式b)每次迭代(在评估增量表达式之前)创建新的词汇环境
  • 将具有这些名称的所有变量的值从一个环境复制到下一个环境

您的循环语句for (var i = 0; i < 10; i++) process.nextTick(_ => console.log(i));将反糖转换为简单的

// omitting braces when they don't introduce a block
var i;
i = 0;
if (i < 10)
    process.nextTick(_ => console.log(i))
    i++;
    if (i < 10)
        process.nextTick(_ => console.log(i))
        i++;
        …

for (let i = 0; i < 10; i++) process.nextTick(_ => console.log(i));对复杂得多的

// using braces to explicitly denote block scopes,
// using indentation for control flow
{ let i;
  i = 0;
  __status = {i};
}
{ let {i} = __status;
  if (i < 10)
      process.nextTick(_ => console.log(i))
      __status = {i};
}   { let {i} = __status;
      i++;
      if (i < 10)
          process.nextTick(_ => console.log(i))
          __status = {i};
    }   { let {i} = __status;
          i++;
          …
h7appiyu

h7appiyu2#

我发现探索ES6书中的解释是最好的:
在for循环头中var声明一个变量会为该变量创建一个绑定(存储空间):

const arr = [];
for (var i=0; i < 3; i++) {
    arr.push(() => i);
}
arr.map(x => x()); // [3,3,3]

这三个arrow函数体中的每个i都指向同一个绑定,这就是为什么它们都返回相同的值。
如果让-声明一个变量,则会为每个循环迭代创建一个新绑定:

const arr = [];
for (let i=0; i < 3; i++) {
    arr.push(() => i);
}

arr.map(x => x()); // [0,1,2]

这一次,每个i引用一个特定迭代的绑定,并保留当时的当前值,因此,每个arrow函数返回不同的值。

kgsdhlau

kgsdhlau3#

let引入了块作用域和等价绑定,就像函数创建一个带闭包的作用域一样。我相信规范的相关章节是13.2.1,其中的注解提到let声明是LexicalBinding的一部分,并且都存在于词法环境中。13.2.2节声明var声明附加到VariableEnvironment,而不是词法绑定。
MDN的解释也支持这一点,指出:
它通过在单个代码块的词法作用域中绑定零个或多个变量来工作
这表明变量被绑定到块,块在每次迭代时都需要新的LexicalBinding(我相信,在这一点上不是100%),而不是周围的LexicalEnvironment或VariableEnvironment,后者在调用期间是恒定的。
简而言之,当使用let时,闭包在循环体上,变量每次都不一样,所以必须重新捕获;当使用var时,变量在周围的函数上,所以不需要重新关闭,每次迭代都传递相同的引用。
调整示例以在浏览器中运行:

// prints '10' 10 times
for (var i = 0; i < 10; i++) {
  setTimeout(_ => console.log('var', i), 0);
}

// prints '0' through '9'
for (let i = 0; i < 10; i++) {
  setTimeout(_ => console.log('let', i), 0);
}

当然显示了后者打印每个值。如果你看看巴别塔是如何传递这个的,它会产生:

for (var i = 0; i < 10; i++) {
  setTimeout(function(_) {
    return console.log(i);
  }, 0);
}

var _loop = function(_i) {
  setTimeout(function(_) {
    return console.log(_i);
  }, 0);
};

// prints '0' through '9'
for (var _i = 0; _i < 10; _i++) {
  _loop(_i);
}

假设巴别塔是相当一致的,这符合我对规范的解释。

tf7tbtn2

tf7tbtn24#

最近我也对这个问题感到困惑,根据以上回答,我的理解是:

for (let i=0;i<n;i++)
{
   //loop code
}

相当于

// initial
{
    let i=0
}
// loop
{
    // Sugar: For-Let help you to redefine i for binding it into current block scope
    let i=__i_value_from_last_loop__

    if (i<=n){
        //loop code
    }
    i++
}
3wabscal

3wabscal5#

让我们看一下采访中主要询问的setTimeout中的“let”和“var”。

(function timer() { 
   for (var i=0; i<=2; i++) 
       { setTimeout(function clog() {console.log(i)}, i*1000); } 
    })();

(function timer() { 
   for (let i=0; i<=2; i++) 
       { setTimeout(function clog() {console.log(i)}, i*1000); } 
    })();

让我们详细看看这段代码是如何在javascript编译器中执行的。由于函数作用域,“var”的答案是“222”,而“let”的答案是“012”,因为它是块作用域。
现在让我们看看当它编译为“var”时它看起来是什么样的细节。(这比在音频或视频中解释代码有点困难,但我会尽我所能给予你。)

var i = 0;

if(i <=2){
setTimeout(() => console.log(i));
}
i++;  // here the value of "i" will be 1

if(i <=2){
setTimeout(() => console.log(i));
}
i++;   // here the value of "i" will be 2

if(i <=2){
setTimeout(() => console.log(i));
}
i++;  // here the value of "i" will be 3

代码最终执行后,它将打印所有console.log,其中“i”的值为6。因此,最终输出为:222
In“let i”将在每个作用域中声明。这里要注意的导入点是**“i”将从前一个作用域中获取值**而不是从声明中获取值。(下面的代码只是一个例子,说明它在编译器中的样子,尝试它不会起作用)

{
    //Scope  1
    { 
    let i;  
    i= 0;
    
    
    if(i<=2) {
        setTimeout(function clog() {console.log(i)};);
    }
    i++;   // Here "i" will be increated to 1
    
    }
    
    //Scope 2  
    // Second Interation run
    {
    let i;
    i=0;
    
        // Even “i” is declared here i= 0 but it will take the value from the previous scope
    // Here "i" take the value from the previous scope as 1
    if(i<=2) {    
        setTimeout(function clog() {console.log(i)}; );
    }
    
    i++;   // Here “i” will be increased to 2
    
    }
    
    
    //Scope 3 
    // Second Interation run
    {
    let i;
    i=0;
    
    // Here "i" take the value from the previous scope as 2
    if(i<=2) {   
        setTimeout(function clog() {console.log(i)}; );
    }
    
    i++;   // Here "i" will be increated to 3
    
    }
    

}

因此,它将根据块范围打印“012”值。

dddzy1tm

dddzy1tm6#

Let是一个块作用域,在for循环内部声明的Var,甚至可以在for循环外部访问,因为var只是函数作用域,你不能从外部访问函数内部定义的var,每次迭代都会创建一个新的Let,但是因为var是函数作用域,并且可以在for循环外部使用,所以它就变成了全局变量,每次迭代都会更新相同的var变量。

相关问题