你不懂JS:ES6与未来

Kyle Simpson 是一个严谨的实用主义者。 我想不出比这更高的赞美。对我来说,这是一个软件开发者必须具备的两个最重要的素质。是的:必须,不是应当。将 JavaScript 编程语言层层梳理,并将它们用易懂而且有意的部分表现出来,Kyle 的这种敏锐的能力无人能出其右。 对于 你不懂JS 系列的读者来说 ES6 与未来 使人感到十分熟悉:他们将深深地沉浸在从明显到非常微妙的每一件事中 —— 揭示那些要么被想当然地接受、要么甚至从未被考虑过的语义。至今为止,你不懂JS 系列丛书已经向它的读者们讲解了他们至少在某种程度上熟悉的内容。他们不是见过就是听说过那些主题很重要;也许他们甚至曾经有过相关的经验。而这一卷讲解了只有在很少一部分的JavaScript 开发者社区中才曝光过的内容:在 ECMAScript 2015 语言规范中给这门语言引入的革命性改变。 在过去的几年中,我目睹了 Kyle 不知疲倦地努力学习这些内容,直到只有少数专业人士才能与之媲美的专家级水准。这真是一个壮举,试想就在他撰写的时候,语言规范的文档还没有正式发布哩!但我说的是真的,而且我读了 Kyle 为这本书写的每一个字。我追随着每一次修改,而且每一次它的内容只会变得更好,并提供更深一层的理解。 这本书会将你暴露在新的与未知的事物中来震撼你理解的感官。它意在通过赐予你新的力量来使你的知识更上一个台阶。它存在的目的是为了给你自信,去完全地拥抱 JavaScript 编程的下一个新纪元。
展开查看详情

1. 目 录 致谢 阅前必读 序 第一章:ES?现在与未来 版本 转译 复习 第二章:语法 块儿作用域声明 扩散/剩余 默认参数值 解构 对象字面量扩展 模板字面量 箭头函数 for..of 循环 正则表达式扩展 数字字面量扩展 Unicode Symbol 复习 第三章:组织 迭代器 Generators 模块 类 复习 第四章:异步流程控制 Promises Generators + Promises 复习 第五章:集合 类型化数组(TypedArrays) 本文档使用 书栈(BookStack.CN) 构建 - 1 -

2. Maps WeakMaps Sets WeakSets 复习 第六章:新增 API Array Object Math Number String 复习 第七章:元编程 函数名 元属性 通用Symbol 代理 Reflect API 特性测试 尾部调用优化(TCO) 复习 第八章:ES6 以后 async function Object.observe(..) 指数操作符 对象属性与 ... Array#includes(..) SIMD WebAssembly (WASM) 复习 附录A:鸣谢 本文档使用 书栈(BookStack.CN) 构建 - 2 -

3.本文档使用 书栈(BookStack.CN) 构建 - 3 -

4.致谢 致谢 当前文档 《你不懂JS:ES6与未来(You Dont Know JS)》 由 进击的皇虫 使用 书栈 (BookStack.CN) 进行构建,生成于 2018-02-08。 书栈(BookStack.CN) 仅提供文档编写、整理、归类等功能,以及对文档内容的生成和导出工 具。 文档内容由网友们编写和整理,书栈(BookStack.CN) 难以确认文档内容知识点是否错漏。如 果您在阅读文档获取知识的时候,发现文档内容有不恰当的地方,请向我们反馈,让我们共同携手, 将知识准确、高效且有效地传递给每一个人。 同时,如果您在日常生活、工作和学习中遇到有价值有营养的知识文档,欢迎分享到 书栈 (BookStack.CN) ,为知识的传承献上您的一份力量! 如果当前文档生成时间太久,请到 书栈(BookStack.CN) 获取最新的文档,以跟上知识更新换 代的步伐。 文档地址:http://www.bookstack.cn/books/You-Dont-Know-JS-es6-beyond 书栈官网:http://www.bookstack.cn 书栈开源:https://github.com/TruthHun 分享,让知识传承更久远! 感谢知识的创造者,感谢知识的分享者,也感谢每一位阅读到此处的 读者,因为我们都将成为知识的传承者。 本文档使用 书栈(BookStack.CN) 构建 - 4 -

5.阅前必读 阅前必读 你不懂JS:ES6与未来 你不懂JS:ES6与未来 从 O’Reilly 购买数字/印刷版 第一章:ES?现在与未来 第二章:语法 第三章:组织 第四章:异步流程控制 第五章:集合 第六章:新增 API 第七章:元编程 第八章:ES6 之后 附录A:鸣谢 本文档使用 书栈(BookStack.CN) 构建 - 5 -

6.阅前必读 本文档使用 书栈(BookStack.CN) 构建 - 6 -

