ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] ## 9.1 概述 ES6 提供了两种声明变量的新方法: `let` 和 `const` ,它们主要取代 ES5 声明变量的方式:`var` 。 ### 9.1.1 let `let` 与 `var` 类似,但它声明的变量是具有块级作用域的(block-scoped),它只存在于当前块中。 `var` 是函数作用域(function-scoped)。 在下面的代码中,您可以看到 `let` 声明的 变量 `tmp` 只存在于 从(A)行开始的块中: ```js 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`,但是您声明的变量必须立即初始化,并且该值之后不能更改。 ```js const foo; // SyntaxError: missing = in const declaration const bar = 123; bar = 456; // TypeError: `bar` is read-only ``` 因为 `for-of` 在每次循环迭代中创建一个绑定(变量的存储空间),所以可以使用 `const` 声明循环变量 ```js for (const x of ['a', 'b']) { console.log(x) } // Output: // a // b ``` ### 9.1.3 声明变量的方式 下表概述了在 ES6 中声明变量的六种方式(受 [kangax 表](https://twitter.com/kangax/status/567330097603284992)的启发 ): | | 提升形式 | 作用域形式 | 创建全局属性 | | ---------- | ------------------ | ------------- | ------------ | | `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 通过 `let` 和 `const` 阻止作用域 `let` 和 `const` 创建了块作用域的变量 - 它们只存在于包围它们的最里面的块中。 以下代码演示了 `const` 声明的变量 `tmp` 仅存在于 `if` 语句的块中: ```js function func() { if (true) { const tmp = 123; } console.log(tmp); // ReferenceError: tmp is not defined } ``` 相比之下,`var` 声明的变量是函数级别的: ```js function func() { if (true) { var tmp = 123; } console.log(tmp); // 123 } ``` 块作用域意味着您可以在函数中隐藏变量: ```js function func() { const foo = 5; if (···) { const foo = 10; // shadows outer `foo` console.log(foo); // 10 } console.log(foo); // 5 } ``` ## 9.3 `const` 创建不可变变量 `let` 创建的变量是可变的: ```js let foo = 'abc'; foo = 'def'; console.log(foo); // def ``` 常量(`const`创建的变量)是不可变的,不能再给它们赋不同的值: ```js const foo = 'abc'; foo = 'def'; // TypeError ``` **规范细节:更改 `const` 变量总是抛出 `TypeError`** 通常,根据 [SetMutableBinding()](http://www.ecma-international.org/ecma-262/6.0/#sec-declarative-environment-records-setmutablebinding-n-v-s) ,更改不可变绑定仅在严格模式下导致异常。 但 `const` 声明的变量总是产生严格的绑定 - 参见 [FunctionDeclarationInstantiation(func, argumentsList)](http://www.ecma-international.org/ecma-262/6.0/#sec-functiondeclarationinstantiation) 。 ### 9.3.1 陷阱:`const` 不会使值不可变 `const` 只意味着一个变量总是具有相同的值,但它并不意味着该值本身是不可变的或成为不可变的。 例如, `obj` 是一个常量,但它指向的值是可变的 - 我们可以为它添加一个属性: ```js const obj = {}; obj.prop = 123; console.log(obj.prop); // 123 ``` 但是,我们不能为 `obj` 分配不同的值: ```js obj = {}; // TypeError ``` 如果您希望 `obj` 的值是不可变的,那么您必须自己处理它。例如, [冻结它](http://speakingjs.com/es5/ch17.html#freezing_objects): ```js const obj = Object.freeze({}); obj.prop = 123; // TypeError ``` #### 9.3.1.1 陷阱:`Object.freeze()`是浅层的 请记住,`Object.freeze()` 是浅层的,它只会冻结其参数的属性,而不会冻结其属性中存储的对象。例如,对象 `obj` 被冻结: ```js > 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` 不是。 ```js > obj.foo.qux = 'abc'; > obj.foo.qux 'abc' ``` ### 9.3.2 循环体中的 `const` 一旦创建了 `const` 变量,就无法更改它。但这并不意味着您无法重新进入其作用域并赋予新的值重新开始,每次循环都像是一次轮回。例如,通过循环: ```js 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) 由 `let` 或`const`声明的变量具有所谓的 临时死区(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 中,如果获取或设置变量,则抛出异常: ```js 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 会在初始化后并将结果赋值给变量后结束: ```js let foo = console.log(foo); // 打印:ReferenceError ``` 以下代码演示死区实际上是临时的 (基于时间)而不是空间(基于位置): ```js 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` 访问时间死区中的变量,则会出现异常: ```js if (true) { console.log(typeof foo); // ReferenceError (TDZ) console.log(typeof aVariableThatDoesntExist); // 'undefined' let foo; } ``` 为什么? 理由如下:`foo` 不是未声明的,它是未初始化的。你应该意识到它的存在,但你没有。因此,被警告似乎是可取的。 此外,这种检查仅对有条件地创建全局变量有用。在正常程序中不需要做的。 #### 9.4.4.1 有条件地创建变量 在有条件地创建变量时,您有两种选择。 选项1 - `typeof` 和 `var`: ```js if (typeof someGlobal === 'undefined') { var someGlobal = { ··· }; } ``` 此选项仅适用于全局范围(因此不在ES6模块内)。 选项2 - `window`: ```js if (!('someGlobal' in window)) { window.someGlobal = { ··· }; } ``` ### 9.4.5 为什么会出现临时死区(TDZ)? `const` 和 `let` 产生TDZ,有几个原因: * 捕获编程错误:能够在声明之前访问变量很奇怪。如果你这样做了,可能是意外,你就应该得到警告。 * 对于 `const`:使 `const` 正常工作很困难。[引用Allen Wirfs-Brock](https://mail.mozilla.org/pipermail/es-discuss/2012-September/024996.html):“TDZs ......为const提供了理性的语义。 对该主题进行了重要的技术讨论,并且TDZ 成为最佳解决方案。“ `let` 也有一个临时死区,这样 `let` 和 `const` 之间的切换不会以意料之外的方式改变行为。 * 面向未来的防护:JavaScript最终可能会有一些防护,一种在运行时强制执行变量具有正确值的机制(想想运行时类型检查)。如果变量的值在声明之前 `undefined`,那么该值可能与 其保护所给出的保证 相冲突。 ### 9.4.6 进一步阅读 本节的来源: * “[let/const的性能问题](https://esdiscuss.org/topic/performance-concern-with-let-const) ” * “[Bug 3009 -在TDZ变量上的 typeof](https://bugs.ecmascript.org/show_bug.cgi?id=3009) ” ## 9.5 循环头部中的 `let` 和 `const` 以下循环允许您在其头部声明变量: * `for ` * `for-in ` * `for-of ` 要进行声明,可以使用`var`, `let` 或 `const` 。 他们每个都有不同的影响,我将在下面解释。 ### 9.5.1 `for` 循环 在for循环头部中的`var` 变量为该变量创建单个绑定 (存储空间): ```js const arr = []; for (var i=0; i < 3; i++) { arr.push(() => i); } arr.map(x => x()); // [3,3,3] ``` 三个箭头函数体中的每个`i` 指向相同的绑定,这就是它们都返回相同值的原因。 `let` 变量,则为每个循环迭代创建一个新绑定: ```js const arr = []; for (let i=0; i < 3; i++) { arr.push(() => i); } arr.map(x => x()); // [0,1,2] ``` 这一次,每个 `i` 指的是一个特定迭代的绑定,并保留当时的值。因此,每个箭头函数返回不同的值。 `const`像 `var` 一样的工作,但是你不能改变`const` 变量的初始值: ```js // TypeError: Assignment to constant variable // (due to i++) for (const i=0; i<3; i++) { console.log(i); } ``` 为每次迭代获取新的绑定起初可能看起来很奇怪,但是每当使用循环 创建引用循环变量的函数时,它非常有用,后面的小节将对此进行解释。 **`for` 循环:规范中的每个迭代绑定** [`for` 循环的求值](http://www.ecma-international.org/ecma-262/6.0/#sec-for-statement-runtime-semantics-labelledevaluation) 将`var` 作为第二种情况处理,并将 `let/const` 作为第三种情况。 只有 `let` 变量被添加到列表 `perIterationLets`(步骤9)中,它作为倒数第二个参数 `perIterationBindings` 传递给`ForBodyEvaluation()` 。 ### 9.5.2 `for-of` 循环和 `for-in` 循环 在 `for-of` 循环中,`var`创建单个绑定: ```js const arr = []; for (var i of [0, 1, 2]) { arr.push(() => i); } arr.map(x => x()); // [2,2,2] ``` `const` 每次迭代创建一个不可变的绑定: ```js 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](http://www.ecma-international.org/ecma-262/6.0/#sec-runtime-semantics-forin-div-ofbodyevaluation-lhs-stmt-iterator-lhskind-labelset)处理。在步骤5.b中,创建一个新环境,并通过[BindingInstantiation](http://www.ecma-international.org/ecma-262/6.0/#sec-runtime-semantics-bindinginstantiation)为其添加绑定(对于`let`可变的,对于`const`不可变的)。当前迭代值存储在变量`nextValue`,用于以两种方式之一初始化绑定: 单变量声明(步骤5.hi):通过 [`InitializeReferencedBinding`](http://www.ecma-international.org/ecma-262/6.0/#sec-initializereferencedbinding) 处理 解构(步骤5.i.iii):通过[一个 BindingInitialization 情况](http://www.ecma-international.org/ecma-262/6.0/#sec-for-in-and-for-of-statements-runtime-semantics-bindinginitialization) ( ForDeclaration )来处理,它调用[另一个BindingInitialization](http://www.ecma-international.org/ecma-262/6.0/#sec-destructuring-binding-patterns-runtime-semantics-bindinginitialization) ( BindingPattern )的情况。 ### 9.5.3 为什么每次迭代绑定是有用的? 以下是显示三个链接的HTML页面: 1. 如果单击“yes”,则将其翻译为“ja”。 2. 如果单击“no”,则将其翻译为“nein”。 3. 如果单击“perhaps”,则会将其翻译为“vielleicht”。 ```js <!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` 与参数具有相同名称的变量,则会出现静态(加载时)错误: ```js function func(arg) { let arg; // static error: duplicate declaration of `arg` } ``` 在块内执行相同操作会影响参数: ```js function func(arg) { { let arg; // shadows parameter `arg` } } ``` 相比之下, `var` 与参数同名的变量什么都不做,就像在同一作用域中重新声明 `var`变量一样。 ```js 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` 语句,并受临时死区的影响: ```js // 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 参数默认值看不到主体的作用域 参数默认值的范围与主体作用域分开(前者围绕后者)。这意味着在参数默认值中定义的方法或函数不会看到主体的局部变量 ```js const foo = 'outer'; function bar(func = x => foo) { const foo = 'inner'; // 不会被看到哦~ console.log(func()); // outer } bar(); ``` ## 9.7 全局对象 JavaScript的[全局对象](http://speakingjs.com/es5/ch16.html#global_object) (Web浏览器中的 `window` ,Node.js中的 `global`)更像是一个bug而不是一个功能,特别是在性能方面。这就是ES6 引入区别的原因: * 全局对象的所有属性都是全局变量。在全局范围中,以下声明创建了这样的属性: * `var`声明 * 函数声明 * 但是现在还有全局变量不是全局对象的属性。在全局范围中,以下声明创建了此类变量: `let` 声明 `const` 声明 类声明 注意,模块的主体不是在全局范围内执行的,只有脚本才是。因此,各种变量的环境形成以下链。 ![variables----environment_chain](https://box.kancloud.cn/080c52bd53b13995fc195d4e90abd43e_270x385.jpg =165x213) ## 9.8 函数声明和类声明 函数声明... * 是块级作用域,比如 `let`。 * 在全局对象中创建属性(在全局作用域中),如`var` 。 * 被提升:与其在其作用域中声明的位置无关,函数声明总是在作用域中的开头创建。 以下代码演示了函数声明的提升: ```js { // Enter a new scope console.log(foo()); // OK, due to hoisting function foo() { return 'hello'; } } ``` 类声明... * 是块级作用域。 * 不要在全局对象上创建属性。 * 没有提升。 类没有被提升可能会令人惊讶,因为在底层引擎下,它们创建了函数。 这种行为的基本原理是它们的 `extends` 子句的值是通过表达式定义的,并且这些表达式必须在适当的时间执行。 ```js { // 进入一个新的作用域 const identity = x => x; // 目前处于 `MyClass` 的TDZ 中 const inst = new MyClass(); // ReferenceError //注意 `extends` 子句的表达式 class MyClass extends identity(Object) { } } ``` ## 9.9 编码样式: const与let对比var 我建议总是使用 `let` 或 `const`: 1. 使用 `const`:只要变量永远不会改变其值,就可以使用它。 换句话说:变量不应该是赋值的左边,也不应该是++或-的操作数。允许更改 `const` 变量引用的对象: ```js const foo = {}; foo.prop = 123; // OK ``` 你甚至可以在 `for-of` 循环中使用`const`,因为每次循环迭代都会创建一个(不可变的)绑定: ```js for (const x of ['a', 'b']) { console.log(x); } // Output: // as // b ``` 在`for-of` 循环体内,`x`不能改变。 2. 稍后会更改变量的初始值时,则应该使用 `let` 。 ```js let counter = 0; // initial value counter++; // change let obj = {}; // initial value obj = { foo: 123 }; // change ``` 3. 避免 `var`。 如果遵循这些规则,`var` 将仅出现在遗留代码中,作为需要仔细重构的信号。 `var` 会做一件`let` 和 `const` 不会做的事情:通过它声明的变量成为了全局对象的属性。然而,这通常不是一件好事。您可以通过分配到 `window` (在浏览器中)或 `global`(在Node.js中)来实现相同的效果。 ### 9.9.1 替代的方法 对于上面提到的样式规则,另一种选择是只对完全不可变的东西(原始值和冻结的对象)使用 `const` 。然后有两种方法: 1. 优先 `const` : `const` 标记不可变的绑定。 2. 优先 `let` :`const` 标记不可变的值。 我略微倾向于 1,但 2 也可以。