ES6中的块作用域

简介

ES6是JavaScript语言的一次重大更新,于2015年6月正式发布。它的目标是利用Javascript可以构建复杂的应用程序。

各大浏览器推出的最新版本已经对ES6有了更高的支持度,实际的支持情况可以查看kangax.github.io/es5-compat-table/es6/, 如果你不放心你的代码是否可以在所有的浏览器上运行,可以使用Babel转码器将ES6转为ES5的代码。

语法

本文的示例代码使用ES6,你可以利用ES6Fiddle,或者Babel转码器命令行工具来运行代码。

块作用域声明

在Javascript中,作用域是function范围内的。如果你需要一个块作用域,最常用的方式就是使用IIFE(立即执行函数)。例如:

1
2
3
4
5
6
7
8
var a = 2;

(function IIFE(){
var a = 3;
console.log( a ); // 3
})();

console.log( a ); // 2

let 声明

有了ES6,现在我们可以创建块作用域。我们不要 var 标识符,取而代之的是 let 标识符:

1
2
3
4
5
6
7
8
var a = 2;

{
let a = 3;
console.log( a ); // 3
}

console.log( a ); // 2

这是一个定义块作用域最好的方式,利用 {...} 将作用域括起来, 建议将所有的变量声明放在最顶端,例如:

1
2
3
4
{  
let a = 2, b, c;
// ..
}

这是一个良好的编码习惯。

有另外一种let 声明的实验形式(不是标准形式),叫做 let-块,是这样的:

1
2
3
let (a = 2, b, c) {
// ..
}

这种声明形式更加的清晰,对比上面的两种形式,它们是十分相似的。不幸的是ES6标准采用了上面这种形式,而不是下面这种更加清晰的清晰的方案。

为了说明 let .. 声明的implicit,考虑下面这种使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let a = 2;

if (a > 1) {
let b = a * 3;
console.log( b ); // 6

for (let i = a; i <= b; i++) {
let j = i + 10;
console.log( j );
}
// 12 13 14 15 16

let c = a + b;
console.log( c ); // 8
}

哪些变量仅仅在 if 语句中存在,哪些变量在for 语句中存在?

答案是: if 语句包含了ab , for 语句包含了ij

我们再看一种情况,如果let 的声明太晚会引起一些错误。不像var 会有声明的提升:

1
2
3
4
5
6
7
{
console.log( a ); // undefined
console.log( b ); // ReferenceError!

var a;
let b;
}

执行console.log(b) 时会有个错误,因为你调用了一个还没有被初始化的变量。变量b 在作用域内已经被声明了,但是直到 let b 语句执行之前, 变量b 没有被初始化,在这之前想要获取b 的值会引发一个错误。注意:转化为ES5的代码,并不会引发这个错误。

let + for

考虑:

1
2
3
4
5
6
7
8
9
var funcs = [];

for (let i = 0; i < 5; i++) {
funcs.push( function(){
console.log( i );
} );
}

funcs[3](); // 3

let i 语句为循环的每一次迭代重新定义了一个新的 i。 这意味在循环内部创建了一个闭包。

如果你使用 var i ,最后输出的结果是5而不是3, 这是因为作用域内仅仅一个变量 i 被闭包闭合,而不是每一次都有一个新的变量 i

可以通过下面的代码,实现相同的效果:

1
2
3
4
5
6
7
8
9
10
var funcs = [];

for (var i = 0; i < 5; i++) {
let j = i;
funcs.push( function(){
console.log( j );
} );
}

funcs[3](); // 3

在上面的代码中我们每次都创建一个新的变量j,所以和前面的代码有相同的原理。

const声明

有另外一种块作用域声明方式:const 用来创建常量。

常量是初始化后read-only的变量。

1
2
3
4
5
6
{
const a = 2;
console.log( a ); // 2

a = 3; // TypeError!
}

常量并不是对值的限制,而是对变量赋值的限制。const 并不是让变量的值不能变,而是让变量不能够被赋值。如果值是一个数组或者一个对象,值的内容是可以被修改的:

1
2
3
4
5
6
7
{
const a = [1,2,3];
a.push( 4 );
console.log( a ); // [1,2,3,4]

a = 42; // TypeError!
}

变量 a 并不是一个常数组,而是它有一个到这数组的常量引用,这个数组是可变的。

const or not

有传闻说JS引擎能够对 const 进行优化, 相对于这一点,更重要的是你是否需要一个常量值。

所以建议仅仅在你需要显示表明这个值不可变的情况下使用 const, 而不是依赖 const 对代码的效率进行优化。

块作用域中的函数

考虑如下代码:

1
2
3
4
5
6
7
8
9
{
foo(); // works!

function foo() {
// ..
}
}

foo(); // ReferenceError

foo() 函数在块作用域中声明,多以无法在作用域外被调用。需要注意的是,在作用域内,像以前一样,函数声明具有声明提升的效果。

像下面这样的代码就会有问题:

1
2
3
4
5
6
7
8
9
10
11
12
if (something) {
function foo() {
console.log( "1" );
}
}
else {
function foo() {
console.log( "2" );
}
}

foo(); // ??

在之前的ES环境中,不管something的值,运行结果是2。因为函数声明的提升效果,而且往往后面的声明会战胜前面的声明。

在ES6中,会得到一个引用错误。

总结

在ES6中可以使用 let, const 得到块作用域,在作用域中声明的变量,作用域之外无法访问。并且,函数的声明也是块作用域,在作用域内, let 的声明没有变量提升的效果,函数的声明仍旧有提升的效果。