let和const变量声明的特点

本文的结构是:前文就是一个完整的内容提炼;后文是作为拓展和详细的介绍:


(本文写于2017年,虽然ES8都出来了,但是需要维护的用var的旧工程依然是不少啊)

ES6提出了两个新的声明变量的命令:letconst 。在使用层面就是:在做新的 js 项目的时候,var可以完全被它们俩替换掉。

用let替代var时,你需要注意到它们的不同点有三个:

1.块级作用域:

for (var i = 0; i < 10; i++) {}
console.log(i);   // 10

for (let j = 0; j < 10; j++) {}
console.log(j);   // ReferenceError: j is not defined

在执行到 console.log(i) 的时候,“标识符解析”所想要解析到的 i 仍在其所属的函数作用域的执行上下文内。基于此,log的打印结果是:此时 i 还没有被销毁。

在执行到 console.log(j) 的时候,“标识符解析”所想要解析到的 j 已经在本例中各个“声明了 j 的块级作用域的执行上下文”外了。也就是此时解析不到对于变量 j 的声明。

2.变量提升: var会变量提升,但let不会:

console.log(bar); // undefined
var bar = 1;

console.log(baz); // ReferenceError: baz is not defined
let baz = 1;

3.不允许重复声明: let声明的变量 不允许在同一个作用域中被重复声明 (,幸运的是,浏览器会很清晰地报错以提醒你这条规则):

var count = 30;
let count = 40; //Uncaught SyntaxError: Identifier 'count' has already been declared

对于循环体的块级作用域的情况,则是要求 js 解析器:每一次循环中创建的各个循环体块级作用域都能有属于它们自己的 循环变量 的副本。详细说明见后文。


const同样有以上的三条规则,即:

1.块级作用域

2.const不再能变量提升

3.const声明的变量是不允许被重复声明的

另外,const 的特点是一经声明,该变量值就不能被赋新的值

  • 一经声明,该变量值就不能被赋新的值
    • const声明的变量只可以在声明时赋值
    • 在声明的之后,赋新的值给该变量会报错

因而,在使用和操作的层面:如果要声明一个常量,通常推荐使用 const 来声明。


以下为详细内容:

let

let的规则被提出,就是希望帮助你捕捉可能造成bug的地方,写出更健壮的代码。除了NaN错误以外,每一个异常都能在当前行抛出。

let的块级作用域

letvar一样,也可以用来声明变量,但let有着更精确的作用域规则:

let声明的变量拥有块级作用域。 也就是说用let声明的变量的作用域只是外层块,而不是整个外层函数。

【注意该话题下的细节】:

  • let声明的全局变量不是全局对象的属性。 这就意味着,你不可以通过window.变量名的方式访问这些变量。它们只存在于一个不可见的块的作用域中,这个块理论上是Web页面中运行的所有JS代码的外层块
  • 形如for (let i…)的循环在每次迭代时都为 i 创建新的绑定。
代码示例:

可以先看一个 “循环内变量过度共享” 的例子:

var messages = ["A", "B", "C"];
for (var i = 0; i < messages.length; i++) {
	setTimeout(function () {
		console.log(i);
	}, i * 500);
}
// 控制台输出三次 "3",比较不符合常规的惯性思维

解释该例:

首先执行 3 次循环。当循环结束执行时,

在 var 声明的变量是属于其函数作用域(的执行上下文)的 的规则下,本例最外层的那个作用域(的执行上下文),在执行完全局代码的时机保留了循环执行 3 次之后变量 i 的值,即为3。(此时 setTimeout 回调尚未被执行。)

然后,再到分别依次执行 setTimeout 回调的时候,3 个 setTimeout 回调函数的执行上下文,能解析到的是其父级作用域(也就是本例最外层的那个作用域)的 i,值为3。

// 而如果我们在该例中使用的是let:
var messages = ["A", "B", "C"];
for (let i = 0; i < messages.length; i++) {
	setTimeout(function () {
		console.log(i);
	}, i * 500);
}
// 控制台输出 "A" "B" "C",符合我们常规的预期

for (let i...)循环执行三次,每次循环都会产生新的块级作用域, 并且 为 每次循环里面的函数 将捕捉当时循环变量 i 的不同值作为副本(换而言之,而不是所有循环体都捕捉循环变量的同一个值)。

