let和const旧话新编

本文的结构是:首先看完了前文就能快速把代码从ES5的变量声明改到ES6,后续部分是作为拓展和详细的介绍:


(虽然现在ES8都出来了,但是依然是一直用var的工程也不少啊)

ES6提出了两个新的声明变量的命令:letconst 。说大白话就是:除了极为极为少数的情况,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 还没有被销毁。

在 console.log(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

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

1.块级作用域

2.const不再能变量提升

3.const定义的变量是不能被重复定义的

另外,更适合用const而不是let的地方是:声明一个常量,尤其是全局常量的时候。

另外,const声明的变量只可以在声明时赋值,它被设计的本意就是不可随意修改,修改变量会报错,这是最大的特点。


以下为详细内容:

let

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

比如,如果你只是在代码中将var全局搜索替换为let,一些依赖var声明的独特特性(可能你不是故意这样写)的代码可能无法正常运行。再根据异常提示来修改你的代码,会使你的代码更加健壮。

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(messages[i]);
	}, i * 500);
}
// 控制台输出三次 "undefined"

控制台的输出结果可能会不同于你的预期,输出了三次“undefined”。

// 解释该例:

在 var声明的变量是函数作用域的 的规则下,一共循环了三次,其实用到的都是同一个子函数,这个子函数对应的闭包最终保留了循环三次之后变量 i 的值,即为3。

换而言之,由 任务队列 的知识我们可知:

当循环结束执行时,i 的值为3(因为messages.length的值为3),此时回调尚未被触发。

然后,所以当第一个timeout执行时,此时i的值已为3,所以打印出来的是messages[3]的值即为“undefined”。

之后同理,第二个、第三个timeout执行时,都是打印messages[3],也就是“undefined”。

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

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

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

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

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

  • 第一个timeout函数对应的闭包中保留了一个值为0的 i 的副本,它执行时输出messages[0]
  • 第二个timeout函数对应的闭包中保留了一个值为1的 i 的副本,它执行时输出messages[1]
  • 第三个timeout函数对应的闭包中保留了一个值为2的 i 的副本,它执行时输出messages[2]

(所谓的俗称的“加载”,其实就是 执行流 执行到该函数的声明。)

结合这个例子,我们对闭包的理解为:如果一个子函数需要在 其父函数的作用域 之外被执行,那么在这个子函数被加载时, 会先去分析子函数用到了父函数的哪些变量和指针,并且在内存中形成一个闭包保存这个时刻的这些父函数的变量和指针的值,以供 对应的子函数在执行的时候 使用。

  • 可见,子函数是先会被“加载”,然后再会被“执行”。而闭包是在子函数被“加载”的时候形成的。
    • 比如,在上例中,setTimeout一次的时候,子函数会被加载一次。(由于在三次循环各不同的块级作用域中,function () { console.log(messages[i]); }总共被加载了三次,所以可视为三个子函数。)
    • 再比如,如果有一个子函数被父函数return的时候,子函数会被加载一次。
  • 可见,闭包和子函数是一一对应的,一个闭包对应着一个子函数。
  • PS:“子函数在 其父函数的作用域 之外被执行”这种说法等价于:“子函数在 父函数的执行环境被销毁后 被执行”。

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

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

刚才的例子有点扯远了,涉及到的知识太多,反正只要记住let有块级作用域,就基本上没问题了。

let不再变量提升

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

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

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

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

PS:但是,在let或const声明的作用域之外使用该变量就不会报错

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重定义语法错误。

  • 如果你的多个脚本中都声明了相同的全局变量,你最好继续用var声明这些变量。因为如果你换用了let,后加载的脚本都会执行失败并抛出错误;或者你可以考虑使用ES6内建的模块机制。

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);
}

能用let和const的环境

如果要在web上使用letconst特性,你需要使用一个诸如BabelTraceurTypeScript的ES6转译器。(Babel和Traceur暂不支持临时死区特性。)

io.js支持letconst,但是只在严格模式下编码可以使用。Node.js同样支持,但是需要启用--harmony选项。

本人对“临时死区”的理解有限,但是想深入理解的话再单独研究它就可以了。

文章目录
  1. 1. 以下为详细内容:
    1. 1.1. let
      1. 1.1.1. let的块级作用域
        1. 1.1.1.1. // 例如:(如果你看不懂解释,只要记住例子的结果就行了)
        2. 1.1.1.2. // 解释该例:
        3. 1.1.1.3. // 而如果我们在该例中使用的是let:
      2. 1.1.2. let不再变量提升
      3. 1.1.3. let不可以重复定义
    2. 1.2. const
      1. 1.2.1. // 注意1:
      2. 1.2.2. // 注意2:
  2. 2. 能用let和const的环境
|