详解 let 和 var

659 阅读13分钟

来源:exploringjs.com/es6/ch_vari…

ES6 提供了两种声明变量的新方法: letconst ,它们主要取代 ES5 声明变量的方式:var

9.1.1 let

letvar 类似,但它声明的变量是具有块级作用域的(block-scoped),它只存在于当前块中。 var 是函数作用域(function-scoped)。

在下面的代码中,您可以看到 let 声明的 变量 tmp 只存在于 从(A)行开始的块中:

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

9.1.2 const

const 作用类似于 let,但是您声明的变量必须立即初始化,并且该值之后不能更改。

const foo;
    // SyntaxError: missing = in const declaration

const bar = 123;
bar = 456;
    // TypeError: `bar` is read-only

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

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

9.1.3 声明变量的方式

下表概述了在 ES6 中声明变量的六种方式(受 kangax 表的启发 ):

提升形式 作用域形式 创建全局属性
var Declaration Function Yes
let Temporal dead zone Block No
const Temporal dead zone Block No
function Complete Block Yes
classs No Block No
import Complete Module-global No

9.2 通过 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
}

块作用域意味着您可以在函数中隐藏变量:

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

9.3 const 创建不可变变量

let 创建的变量是可变的:

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

常量(const创建的变量)是不可变的,不能再给它们赋不同的值:

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

规范细节:更改 const 变量总是抛出 TypeError

通常,根据 SetMutableBinding() ,更改不可变绑定仅在严格模式下导致异常。 但 const 声明的变量总是产生严格的绑定 - 参见 FunctionDeclarationInstantiation(func, argumentsList)

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

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

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

但是,我们不能为 obj 分配不同的值:

obj = {}; // TypeError

如果您希望 obj 的值是不可变的,那么您必须自己处理它。例如, 冻结它

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

9.3.1.1 陷阱:Object.freeze()是浅层的

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

> const obj = Object.freeze({ foo: {} });
> obj.bar = 123
TypeError: Can't 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'

9.3.2 循环体中的 const

一旦创建了 const 变量,就无法更改它。但这并不意味着您无法重新进入其作用域并赋予新的值重新开始,每次循环都像是一次轮回。例如,通过循环:

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) 中。在每次循环迭代期间,它们的常量有不同的值。

9.4 临时死区(temporal dead zone)

letconst声明的变量具有所谓的 临时死区(TDZ):当进入其作用域时,在执行到达声明之前不能访问(获取或设置)它。让我们比较 var 变量(没有TDZ)和let变量(有TDZ)的生命周期。

9.4.1 var 变量的生命周期

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

  • 当进入 var 变量的作用域(其所在的函数)时,将为它创建存储空间(绑定)。 通过将变量设置为undefined,立即初始化变量。
  • 当作用域内的执行到达声明时,该变量被设置为初始化 程序指定的值(赋值)- 如果有的话。如果没有,则变量的值仍未 undefined

9.4.2 let 变量的生命周期

通过 let 声明的变量有临时死区,它们的生命周期如下所示:

  • 当进入 let 变量的作用域(其所在的块)时,将为其创建存储空间(绑定)。变量仍然未初始化。
  • 获取或设置未初始化的变量会导致 ReferenceError
  • 当范围内的执行到达声明时,该变量被设置为初始化程序指定的值(赋值)- 如果有的话。如果没有,则将变量的值设置为 undefined

const变量与 let 变量的工作方式类似,但它们必须具有初始化程序(即立即设置值)并且不能更改。

9.4.3 示例

在 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); // 这里没有问题
    };

    // 当前处于 TDZ 内部
    // 如果 访问 `myVar` 会出现异常 `ReferenceError`

    let myVar = 3; // TDZ 结束
    func(); // 此时 TDZ 不存在,调用func
}

9.4.4 typeof 会为TDZ 中的变量抛出 ReferenceError

如果通过 typeof 访问时间死区中的变量,则会出现异常:

if (true) {
    console.log(typeof foo); // ReferenceError (TDZ)
    console.log(typeof aVariableThatDoesntExist); // 'undefined'
    let foo;
}

为什么? 理由如下:foo 不是未声明的,它是未初始化的。你应该意识到它的存在,但你没有。因此,被警告似乎是可取的。

此外,这种检查仅对有条件地创建全局变量有用。在正常程序中不需要做的。

9.4.4.1 有条件地创建变量

在有条件地创建变量时,您有两种选择。

选项1 - typeofvar

if (typeof someGlobal === 'undefined') {
    var someGlobal = { ··· };
}

此选项仅适用于全局范围(因此不在ES6模块内)。

选项2 - window

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

9.4.5 为什么会出现临时死区(TDZ)?

constlet 产生TDZ,有几个原因:

  • 捕获编程错误:能够在声明之前访问变量很奇怪。如果你这样做了,可能是意外,你就应该得到警告。
  • 对于 const:使 const 正常工作很困难。引用Allen Wirfs-Brock:“TDZs ......为const提供了理性的语义。 对该主题进行了重要的技术讨论,并且TDZ 成为最佳解决方案。“ let 也有一个临时死区,这样 letconst 之间的切换不会以意料之外的方式改变行为。
  • 面向未来的防护:JavaScript最终可能会有一些防护,一种在运行时强制执行变量具有正确值的机制(想想运行时类型检查)。如果变量的值在声明之前 undefined,那么该值可能与 其保护所给出的保证 相冲突。

9.4.6 进一步阅读

本节的来源:

9.5 循环头部中的 letconst

以下循环允许您在其头部声明变量:

  • for
  • for-in
  • for-of

要进行声明,可以使用varletconst 。 他们每个都有不同的影响,我将在下面解释。