因为let是块级作用域规则,

而每一次循环都是进入新的块级作用域,都会创建一个新的变量 i,并将其初始化为 i 的当前值,所以每一次循环中创建的各个循环体块级作用域都能有属于它们自己的 i 的副本(值分别为 0 1 2)。

由于这些timeout函数都需要被推入 任务队列 ,稍后再执行,所以最终为它们每一个都保留了一个因 setTimeout 所需要而产生的闭包。

  • 第一个timeout函数所对应的闭包(即本例第一次for循环产生的块级作用域)中保留了一个值为0的 i 的副本
  • 第二个timeout函数所对应的闭包(即本例第二次for循环产生的块级作用域)中保留了一个值为1的 i 的副本
  • 第三个timeout函数所对应的闭包(即本例第三次for循环产生的块级作用域)中保留了一个值为2的 i 的副本

所以在上例中,直接可以将var替换为let修复bug。

这种解决办法,适用于现有的三种循环方式:for-offor-in、以及传统的用分号分隔的类C语言的循环。

let不再变量提升

let声明的变量直到执行流到达该变量被声明的代码行时才会被装载,所以在到达之前使用该变量会触发错误 。举个例子:

  function foo() {
  	console.log("当前值: " + x);  // 引用错误(ReferenceError)
  	// ...
  	let x = "hello";
  }
  foo()

不可访问的这段时间里,变量 x 一直处于作用域中,虽已声明,但是尚未装载,它位于 临时死区(Temporal Dead Zone,简称TDZ)中。

换句话说就是:let和const声明的变量不会被提升到作用域顶部,如果在声明之前访问这些变量,就会引发错误。因而从作用域顶部到声明变量语句之前的这个区域,被称为“临时死区”。

PS:但是,在let或const声明的作用域之外使用该变量就 不会报错:(也就是,(由作用域shadow的基本规则推导出:) TDZ 现象仅限于在let声明所属的作用域内)

console.log(typeof value);
if(true){
    let value = "blue";
}

let声明的变量不允许重复声明

let声明的变量 不允许在同一个作用域中被重复声明。

此时,重声明会抛出一个语法错误(SyntaxError)。

var count = 30;
let count = 40; //Uncaught SyntaxError: Identifier 'count' has already been declared

这一条规则也可以帮助你检测琐碎的小问题。当你全局搜索var替换为let时可能会发现let重复声明的语法错误。

而对于循环体的块级作用域的处理规则,参考上一个小节中的例子。

const

const声明的变量 一经声明,该变量值就不能被赋新的值,这是最大的特点。

const声明的变量与let声明的变量类似,它们的不同之处在于:

1.const声明的变量不可随意修改,否则会导致SyntaxError(语法错误):

const MY_PI = 3.14159; // 正确

MY_PI = 5000; // 语法错误(SyntaxError)
MY_PI++; // 虽然换了一种方式,但仍然会导致语法错误

2.用 const 声明变量的语句必须要赋值,否则也抛出语法错误。 这个一般代码编辑器都会提示的,不用担心。

const MY_NEW_PI;  // 依然是语法错误

// 注意1:

const声明不允许修改绑定,但允许修改值。

即:对象属性不在const的保护范围之内:

// 常量可以声明为对象
const MY_OBJECT = {"key": "value"};

// 重写对象会失败
MY_OBJECT = {"OTHER_KEY": "value"};

// 但对象属性并不在保护的范围内,下面这个声明会成功执行
MY_OBJECT.key = "otherValue";

// 也可以用来声明数组
const MY_ARRAY = [];
// 可以向数组填充数据
MY_ARRAY.push('A'); // ["A"]
// 但是,将一个新数组赋给变量会引发错误
MY_ARRAY = ['B']

// 注意2:

因为const声明的变量不能改变,所以普通的for循环不能用const声明循环变量。

而由于for-in循环中每次迭代不会修改已有绑定,而是创建一个新绑定,所以在for-in循环中可以使用const:

for(const key in obj){
       console.log(key);
}
文章目录
  1. 以下为详细内容:
    1. let
      1. let的块级作用域
        1. 代码示例:
        2. // 而如果我们在该例中使用的是let:
      2. let不再变量提升
      3. let声明的变量不允许重复声明
    2. const
      1. // 注意1:
      2. // 注意2: