ES6 里的变量和作用域

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

1. 块级作用域的let和const

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

function func() {
    if (true) {
        let tmp = 123;
    }
    console.log(tmp); // tmp未定义
}

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

function func() {
    if (true) {
        var tmp = 123;
    }
    console.log(tmp); // 123
}

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

function func() {
    let foo = 5;
    if (···) {
        let foo = 10; // shadows outer `foo`
        console.log(foo); // 10
    }
    console.log(foo); // 5
}

2. const创建不可变的变量

let创建的变量是可变的

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

const创建变量是不可变的

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

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

const obj = {};
obj.prop = 123;
console.log(obj.prop); // 123

obj = {}; // TypeError

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

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

2.1 循环体内的const

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

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?

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

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

不过你可以使用const修饰引用类型的变量。

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

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

const EMPTY_ARRAY = Object.freeze([]);

3. 临时禁区(TDZ)

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

使用 var 修饰的变量没有 TDZ。

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

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

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

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

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

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 是临时的(基于时间)的而不是基于位置的:

if (true) { // 一个新的作用域, TDZ 开始
    const func = function () {
        console.log(myVar); // OK!
    };

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

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

3.1 TDZ的类型检查

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

if (true) {
    console.log(typeof tmp); // ReferenceError
    let tmp;
}

我不认为这将在实践中是一个问题。因为你不能有条件的给某一个作用域加上let修饰符。事实上你仍然可以使用var修饰符创建全局变量

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

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

在循环体中,你每次迭代重新绑定用let修饰的变量。允许你这样做的循环:for, for-infor-of

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声明的循环体中,,每次迭代室友一个单一的值

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 形参和局部变量

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

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

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

function func(arg) {
    {
        let arg; // 影子参数 `arg`
    }
}

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

function func(arg) {
    var arg; // does nothing
}
function func(arg) {
    {
        var arg; // does nothing
    }
}

5.2 默认形参与TDZ

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

// 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

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

// 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中的全局对象(浏览器是windows,Node.js是global)的bug比特性还要多,尤其在性能这一块,这也就是不奇怪ES6有以下描述:

  • 全局对象的属性都是全局变量。在全局范围,varfunction 声明创建这些属性
  • 是全局变量但不是全局对象的属性。在全局范围,letconst, Class 声明创建这些属性

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

函数声明:

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

下面代码解释了声明提升

{ // Enter a new scope

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

类的声明:

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

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

{ // 进入新的作用域
    
    const identity = x => x;

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

    //注意 `extends`
    class MyClass extends identity(Object) {
    }
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容