9.5.1 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 指的是一个特定迭代的绑定,并保留当时的值。因此,每个箭头函数返回不同的值。

constvar 一样的工作,但是你不能改变const 变量的初始值:

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

为每次迭代获取新的绑定起初可能看起来很奇怪,但是每当使用循环 创建引用循环变量的函数时,它非常有用,后面的小节将对此进行解释。

for 循环:规范中的每个迭代绑定 for 循环的求值var 作为第二种情况处理,并将 let/const 作为第三种情况。 只有 let 变量被添加到列表 perIterationLets(步骤9)中,它作为倒数第二个参数 perIterationBindings 传递给ForBodyEvaluation()

9.5.2 for-of 循环和 for-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 也为每次迭代创建一个绑定,但它创建的绑定是可变的。

for-in 循环与 for-of 循环的工作方式类似。

for-of循环:规范中的每次迭代绑定 for-of 中的每次迭代绑定由 ForIn/OfBodyEvaluation处理。在步骤5.b中,创建一个新环境,并通过BindingInstantiation为其添加绑定(对于let可变的,对于const不可变的)。当前迭代值存储在变量nextValue,用于以两种方式之一初始化绑定:

单变量声明(步骤5.hi):通过 InitializeReferencedBinding 处理 解构(步骤5.i.iii):通过一个 BindingInitialization 情况 ( ForDeclaration )来处理,它调用另一个BindingInitialization ( BindingPattern )的情况。

9.5.3 为什么每次迭代绑定是有用的?

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

  1. 如果单击“yes”,则将其翻译为“ja”。
  2. 如果单击“no”,则将其翻译为“nein”。
  3. 如果单击“perhaps”,则会将其翻译为“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>

显示的内容取决于可变target((B)行)。 如果我们在行(A)中使用了var 而不是 const,那么整个循环只会有一个绑定,之后 target 将具有值 vielleicht 。 因此,无论您点击什么链接,您都会得到翻译vielleicht

值得庆幸的是,使用 const,我们每个循环迭代都有一个绑定,并正确地显示转换。

9.6 参数作为变量

9.6.1 参数与局部变量

如果let 与参数具有相同名称的变量,则会出现静态(加载时)错误:

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

在块内执行相同操作会影响参数:

function func(arg) {
    {
        let arg; // shadows parameter `arg`
    }
}

相比之下, var 与参数同名的变量什么都不做,就像在同一作用域中重新声明 var变量一样。

function func(arg) {
    var arg; // does nothing
}
function func(arg) {
    {
        // We are still in same `var` scope as `arg`
        var arg; // does nothing
    }
}

9.6.2 参数默认值和 临时死区

如果参数具有默认值,则它们将被视为一系列 let 语句,并受临时死区的影响:

// OK: `y` accesses `x` after it has been declared
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

9.6.3 参数默认值看不到主体的作用域

参数默认值的范围与主体作用域分开(前者围绕后者)。这意味着在参数默认值中定义的方法或函数不会看到主体的局部变量

const foo = 'outer';
function bar(func = x => foo) {
    const foo = 'inner'; // 不会被看到哦~
    console.log(func()); // outer
}
bar();

9.7 全局对象

JavaScript的全局对象 (Web浏览器中的 window ,Node.js中的 global)更像是一个bug而不是一个功能,特别是在性能方面。这就是ES6 引入区别的原因:

  • 全局对象的所有属性都是全局变量。在全局范围中,以下声明创建了这样的属性:
    • var声明
    • 函数声明
  • 但是现在还有全局变量不是全局对象的属性。在全局范围中,以下声明创建了此类变量: let 声明 const 声明 类声明

注意,模块的主体不是在全局范围内执行的,只有脚本才是。因此,各种变量的环境形成以下链。

variables----environment_chain

9.8 函数声明和类声明

函数声明...

  • 是块作用域,比如 let
  • 在全局对象中创建属性(在全局作用域中),如var
  • 被提升:与其在其作用域中声明的位置无关,函数声明总是在作用域中的开头创建。

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

{ // Enter a new scope

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

类声明...

  • 是块作用域。
  • 不要在全局对象上创建属性。
  • 没有提升。

类没有被提升可能会令人惊讶,因为在底层引擎下,它们创建了函数。 这种行为的基本原理是它们的 extends 子句的值是通过表达式定义的,并且这些表达式必须在适当的时间执行。

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

    // 目前处于 `MyClass` 的TDZ 中
    const inst = new MyClass(); // ReferenceError

    //注意 `extends` 子句的表达式
    class MyClass extends identity(Object) {
    } 
}

9.9 编码样式: const与let对比var

我建议总是使用 letconst

  1. 使用 const:只要变量永远不会改变其值,就可以使用它。 换句话说:变量不应该是赋值的左边,也不应该是++或-的操作数。允许更改 const 变量引用的对象:

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

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

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

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

  2. 稍后会更改变量的初始值时,则应该使用 let

        let counter = 0; // initial value
        counter++; // change
    
        let obj = {}; // initial value
        obj = { foo: 123 }; // change
    
  3. 避免var 。

如果遵循这些规则,var 将仅出现在遗留代码中,作为需要仔细重构的信号。

var 会做一件letconst 不会做的事情:通过它声明的变量成为了全局对象的属性。然而,这通常不是一件好事。您可以通过分配到 window (在浏览器中)或 global(在Node.js中)来实现相同的效果。

9.9.1 替代的方法

对于上面提到的样式规则,另一种选择是只对完全不可变的东西(原始值和冻结的对象)使用 const 。然后有两种方法:

  1. 优先 constconst 标记不可变的绑定。
  2. 优先 letconst 标记不可变的值。

我略微倾向于 1,但 2 也可以。