ES6中的变量和作用域

发布于 大漠

ES6提供了两种新的声明变量的方法:letconst,它们主要替换ES5中使用var声明变量。

let

let工作类似于var,但是它声明的变量只在块作用域,也只存在于当前块作用域。var工作于函数作用域。

在下面的代码中,你可以看到let声明的变量tmp只在if块中用效:

function order(x, y) {
    if (x > y) {
        let tmp = x;
        x = y;
        y = tmp;
    }
    console.log(tmp === x); // => ReferenceError: tmp is not defined
    return [x, y];
}
order(5, 2);

const

const的工作方式类似于let,但是你声明的变量必须立即初始化,并且值是不能改变。

const foo; // => SyntaxError: Missing initializer in const declaration
const bar = 123;
bar = 456; // => TypeError: Assignment to constant variable.

由于for-of每次循环迭代创建一个绑定(一个变量的存储空间),所以循环内可以使用const声明变量:

for (const x of ['a', 'b']) {
    console.log(x);
}
// Output:
// => a
// => b

声明变量的方法

下面的表格概述了在ES6中可以声明变量的六种方法:

声明变量方法 提升 作用域 创建全局属性
var Declaration Function Yes
let TDZ Block No
const TDZ Block No
function Complete Block Yes
class No Block No
import Complete Module-global No

通过letconst创建块作用域

letconst创建的变量只存在块作用域,它们只存在于包围它们的最内层块中。下面的代码演示了const声明的变量tmp只存在于if语句块中:

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

相比之下,var声明的变量是在函数作用域内:

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

块作用域意味着你可以在函数内有隐藏变量(Shadow Variables):