7.序 序 你不懂JS:ES6 与未来 序 你不懂JS:ES6 与未来 序 Kyle Simpson 是一个严谨的实用主义者。 我想不出比这更高的赞美。对我来说,这是一个软件开发者必须具备的两个最重要的素质。是的:必 须,不是 应当。将 JavaScript 编程语言层层梳理,并将它们用易懂而且有意的部分表现出来, Kyle 的这种敏锐的能力无人能出其右。 对于 你不懂JS 系列的读者来说 ES6 与未来 使人感到十分熟悉:他们将深深地沉浸在从明显到非 常微妙的每一件事中 —— 揭示那些要么被想当然地接受、要么甚至从未被考虑过的语义。至今为止, 你不懂JS 系列丛书已经向它的读者们讲解了他们至少在某种程度上熟悉的内容。他们不是见过就是 听说过那些主题很重要;也许他们甚至曾经有过相关的经验。而这一卷讲解了只有在很少一部分的 JavaScript 开发者社区中才曝光过的内容:在 ECMAScript 2015 语言规范中给这门语言引入的 革命性改变。 在过去的几年中,我目睹了 Kyle 不知疲倦地努力学习这些内容,直到只有少数专业人士才能与之媲 美的专家级水准。这真是一个壮举,试想就在他撰写的时候,语言规范的文档还没有正式发布哩!但 我说的是真的,而且我读了 Kyle 为这本书写的每一个字。我追随着每一次修改,而且每一次它的内 容只会变得更好,并提供更深一层的理解。 这本书会将你暴露在新的与未知的事物中来震撼你理解的感官。它意在通过赐予你新的力量来使你的 知识更上一个台阶。它存在的目的是为了给你自信,去完全地拥抱 JavaScript 编程的下一个新纪 元。 Rick Waldron @rwaldron Bocoup 的开放 Web 工程师 Ecma/TC39 jQuery 代表 本文档使用 书栈(BookStack.CN) 构建 - 7 -

8.序 本文档使用 书栈(BookStack.CN) 构建 - 8 -

9.第一章:ES?现在与未来 第一章:ES?现在与未来 第一章:ES?现在与未来 第一章:ES?现在与未来 在你一头扎进这本书之前,你应当可以熟练地使用(在本书写作时)最近版本的JavaScript,也就 是通常所说的 ES5(技术上讲是ES 5.1)。这里,我们打算好好谈谈即将到来的 ES6,同时放眼未 来去看看JS将会如何继续进化。 如果你还在JavaScript上寻找信心,我强烈推荐你首先读一读本系列的其他书目: 入门与进阶:你是编程和JS的新手吗?这就是你在开启学习的旅程前需要查看的路线图。 作用域与闭包:你知道JS的词法作用域是基于编译器(不是解释器!)语义的吗?你能解释闭包 是如何成为词法作用域和函数作为值的直接结果的吗? this与对象原型:你能复述 this 绑定的四个简单规则吗?你有没有曾经在JS中对付着去山 寨“类”,而不是采取更简单的“行为委托”设计模式?你听说过 链接到其他对象的对象 (OOLO)吗? 类型与文法:你知道JS中的内建类型吗?更重要的是,你知道如何在类型之间正确且安全地使用 强制转换吗?你对JS文法/语法的微妙之处感到有多习惯? 异步与性能:你还在使用回调管理你的异步处理吗?你能解释promise是为什么/如何解决了“回 调地狱”的吗?你知道如何使用generator来改进异步代码的易读性吗?到底是什么构成了JS程 序和独立操作的成熟优化? 如果你已经读过了这些书目而且对它们涵盖的内容感到十分轻松,那么现在是时候让我们深入JS的进 化过程来探索所有即将到来的以及未来会发生的改变了。 与ES5不同,ES6不仅仅是向语言添加的一组不算太多的新API。它包含大量的新的语法形式,其中的 一些你可能会花上相当一段时间才能适应。还有几种新的组织形式和为各种数据类型添加的新API。 对这门语言来说ES6十分激进。就算你认为你懂得ES5的JS,ES6也满是 你还不懂的 新东西,所以 做好准备!这本书探索所有你需要迅速掌握的ES6主要主题,并且窥见一下那些你应当注意的正在步 入正轨的未来特性。 警告: 这本书中的所有代码都假定运行在ES6+的环境中。在写作本书时,浏览器和JS环境(比如 Node.js)对ES6的支持相当不同,因此你的感觉可能将会不同。 版本 转译 复习 本文档使用 书栈(BookStack.CN) 构建 - 9 -

10.第一章:ES?现在与未来 本文档使用 书栈(BookStack.CN) 构建 - 10 -

