在 ES5 中,变量声明只有 var 和 function 以及隐式声明三种,在 ES6 中则增加了 let、const、import 和 class 四种。
1. let
1.1 块级作用域
let 声明的变量的作用域是块级作用域(这个特性有点类似于后台语言),ES5 并没有块级作用域,只有函数作用域和全局作用域。
{
let a = ‘ES6’;
var b = ‘ES5’;
}
console.log(b) // ES5
console.log(a) // ReferenceError: a is not defined.
那么 let 的块级作用域有什么好处呢?
let 非常适合用于 for 循环内部的块级作用域。JS 中的 for 循环体比较特殊,每次执行都是一个全新的独立的块作用域,用 let 声明的变量传入到 for 循环体的作用域后,不会发生改变,不受外界的影响。看一个常见的面试题目:
for (var i = 0; i <10; i++) {
setTimeout(function() {// 同步注册回调函数到异步的宏任务队列。
console.log(i); // 执行此代码时,同步代码 for 循环已经执行完成
}, 0);
}
// 输出结果
10(共 10 个)
// 这里变量为 i 的 for 循环中,i 是一个全局变量,在全局范围内都有效,所以每一次循环,新的 i 值都会覆盖旧值,导致最后输出的是最后一轮 i 的值,即 i 的最终结果为 10,实际上都是 console.log(10)。涉及的知识点:JS 的事件循环机制,setTimeout 的机制等
把 var 改成 let 声明:
for (let i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i); // 当前的 i 仅在本轮的循环中有效,就是说每一次循环,i 其实都是一个新产生的变量。// 用 let 声明的变量 i 只作用于循环体内部,不受外界干扰。
}, 0);
}
// 输出结果:
0 1 2 3 4 5 6 7 8 9
1.2 暂时性死区(Temporal Dead Zone)
在一个块级作用域中,变量唯一存在,一旦在块级作用域中用 let 声明了一个变量,那么这个变量就唯一属于这个块级作用域,不受外部变量的影响,如下面所示。
var tmp = ‘bread and dream’;
if(true){
tmp = ‘dream or bread’; //ReferenceError
let tmp;
}
这个例子中 tmp = ‘dream or bread’ 的赋值会报错,因为在 if 块中的 let 对 tmp 变量进行了声明,导致该 tmp 绑定了这个作用域,而 let 临时死区导致了并不能在声明前使用,所以在声明前对变量赋值会报错。
暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
暂时性死区的意义也是让我们标准化代码,将所有变量的声明放在作用域的最开始。
1.3 不允许重复声明
(1) 在相同的作用域内,用 let 声明变量时,只允许声明一遍,但 var 是可以多次声明的。大家都知道 ES5 多次声明会造成变量覆盖而且不会报错,这就给调试增加了难度,而 let 能够直接扼杀这个问题在摇篮之中,因为会直接报错。
// 不报错
function demo() {
var a = ‘bread and dream’;
var a = ‘dream or bread’;
}
// 报错,Duplicate declaration “a”
function demo() {
let a = ‘bread and dream’;
var a = ‘dream or bread’;
}
// 报错,Duplicate declaration “a”
function demo() {
let a = ‘bread and dream’;
let a = ‘dream or bread’;
}
(2) 不能在函数内部重新声明参数:
function demo1(arg) {
let arg; // 报错
}
demo1()
function demo2(arg) {
{
let arg; // 不报错
}
}
demo2()
2. const
2.1 用于声明常量
const 声明的常量是不允许改变的,只读属性,这意味常量声明时必须同时赋值,只声明不赋值,就会报错,通常常量以大写字母命名。
const Person; // 错误,必须初始化
const Person = ‘bread and dream’;// 正确
const Person2 = ‘no’;
Person2 = ‘dream or bread’; // 报错, 不能重新赋值
这样做的两个好处:一是阅读代码的人立刻会意识到不应该修改这个值,二是防止了无意间修改变量值所导致的错误。比如我们使用 nodejs 的一些模块的时候,我们只是使用对应的模块(如 http 模块),但是并不需要修改 nodejs 的模块,这个时候就可以声明成 const,增加了代码的可读性和避免错误。
2.2 支持块级作用域
const 和 let 类似,也是支持块级作用域.
if (true) {
const MIN = 5;
}
MIN // Uncaught ReferenceError: MIN is not defined
2.3 不支持变量提升,有暂时性死区
const 声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。
if (true) {
console.log(MIN); // ReferenceError
const MIN = 5;
}
2.4 特殊情况
如果声明的常量是一个对象,那么对于对象本身是不允许重新赋值的,但是对于对象的属性是可以赋值的。
const obj = {};
obj.a = ‘xiao hua’;
console.log(obj.a); //’xiao hua’
实际上 const 能保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。
但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针。
至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
如果要彻底将对象冻结(不可修改其属性),应该使用 Object.freeze(obj) 方法。同理,数组也是一样的。
3. import
ES6 采用 import 来代替 node 等的 require 来导入模块。
import {$} from ‘./jquery.js’
$ 对象就是 jquery 中 export 暴露的对象。
如果想为输入的变量重新取一个名字,import 命令要使用 as 关键字,将输入的变量重命名。
import {JQ as $} from ‘./jquery.js’;
注意,import 命令具有提升效果,会提升到整个模块的头部,首先执行。
4. class
ES6 引入了类的概念,有了 class 这个关键字。类的实质还是函数对象。
先定义一个类:
// 定义类
class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
setSex(_sex) {
this.sex=_sex;
}
}
constructor 方法,就是构造方法,也就是 ES5 时代函数对象的主体,而 this 关键字则代表实例对象。
上面的类也可以改成 ES5 的写法:
function Animal(name, age){
this.name = name;
this.age = age;
}
Animal.prototype.setSex = function (_sex) {
this.sex=_sex;
}
其实,大多数类的特性都可以通过之前的函数对象与原型来推导。
生成类的实例对象的写法,与 ES5 通过构造函数生成对象完全一样,也是使用 new 命令。
class Animal {}
let dog = new Animal();
在类的实例上面调用方法,其实就是调用原型上的方法,因为类上的方法其实都是添加在原型上。
Class 其实就是一个 function,但是有一点不同,Class 不存在变量提升,也就是说 Class 声明定义必须在使用之前。
5. 总结
在 ES6 之前,JavaScript 是没有块级作用域的,如果在块内使用 var 声明一个变量,它在代码块外面仍旧是可见的。ES6 规范给开发者带来了块级作用域,let 和 const 都添加了块级作用域,使得 JS 更严谨和规范。
let 与 const 相同点:
块级作用域
有暂时性死区
约束了变量提升
禁止重复声明变量
let 与 const 不同点:
const 声明的变量不能重新赋值,也是由于这个规则,const 变量声明时必须初始化,不能留到以后赋值。
合理的使用 ES6 新的声明方式,不管是面试还是工作中都有实际的应用,尤其是工作中,大家一定要尽量的多使用新的声明方式,不但可以让代码更规范,更可以避免不必要的 bug,浪费调试时间,进而提高工作效率。