function func() {
    const foo = 5;
    if (true) {
        const foo = 10;   // 隐藏在外面的变量`foo`
        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: Assignment to constant variable.

改变const声明变量总是抛出一个类型错误。通常,更改不可变绑定只会在来格模式下造成异常。如SetMutableBinding()。但使用const声明的变量总是会产生来格模式绑定,比如FunctionDeclarationInstantiation(func, argumentsList)

陷阱:const不会使值变得不可变

const仅仅意味着一个变量总是具有相同的值,但它并不意味着值本身是不可变的。例如,obj是一个常量,但它指向的值是可变的 —— 我们可以向它添加一个属性:

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

然而,我们不能给obj赋予一个不同的值:

obj = {}; // => TypeError: Assignment to constant variable.

如果你希望obj的值是不可变的,那么你必须自己处理它。例如,通过冻结它(freezing):

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

'use strict';
const obj = Object.freeze({});
obj.prop = 123; // => TypeError: Cannot add property prop, object is not extensible

陷阱:Object.freeze()

记住,Object.freeze()是浅的,它只会冻结其参数的属性,而不是存储在其属性中的对象。例如:

const obj = Object.freeze({foo: {}});
obj.bar = 123;
obj.foo = {}

把上面的代码换到严格模式下,结果就不一样了。

'use strict';
const obj = Object.freeze({foo: {}});
obj.bar = 123;  // => TypeError: Cannot add property bar, object is not extensible
obj.foo = {};   // => TypeError: Cannot assign to read only property 'foo' of #<Object>

但是对象obj.foo并不受到影响:

obj.foo.qux = 'abc';
obj.foo.qux; // => abc

循环体内的const

一旦使用const创建的变量就不能更改它,但这并不意味着你不能重新进入它的作用域重新赋予新的值。例如下面的示例,在一个for循环中可以重新赋予新的值。

function logArgs(...args) {
    for (const [index, elem] of args.entries()) {  // (A)
        const message = index + '. ' + elem; // (B)
        console.log(message);
    }
}
logArgs('Hello', 'everyone');
// Output:
// => 0. Hello
// => 1. everyone

上面的代码中有两个const声明,在A行和B行。在每个循环迭代中,它们的常量有不同的值。

临时死区(TDZ)

使用let或者const声明的变量有一个所谓的临时死区(TDZ):当进入它的作用域时,它不能被访问(获取或设置),直到执行到达声明为止。让我们来比较var声明变量(它没有TDZ)和let声明的变量(具有TDZ)。

var声明变量的生命周期

var变量没有TDZ。他们的生命周期包括以下步骤:

  • 当进入var变量的作用域(函数内)时,就会为它创建存储空间(绑定)。通过将变量设置为undefined,可以立即初始化该变量
  • 当在作用域执行到达声明时,变量被设置为初始化器指定的值(如果有赋值)。如果没有,变量的值仍然是undefined

let声明变量的生命周期

let声明的变量有TDZ。他们的生命周期是这样的:

  • 当进入let变量的作用域(块)时,就会为它创建存储空间(绑定)。但仍未初始化的变量
  • 获取或设置未初始化的变量会导致ReferenceError错误
  • 当在作用域执行到达声明时,变量被设置为初始化器指定的值(如果有赋值)。如果没有,变量的值将被设置为undefined

const变量的工作方式类似于let,但它闪必须有一个初始化器(即,立即设置一个值),不能更改。

示例

在TDZ中,如果有一个变量,则抛出一个异常:

let tmp = true;
if (true) { // 进入新的作用域,TDZ开始
    // 创建未初始化绑定的`tmp`
    console.log(tmp); // => ReferenceError

    let tmp;  // TDZ结束,`tmp`初始化值`undefined`
    console.log(tmp); // => undefined

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

如果有一个初始化器,那么TDZ在初始化的时候就被评估结束,结果被分配给变量:

let foo = console.log(foo); // ReferenceError

下面的代码演示的死区实际上是时间(基于时间)而不是空间(基于位置):

if (true) {// 进入新的作用域, TDZ开始 
    const func = function () {
        console.log(myVar); // => 3
    }
    // 这里我们在TDZ中
    // 访问`myVar`导至一个参考错误 
    let myVar = 3; // TDZ 结束 
    func(); // TDZ之外 
}

在TDZ中typeof会抛出一个引用错误

如果你通过typeof来访问TDZ的一个变量,你会得到一个例外:

if (true) {
    console.log(typeof foo); // => ReferenceError: foo is not defined (TDZ)
    console.log(typeof aVariableThatDoesntExist); // => undefined
    let foo;
}

为什么?其原理如下:foo不是未声明的,而是未初始化的。你应该意识到它的存在,但事实不是如此。因此被抛出ReferenceError不可取的,也不令人感到意外了。

此外,这种检查只适用于有条件地创建全局变量。这是你在正常程序中不需要做的事情。

有条件地创建变量

当涉及到有条件地创建变量时,你有两个选项:

typeofvar

if (typeof someGlobal === 'undefined') {
    var someGlobal = {
        // ...
    }
}

此选项只在全局范围内工作(因此不在ES6模块内)。

window

if (!('someGlobal' in window)) {
    window.someGlobal = {
        // ...
    }
}

为什么会有一个TDZ?

为什么constlet会有TDZ,主要有以下几个原因:

  • 要捕捉编程错误:在声明前访问一个变量本来就是很奇怪的。如果你这样做了,通常是意外,你应该得到相应的警告
  • 对于const:使const正确地工作是蛮困难的。引用@Allen的话说“TDZ为常量提供一个理性的语义”。对于这个话题进行了大量的技术讨论,而其中TDZ是最好的解决方案。让我们也有一个暂时的死亡区域,这样一来,letconst之间的切换不会以意想不到的方式改变行为
  • JavaScript可能最终有了警卫,在运行时执行的机制是变量具有正确的值(比如运行时类型检查)。如果变量的值在声明前未定义,那么该值可能与它的保护所提供的保证相冲突

循环头中的letconst

在下面的循环中允许你在其头部中声明变量:

  • for
  • for-in
  • for-of

你可以使用varletconst都可以在循环头部声明变量。每一个都有不同的效果,后面会详细阐述。

for循环

for循环的头部中使用var声明变量,为该变量创建一个单独的绑定(存储空间):

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

在箭头函数的主体内,每个i都表示相同的绑定,这就是为什么它们都返回相同的值。

如果你使用let声明变量,则为每个循环迭代创建一个新的绑定:

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

这一次,每个i指的是迭代时的特定的绑定,并保留当时的值。因此,每个箭头函数返回一个不同的值。

const工作类似于let,但是不能改为一个const声明的变量的初始值:

// TypeError: Assignment to constant variable
// (due to i++)
for (const i=0; i<3; i++) {
    console.log(i);
}

首先,为每次迭代获理一个新的绑定,本来看起来很奇怪,但是当你使用循环来创建变量,在函数中引用这个变量时,它就非常有用,正如后面的部分所解释的那样

for-offor-in

for-of循环中,var创建一个单独的绑定:

const arr = [];
for (var i of [0, 1, 2]) {
    arr.push(() => i);
}
arr.map(x => x()); // => [2,2,2]

const每次迭代创建一个不可变绑定:

const arr = [];
for (const i of [0, 1, 2]) {
    arr.push(() => i);
}
arr.map(x => x()); // => [0,1,2]

let在每次迭代中也可以创建一个绑定,但是它所创建的绑定是可变的。

let arr = [];
for (let i of [0, 1, 2]) {
    arr.push(() => i);
}
arr.map(x => x()); // => [0,1,2]

for-in循环的工作类似于for-of循环。

为什么每个迭代绑定有用

下面是一个显示三个链接的HTML页面:

1. If you click on “yes”, it is translated to “ja”.
2. If you click on “no”, it is translated to “nein”.
3. If you click on “perhaps”, it is translated to “vielleicht”.

代码如下:

<!doctype html>
<html>
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
        <div id="content"></div>
        <script>
            const entries = [
                ['yes', 'ja'],
                ['no', 'nein'],
                ['perhaps', 'vielleicht'],
            ];
            const content = document.getElementById('content');
            for (const [source, target] of entries) { // (A)
                content.insertAdjacentHTML('beforeend',
                    `<div><a id="${source}" href="">${source}</a></div>`);
                document.getElementById(source).addEventListener(
                    'click', (event) => {
                        event.preventDefault();
                        alert(target); // (B)
                    });
            }
        </script>
    </body>
</html>

显示的内容取决于变量目标(B行),如果我们在A行中使用var而不是const,那么整个循环将会有一个单一的绑定,目标将会有vielleicht的值。因此,无论你点击什么链接,你都会得到vielleicht

幸运的是,在const中,每个迭代得到一个绑定,并且得到正确的值。

const vs let vs var

我建议总是使用letconst

我比较喜欢const。你可以在不需要改变变量的值时使用它。换句话说,变量不应该是赋值或操作数,比如++--。更改const变量引用的对象:

const foo = {};
foo.prop = 123; // OK

你甚至可以在for-of循环中使用const,因为每个循环迭代都创建一个(不可变的)绑定:

for (const x of ['a', 'b']) {
    console.log(x);
}
// Output:
// => a
// => b

for-of循环体内,x不可以改变。

否则,使用let可以在稍后修改变量的初始值。

let counter = 0;     // initial value
counter++;           // change

let obj = {};        // initial value
obj = { foo: 123 };  // change

如果遵循这些规则,var只会出现在历史代码中,这是一个需要仔细重构代码的信号。

var只做一件事,那就是letconst不可做的事:通过它声明的变量成为全局对象。然而,这通常不是一件好事。通过分配给window(Web浏览器中)或global(在Node.js),你可以达到同样的效果。

除了刚才提到的规则之外,另一种方法是只对完全不可变的东西(原始值和冻结对象)使用const。然后我们有两种方法:

  • 选择constconst标记不可变的绑定
  • 选择constconst声明常量,是不可变的值

大漠

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

如需转载,烦请注明出处:https://www.fedev.cn/javascript/es6-scoping-variables.htmljordans for sale paypal