likes
comments
collection
share

怎么理解for循环中的let?

作者站长头像
站长
· 阅读数 54

前言

我们先看一个经典面试题:

var a = [];
for (var i = 0; i < 10; i++) {
  // 作用域a
  a[i] = function () {
    // 作用域b
    console.log(i);
  };
}
a[6](); // 10
console.log(i) // 10,可以访问到i

将 var 换成 let

var a = [];
for (let i = 0; i < 10; i++) {
  // 作用域a
  a[i] = function () {
    // 作用域b
    console.log(i);
  };
}
a[6](); // 6

我们可以看到两次执行的结果不一样,var 打印 10,而 let 打印的是6。

这是为什么呢?

有一点共识:在执行函数调用时,for循环已循环完毕。

  • 使用 var :结果为 10 。是因为函数内并没有对变量 i 的声明,js引擎会去作用域 [[scopes]] 找。然后会在全局作用域(因为val的变量提升)中找到 i 的值。

  • 使用 let :结果为 6。首先得知道,使用 let 后,这里会多一个块级作用域。所以函数是定义在块级作用域中,而不是全局作用域中。函数在定义时,作用域[[scopes]]指向了该块级作用域,而该块级作用域中的词法环境包含了 i 的声明和值。

块级作用域

ES6 中引入了 letconstconstlet具有“块级作用域”的特性,并且声明变量也不提升,同样存在暂时性死区,只能在声明位置后面使用。

Tip

块级作用域必须显式使用 {} 括起来。

下面的语句会报错:

let x = 1;

switch(x) {
  case 0:
    let foo;
    break;
  case 1:
    let foo; // SyntaxError for redeclaration.
    break;
}

加上 {}

let x = 1;

switch(x) {
  case 0: {
    let foo;
    break;
  }
  case 1: {
    let foo; 
    break;
  }
}

Tip

块级作用域存在暂时性死区(Temporal dead zone,TDZ)

var声明的变量,如果在声明前访问了变量,变量将会返回 undefinedletvar 不同,以下代码演示了在使用 let 和 var 声明变量的行之前访问变量的不同结果。

{ // TDZ starts at beginning of scope
  console.log(bar); // undefined
  console.log(foo); // ReferenceError
  var bar = 1;
  let foo = 2; // End of TDZ (for foo)
}

使用术语“temporal”是因为区域取决于执行顺序(时间),而不是编写代码的顺序(位置)。例如,下面的代码会生效,是因为即使使用 let 变量的函数出现在变量声明之前,但函数的执行是在暂时性死区的外面。

{
  // TDZ starts at beginning of scope
  const func = () => console.log(letVar); // OK

  // Within the TDZ letVar access throws `ReferenceError`

  let letVar = 3; // End of TDZ (for letVar)
  func(); // Called outside TDZ!
}

关于块级作用域的知识,大家可以自己去补充。我们继续前言提到的问题。

for循环中的let

我们将代码放在chrome中去执行:

怎么理解for循环中的let?

我们可以看到在执行 console.log(i) 时,作用域链有3部分:

  • 本地:因为我们把匿名函数赋值给了数组元素,所以这里this指向数组。
  • 代码块:这个代码块作用域是 let 创建的,并绑定到了函数上。或者说函数作用域[[scopes]]指向了块级作用域的引用(这里和闭包有些相似)。
  • 全局作用域:window

看到这里不难理解使用 let 打印结果为 6 了吧。

Tip

函数的作用域是在函数定义的时候确定的。 函数作用域指的是变量在代码中的可访问性,即哪些变量可以在哪些地方被访问到。

let的变量除了作用域是在for区块中,而且会为每次循环执行建立新的词法环境(LexicalEnvironment),并被内部函数引用。

再拓展一下,上述代码经过babel编译为ES5的代码:

"use strict";

var a = [];

var _loop = function _loop(i) {
  a[i] = function () {
    console.log(i);
  };
};

for (var i = 0; i < 10; i++) {
  _loop(i);
}
a[6](); // 6

这里看到 _loop 使用了闭包持久化了数据 i。

Tip

知道了块级作用域,js在执行过程中会为每一个块创建一个块级作用域的词法环境。接着看下面的内容。

JavaScript是如何支持块级作用域的

现在你知道了ES可以通过使用let或者const关键字来实现块级作用域,不过你是否有过这样的疑问:“在同一段代码中,ES6是如何做到既要支持变量提升的特性,又要支持块级作用域的呢?”

那么接下来,我们就要站在执行上下文的角度来揭开答案

你已经知道JavaScript引擎是通过变量环境实现函数级作用域的,那么ES6又是如何在函数级作用域的基础之上,实现对块级作用域的支持呢?你可以先看下面这段代码

function foo(){
    var a = 1
    let b = 2
    {
      let b = 3
      var c = 4
      let d = 5
      console.log(a)
      console.log(b)
    }
    console.log(b) 
    console.log(c)
    console.log(d)
}   
foo()

当执行上面这段代码的时候,JavaScript引擎会先对其进行编译并创建执行上下文,然后再按照顺序执行代码,关于如何创建执行上下文我们在前面的文章中已经分析过了,但是现在的情况有点不一样,我们引入了let关键字,let关键字会创建块级作用域,那么let关键字是如何影响执行上下文的呢?

接下来我们就来一步步分析上面这段代码的执行流程。

第一步是编译并创建执行上下文,下面是我画出来的执行上下文示意图,你可以参考下

怎么理解for循环中的let?

通过上图,我们可以得出以下结论:

  • 函数内部通过var声明的变量,在编译阶段全都被存放到变量环境里面了。
  • 通过let声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)中。
  • 在函数的作用域内部,通过let声明的变量并没有被存放到词法环境中。
  • 接下来,第二步继续执行代码,当执行到代码块里面时,变量环境中a的值已经被设置成了1,词法环境中b的值已经被设置成了2,

这时候函数的执行上下文就如下图所示:

怎么理解for循环中的let?

从图中可以看出,当进入函数的作用域块时,作用域块中通过let声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量,比如在作用域外面声明了变量b,在该作用域块内部也声明了变量b,当执行到作用域内部时,它们都是独立的存在。

其实,在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。需要注意下,我这里所讲的变量是指通过let或者const声明的变量。

再接下来,当执行到作用域块中的console.log(a)这行代码时,就需要在词法环境和变量环境中查找变量a的值了,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给JavaScript引擎,如果没有查找到,那么继续在变量环境中查找。

这样一个变量查找过程就完成了,你可以参考下图:

怎么理解for循环中的let?

从上图你可以清晰地看出变量查找流程,不过要完整理解查找变量或者查找函数的流程,就涉及到作用域链了,这个我们会在下篇文章中做详细介绍。

当作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出,最终执行上下文如下图所示:

怎么理解for循环中的let?

通过上面的分析,想必你已经理解了词法环境的结构和工作机制,块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript引擎也就同时支持了变量提升和块级作用域了。

总结

  • 执行上下文是在代码执行时创建的,而函数的作用域是在定义的时候就确定好了的。
  • for循环中使用 let 代替 val,let 会为每次循环创建一个新的块级作用域的词法环境,该词法环境会被内部的函数引用。
  • js在寻找变量时,先在词法环境中找,再去变量环境找,然后去作用域中去找。

相关文档

转载自:https://juejin.cn/post/7236987648295927866
评论
请登录