译-ES6里的变量和作用域

February 25, 2015

Reading time ~10 minutes

http://www.2ality.com/2015/02/es6-scoping.html 原文链接

在本文将有大量的例子介绍在ES6中作用域和变量的使用方法

1.块级作用域的let和const

1
let
1
const
创造块级作用域,他仅仅存在于包裹他们的最内层的块。下面代码演示了使用
1
let
修饰的
1
tmp变量
仅仅存在于最里层的
1
if
申明里。

1
2
3
4
5
6
function func() {
    if (true) {
        let tmp = 123;
    }
    console.log(tmp); // tmp未定义
}

相比之下,用var申明的变量在函数级作用域

1
2
3
4
5
6
function func() {
    if (true) {
        var tmp = 123;
    }
    console.log(tmp); // 123
}

块级作用域意味着你在函数里只要是两个不同的块,那么变量名称可以重复。(原文为影子变量

1
2
3
4
5
6
7
8
function func() {
    let foo = 5;
    if (···) {
        let foo = 10; // shadows outer `foo`
        console.log(foo); // 10
    }
    console.log(foo); // 5
}

2.
1
const
创建不可变的变量

1
let
创建的变量是可变的

1
2
3
let foo = 'abc';
foo = 'def';
console.log(foo); // def

1
const
创建变量是不可变的

1
2
const foo = 'abc';
foo = 'def'; // TypeError

注意,

1
const
并不影响所赋的值是否可变,如果所赋的值是一个对象,那么并不能保证该对象不变。他只是保存一个对象的引用。

1
2
3
4
5
const obj = {};
obj.prop = 123;
console.log(obj.prop); // 123

obj = {}; // TypeError

如果你想改变量是真正不可变的,那么直接冻结他的值

1
2
const obj = Object.freeze({});
obj.prop = 123; // TypeError

2.1 循环体内的const

一旦

1
const
变量创建,那么他就不能改变。但这并不意味着你不能重新声明一个新值,比如在循环体内。

1
2
3
4
5
6
7
8
9
10
11
function logArgs(...args) {
    for (let [index, elem] of args.entries()) {
        const message = index + '. ' + elem;
        console.log(message);
    }
}
logArgs('Hello', 'everyone');

// Output:
// 0. Hello
// 1. everyone

2.2 什么时候我该使用let,什么时候该使用const?

1
2
const foo = 1;
foo++; // TypeError

如果你想创建的可变变量为基本类型,则,不能使用const。

不过你可以使用

1
const
修饰引用类型的变量。

1
2
const bar = [];
bar.push('abc'); // array是可变的

按照最佳实践,一般会把常量(真正不变的)使用大写来表示。

1
const EMPTY_ARRAY = Object.freeze([]);

3.临时禁区(TDZ)

1
const
1
let
修饰的变量我叫做它是
1
临时禁区
(TDZ)。当进入这个作用域,外界就无法访问这些被修饰的变量知道运行结束。

使用

1
var
修饰的变量没有TDZ。

  • 当进入有
    1
    var
    
    修饰的变量的作用域中,会在内存中立即创建空间,立即初始化变量,并且设置成
    1
    undifined
    
  • 在执行过程中如遇到赋值关键字则给变量赋值,否则还是为
    1
    undifined
    

使用

1
let
关键字的拥有TDZ,这意味着它的生命周期如下:

  • 当进入有
    1
    let
    
    修饰的变量的作用域中,会在内存中立即创建这个变量,不会初始化这个变量。
  • 获取或设置未初始化的变量会导致引用错误(ReferenceError).
  • 在执行过程中如遇到声明处则初始化且给变量赋值,如果不赋值则为undefined。

1
const
的机制与
1
let
相似,但他必须赋一个值且不能被改变。

在TDZ中,如果获取或者设置一个未初始化会抛出异常。

1
2
3
4
5
6
7
8
9
10
11
if (true) { // 一个新的作用域, TDZ 开始
    //tmp未初始化
    tmp = 'abc'; // ReferenceError
    console.log(tmp); // ReferenceError

    let tmp; // TDZ 结束, `tmp` 被初始化为 `undefined`
    console.log(tmp); // undefined

    tmp = 123;
    console.log(tmp); // 123
}

下面例子演示了TDZ是临时的(基于时间)的而不是基于位置的:

1
2
3
4
5
6
7
8
9
10
if (true) { // 一个新的作用域, TDZ 开始
    const func = function () {
        console.log(myVar); // OK!
    };

    // 在这里已经进入了TDZ,访问 `myVar` 会导致 ReferenceError

    let myVar = 3; TDZ 结束
    func(); // called outside TDZ
}

3.1TDZ的类型检查

一个变量不能再TDZ里访问意味着你也不能在该变量使用

1
typeof

1
2
3
4
if (true) {
    console.log(typeof tmp); // ReferenceError
    let tmp;
}

我不认为这将在实践中是一个问题。因为你不能有条件的给某一个作用域加上

1
let
修饰符。事实上你仍然可以使用
1
var
修饰符创建全局变量

1
2
3
4
if (typeof myVarVariable === 'undefined') {
    // `myVarVariable`不存在,则创建它
    window.myVarVariable = 'abc';
}

4.在循环体的头部中使用
1
let
修饰符

在循环体中,你每次迭代重新绑定用

1
let
修饰的变量。允许你这样做的循环:
1
for
,
1
for-in
1
for-of

1
2
3
4
5
6
7
if (typeof myVarVariable === 'undefined') {
    let arr = [];
    for (let i=0; i < 3; i++) {
        arr.push(() => i);
    }
    console.log(arr.map(x => x())); // [0,1,2]
}

相比之下,用var声明的循环体中,,每次迭代室友一个单一的值

1
2
3
4
5
6
7
if (typeof myVarVariable === 'undefined') {
    let arr = [];
    for (var i=0; i < 3; i++) {
        arr.push(() => i);
    }
    console.log(arr.map(x => x())); // [3,3,3]
}

为每次迭代得到一个新的绑定似乎有些奇怪,但当你使用循环创建函数(例如回调事件处理)它是非常有用。

5.形参

5.1 形参和局部变量

如果你声明的变量名正好与形参一致,那么会爆出一个静态错误

1
2
3
function func(arg) {
    let arg; // static error: duplicate declaration of `arg`
}

在函数里面再嵌套一个块则会避免这个问题

1
2
3
4
5
function func(arg) {
    {
        let arg; // 影子参数 `arg`
    }
}

相比之下,用

1
var
修饰的与形参同名的变量不会出现错误,表现的形式是覆盖了形参。

1
2
3
function func(arg) {
    var arg; // does nothing
}
1
2
3
4
5
function func(arg) {
    {
        var arg; // does nothing
    }
}

5.2 默认形参与TDZ

如果形参有默认值,他们被当做一个序列

1
2
3
4
5
6
7
8
9
10
11
// OK: 声明之后访问x
function foo(x=1, y=x) {
    return [x, y];
}
foo(); // [1,1]

// 异常,在YDZ里试图访问y
function bar(x=y, y=2) {
    return [x, y];
}
bar(); // ReferenceError

5.3 默认形参与TDZ

形参默认值的范围是独立于body的作用域(前者围绕后者)。这意味着“inside”定义的方法或函数参数的默认值不知道body的局部变量。

1
2
3
4
5
6
7
8
9
10
11
// OK: 在x已经声明后y访问x
function foo(x=1, y=x) {
    return [x, y];
}
foo(); // [1,1]

// 异常: `x` 试图在TDZ访问 `y`
function bar(x=y, y=2) {
    return [x, y];
}
bar(); // ReferenceError

6.全局对象

JS中的全局对象(浏览器是

1
windows
,Node.js是global)的bug比特性还要多,尤其在性能这一块,这也就是不奇怪ES6有以下描述:

  • 全局对象的属性都是全局变量。在全局范围,
    1
    var
    
    1
    function
    
    声明创建这些属性
  • 是全局变量但不是全局对象的属性。在全局范围,
    1
    let
    
    1
    const
    
    ,
    1
    Class
    
    声明创建这些属性

7.函数的声明和类的声明

函数声明:

  • 块级作用域,像
    1
    let
    
  • 在全局对象创建属性(在全局范围),像var。
  • 声明提升:独立的一个函数声明中提到它的范围,它总是创建之初的范围

下面代码解释了声明提升

1
2
3
4
5
6
7
{ // Enter a new scope

    console.log(foo()); // OK, due to hoisting
    function foo() {
        return 'hello';
    }
}

类的声明:

  • 块级作用域
  • 不会再全局对象上创建属性
  • 不会声明提升

类不升起可能令人惊讶,因为他们创建函数。这种行为的理由是,他们继承条款定义的值通过表达式,表达式必须在适当的时间执行。

1
2
3
4
5
6
7
8
9
10
11
{ // 进入新的作用域
    
    const identity = x => x;

    //这儿是`MyClass`的TDZ
    let inst = new MyClass(); // ReferenceError

    //注意 `extends`
    class MyClass extends identity(Object) {
    }
}

8.扩展阅读

1.Using ECMAScript 6 today

2.Destructuring and parameter handling in ECMAScript 6

文章来自 http://www.hacke2.cn