古语云:“纸上得来终觉浅,绝知此事要躬行”。的确,不管看了多少本书,如果自己不实践,那么就很难领会其中的精髓。自己研读过许多ES6相关的书籍和资料,平时工作中也会用到,但在用到时经常需要上搜索引擎中查询相关知识概念,并且对很多知识也仅仅是略知一二,没有领会到其中的原理。为此,开辟了《ES6躬行记》系列,将ES6相关的知识系统的记录下来,以便自己翻阅,也希望能帮助到广大网友。
在ES6之前的版本中,用于声明变量的关键字只有var,并且没有块级作用域,只有函数作用域和全局作用域,但在ES6中已改变这种状况。ES6引入了let和const两个关键字,它们既可以用于声明变量,还能够将变量绑定到当前所处的任意作用域中,换句话说,就是把变量的作用域封闭在所处的代码块(即花括号字符“{”和“}”之间的区域,例如if条件语句中的代码)中,如此一来就形成了块级作用域。let和const两个关键字与var之间的不同,简单的说有以下三点:
(1)不允许声明提升。
(2)不允许重复声明。
(3)不覆盖全局变量。
而在let与const之间也有不同之处,在下面的内容中会重点讲解。
一、let
let可以理解为var的升级版本,摒弃或纠正了var的一些会导致代码混乱的特性,从而使得代码的逻辑更清晰,可维护性更高。
1)提升
首先要介绍的是用let声明的变量,其声明语句不会再被提升。下面将两个变量分别用var和let声明,再输出它们的结果,具体如下所示。
console.log(outer); //undefined
console.log(inner); //抛出未定义的引用错误 {
console.log(outer); //undefined
console.log(inner); //抛出未定义的引用错误
var outer = true;
let inner = true;
console.log(outer); //true
console.log(inner); //true
} console.log(outer); //true
console.log(inner); //抛出未定义的引用错误
两个变量都在代码块中执行赋值操作。第一个outer变量由于是用var声明的,因此它的声明语句能够被提升,而在第一次和第二次输出时,因为还没被赋值,所以输出的结果都为undefined,在赋完值后,输出的结果就都是true。第二个inner变量是用let声明的,由于声明语句不能被提升到代码块的外部,因此它的作用域只限于代码块内,在代码块外就无法访问到它,并且在声明它之前也无法被访问(因为声明语句也不会被提升至作用域顶部),此时变量正处于临时死区中。如果强行访问,那么就会抛出未定义的引用错误。
临时死区(Temporal Dead Zone,简称TDZ)也叫暂时性死区,用let或const声明的变量,在声明之前都会被放到TDZ中,而在此时访问这些变量就会触发运行时错误。这种语法设计能促进工程师们在平时养成良好的编码习惯,减少或杜绝一些由于始料未及的原因而产生的程序BUG。有一点要注意,TDZ并不是由ECMAScript标准命名的,它是JavaScript社区的一种约定俗成的叫法。
2)重复声明
接下来介绍let的第二个特性:不允许重复声明。注意,这里有一个前置条件,那就是只有在相同作用域时,才不允许同一个变量重复声明。ES6引入的这个重复声明的检查机制,可以避免在多人协作开发的时候,因多声明一个或多个同名的变量,而影响别人代码逻辑的情况出现。下面的示例就分了两个作用域分别说明,并且对比了var和let对待重复声明的处理结果。
var duplicate;
let repeat; var duplicate; //var声明的变量可重复声明
let duplicate; //抛出重复声明的语法错误
let repeat; //抛出重复声明的语法错误
{
let repeat; //不同作用域,可正常声明
}
3)全局作用域
最后介绍的是let在全局作用域中的特性。当用var在全局作用域中声明变量的时候,该变量不但会成为全局变量,而且还会成为全局对象(例如浏览器中的window对象)的一个属性。全局对象以及它的属性可以在任何位置被访问,这样就影响了函数的封装,不利于代码的模块化,并且新声明的全局变量有可能会覆盖全局对象中已存在的属性。上述是两个比较有代表性的弊端,为了解决这些问题,ES6规定用let可将全局变量和全局对象断开联系。下面用两组代码分别演示断开联系(第一组)和覆盖已有属性(第二组),注意,在第二组代码中为了方便对比,忽略了重复声明的错误。
//第一组
var global = true;
console.log(window.global); //true
let whole = true;
console.log(window.whole); //undefined //第二组
var Math = true;
console.log(window.Math); //true
let Math = true;
console.log(window.Math); //Math对象
二、const
const不但拥有上面所述的let的三个特性,并且还能声明一个常量。常量是指一个定义了初始值后固定不变的只读变量。在过去,常量都是通过命名规范(例如用大写字母、下画线等字符)定义的,虽然实现起来很便捷,但由于缺少约束,因此这种常量很容易被修改。而在ES6引入了const关键字后,就能像其它编程语言那样声明常量了。有一点要注意,const与let不同,在声明时必须初始化(即赋值),并且在设定后,其值无法再更改,如下代码所示。
const number; //抛出未初始化的语法错误
const digit = 10;
digit = 20; //抛出赋值给常量的类型错误
此处要强调一点,const限制的其实是变量与内存地址之间的绑定,也就是说,const让变量无法更改所对应的内存地址。如果是基本类型(例如布尔值、数字等)的变量,那么对应的内存地址中保存的就是值;如果是引用类型(例如对象)的变量,那么对应的内存地址中保存的是指向实际数据的一个指针。由此可知,当用const声明的变量,其初始化的值是对象时,可以修改对象中的属性或方法,具体可参考下面的代码。
const obj = {};
obj.name = "strick";
obj.age = function() {
return 29;
};
三、循环中的let和const
ES6规定了let声明在循环内部的行为,以for循环为例,如下所示。
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 0);
}
在控制台输出的结果依次为0、1、2,没有出现循环中的异步回调问题。这是因为在每次循环的时候,都会重新创建一个叫做i的同名变量,并将其初始化为计算后的值,而循环体内调用的i变量不会受其它同名变量的影响,所以能够在定时器的回调函数中正确显示该变量的值。在ES5及之前的版本中,如果要解决异步回调问题,就需要像下面这样借助立即执行函数表达式(IIFE)才能得到预期的效果。
for (var i = 0; i < 3; i++) {
(function(n) {
setTimeout(function() {
console.log(n);
}, 0);
})(i);
}
const声明在循环内部的行为与let声明类似,不过,由于其值无法修改,因此在for循环中重新赋值(例如执行增量操作)将会抛出异常。但只要避开重新赋值,就能正常循环迭代,例如用for-in执行const声明的循环,如下所示。
var author = {
name: "strick",
age: 29
};
for (const key in author) {
console.log(key);
}