11.版本 版本 JavaScript标准在官方上被称为“ECMAScript”(缩写为“ES”),而且直到最近才刚刚完全采用顺 序数字来标记版本(例如,“5”代表“第五版”)。 最早的版本,ES1和ES2,并不广为人知也没有大范围地被实现。ES3是JavaScript第一次广泛传播 的基准线,并且构成了像IE6-8和更早的Android 2.x移动浏览器的JavaScript标准。由于一些超 出我们讨论范围的政治原因,命运多舛的ES4从未问世。 在2009年,ES5正式定稿(在2011年出现了ES5.1),它在浏览器的现代革新和爆发性增长(比如 Firefox,Chrome,Opera,Safari,和其他许多)中广泛传播,并作为JS标准稳定下来。 预计下一个版本的JS(从2013年到2014年和之后的2015年中的内容),在人们的讨论中显然地经常 被称为ES6。 然而,在ES6规范的晚些时候,有建议提及未来的版本号也许会切换到编年制,比如用ES2016(也叫 ES7)来指代在2016年末之前被定稿的任何版本。有些人对此持否定意见,但是相对于后来的 ES2015来说,ES6将很可能继续维持它占统治地位的影响力。可是,ES2016事实上可能标志了新的 编年制。 还可以看到,JS进化的频度即使与一年一度的定版相比都要快得多。只要一个想法开始标准化讨论的 进程,浏览器就开始为这种特性建造原型,而且早期的采用者就开始在代码中进行实验。 通常在一个特性被盖上官方承认的印章以前,由于这些早期的引擎/工具的原型它实际上已经被标准化 了。所以也可以认为未来的JS版本将是一个特性一个特性的更新,而非一组主要特性的随意集合的更 新(就像现在),也不是一年一年的更新(就像可能将变成的那样)。 简而言之,版本号不再那么重要了,JavaScript开始变得更像一个常青的,活的标准。应对它的最 佳方法是,举例来说,不再将你的代码库认为是“基于ES6”的,而是考虑它支持的一个个特性。 本文档使用 书栈(BookStack.CN) 构建 - 11 -

