ES6中的变量和作用域

发布于 大漠

这篇文章主要是探讨如何处理ES6中的变量和作用域。

通过let和const确定块作用域

使用letconst创建块作用域,他们声明的变量只存在块内。比如下面的示例,let声明的变量tmp只存在于if语句块,也只有在if语句块内有效。

function func () {
    if (true) {
        let tmp = 123;
    }
    console.log(tmp); // ReferenceError: tmp is not defined
}

相比之下,使用var声明的变量,在整个函数域内都有效:

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

块作用域也可以存在于整个函数内:

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

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

如果你想让obj是一个真正的常数,可以使用freeze方法来冻结其值:

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

循环体内的const

一旦通过const创建的变量它就是一个常量,它是不能被改变的。但这也并不意味着你不能重新给其设置一个新值。例如,可以通过一个循环来操作:

function logArgs (...args) {
    for (let [index, elem] of args.entries()) {
        const message = index + '. ' + elem;
        console.log(message);
    }
}
logArgs("Hello", "everyon");

输出的结果

0. Helloe
1. everyone

什么应该使用let,什么时候应该使用const

如果你想改变一个变量保存其原始值,你就不能使用const来声明:

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

然而,你可以使用const声明变量,来引用可变量的值:

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

我还在仔细考虑使用什么方式才是最好的方式,但是目前情况使用的都是像前面的示例,因为bar变量是可变的。我使用const表明变量和值是不可变的:

const EMPTY_ARRAY = Object.freeze([]);

暂时性死区

使用letconst声明的变量有一个所谓的暂时性死区(TDZ):当进入作用域范围,它就不能接受(gotset)访问,直到其被声明。

我们来来看一个有关于var变量的生命周期,它没有暂时性死区:

  • var声明了一个变量,其就有一个存储空间(创建一个所谓的绑定)。变量就初始化了,其默认值为undefined
  • 当执行的范围到达声明处,变量设置为指定的值(如果有赋值的话),如果变量没有赋值,其值仍然是undefined

通过let声明变量存在暂时性死区,这意味着他们的生命周期如下:

  • 当使用let创建一个变量,其就有一个块作用域,也具有一个存储空间(也就是创建一个所谓的绑定)。其值仍未初始化变量
  • 获取或设置一个未初始化的变量,得到一个错误ReferenceError
  • 当执行范围内到达声明的变量处,如果有赋值的话,变量的初始值为指定的初始化值。如果没有,变量的值仍为undefined

使用const声明的变量工作类似于let,但它必须要有一个初始化值,而且不能被改变。

在一个TDZ内,通过if语句秋设置或获取一个变量将会报错:

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
}

下面的例子演示了死区是时间(基于时间),而不是空间(基于位置):

if (true) { // 进入新作用域,TDZ开始
    const func = function () {
        console.log(myVar); // OK
    }
    //在TDZ内访问myVar,会引起ReferenceError错误
    let myVar = 3; // TDZ结束
    func (); // 调用外面的TDZ
}

typeof和TDZ

一个变量在难以接近TDZ时,这也意味着你不能使用typeof:

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

在实践中我不认为这是一个问题,因为你不能有条的通过let声明变量范围。相反,你可以使用var声明变量,而且可以通过window创建一个全局变量:

if (typeof myVarVariable === 'undefined') {
    // `myVarVariable` does not exist => create it
    window.myVarVariable = 'abc';
}

循环头中的let

在循环中,你可以通过let声明变量,为每次迭代重新绑定变量。比如在forfor-infor-of循环中。

看起来像下面:

let arr = [];
for (let i = 0; i < 3; i++) {
    arr.push(() = > i);
}
console.log(arr.map(x => x())); // [0,1,2]

相比之下,使用var声明的变量将在整个循环中都可以工作:

let arr = [];
for (var i = 0; i < 3; i++) {
    arr.push(() => i);
}
console.log(arr.map(x => x())); // [3,3,3]

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

参数

参数和局部变量

如果使用let声明变量,它有一个相同的名称,称作参数。静态加载会出错:

function func (arg) {
    let arg; // Uncaught SyntaxError: Identifier 'arg' has already been declared
}

同样的,将其放在一个作用块里:

function func (arg) {
    {
        let arg; // undefined
    }
}

相比之下,使用var声明一个和参数相同变量,其作用范围在同一个范围内:

function func (arg) {
    var arg;
}

或者

function func (arg) {
    {
        var arg;
    }
}

参数默认值和TDZ

如果参数有默认值,他们会当作一系列的let语句,而且存在TDZ。

// OK: `y` accesses `x` after it has been declared
function foo(x=1, y=x) {
    return [x, y];
}
foo(); // [1,1]

// Exception: `x` tries to access `y` within TDZ
function bar(x=y, y=2) {
    return [x, y];
}
bar(); // ReferenceError

默认参数不知道其自身的范围

参数默认值的范围是独立于其自身范围。这意味着内部定义的方法或函数参数的默认值不知道其内部的局部变量:

let foo = 'outer';
function bar(func = x => foo) {
    let foo = 'inner';
    console.log(func()); // outer
}
bar();

全局对象

JavaScript全局对象(浏览器中的window,Node.js中的global)存在的问题比他的特性多,尤其是性能。这也是为什么ES6中不引用的原因。

全局对象的属性都是全局变量,在全局作用域下都有效,可以通过varfunction方式声明。

但现在全局变量也不是全局对象。在全局作用域下,可以通过letconst或者class声明。

函数声明和class声明

function声明:

  • let一样,声明的是一个块作用域
  • var一样,在全局对象创建属性(全局作用域)
  • 存在生命提升:独立于一个函数声明中提到它的范围,它总是存在于开始时创建的范围内

下面的代码演示了函数声明的提升:

{ // Enter a new scope

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

类声明:

  • 是一个块作用域
  • 不能在全局对象上创建属性
  • 不存在生命提升

class不存在生命提升可能令人惊讶,那是因为其存在于引擎下,而不是一个函数。这种行为的理由是,他们扩展表达式。这些表达式在适当的时间内被执行。

{ // Enter a new scope

    const identity = x => x;

    // Here we are in the temporal dead zone of `MyClass`
    let inst = new MyClass(); // ReferenceError

    // Note the expression in the `extends` clause
    class MyClass extends identity(Object) {
    }
}

本文根据@Dr. Axel Rauschmayer的《Variables and scoping in ECMAScript 6》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:http://www.2ality.com/2015/02/es6-scoping.html

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.fedev.cn/javascript/variables-and-scoping-in-es6.htmlKobe 11 Mentality