「let」和「const」
ES6终于带来了期待已久的块级作用域:let和const。
块级作用域
let和const声明的变量都具有块级作用域,即它们只存在于包含它们的最内层的块。
而之前的var声明的变量的作用域为函数作用域,即包含它们的最内层函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
if ( true ) {
let a = 'a';
const b = 'b';
var c = 'c'
console.log(a); // => 'a'
console.log(b); // => 'b'
console.log(c); // => 'c'
}
console.log(a); // ReferenceError: a is not defined(抛出异常,终止执行)
console.log(b); // ReferenceError: b is not defined(实际并不会执行到这里)
console.log(c); // => 'c'(实际并不会执行到这里)
}
const用于声明引用不可变的变量
const声明的并不是常量,而是声明的变量的引用不可被改变,即(同一作用域内)一旦声明,不可以被再次赋值,也不可以被再次声明,所有声明的时候必须进行初始化。
1
2
3
4
const foo = 'foo';
// 即使是重新赋值为相同的值,也是不允许的
foo = 'foo'; // SyntaxError: Assignment to constant variable
使用const进行声明而不进行初始化,会抛出异常并且终止程序。
1
const foo; // 抛出异常,终止程序
如果需要声明真正意义上的常量,可以根据需要,使用Object.preventExtensions()、Object.freeze()或者Object.seal()对对象进行处理。
let和const之间的选择
在只有var用于声明变量时,我们只需要使用var声明变量就可以了。现在有了let和const,虽然不会再使用var了,但是却需要在let和const之间做一个选择。
一个简单的原则是:在声明的变量所在的块中,只要该变量不会被再次赋值,就使用const;否则,使用let。
然后你会发现,大多数情况下,使用的是const。
1
2
3
4
5
6
7
8
for ( let i = 0; i < 2; i++ ) {
const double = i * 2;
console.log(i, double);
// =>
// 0 0
// 1 2
}
怎么回事?使用const声明的变量不是不可以被再次赋值的吗?这是因为变量double在循环体执行结束时被销毁,而在下一次循环时都被重新创建。
每次执行块时重新创建块级作用域的变量
就像函数作用域的变量生命周期为函数调用期间,即函数开始执行时创建,函数执行结束时被销毁。同一个函数的每次调用时创建的局部变量相互之间是独立的。
同样的,对于块级作用域的变量而言,它们的生命周期为该变量被创建到块执行结束期间。即同一个块在每次执行时创建的块级变量相互之间是独立的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const arr1 = [],
arr2 = [];
for (let i = 0; i < 2; i++ ) {
setTimeout(function() {
arr1.push(i);
}, 0);
}
for ( var j = 0; j < 2; j++ ){
setTimeout(function() {
arr2.push(j);
}, 0);
}
setTimeout(function() {
console.log(arr1); // => [0, 1]
console.log(arr2); // => [2, 2]
}, 0);
在上面的代码片段中,setTimeout()用于模拟异步。从结果可以看出,当使用let时,每次循环中的变量i是相互独立的。
如果不使用let,则需要在每次循环中引入一个闭包
1
2
3
4
5
6
7
8
9
10
11
12
13
const arr = [];
for ( var i = 0; i < 2; i++ ) {
(function(tmp) { // 在每次循环中引入一个闭包
setTimeout(function() {
arr.push(tmp);
}, 0);
})(i);
}
setTimeout(function() {
console.log(arr);
}, 0);
时间死区(TDZ:temporal dead zone)
使用var声明的变量被提升到函数顶部,即使用var声明的变量的生命周期就是函数的生命周期。
1
2
3
4
5
6
7
(function() {
console.log(foo); // => undefined
var foo = 'foo';
console.log(foo); // => 'foo'
}();
所以,在“var时代”,一个最佳实践就是在函数顶部声明所有需要的变量。
使用let/const声明的变量则不一样,只有在该变量被声明后才可以被访问,这被称为“时间死区”(TDZ)。
1
2
3
4
5
6
7
8
9
10
(function() {
console.log(foo); // => ReferenceError: foo is not defined(抛出异常,程序终止)
console.log(bar); // => ReferenceError: bar is not defined(实际不会执行,用于演示)
const foo = 'foo';
let bar = 'bar';
console.log(foo); // => 'foo'(实际不会执行,用于演示)
console.log(bar); // => 'bar'(实际不会执行,用于演示)
})();
需要特别指出的是:时间死区是基于时间的,不是基于空间的。
1
2
3
4
5
6
7
8
9
(function() {
function foo() {
console.log(bar);
}
let bar = 'bar';
foo(); // => 'bar'
})();
因为时间死区的存在,使用let/const声明变量的最佳实践现在变成了就近声明。
相同作用域内,不能重复声明同一个变量
在同一个作用域内,使用var重复声明一个变量,实际效果是什么也不做;但是使用let重复声明一个变量时会抛出异常。
1
2
3
4
5
(function() {
let foo = 'foo';
let foo; // SyntaxError: Identifier 'foo' has already been declared
})();
什么?你问const呢?const声明的变量连再次赋值都不被允许,重复声明当然也是不被允许的呀。
因为函数的行参相当于这个函数中的局部变量,所以在一个函数的顶级作用域中不能声明和行参同名的变量。
1
2
3
(function(foo) {
let foo = 'foo'; // SyntaxError: Identifier 'foo' has already been declared
})();