ES6中的let、const和var

1 细数 var 的种种原罪

1.1 var 使用时的变量提升

总所周知,JavaScript中的作用域并不是块级作用域,而是函数作用域。所谓的作用域就是可以访问到某一个变量的代码范围,那么函数作用域又是怎么样的呢?
简单的来说,在我们用var声明变量时,这个变量的作用域是向声明的上下两个方向同时延伸,直到到达函数边界,这个函数边界包围的代码范围就是var的作用域。
也因此js引擎会将每一个var声明和函数声明都提升到封闭的函数顶部,这样才可以在变量的使用前把变量先声明,这种js引擎的处理就是“变量提升(hoisting)”。
可以看一个简单的例子:

1
2
3
4
5
6
7
8
function f2 () {
var a = 1;
console.log(a);
console.log(b);
// console.log(c);
var b = 2;
}
f2();

输出结果为:

console1

变量提升固然有其必要性,但是有的情况下会出现undefinedReferenceError,还有相同变量名时产生的错误,都让人感觉困惑难以追查问题。

1.2 闭包的困惑

MDN上对于闭包(Closure)的定义:

闭包是指那些能够访问独立(自由)变量的函数 (变量在本地使用,但定义在一个封闭的作用域中)。换句话说,这些函数可以“记忆”它被创建时候的环境

简单的说,闭包就是指内部函数可以访问外部函数的一种机制,但外部函数并不能访问内部函数的变量。
以下是一段前端面试很喜欢用到的代码:

1
2
3
4
5
6
7
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(new Date(), i);
}, 1000);
}

console.log(new Date(), 1);

结果应该是立即输出一个时间点和5,而在短暂的时间间隔后同时连续5次输出同一个时间点和5(考虑到js的定时机制并不是完全准确的,这里的时间间隔应该是大约在1秒后输出)。
那为什么不是输出不同的时间和0到5呢?
因为这里其实共用了同一个变量i,五个超时回调同时使用一个i,亦即构成了闭包,就会造成在循环完成时,i的值被赋为5,此时所有的超时回调还没有被回调到。
那么应该如何去修改成输出不同的结果呢?在这里可以选择一个比较简单的修改,就是用IIFE(Immediately Invoked Function Expression:声明即刻执行的函数表达式)来解决闭包造成的问题。

1
2
3
4
5
6
7
8
9
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(function () {
console.log(new Date(), j);
}, 1000);
})(i);
}

console.log(new Date(), 1);

可以看出来其实var的这些不便,都是早期的 JavaScript 设计导致的,很多编程语言都是向后兼容的,所以这种设计上的失误无法被修复。所以在 ES6 中推出了新的变量声明关键字let,来让作用域规则更加合理。

2 新 var: let

2.1 let 和 var 的区别

MDN上对于let语句的定义如下:

let 语句声明一个块级作用域的本地变量,并且可选的赋予初始值。

用法和 var 一样,进一步的描述为:

let允许你声明一个作用域被限制在块级中的变量、语句或者表达式。与var关键字不同的是,var声明的变量只能是全局或者整个函数块的

简单的说,let 的作用域是块,而 var 的作用域是函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 5;
var b = 10;

if (a === 5) {
let a = 4; // The scope is inside the if-block
var b = 1; // The scope is inside the function

console.log(a); // 4
console.log(b); // 1
}

console.log(a); // 5
console.log(b); // 1

所以可以知道在程序或者函数的顶层,let 并不会想 var 一样在全局对象上创造一个属性:

1
2
3
4
var x = 'global';
let y = 'global';
console.log(this.x); // "global"
console.log(this.y); // undefined

此外还要注意一点,let 变量的重复声明是语法错误,所以如果有一些脚本声明了相同的全局变量,那么在二次加载的时候会出现报错。

let 是一个严格模式下的保留词。在非严格模式的代码中,为了向后兼容,你仍然可以声明变量、函数和名为 let 的实参——你可以写 var let = 'q';,而不是你原本想写的那样。let let; 也是不允许的。

2.2 Const

const 声明创建一个只读的常量。这不意味着常量指向的值不可变,而是变量标识符的值只能赋值一次。

1
2
3
4
const PI = 3.14;

PI // 3.14
PI = 3; // TypeError: Assignment to constant variable.

const 的声明需要立即进行赋值,否则会报错。

1
const FOO; // SyntaxError: missing = in const declaration

在给一个常量定义成对象时需要注意区分这里的常量指的是这个对象的地址,因为复合类型的变量,变量名不指向数据,而是指向数据所在的地址(这里跟C里的指针十分类似)。

可以简单地理解成存储数据的地址是常量,这个地址是不变的,而该存储单元存储的数据是可以改变的。

1
2
3
4
5
6
const MY_OBJECT = {"key": "value"};

MY_OBJECT = {"OTHER_KEY": "value"}; // TypeError: Assignment to constant variable.

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

还有个关于数组的例子:

1
2
3
4
const a = [];
a.push('Hello'); // ['Hello']
a.length = 0; // []
a = ['Dave']; // Assignment to constant variable.

参考资料:
let 和 const
80% 应聘者都不及格的 JS 面试题
let - JavaScript|MDN