12.转译 转译 填补(Shims/Polyfills) 由于特性的快速进化,给开发者们造成了一个糟糕的问题,他们强烈地渴望立即使用新特性,而同时 被被现实打脸 —— 他们的网站/app需要支持那些不支持这些特性的老版本浏览器。 在整个行业中ES5的方式似乎已经无力回天了,它典型的思维模式是,代码库等待几乎所有的前ES5环 境从它们的支持谱系中除名之后才开始采用ES5。结果呢,许多人最近(在本书写作时)才开始采 用 strict 模式这样的东西,而它早在五年前就在ES5中定稿了。 对于JS生态系统的未来来说,等待和落后于语言规范那么多年被广泛地认为是一种有害的方式。所有 负责推动语言演进的人都渴望这样的事情;只要新的特性和模式以规范的形式稳定下来,并且浏览器 有机会实现它们,开发者就开始基于这些新的特性和模式进行编码。 那么我们如何解决这个看起来似乎矛盾的问题?答案是工具,特别是一种称为 转译 (transpiling) 的技术(转换+编译)。大致上,它的想法是使用一种特殊的工具将你的ES6代码 转换为可以在ES5环境中工作的等价物(或近似物!)。 例如,考虑属性定义缩写(见第二章的“对象字面扩展”)。这是ES6的形式: 1. var foo = [1,2,3]; 2. 3. var obj = { 4. foo // 意思是 `foo: foo` 5. }; 6. 7. obj.foo; // [1,2,3] 这(大致)是它如何被转译: 1. var foo = [1,2,3]; 2. 3. var obj = { 4. foo: foo 5. }; 6. 7. obj.foo; // [1,2,3] 这是一个微小但令人高兴的转换,它让我们在一个对象字面声明中将 foo: foo 缩写为 foo ,如果 名称相同的话。 转译器为你实施这些变形,这个过程通常是构建工作流的一个步骤 —— 与你进行linting,压缩,和 其他类似操作相似。 本文档使用 书栈(BookStack.CN) 构建 - 12 -

13.转译 填补(Shims/Polyfills) 不是所有的ES6新特性都需要转译器。填补(也叫shims)是一种模式,在可能的情况下,它为一个 新环境的行为定义一个可以在旧环境中运行的等价行为。语法是不能填补的,但是API经常是可以 的。 例如, Object.is(..) 是一个用来检查两个值严格等价性的新工具,它不带有 === 对 于 NaN 和 -0 值的那种微妙的例外。 Object.is(..) 的填补相当简单: 1. if (!Object.is) { 2. Object.is = function(v1, v2) { 3. // 测试 `-0` 4. if (v1 === 0 && v2 === 0) { 5. return 1 / v1 === 1 / v2; 6. } 7. // 测试 `NaN` 8. if (v1 !== v1) { 9. return v2 !== v2; 10. } 11. // 其他的一切情况 12. return v1 === v2; 13. }; 14. } 提示:注意外部的 if 语句守护性地包围着填补的内容。这是一个重要的细节,它意味着这个代码段 仅仅是为这个API还未定义的老环境而定义的后备行为;你想要覆盖既存API的情况是非常少见的。 有一个被称为“ES6 Shim”( https://github.com/paulmillr/es6-shim/ )的了不起的ES6 填补集合,你绝对应该将它采纳为任何新JS项目的标准组成部分! 看起来JS将会继续一往无前的进化下去,同时浏览器也会持续地小步迭代以支持新特性,而不是大块 大块地更新。所以跟上时代的最佳策略就是在你的代码库中引入填补,并在你的构建流程中引入一个 转译器步骤,现在就开始习惯新的现实。 如果你决定维持现状,等待不支持新特性的所有浏览器都消失才开始使用新特性,那么你将总是落后 于时代。你将可悲地错过所有新发明的设计 —— 而它们使编写JavaScript更有效,更高效,而且更 健壮。 本文档使用 书栈(BookStack.CN) 构建 - 13 -

14.复习 复习 本文档使用 书栈(BookStack.CN) 构建 - 14 -

15.第二章:语法 第二章:语法 第二章:语法 第二章:语法 如果你曾经或多或少地写过JS,那么你很可能对它的语法感到十分熟悉。当然有一些奇怪之处,但是 总体来讲这是一种与其他语言有很多相似之处的,相当合理而且直接的语法。 然而,ES6增加了好几种需要费些功夫才能习惯的新语法形式。在这一章中,我们将遍历它们来看看 葫芦里到底卖的什么药。 提示: 在写作本书时,这本书中所讨论的特性中的一些已经被各种浏览器(Firefox,Chrome,等 等)实现了,但是有一些仅仅被实现了一部分,而另一些根本就没实现。如果直接尝试这些例子,你 的体验可能会夹杂着三种情况。如果是这样,就使用转译器尝试吧,这些特性中的大多数都被那些工 具涵盖了。ES6Fiddle(http://www.es6fiddle.net/) 是一个了不起的尝试ES6的游乐场,简 单易用,它是一个Babel转译器的在线REPL(http://babeljs.io/repl/ )。 块儿作用域声明 扩散/剩余 默认参数值 解构 对象字面量扩展 模板字面量 箭头函数 for..of 循环 正则表达式扩展 数字字面量扩展 Unicode Symbol 复习 本文档使用 书栈(BookStack.CN) 构建 - 15 -

16.块儿作用域声明 块儿作用域声明 块儿作用域声明 let 声明 let + for const 声明 const 用还是不用 块儿作用域的函数 块儿作用域声明 你可能知道在JavaScript中变量作用域的基本单位总是 function 。如果你需要创建一个作用域的 块儿,除了普通的函数声明以外最流行的方法就是使用立即被调用的函数表达式(IIFE)。例如: 1. var a = 2; 2. 3. (function IIFE(){ 4. var a = 3; 5. console.log( a ); // 3 6. })(); 7. 8. console.log( a ); // 2 let 声明 但是,现在我们可以创建绑定到任意的块儿上的声明了,它(勿庸置疑地)称为 块儿作用域。这意味 着一对 { .. } 就是我们用来创建一个作用域所需要的全部。 var 总是声明附着在外围函数(或者 全局,如果在顶层的话)上的变量,取而代之的是,使用 let : 1. var a = 2; 2. 3. { 4. let a = 3; 5. console.log( a ); // 3 6. } 7. 8. console.log( a ); // 2 迄今为止,在JS中使用独立的 { .. } 块儿不是很常见,也不是惯用模式,但它总是合法的。而且那 些来自拥有 块儿作用域 的语言的开发者将很容易认出这种模式。 本文档使用 书栈(BookStack.CN) 构建 - 16 -

17.块儿作用域声明 我相信使用一个专门的 { .. } 块儿是创建块儿作用域变量的最佳方法。但是,你应该总是 将 let 声明放在块儿的最顶端。如果你有多于一个的声明,我推荐只使用一个 let 。 从文体上说,我甚至喜欢将 let 放在与开放的 { 的同一行中,以便更清楚地表示这个块儿的目的 仅仅是为了这些变量声明作用域。 1. { let a = 2, b, c; 2. // .. 3. } 它现在看起来很奇怪,而且不大可能与其他大多数ES6文献中推荐的文法吻合。但我的疯狂是有原因 的。 这是另一种实验性的(不是标准化的) let 声明形式,称为 let 块儿,看起来就像这样: 1. let (a = 2, b, c) { 2. // .. 3. } 我称这种形式为 明确的 块儿作用域,而与 var 相似的 let 声明形式更像是 隐含的,因为它在 某种意义上劫持了它所处的 { .. } 。一般来说开发者们认为 明确的 机制要比 隐含的 机制更好一 些,我主张这种情况就是这样的情况之一。 如果你比较前面两个形式的代码段,它们非常相似,而且我个人认为两种形式都有资格在文体上称为 明确的 块儿作用域。不幸的是,两者中最 明确的 let (..) { .. } 形式没有被ES6所采用。它可 能会在后ES6时代被重新提起,但我想目前为止前者是我们的最佳选择。 为了增强对 let .. 声明的 隐含 性质的理解,考虑一下这些用法: 1. let a = 2; 2. 3. if (a > 1) { 4. let b = a * 3; 5. console.log( b ); // 6 6. 7. for (let i = a; i <= b; i++) { 8. let j = i + 10; 9. console.log( j ); 10. } 11. // 12 13 14 15 16 12. 13. let c = a + b; 14. console.log( c ); // 8 15. } 本文档使用 书栈(BookStack.CN) 构建 - 17 -

18.块儿作用域声明 不要回头去看这个代码段,小测验:哪些变量仅存在于 if 语句内部?哪些变量仅存在于 for 循环 内部? 答案: if 语句包含块儿作用域变量 b 和 c ,而 for 循环包含块儿作用域变量 i 和 j 。 你有任何迟疑吗? i 没有被加入外围的 if 语句的作用域让你惊讶吗?思维上的停顿和疑问 —— 我称之为“思维税” —— 不仅源自于 let 机制对我们来说是新东西,还因为它是 隐含的。 还有一个灾难是 let c = .. 声明出现在作用域中太过靠下的地方。传统的被 var 声明的变量,无 论它们出现在何处,都会被附着在整个外围的函数作用域中;与此不同的是, let 声明附着在块儿 作用域,而且在它们出现在块儿中之前是不会被初始化的。 在一个 let .. 声明/初始化之前访问一个用 let 声明的变量会导致一个错误,而对于 var 声明 来说这个顺序无关紧要(除了文体上的区别)。 考虑如下代码: 1. { 2. console.log( a ); // undefined 3. console.log( b ); // ReferenceError! 4. 5. var a; 6. let b; 7. } 警告: 这个由于过早访问被 let 声明的引用而引起的 ReferenceError 在技术上称为一个 临时死 区(Temporal Dead Zone —— TDZ) 错误 —— 你在访问一个已经被声明但还没被初始化的变 量。这将不是我们唯一能够见到TDZ错误的地方 —— 在ES6中它们会在几种地方意外地发生。另外, 注意“初始化”并不要求在你的代码中明确地赋一个值,比如 let b; 是完全合法的。一个在声明时没 有被赋值的变量被认为已经被赋予了 undefined 值,所以 let b; 和 let b = undefined; 是一样 的。无论是否明确赋值,在 let b 语句运行之前你都不能访问 b 。 最后一个坑:对于TDZ变量和未声明的(或声明的!)变量, typeof 的行为是不同的。例如: 1. { 2. // `a` 没有被声明 3. if (typeof a === "undefined") { 4. console.log( "cool" ); 5. } 6. 7. // `b` 被声明了,但位于它的TDZ中 8. if (typeof b === "undefined") { // ReferenceError! 9. // .. 10. } 11. 12. // .. 本文档使用 书栈(BookStack.CN) 构建 - 18 -

19.块儿作用域声明 13. 14. let b; 15. } a 没有被声明,所以 typeof 是检查它是否存在的唯一安全的方法。但是 typeof b 抛出了TDZ错 误,因为在代码下面很远的地方偶然出现了一个 let b 声明。噢。 现在你应当清楚为什么我坚持认为所有的 let 声明都应该位于它们作用域的顶部了。这完全避免了 偶然过早访问的错误。当你观察一个块儿,或任何块儿的开始部分时,它还更 明确 地指出这个块儿 中含有什么变量。 你的块儿( if 语句, while 循环,等等)不一定要与作用域行为共享它们原有的行为。 这种明确性要由你负责,由你用毅力来维护,它将为你省去许多重构时的头疼和后续的麻烦。 注意: 更多关于 let 和块儿作用域的信息,参见本系列的 作用域与闭包 的第三章。 let + for 我偏好 明确 形式的 let 声明块儿,但对此的唯一例外是出现在 for 循环头部的 let 。这里的 原因看起来很微妙,但我相信它是更重要的ES6特性中的一个。 考虑如下代码: 1. var funcs = []; 2. 3. for (let i = 0; i < 5; i++) { 4. funcs.push( function(){ 5. console.log( i ); 6. } ); 7. } 8. 9. funcs[3](); // 3 在 for 头部中的 let i 不仅是为 for 循环本身声明了一个 i ,而且它为循环的每一次迭代都 重新声明了一个新的 i 。这意味着在循环迭代内部创建的闭包都分别引用着那些在每次迭代中创建 的变量,正如你期望的那样。 如果你尝试在这段相同代码的 for 循环头部使用 var i ,那么你会得到 5 而不是 3 ,因为在 被引用的外部作用域中只有一个 i ,而不是为每次迭代的函数都有一个 i 被引用。 你也可以稍稍繁冗地实现相同的东西: 1. var funcs = []; 2. 本文档使用 书栈(BookStack.CN) 构建 - 19 -

20.块儿作用域声明 3. for (var i = 0; i < 5; i++) { 4. let j = i; 5. funcs.push( function(){ 6. console.log( j ); 7. } ); 8. } 9. 10. funcs[3](); // 3 在这里,我们强制地为每次迭代都创建一个新的 j ,然后闭包以相同的方式工作。我喜欢前一种形 式;那种额外的特殊能力正是我支持 for(let .. ) .. 形式的原因。可能有人会争论说它有点儿 隐 晦,但是对我的口味来说,它足够 明确 了,也足够有用。 let 在 for..in 和 for..of (参见“ for..of 循环”)循环中也以形同的方式工作。 const 声明 还有另一种需要考虑的块儿作用域声明: const ,它创建 常量。 到底什么是一个常量?它是一个在初始值被设定后就成为只读的变量。考虑如下代码: 1. { 2. const a = 2; 3. console.log( a ); // 2 4. 5. a = 3; // TypeError! 6. } 变量持有的值一旦在声明时被设定就不允许你改变了。一个 const 声明必须拥有一个明确的初始 化。如果想要一个持有 undefined 值的 常量,你必须声明 const a = undefined 来得到它。 常量不是一个作用于值本身的制约,而是作用于变量对这个值的赋值。换句话说,值不会因 为 const 而冻结或不可变,只是它的赋值被冻结了。如果这个值是一个复杂值,比如对象或数组, 那么这个值的内容仍然是可以被修改的: 1. { 2. const a = [1,2,3]; 3. a.push( 4 ); 4. console.log( a ); // [1,2,3,4] 5. 6. a = 42; // TypeError! 7. } 变量 a 实际上没有持有一个恒定的数组;而是持有一个指向数组的恒定的引用。数组本身可以自由 本文档使用 书栈(BookStack.CN) 构建 - 20 -

21.块儿作用域声明 变化。 警告: 将一个对象或数组作为常量赋值意味着这个值在常量的词法作用域消失以前是不能够被垃圾回 收的,因为指向这个值的引用是永远不能解除的。这可能是你期望的,但如果不是你就要小心! 实质上, const 声明强制实行了我们许多年来在代码中用文体来表明的东西:我们声明一个名称全 由大写字母组成的变量并赋予它某些字面值,我们小心照看它以使它永不改变。 var 赋值没有强制 性,但是现在 const 赋值上有了,它可以帮你发现不经意的改变。 const 可以 被用于 for , for..in ,和 for..of 循环(参见“ for..of 循环”)的变量声 明。然而,如果有任何重新赋值的企图,一个错误就会被抛出,例如在 for 循环中常见的 i++ 子 句。 const 用还是不用 有些流传的猜测认为在特定的场景下,与 let 或 var 相比一个 const 可能会被JS引擎进行更多 的优化。理论上,引擎可以更容易地知道变量的值/类型将永远不会改变,所以它可以免除一些可能的 追踪工作。 无论 const 在这方面是否真的有帮助,还是这仅仅是我们的幻想和直觉,你要做的更重要的决定是 你是否打算使用常量的行为。记住:源代码扮演的一个最重要的角色是为了明确地交流你的意图是什 么,不仅是与你自己,而且还是与未来的你和其他的代码协作者。 一些开发者喜欢在一开始将每个变量都声明为一个 const ,然后当它的值在代码中有必要发生变化 的时候将声明放松至一个 let 。这是一个有趣的角度,但是不清楚这是否真正能够改善代码的可读 性或可推理性。 就像许多人认为的那样,它不是一种真正的 保护,因为任何后来的想要改变一个 const 值的开发者 都可以盲目地将声明从 const 改为 let 。它至多是防止意外的改变。但是同样地,除了我们的直 觉和感觉以外,似乎没有客观和明确的标准可以衡量什么构成了“意外”或预防措施。这与类型强制上 的思维模式类似。 我的建议:为了避免潜在的令人糊涂的代码,仅将 const 用于那些你有意地并且明显地标识为不会 改变的变量。换言之,不要为了代码行为而 依靠 const ,而是在为了意图可以被清楚地表明时, 将它作为一个表明意图的工具。 块儿作用域的函数 从ES6开始,发生在块儿内部的函数声明现在被明确规定属于那个块儿的作用域。在ES6之前,语言规 范没有要求这一点,但是许多实现不管怎样都是这么做的。所以现在语言规范和现实吻合了。 考虑如下代码: 1. { 本文档使用 书栈(BookStack.CN) 构建 - 21 -

22.块儿作用域声明 2. foo(); // 好用! 3. 4. function foo() { 5. // .. 6. } 7. } 8. 9. foo(); // ReferenceError 函数 foo() 是在 { .. } 块儿内部被声明的,由于ES6的原因它是属于那里的块儿作用域的。所以 在那个块儿的外部是不可用的。但是还要注意它在块儿里面被“提升”了,这与早先提到的遭受TDZ错 误陷阱的 let 声明是相反的。 如果你以前曾经写过这样的代码,并依赖于老旧的非块儿作用域行为的话,那么函数声明的块儿作用 域可能是一个问题: 1. if (something) { 2. function foo() { 3. console.log( "1" ); 4. } 5. } 6. else { 7. function foo() { 8. console.log( "2" ); 9. } 10. } 11. 12. foo(); // ?? 在前ES6环境下,无论 something 的值是什么 foo() 都将会打印 "2" ,因为两个函数声明被提升 到了块儿的顶端,而且总是第二个有效。 在ES6中,最后一行将抛出一个 ReferenceError 。 本文档使用 书栈(BookStack.CN) 构建 - 22 -

23.扩散/剩余 扩散/剩余 本文档使用 书栈(BookStack.CN) 构建 - 23 -

24.默认参数值 默认参数值 本文档使用 书栈(BookStack.CN) 构建 - 24 -

25.解构 解构 解构 对象属性赋值模式 不仅是声明 重复赋值 解构赋值表达式 太多,太少,正合适 默认值赋值 嵌套解构 参数解构 解构默认值 + 参数默认值 嵌套默认值:解构与重构 解构 ES6引入了一个称为 解构 的新语法特性,如果你将它考虑为 结构化赋值 那么它令人困惑的程度可 能会小一些。为了理解它的含义,考虑如下代码: 1. function foo() { 2. return [1,2,3]; 3. } 4. 5. var tmp = foo(), 6. a = tmp[0], b = tmp[1], c = tmp[2]; 7. 8. console.log( a, b, c ); // 1 2 3 如你所见,我们创建了一个手动赋值:从 foo() 返回的数组中的值到个别的变量 a , b , 和 c ,而且这么做我们就(不幸地)需要 tmp 变量。 相似地,我们也可以用对象这么做: 1. function bar() { 2. return { 3. x: 4, 4. y: 5, 5. z: 6 6. }; 7. } 8. 9. var tmp = bar(), 本文档使用 书栈(BookStack.CN) 构建 - 25 -

26.解构 10. x = tmp.x, y = tmp.y, z = tmp.z; 11. 12. console.log( x, y, z ); // 4 5 6 属性值 tmp.x 被赋值给变量 x , tmp.y 到 y 和 tmp.z 到 z 也一样。 从一个数组中取得索引的值,或从一个对象中取得属性并手动赋值可以被认为是 结构化赋值。ES6为 解构 增加了一种专门的语法,具体地称为 数组解构 和 对象结构。这种语法消灭了前一个代码段中 对变量 tmp 的需要,使它们更加干净。考虑如下代码: 1. var [ a, b, c ] = foo(); 2. var { x: x, y: y, z: z } = bar(); 3. 4. console.log( a, b, c ); // 1 2 3 5. console.log( x, y, z ); // 4 5 6 你很可能更加习惯于看到像 [a,b,c] 这样的东西出现在一个 = 赋值的右手边的语法,即作为要被 赋予的值。 解构对称地翻转了这个模式,所以在 = 赋值左手边的 [a,b,c] 被看作是为了将右手边的数组拆解 为分离的变量赋值的某种“模式”。 类似地, { x: x, y: y, z: z } 指明了一种“模式”把来自于 bar() 的对象拆解为分离的变量赋值。 对象属性赋值模式 让我们深入前一个代码段中的 { x: x, .. } 语法。如果属性名与你想要声明的变量名一致,你实际 上可以缩写这个语法: 1. var { x, y, z } = bar(); 2. 3. console.log( x, y, z ); // 4 5 6 很酷,对吧? 但 { x, .. } 是省略了 x: 部分还是省略了 : x 部分?当我们使用这种缩写语法时,我们实际上 省略了 x: 部分。这看起来可能不是一个重要的细节,但是一会儿你就会了解它的重要性。 如果你能写缩写形式,那为什么你还要写出更长的形式呢?因为更长的形式事实上允许你将一个属性 赋值给一个不同的变量名称,这有时很有用: 1. var { x: bam, y: baz, z: bap } = bar(); 2. 3. console.log( bam, baz, bap ); // 4 5 6 本文档使用 书栈(BookStack.CN) 构建 - 26 -

27.解构 4. console.log( x, y, z ); // ReferenceError 关于这种对象结构形式有一个微妙但超级重要的怪异之处需要理解。为了展示为什么它可能是一个你 需要注意的坑,让我们考虑一下普通对象字面量的“模式”是如何被指定的: 1. var X = 10, Y = 20; 2. 3. var o = { a: X, b: Y }; 4. 5. console.log( o.a, o.b ); // 10 20 在 { a: X, b: Y } 中,我们知道 a 是对象属性,而 X 是被赋值给它的源值。换句话说,它的语 义模式是 目标: 源 ,或者更明显地, 属性别名: 值 。我们能直观地明白这一点,因为它和 = 赋值 是一样的,而它的模式就是 目标 = 源 。 然而,当你使用对象解构赋值时 —— 也就是,将看起来像是对象字面量的 { .. } 语法放在 = 操 作符的左手边 —— 你反转了这个 目标: 源 的模式。 回想一下: 1. var { x: bam, y: baz, z: bap } = bar(); 这里面对称的模式是 源: 目标 (或者 值: 属性别名 )。 x: bam 意味着属性 x 是源值而 bam 是 被赋值的目标变量。换句话说,对象字面量是 target <-- source ,而对象解构赋值是 source --> target 。看到它是如何反转的了吗? 有另外一种考虑这种语法的方式,可能有助于缓和这种困惑。考虑如下代码: 1. var aa = 10, bb = 20; 2. 3. var o = { x: aa, y: bb }; 4. var { x: AA, y: BB } = o; 5. 6. console.log( AA, BB ); // 10 20 在 { x: aa, y: bb } 这一行中, x 和 y 代表对象属性。在 { x: AA, y: BB } 这一 行, x 和 y 也 代表对象属性。 还记得刚才我是如何断言 { x, .. } 省去了 x: 部分的吗?在这两行中,如果你在代码段中擦 掉 x: 和 y: 部分,仅留下 aa, bb 和 AA, BB ,它的效果 —— 从概念上讲,实际上不能 —— 将 是从 aa 赋值到 AA 和从 bb 赋值到 BB 。 所以,这种平行性也许有助于解释为什么对于这种ES6特性,语法模式被故意地反转了。 本文档使用 书栈(BookStack.CN) 构建 - 27 -

28.解构 注意: 对于解构赋值来说我更喜欢它的语法是 { AA: x , BB: y } ,因为那样的话可以在两种用法中 一致地使用我们更熟悉的 target: source 模式。唉,我已经被迫训练自己的大脑去习惯这种反转了, 就像一些读者也不得不去做的那样。 不仅是声明 至此,我们一直将解构赋值与 var 声明(当然,它们也可以使用 let 和 const )一起使用,但 是解构是一种一般意义上的赋值操作,不仅是一种声明。 考虑如下代码: 1. var a, b, c, x, y, z; 2. 3. [a,b,c] = foo(); 4. ( { x, y, z } = bar() ); 5. 6. console.log( a, b, c ); // 1 2 3 7. console.log( x, y, z ); // 4 5 6 变量可以是已经被定义好的,然后解构仅仅负责赋值,正如我们已经看到的那样。 注意: 特别对于对象解构形式来说,当我们省略了 var / let / const 声明符时,就必须将整 个赋值表达式包含在 () 中,因为如果不这样做的话左手边作为语句第一个元素的 { .. } 将被视 为一个语句块儿而不是一个对象。 事实上,变量表达式( a , y ,等等)不必是一个变量标识符。任何合法的赋值表达式都是允许 的。例如: 1. var o = {}; 2. 3. [o.a, o.b, o.c] = foo(); 4. ( { x: o.x, y: o.y, z: o.z } = bar() ); 5. 6. console.log( o.a, o.b, o.c ); // 1 2 3 7. console.log( o.x, o.y, o.z ); // 4 5 6 你甚至可以在解构中使用计算型属性名。考虑如下代码: 1. var which = "x", 2. o = {}; 3. 4. ( { [which]: o[which] } = bar() ); 5. 6. console.log( o.x ); // 4 本文档使用 书栈(BookStack.CN) 构建 - 28 -

29.解构 [which]: 的部分是计算型属性名,它的结果是 x —— 将从当前的对象中拆解出来作为赋值的源 头的属性。 o[which] 的部分只是一个普通的对象键引用,作为赋值的目标来说它与 o.x 是等价 的。 你可以使用普通的赋值来创建对象映射/变形,例如: 1. var o1 = { a: 1, b: 2, c: 3 }, 2. o2 = {}; 3. 4. ( { a: o2.x, b: o2.y, c: o2.z } = o1 ); 5. 6. console.log( o2.x, o2.y, o2.z ); // 1 2 3 或者你可以将对象映射进一个数组,例如: 1. var o1 = { a: 1, b: 2, c: 3 }, 2. a2 = []; 3. 4. ( { a: a2[0], b: a2[1], c: a2[2] } = o1 ); 5. 6. console.log( a2 ); // [1,2,3] 或者从另一个方向: 1. var a1 = [ 1, 2, 3 ], 2. o2 = {}; 3. 4. [ o2.a, o2.b, o2.c ] = a1; 5. 6. console.log( o2.a, o2.b, o2.c ); // 1 2 3 或者你可以将一个数组重排到另一个数组中: 1. var a1 = [ 1, 2, 3 ], 2. a2 = []; 3. 4. [ a2[2], a2[0], a2[1] ] = a1; 5. 6. console.log( a2 ); // [2,3,1] 你甚至可以不使用临时变量来解决传统的“交换两个变量”的问题: 1. var x = 10, y = 20; 2. 本文档使用 书栈(BookStack.CN) 构建 - 29 -