ES6—let与const,块级作用域是如何实现的

3,943 阅读6分钟

这是我参与更文挑战的第1天,活动详情查看:更文挑战

本文会介绍letconst基本用法和基本特性,如已掌握可直接拉到4.x

ES6新增了letconst命令来声明变量,下面我们先来了解一下它们的一些特性。

1. let命令

1.1 基本用法

ES6新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。

{
  let testA = 'A'
  var testB = 'B'
}

console.log(testB) // 'B'
console.log(testA) // testA is not defined

以上代码中,var声明的变量返回正确值,let声明的变量is not defined。说明let声明的变量只在它所在的代码块生效。

1.2 不存在变量提升

  • var命令会发生变量提升(本文不详细介绍变量提升,感兴趣的可以看冴羽大佬的github),即变量可以在声明之前使用,值为undefined。按正常逻辑,变量应该在声明语句之后才可以使用。

  • let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。

console.log(wave); // undefined
var wave = 'wave'

console.log(sky) // ReferenceError: Cannot access 'sky' before initialization
let sky = 'sky'

1.3 暂时性死区

只要块级作用域内存在let命令,它所声明的变量就绑定(如何绑定我们后面会详细说到)了这个区域,不再受外部影响。

var tmp = 123

if(true) {
    tmp = '123'
    let tmp
}

以上代码中,全局存在tmp变量,块级作用域中let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以let声明变量前,对tmp赋值会报错。

ES6明确规定,如果块区中存在let或者const命令,这个块区对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

总之,在代码块内,使用let或者const命令声明变量之前,该变量都是不可用的。这在语法上称为暂时性死区。主要是为了减少运行时的错误,防止变量声明前就使用这个变量,从而导致不可预料的问题。

1.4 不可重复声明

重复声明会报错

2. const命令

2.1 基本用法

  • const声明一个只读的常量。一旦声明,常量的值就不能改变。
const PI = 3.1415;
PI // 3.1415

PI = 3;
// TypeError: Assignment to constant variable.
  • const声明的变量不得改变值,意味着就必须初始化,不能留到以后赋值。
const foo;
// SyntaxError: Missing initializer in const declaration
  • constlet一样也是具有块级作用域的特性,并且声明变量也不提升,同样存在暂时性死区,只能在声明位置后面使用。

2.2 本质

const实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得变动。对于简单类型数据,值就保存在变量指向的那个内存地址。而对于引用类型,变量指向的内存地址,保存的只是一个指向数据的指针,const只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就控制不了了。

const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

3. 注意

ES6的块级作用域必须有大括号,如果没有大括号,JavaScript引擎就认为不存在块级作用域。

// 第一种写法,报错
if (true) let x = 1;

// 第二种写法,不报错
if (true) {
  let x = 1;
}

4. 块级作用域原理

前面在介绍constlet的时候,都提到了一个词块级作用域,下面我们来详细谈谈块级作用域是什么,原理是什么

4.1 首先我们得先从作用域讲起

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗的讲就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和声明周期

在ES6之前,只有两种作用域:全局作用域和函数作用域

没有块级作用域会导致很多问题:

  1. 变量覆盖
  2. 命名冲突
  3. 变量提升
  4. 内存泄漏

4.2 块级作用域是如何实现的?

现在我们已经知道了可以通过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 关键字是如何影响执行上下文的呢?

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

第一步是编译并创建执行上下文,你可以参考下图:

image.png

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

  • 通过var声明的变量,在编译阶段被放在变量环境中了
  • 通过let声明的变量,在编译过程中会被放到词法环境中
  • 在函数的作用域块内部,通过 let 声明的变量并没有被存放到词法环境中

继续执行:

image.png

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

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

参考文献:

  1. ECMAScript 6 入门 阮一峰

  2. 李兵 - 块级作用域:var缺陷以及为什么要引入let和const?