├── .gitignore ├── SUMMARY.md ├── README.md ├── appendix_b.md ├── Introduction.md ├── appendix_a.md ├── chapter_1.md ├── chapter_5.md ├── chapter_13.md ├── chapter_4.md ├── chapter_7.md ├── chapter_6.md ├── chapter_2.md └── chapter_9.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Node rules: 2 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 3 | .grunt 4 | 5 | ## Dependency directory 6 | ## Commenting this out is preferred by some people, see 7 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git 8 | node_modules 9 | 10 | # Book build output 11 | _book 12 | 13 | # eBook build output 14 | *.epub 15 | *.mobi 16 | *.pdf -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [关于](README.md) 4 | * [简介](Introduction.md) 5 | * [第一章 - 块级绑定](chapter_1.md) 6 | * [第二章 - 字符串与正则表达式](chapter_2.md) 7 | * [第三章 - 函数](chapter_3.md) 8 | * [第四章 - 扩展的对象功能](chapter_4.md) 9 | * [第五章 - 解构](chapter_5.md) 10 | * [第六章 - Symbols 与 Symbols属性(正在施工)](chapter_6.md) 11 | * [第七章 - Set 与 Map](chapter_7.md) 12 | * [第八章 - 迭代器与生成器](chapter_8.md) 13 | * [第九章 - 类](chapter_9.md) 14 | * [第十章 - 改进的数组功能(未施工)](chapter_10.md) 15 | * [第十一章 - Promises 与 异步编程](chapter_11.md) 16 | * [第十二章 - 代理与反射API(未施工)](chapter_12.md) 17 | * [第十三章 - 模块](chapter_13.md) 18 | * [附录A - 其它改进](appendix_a.md) 19 | * [附录B - 领悟 ECMAScript 7(2016)](appendix_b.md) 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](http://learnbb.net/oc-content/uploads/674/67362.jpg) 2 | 3 | # 关于 4 | 5 | 原书的观看地址:https://leanpub.com/understandinges6/read 6 | 7 | 本书的作者 Nicholas C. Zakas 是一名顶尖的前端工程师,曾出版《JavaScript 高级程序设计(Professional JavaScript for Web Developers)》《高性能 JavaScript(High Performance JavaScript)》等多本经典著作。 8 | 9 | ECMAScript 2015 (ES6)是 ECMAScript 发展的重要里程碑,给前端开发者带来超越以往的深远影响。该标准已于去年正式发布,而作者在几年前便开始在网络出版平台 [leanpub](http://leanpub.com) 上着手于本书的写作,对该标准进行了详细的解释,演示和探究,并于今年正式完结并出版。此书不论是作为学习资料还是技术手册都是难得的佳作。 10 | 11 | 本书的英文原版可以在线上[免费观看](https://leanpub.com/understandinges6/read), 也是我个人翻译的动力之一,由于本人只是出于兴趣而且水平实在有限,难免翻译的过程中会出现错误,希望读者能够谅解。我也尽量推荐去阅读英文原版,既原汁原味又能锻炼自身的英语水平。如遇到错误或有更好,更专业的翻译方式,欢迎向该书报出 [issue](https://github.com/OshotOkill/understandinges6-simplified-chinese/issues) 或提交 [PR](https://github.com/OshotOkill/understandinges6-simplified-chinese/pulls) 12 | 13 | 14 |
15 | 16 | > 本书已在 Amazon 上架,[购买地址](https://www.amazon.com/Understanding-ECMAScript-Definitive-JavaScript-Developers/dp/1593277571/ref=sr_1_1?ie=UTF8&qid=1473866321&sr=8-1&keywords=understanding+ecmascript+6) 17 | 18 |
19 | 20 | > 汉化正在进行中,勘误和修正将在汉化完全之后施行。 21 | 22 |
23 | 24 | ### 目录 25 | 26 | 27 | [第一章: **块级绑定(How Block Bindings Work)**](https://oshotokill.gitbooks.io/understandinges6-simplified-chinese/content/chapter_1.html) 28 | 29 | 30 | [第二章: **字符串及正则表达式(Strings and Regular Expressions )**](https://oshotokill.gitbooks.io/understandinges6-simplified-chinese/content/chapter_2.html) 31 | 32 | 33 | [第三章: **ECMAScript 6 中的函数(Functions in ECMAScript 6)**](https://oshotokill.gitbooks.io/understandinges6-simplified-chinese/content/chapter_3.html) 34 | 35 | 36 | [第四章: **扩展的对象功能(Expanded Object Functionality)**](https://oshotokill.gitbooks.io/understandinges6-simplified-chinese/content/chapter_4.html) 37 | 38 | 39 | [第五章: **解构(Destructuring for Easier Data Access)**](https://oshotokill.gitbooks.io/understandinges6-simplified-chinese/content/chapter_5.html) 40 | 41 | 42 | [第六章: **Symbols 与 Symbols属性(Symbols and Symbol Properties)**](https://oshotokill.gitbooks.io/understandinges6-simplified-chinese/content/chapter_6.html) - 正在施工 43 | 44 | 45 | [第七章: **Sets 与 Maps(Sets and Maps)**](https://oshotokill.gitbooks.io/understandinges6-simplified-chinese/content/chapter_7.html) 46 | 47 | 48 | [第八章: **迭代器与生成器(Iterators and Generators)**](https://oshotokill.gitbooks.io/understandinges6-simplified-chinese/content/chapter_8.html) 49 | 50 | 51 | [第九章: **类 (Introducing JavaScript Classes)**](https://oshotokill.gitbooks.io/understandinges6-simplified-chinese/content/chapter_9.html) 52 | 53 | 54 | [第十章: **改进的数组功能(Improved Array Capabilities)**](https://oshotokill.gitbooks.io/understandinges6-simplified-chinese/content/chapter_10.html) - 未施工 55 | 56 | 57 | [第十一章: **Promise 与 异步编程(Promises and Asynchronous Programming)**](https://oshotokill.gitbooks.io/understandinges6-simplified-chinese/content/chapter_11.html) 58 | 59 | 60 | [第十二章: **代理与反射API(Proxies and the Reflection API)**](https://oshotokill.gitbooks.io/understandinges6-simplified-chinese/content/chapter_12.html) - 未施工 61 | 62 | 63 | [第十三章: **模块(Encapsulating Code with Modules)**](https://oshotokill.gitbooks.io/understandinges6-simplified-chinese/content/chapter_13.html) 64 | 65 | 66 |
67 | 68 | [附录 A: **其它改进(Smaller ECMAScript 6 Changes)**](https://oshotokill.gitbooks.io/understandinges6-simplified-chinese/content/appendix_a.html) 69 | 70 | 71 | [附录 B: **领悟 ECMAScript 7(2016)(Understanding ECMAScript 7 (2016))**](https://oshotokill.gitbooks.io/understandinges6-simplified-chinese/content/appendix_b.html) 72 | -------------------------------------------------------------------------------- /appendix_b.md: -------------------------------------------------------------------------------- 1 | # 附录B - 领悟 ECMAScript 7(2016) 2 | 3 | 4 | ECMAScript 6 的正式发布耗时四年,在那之后,TC-39 发觉这么长的发布周期很不合理。于是,他们决定将发布周期固定为一年以便让新的语言特性能尽早地投入到开发中。 5 | 6 | 发布周期的缩短意味着新的 ECMAScript 版本中的特性会少于 ECMAScript 6 。为了适应这种变化,新版本将不再以显著的版本号,而是发布的年份来命名。因此 ECMAScript 6 也被称为 ECMAScript 2015,同时 ECMAScript 7 的正式名称为 ECMAScript 2016 。TC-39 也预计未来的 ECMAScript 版本全部使用年份命名。 7 | 8 | ECMAScript 2016 在 2016 年 3 月正式发布并向该语言添加了三项内容:一个新的数学运算符,数组方法和语法错误。本附录中就涵盖所有内容。 9 | 10 |
11 | 12 | ### 本章小结 13 | * [求幂运算符](#The-Exponentiation-Operator) 14 | * [Array.prototype.includes() 方法](#The-Array-prototype-includes-Method) 15 | * [函数作用域中严格模式的变更](#Change-to-Function-Scoped-Strict-Mode) 16 | 17 |
18 | 19 | ### 求幂运算符(The Exponentiation Operator) 20 | 21 | 22 | ECMAScript 2016 对 JavaScript 语法的唯一改进就是引入了求幂运算符,用来改进对基数的指数运算。JavaScript 已经有 Math.pow() 方法可以求幂,不过 JavaScript 也是为数不多的不能使用运算符而只能通过调用方法来完成求幂操作的语言之一(同时一些开发者也指出使用操作符的可读性更强)。 23 | 24 | 求幂运算符是两颗星号(**),左侧为基数,右侧为指数。例如: 25 | 26 | ```js 27 | let result = 5 ** 2; 28 | 29 | console.log(result); // 25 30 | console.log(result === Math.pow(5, 2)); // true 31 | ``` 32 | 33 | 该例的计算 5 的 2 次方,结果 25 。你也可以使用 Math.pow() 并得出相同的结果。 34 | 35 |
36 | 37 | #### 运算符的优先级(Order of Operations) 38 | 39 | 40 | 求幂运算符的优先级是在二元运算符中最高的(但是低于一元运算符)。这意味着在复合运算中它会被优先计算,如下所示: 41 | 42 | ```js 43 | let result = 2 * 5 ** 2; 44 | console.log(result); // 50 45 | ``` 46 | 47 | 5 与 2 的求幂运算先发生,并再与 2 相乘得出最终结果为 50 。 48 | 49 |
50 | 51 | #### 操作数的限制(Operand Restriction) 52 | 53 | 54 | 相比其它运算符,求幂运算符有一个独特的限制。运算符左侧不能为除了 ++ 或 -- 之外的一元表达式。例如,下面的示例会抛出错误。 55 | 56 | ```js 57 | // 语法错误 58 | let result = -5 ** 2; 59 | ``` 60 | 61 | 该例中,-5 会抛出语法错误,因为它包含歧义。- 作用的到底是 5 还是 5 ** 2 的结果呢?禁止左侧的一元表达式消除了这个歧义。为了阐明你的意图,你需要给 -5 或 5 ** 2 像下例这样添加圆括号: 62 | 63 | ```js 64 | // 没问题 65 | let result1 = -(5 ** 2); // 等于 -25 66 | 67 | // 同样也没问题 68 | let result2 = (-5) ** 2; // 等于 25 69 | ``` 70 | 71 | 如果你给求幂表达式添加括号,那么 - 会作用于它的结果。当你给 -5 添加括号时,意味着你想将它作为乘数。 72 | 73 | 如果在求幂运算符左侧使用 ++ 或 -- ,圆括号就没有必要使用,因为它们作用于操作数的行为十分明确。++ 或 -- 作为前缀时会在任何其它的操作发生之前修改操作数的值,而作为后缀则会在整个表达式计算完成之前不会有任何操作。这两种在求幂运算符左侧的用例都很安全,如下所示: 74 | 75 | ```js 76 | let num1 = 2, 77 | num2 = 2; 78 | 79 | console.log(++num1 ** 2); // 9 80 | console.log(num1); // 3 81 | 82 | console.log(num2-- ** 2); // 4 83 | console.log(num2); // 1 84 | ``` 85 | 86 | 该例中,num1 在求幂操作之前值发生了增长,所以 num1 的值为 3 并在求幂后结果为 9 。对于 num2 来讲,在求幂操作完成前它的值始终为 2,之后它的值才降为 1 。 87 | 88 |
89 | 90 | ### Array.prototype.includes() 方法(The Array.prototype.includes() Method) 91 | 92 | 93 | 你可能会想起 ECMAScript 6 添加的查看子字符串是否存在的 String.prototype.include() 方法。起初,ECMAScript 6 也想要引入 Array.prototype.includes() 方法来延续字符串与数组相似的传统。但是 Array.prototype.includes() 的相关规范没有在 ECMAScript 6 发布的期限内定稿,所以 Array.prototype.includes() 才会被推迟到 ECMAScript 2016 。 94 | 95 |
96 | 97 | #### 如何使用 Array.prototype.includes() (How to Use Array.prototype.includes()) 98 | 99 | 100 | Array.prototype.includes() 方法接收两个参数:要搜索的值和开始搜索的初始索引。当第二个参数被提供时,includes() 会从该索引处往后搜索(默认的初始索引值为 0)。如果想要搜索的值在数组中存在就会返回 true,否则为 false。例如: 101 | 102 | ```js 103 | let values = [1, 2, 3]; 104 | 105 | console.log(values.includes(1)); // true 106 | console.log(values.includes(0)); // false 107 | 108 | // start the search from index 2 109 | console.log(values.includes(1, 2)); // false 110 | ``` 111 | 112 | 这里,调用 values.includes() 并传入 1 会返回 true,而传入 0 返回的是 false ,因为 0 在数组中不存在。当第二个参数被添加后,数组会从索引值为 2 的位置开始搜索(包括的值为 3)并返回 false,因为从索引的位置到数组末尾不存在 1 这个值。 113 | 114 |
115 | 116 | #### 值的比较(Value Comparison) 117 | 118 | 119 | includes() 方法中使用 === 操作符来做值的比较。不过有一点除外: NaN 被认为和另一个 NaN 相等,即使 Nan === Nan 的计算结果为 false 。这和 indexOf() 方法的行为不同,因为后者严格使用 === 判断。为了阐明这项差异,考虑如下的代码: 120 | 121 | ```js 122 | let values = [1, NaN, 2]; 123 | 124 | console.log(values.indexOf(NaN)); // -1 125 | console.log(values.includes(NaN)); // true 126 | ``` 127 | 128 | values.indexOf() 方法返回 -1,即使 NaN 存在于 values 数组中。另一方面,由于比较操作符开了小差,values.includes() 会返回 true 。 129 | 130 |
131 | 132 | > **注意**: 如果你只想知道某个值在数组是否存在而不关心它的索引位置,我推荐使用 includes(),因为它对待 NaN 的方式和 indexOf() 方法不同。如果你想获取某个值在数组中的位置,那么必须使用 indexOf() 方法。 133 | 134 |
135 | 136 | 在实现中另一项怪异(quirk)之处是 +0 和 -0 被认为是相同的。在这个方面,indexOf() 和 includes() 的行为是一致的: 137 | 138 | ```js 139 | let values = [1, +0, 2]; 140 | 141 | console.log(values.indexOf(-0)); // 1 142 | console.log(values.includes(-0)); // true 143 | ``` 144 | 145 | 在这里,当传入 -0 作为参数时,indexOf() 和 includes() 会匹配 +0,因为它们认为这两个值是相同的。注意这和 Object.is() 方法的行为是相反的,后者认为 +0 和 -0 是不同的值。 146 | 147 |
148 | 149 | ### 函数作用域中严格模式的变更(Change to Function-Scoped Strict Mode) 150 | 151 | 152 | ECMAScript 5 引入了严格模式,此时它相比 ECMAScript 6 的严格模式要简单些。先不管这些,ECMAScript 6 仍然允许你使用 "use strict" 在全局作用域(使所有的代码运行在严格模式下)或函数作用域(只有该函数内部为严格模式)内定义严格模式。后者在 ECMAScript 6 中由于参数定义的多样性会存在一些问题,特别是当你使用解构和默认参数的时候。为了理解这个问题,考虑如下的代码: 153 | 154 | ```js 155 | function doSomething(first = this) { 156 | "use strict"; 157 | 158 | return first; 159 | } 160 | ``` 161 | 162 | 在这里,该命名参数首先由默认参数值 this 赋值。那么你认为 first 的值是什么呢? ECMAScript 6 规范指出 JavaScript 引擎在此时应该视参数运行在严格模式下,所以 first 应该为 undefind 。然而,在函数内部存在 "use strict" 的情况下,要将参数也一并运行在严格模式有一定的困难,因为参数默认值也可以是个函数。该问题的存在使得大部分 JavaScript 引擎都没有实现这个特性(所以 this 的值可能会是全局对象)。 163 | 164 | 咎于该实现的复杂性,ECMAScript 2016 规定如果函数使用了解构参数或默认参数,那么内部使用 "use strict" 声明将是违法的。只有当参数列表是简单的,并没有解构和默认值的情况下,函数主体才能有 "use strict" 出现。下面有一些示例: 165 | 166 | ```js 167 | // 没有问题 - 使用了简单的参数列表 168 | function okay(first, second) { 169 | "use strict"; 170 | 171 | return first; 172 | } 173 | 174 | // 语法错误 175 | function notOkay1(first, second=first) { 176 | "use strict"; 177 | 178 | return first; 179 | } 180 | 181 | // 语法错误 182 | function notOkay2({ first, second }) { 183 | "use strict"; 184 | 185 | return first; 186 | } 187 | ``` 188 | 189 | 你依旧可以在函数只包含简单的参数列表的条件下使用 "use strict",这也是 okay() 正常运行的原因(ECMAScript 5 同理)。notOkay1() 和 notOkay2() 函数都会抛出语法错误,因为它们分别使用了默认参数和解构参数。 190 | 191 | 总的来讲,这项改进同时解决了 JavaScript 开发者的困惑与 JavaScript 引擎的实现难题。 192 | 193 |
194 | -------------------------------------------------------------------------------- /Introduction.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | JavaScript 语言的核心特性是由 ECMA-262 标准定义的,而这个标准定义的语言被称为 ECMAScript,你所熟悉的在浏览器或者是在 Node.js 中运行的 JavaScript 其实是 ECMAScript 的一个超集。浏览器及 Node.js 通过额外的对象和方法添加了更多的功能,但是核心部分和 ECMAScript 仍保持一致。 总的来讲 ECMA-262 的持续发展是 JavaScript 获得如此成功不可或缺的要素, 本书涵盖了到目前为止最近的一次针对该语言的主要更新内容: ECMAScript 6 。 4 | 5 |
6 | 7 | * [ECMASciprt 6 的诞生之路](#The-Road-to-ECMAScript-6) 8 | * [关于本书](#About-This-Book) 9 | * [鸣谢](#Acknowledgments) 10 | 11 |
12 | 13 | ### ECMASciprt 6 的诞生之路 14 | 15 | 在2007年,JavaScript 已行至于交叉路口。Ajax 的流行宣告了动态 web 应用时代的到来,然而 JavaScript 自1999年 ECMA-262 发布了第三版(ES3)以后便从未发生变化,于是 TC-39 委员会便承担了发布下一版的任务,收集了大批草案并命名为 ECMAScript 4。ECMAScript 4 的变革范围十分广泛,语言的各个部分都有大大小小的变化。 添加的新特性中包括一些新语法,模块,类,传统的继承方式(classical inheritance),私有对象成员,可选类型注解(optional type annotations),以及其它等等。 16 | 17 | ECMAScript 4 的变动之大造成了 TC-39 委员会内部的分歧,一部分成员认为这些更改有些过火了。于是一组来自于雅虎,谷歌和微软的成员便自行撰写了下一代 ECMAScript 的草案,称其为 ECMAScript 3.1 ,其中 “3.1” 代表在已有标准之上的小增集。 18 | 19 | ECMAScript 3.1 的语法变动非常少,反而专注于属性特性(property attribute),支持原生 JSON 和在已有的对象之上添加更多方法。虽然早先曾有过 ECMAScript 3.1 与 EMCAScript 4 的融合尝试,不过两者之间巨大的差异和对语言发展方向认识的不同导致尝试失败了。 20 | 21 | 2008年,JavaScript的缔造者 Brendan Eich 认定 TC-39 应该专注于 ECMAScript 3.1 的标准化,ECMASciprt 4 中主要的语法变化和新特性应该搁置到下一代 ECMAScript 标准化之后。委员会的成员一起努力把 ECMAScript 3.1 和 ECMAScript 4 中的精华部分汇聚在一起,称其为 ECMAScript Harmony 。 22 | 23 | ECMAScript 3.1 最终作为 ECMA-262 第五版标准被发布,别名为 ECMAScript 5 。委员会为了在命名上避免和已胎死腹中的 ECMAScript 4 混淆,并未发布该标准。在那之后,以 ECMAScript Harmony 为起始,“harmony” 为精神的下一版标准 ECMAScript 6 的发布工作正式启动。 24 | 25 | ECMAScript 6 中所有选定草案完全被标准化的日期在2015年,因此正式被更名为 “ECMAScript 2015”(不过本书仍称其为 ECMAScript 6 ,因为开发者对这个名字更为熟悉)。该标准中新的特性作用范围十分广泛,包括全新的对象类型,模式以及给已有对象添加新的方法等等。 ECMAScript 6 的兴奋点在于所有的变动都是为了解决开发者在开发过程中实际存在的问题。 26 | 27 |
28 | 29 | ### 关于本书 30 | 31 | 对 ECMAScript 6 特性的深入了解是所有 JavaScript 开发者提升自身水平的关键。在不久的将来,ECMAScript 6 中包含的新特性会是 JavaScript 应用开发的基础,这也是本书所要阐述的。我希望你们能够通过阅读本书来了解 ECMAScript 6 以便在需要使用的时候快速上手。 32 | 33 |
34 | 35 | #### 浏览器及 Node.js 兼容性 36 | 37 | 许多 JavaScript 环境,如浏览器及 Node.js 都正在实现 ECMAScript 6。本书并不关心他们实现的差异性而仅关注在规范中定义的正确行为。因此在你的 JavaScript 环境中,一些行为可能与本书描述的不符。 38 | 39 |
40 | 41 | #### 本书的适用人群 42 | 43 | 本书的目的是给那些已经熟悉 JavaScript 和 ECMAScript 5 的人提供教程,但是并不强求读者对该语言有深入的认识,仅仅是帮助了解 ECMAScript 5 和 ECMAScript 6 之间的差异。本书特别针对的是那些想了解这门语言最新特性并在浏览器或 Node.js 里实现的中级或高级开发者。 44 | 45 | 本书并不适合从未写过 JavaScript 的初学者,你需要一定的基础知识才能通读本书。 46 | 47 |
48 | 49 | #### 总览 50 | 51 | 本书共13章,ECMAScript 6 中不同的部分由各自的章节分别阐释。许多章节都是以 ECMAScript 6 是怎样解决过去开发过程中存在的某处痛点开头,目的是为了让你对这些变更有个大体上的认识,此外所有的章节都包含实际的代码示例,以助你学习新的语法及概念。 52 | 53 |
54 | 55 | 第一章: **块级绑定(How Block Bindings Work)** 56 | 57 | > 讨论了块级声明 let 和 const —— var 的替代者们。 58 | 59 | 60 | 61 | 第二章: **字符串及正则表达式(Strings and Regular Expressions )** 62 | 63 | > 涵盖了新增加的字符串操作和查看方法,以及字符串模板(template strings)等内容。 64 | 65 | 66 | 67 | 第三章: **ECMAScript 6 中的函数(Functions in ECMAScript 6)** 68 | 69 | > 阐述了在 ECMAScript 6 中函数发生的变化,包括箭头函数,默认参数,剩余参数等。 70 | 71 | 72 | 73 | 第四章: **扩展的对象功能(Expanded Object Functionality)** 74 | 75 | > 揭示了对象在创建,修改及使用过程中发生的变化,包括对象字面量以及新的反射方法(reflection methods)。 76 | 77 | 78 | 第五章: **解构(Destructuring for Easier Data Access)** 79 | 80 | > 介绍了对象和数组的解构方法,允许你用更简洁的语法来分解(decompose)对象和数组。 81 | 82 | 83 | 第六章: **Symbols 与 Symbols属性(Symbols and Symbol Properties)** 84 | 85 | > 解释了symbols的概念,这是一种定义属性的新方式。Symbols 是新添加的原始类型,可以用来模糊(并非隐藏)对象的属性和方法。 86 | 87 | 88 | 第七章: **Sets 与 Maps(Sets and Maps)** 89 | 90 | > 展示了新的集合类型的细节,包括 Set,WeakSet,Map 和 WeakMap,这些类型在数组的基础之上添加了一组实用的扩展功能,包括添加语义(adding semantics),去重(de-duping)及针对JavaScript的内存管理(memory management) 91 | 92 | 93 | 第八章: **迭代器与生成器(Iterators and Generators)** 94 | 95 | > 讨论了迭代器和生成器这两个新添加的特性,它们允许你使用另一种强有力的方式操作集合中的数据,而在 ECMAScript 6 之前的版本中这是绝对无法做到的。 96 | 97 | 98 | 第九章: **类 (Introducing JavaScript Classes)** 99 | 100 | > 解释了在JavaScript中首次正式定义的类的概念。类的缺失是使其它语言开发者学习 JavaScript 感到困惑的原因之一,天之后使得 JavaScript 更易理解而且语法更为简洁 101 | 102 | 103 | 第十章: **改进的数组功能(Improved Array Capabilities)** 104 | 105 | > 阐释了原生数组的一些变化及新的有趣的使用方式。 106 | 107 | 108 | 第十一章:**Promise 与异步编程(Promises and Asynchronous Programming)** 109 | 110 | > Promises 成为了语言的一部分,由底层实现并被广泛且流行的库所支持。ECMAScript 6 原生支持并标准化了 promises 。 111 | 112 | 113 | 第十二章: **代理与反射API(Proxies and the Reflection API)** 114 | 115 | > 介绍了 JavaScript 中正式添加的反射API及新的代理对象(proxy object),使你可以拦截任何针对对象的操作。代理给予开发者操控对象空前的自由度和定义新交互方式的无限可能性。 116 | 117 | 118 | 第十三章: **模块(Encapsulating Code with Modules)** 119 | 120 | > 官方正式定义了 JavaScript 中模块的格式,目的是取代这些年涌现的各式各样的模块加载方案。 121 | 122 |
123 | 124 | 附录 A: **其它改进(Smaller ECMAScript 6 Changes)** 125 | 126 | > 集中介绍了 ECMAScript 6 中其它不太常见或者内容较少不大适合写为章节的内容。 127 | 128 | 129 | 附录 B: **领悟 ECMAScript 7(2016)(Understanding ECMAScript 7 (2016))** 130 | 131 | > 介绍了 ECMAScript 7(2016)新添加的两项内容,对 JavaScript 的改进相比 ECMAScript 6 甚微 132 | 133 |
134 | 135 | #### 排版协定 136 | 137 | 本书会使用以下的排版协定: 138 | 139 | * 斜体表示新术语 140 | * 代码或文件名使用等宽字体 141 | 142 | 另外, 长代码会包含在使用等宽字体的代码块中,如: 143 | 144 | ```doSomething 145 | function doSomething() { 146 | // empty 147 | } 148 | ``` 149 | 150 | 在代码块中, console.log() 右侧的注释表示代码执行后出现在浏览器或 Node.js 控制台上的输出内容,如 151 | 152 | ```comment 153 | console.log("Hi"); // "Hi" 154 | ``` 155 | 156 | 如果代码块中的某行代码抛出一个错误,右侧同样会有提示: 157 | 158 | ```error 159 | doSomething(); // error! 160 | ``` 161 | 162 |
163 | 164 | #### 协助于勘误 165 | 166 | 你可以向英文原版提交 issue,建议或者PR: [https://github.com/nzakas/understandinges6](https://github.com/nzakas/understandinges6) 167 | 168 | 简体中文版在这里报出 [issue](https://github.com/OshotOkill/understandinges6-simplified-chinese/issues) 和提交 [PR](https://github.com/OshotOkill/understandinges6-simplified-chinese/pulls) 169 | 170 | 如果你在阅读的过程中抱有疑问,也可以发送邮件给作者:[http://groups.google.com/group/zakasbooks.](http://groups.google.com/group/zakasbooks.) 171 | 172 |
173 | 174 | ### 鸣谢 175 | 176 | 英文原文的贡献者请查看 [原文](https://leanpub.com/understandinges6/read) Introduction 小结末尾 177 | 178 |
179 |
180 | \* 代表翻译存在问题 181 | -------------------------------------------------------------------------------- /appendix_a.md: -------------------------------------------------------------------------------- 1 | # 附录A - 其它改进 2 | 3 | 4 | 除了本书讲述的主要变化之外,ECMAScript 6 还对 JavaScript 做了一些虽小但很有意义的改进,包括简化整型的使用,新的数学运算方法,Unicode 标识符的轻微调整以及规范化 \_\_ proto \_\_ 属性。本附录包含以上所有的内容。 5 | 6 |
7 | 8 | ### 本章小结 9 | * [整型的使用](#Working-with-Integers) 10 | * [新的算术方法](#New-Math-Methods) 11 | * [Unicode 标识符](#Unicode-Identifiers) 12 | * [\_\_proto\_\_ 属性的规范化](#Formalizing-the-proto-Property) 13 | 14 |
15 | 16 | ### 整型的使用(Working with Integers) 17 | 18 | 19 | JavaScript 使用 IEEE 754 编码系统来表示整型(integer)和浮点类型(float),不过多年来它们引发了很多问题。虽然该门语言在幕后忍痛做了很多工作来让开发者不需要关心数型(number)编码的细节,然而问题仍旧会层出不穷的出现。为了解决它们,ECMAScript 6 做了一些工作来让整型更容易辨识和使用。 20 | 21 |
22 | 23 | #### 判断整型(Identifying Integers) 24 | 25 | 26 | 首先,ECMAScript 6 添加了 Number.isInterger() 方法用来判断某个值在 JavaScript 中是否为整型。虽然 JavaScript 使用 IEEE 754 来表示所有的数型,但是浮点类型和整型的存储方式是有差异的。Number.isInteger() 方法在调用时 JavaScript 引擎会根据值的存储形式来判断参数是否为整型。这意味着外观上看起来像浮点类型的数字也会被当作整型来存储,将它传给 Number.isInteger() 会返回 true 。例如: 27 | 28 | ```js 29 | console.log(Number.isInteger(25)); // true 30 | console.log(Number.isInteger(25.0)); // true 31 | console.log(Number.isInteger(25.1)); // false 32 | ``` 33 | 34 | 该段代码中,向 Number.isInteger() 传入 25 和 25.0 都会返回 true,即使后者看起来是浮点类型。在 JavaScript 中仅仅的给数字添加小数点并不会让它自动转化为浮点类型。既然 25.0 实际上等于 25,那么它会被当作整型存储。然而 25.1 由于小数位不为 0,所以将被视为浮点类型。 35 | 36 |
37 | 38 | #### 安全的整型类型(Safe Integers) 39 | 40 | 41 | IEEE 754 只能精确表示 -253 到 253 之间的整型数字,在该 “安全” 范围之外,数字的二进制表达就不再唯一(译者:参考 IEEE 754 的整型规范)。这意味着 JavaScript 只能在 IEEE 754 所能精确表示的范围内保证准确度。例如,考虑下面的例子: 42 | 43 | 44 | ```js 45 | console.log(Math.pow(2, 53)); // 9007199254740992 46 | console.log(Math.pow(2, 53) + 1); // 9007199254740992 47 | ``` 48 | 49 | 该例的输出并不是笔误,然而这两个不同的数值确实由相同的 JavaScript 整型表示。当数值愈发脱离安全范围,这个效果就越明显。 50 | 51 | ECMAScript 6 引入了 Number.isSafeInteger() 方法来检查整型是否在该语言所能精确表达的范围内,同时还添加了 Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER 属性来分别表示安全范围的上下边界。Number.isSafeInteger() 方法判断一个值是否为整型并位于安全的整数范围内,如下所示: 52 | 53 | ```js 54 | var inside = Number.MAX_SAFE_INTEGER, 55 | outside = inside + 1; 56 | 57 | console.log(Number.isInteger(inside)); // true 58 | console.log(Number.isSafeInteger(inside)); // true 59 | 60 | console.log(Number.isInteger(outside)); // true 61 | console.log(Number.isSafeInteger(outside)); // false 62 | ``` 63 | 64 | inside 代表最大的安全整数,所以 Number.isInteger() 和 Number.isSafeInteger() 都会返回 true 。outside 是首个存在问题的整型值,所以他虽然是整型但并不安全。 65 | 66 | 在绝大部分情况下,你只会想使用安全范围内的整型来做 JavaScript 的运算和比较,所以使用 Number.isSafeInteger() 做输入验证会是个好主意。 67 | 68 |
69 | 70 | ### 新的算术方法(New Math Methods) 71 | 72 | 73 | 游戏与图形计算重要性的俱增使得 ECMAScript 6 在给 JavaScript 引入了类型数组(typed array)的同时也意识到 JavaScript 引擎应该更有效率的处理很多数学运算。但是类似于 asm.js (工作 JavaScript 的子集上以提升性能)的优化策略需要更多的信息才能最快处理计算。例如,知道一个数字究竟是 32位整型还是 64 位 浮点类型对基于硬件(hardware-based)的操作来讲非常重要,而且要比基于软件(software-based)的操作要快很多。 74 | 75 | 于是,ECMAScript 6 给 Math 对象添加了几个方法来提升常用的数学运算的性能,同时包含大量数学运算的应用性能也会提升,比如图形编程(graphics program)。下面列出了这些新的方法: 76 | 77 | * `Math.acosh(x)` 返回 x 的反双曲余弦值(Returns the inverse hyperbolic cosine of x)。 78 | * `Math.asinh(x)` 返回 x 的反双曲正弦值(Returns the inverse hyperbolic sine of x)。 79 | * `Math.atanh(x)` 返回 x 的反双曲正切值(Returns the inverse hyperbolic tangent of x)。 80 | * `Math.cbrt(x)` 返回 x 的立方根(Returns the cubed root of x)。 81 | * `Math.clz32(x)` 返回 x 以 32 位整型数字的二进制表达形式开头为 0 的个数(Returns the number of leading zero bits in the 32-bit integer representation of x)。 82 | * `Math.cosh(x)` 返回 x 的双曲余弦值(Returns the hyperbolic cosine of x)。 83 | * `Math.expm1(x)` 返回 ex - 1 的值(Returns the result of subtracting 1 from the exponential function of x)。 84 | * `Math.fround(x)` 返回最接近 x 的单精度浮点数(Returns the nearest single-precision float of x)。 85 | * `Math.hypot(...values)` 返回参数平方和的平方根(Returns the square root of the sum of the squares of each argument)。 86 | * `Math.imul(x, y)` 返回两个参数之间真正的 32 位乘法运算结果(Returns the result of performing true 32-bit multiplication of the two arguments)。 87 | * `Math.log1p(x)` 返回以 自然对数为底的 x + 1 的对数(Returns the natural logarithm of 1 + x)。 88 | * `Math.log10(x)` 返回以 10 为底 x 的对数Returns the base 10 logarithm of x. 89 | * `Math.log2(x)` 返回以 2 为底 x 的对数(Returns the base 2 logarithm of x)。 90 | * `Math.sign(x)` 如果 x 为负数返回 -1;+0 和 -0 返回 0;正数则返回 1(Returns -1 if the x is negative, 0 if x is +0 or -0, or 1 if x is positive)。 91 | * `Math.sinh(x)` 返回 x 的双曲正弦值(Returns the hyperbolic sine of x)。 92 | * `Math.tanh(x)` 返回 x 的双曲正切值(Returns the hyperbolic tangent of x)。 93 | * `Math.trunc(x)` 移除浮点类型小数点后面的数字并返回一个整型数字(Removes fraction digits from a float and returns an integer)。 94 | 95 | 详细这些新的方法和实线细节超出了本书的描述范围。不过如果你的应用需要这些计算的话,那么确保在自己实现它之前先查看一下有没有现成的方法。 96 | 97 |
98 | 99 | ### Unicode 标识符(Unicode Identifiers) 100 | 101 | 102 | ECMAScript 6 提供了比之前版本的更好的 Unicode 支持度,同时也增添了标识符的种类。在 EMCAScript 5 中,你已经可以使用 Unicode 的转义字符串作为标识符。例如: 103 | 104 | ```js 105 | // 在 ECMAScript 5 and 6 中有效 106 | var \u0061 = "abc"; 107 | 108 | console.log(\u0061); // "abc" 109 | 110 | // 等效于: 111 | console.log(a); // "abc" 112 | ``` 113 | 114 | 该例中在 var 声明之后你可以同时使用 \u0061 或 a 来访问这个变量。在 EMCAScript 6 中,你还能使用转义的 Unicode code point 作为标识符,像这样: 115 | 116 | ```js 117 | // 在 ECMAScript 5 and 6 中有效 118 | var \u{61} = "abc"; 119 | 120 | console.log(\u{61}); // "abc" 121 | 122 | // 等效于: 123 | console.log(a); // "abc" 124 | ``` 125 | 126 | 127 | 本例只是使用了等效的 code point 替换了 \u0061 。除此之外它的行为和上例相同。 128 | 129 | 另外,ECMAScript 6 还正式根据 [Unicode Standard Annex #31: Unicode Identifier and Pattern Syntax](http://unicode.org/reports/tr31/) 规范了合法标识符,遵循以下规则: 130 | 131 | 1. 第一个字符必须是 $, _\_,或任何包含 由 ID_start 派生的核心属性(derived core properties)的 Unicode symbol 。* 132 | 2. 随后的字符必须为 $,_\_,\u200c(零宽的 non-joiner 字符),\u200d(零宽的 joiner 字符),或任何包含 由 ID_start 派生的核心属性的 Unicode symbol 。* 133 | 134 | ID_Start 和 ID_Continue 的派生核心属性由 Unicode Identifier and Pattern Syntax 定义以便规定一种正确的方式来使用和命名(如变量和域名)标识符。该规范并不属于 JavaScript 。 135 | 136 |
137 | 138 | ### \_\_proto\_\_ 属性的规范化(Formalizing the \_\_proto\_\_ Property) 139 | 140 | 141 | 在 ECMAScript 5 规范完成之前,一些 JavaScript 引擎实现了一个被称为 \_\_proto\_\_ 的属性来对 [[Prototype]] 施行 get 和 set 操作。实际上,\_\_proto\_\_ 是 Object.getPrototypeOf() 和 Object.setPrototypeo() 方法的先驱。指望所有的 JavaScript 引擎移除这个属性是不现实的(有些流行的库还在使用 \_\_proto\_\_),于是 ECMAScript 6 将 \_\_proto\_\_ 标准化。不过附录 B 中的 ECMA-262 版本对该规范有如下警告: 142 | 143 |
144 | 145 | > 这些特性并不是 ECMAScript 语言的核心部分。开发者在书写新的 ECMAScript 代码时不该使用或者干脆认为它们并不存在。除非这些特性是浏览器中的一部分或服务于与兼容性相关的 ECMAScript 代码,否则 ECMAScript 不鼓励实现它们。 146 | 147 |
148 | 149 | ECMAScript 规范更推荐使用 Object.getPrototypeOf() 和 Object.setPrototypeOf() 是因为 \_\_proto\_\_ 有如下特征: 150 | 151 | 1. \_\_proto\_\_在对象字面量中只能出现一次,否则将会抛出错误。这是唯一受限制的对象字面量属性。 152 | 2. 动态属性形式的 ["\_\_proto\_\_"] 表现类似于一般的属性而且并不会返回或赋值给当前对象。它具有非动态属性的所有特征,这意味着它也是动态属性唯一的例外。 153 | 154 | 虽然你应该避免使用 \_\_proto\_\_ 属性,但是它的规范定义很有意思。在实现了 ECMAScript 6 的引擎中,Object.prototype.\_\_proto\_\_ 是一个访问器属性,它的 get 和 set 方法分别调用 Object.getPrototypeOf() 和 Object.setPrototypeOf() 。这意味着使用 \_\_proto\_\_ 和 Object.getPrototypeOf() 和 Object.setPrototypeOf() 没有本质上的区别,除了 \_\_proto\_\_ 可以直接在对象字面量中使用。以下是它的使用方式: 155 | 156 | ```js 157 | let person = { 158 | getGreeting() { 159 | return "Hello"; 160 | } 161 | }; 162 | 163 | let dog = { 164 | getGreeting() { 165 | return "Woof"; 166 | } 167 | }; 168 | 169 | // 原型为 person 170 | let friend = { 171 | __proto__: person 172 | }; 173 | console.log(friend.getGreeting()); // "Hello" 174 | console.log(Object.getPrototypeOf(friend) === person); // true 175 | console.log(friend.__proto__ === person); // true 176 | 177 | // 将原型设置为 dog 178 | friend.__proto__ = dog; 179 | console.log(friend.getGreeting()); // "Woof" 180 | console.log(friend.__proto__ === dog); // true 181 | console.log(Object.getPrototypeOf(friend) === dog); // true 182 | ``` 183 | 184 | 与调用 Object.create() 来创建 friend 对象相反,该例在标准的对象字面量中添加了 \_\_proto\_\_ 属性。不过,当使用前者这种方式时,你需要为所有额外添加的对象属性定义完整的描述符 185 | 186 |
187 | -------------------------------------------------------------------------------- /chapter_1.md: -------------------------------------------------------------------------------- 1 | # 块级绑定 (Block Bindings) 2 | 3 | 变量声明的工作方式向来是 JavaScript 编程中难以理解的部分之一。在大部分C和类C(C-based)语言中,变量的声明与创建(或绑定)发生在同一位置,然而在 JavaScript 中情况就有所不同,变量的创建方式取决于你如何声明它,ECMAScript 6 提供了额外的选项方便你能自由控制变量的作用范围。本章会演示为什么传统的 var 声明令人费解,并引出 ECMAScript 6 中的块级绑定,罗列一些实践场景来使用它们。 4 | 5 |
6 | 7 | ### 本章小结 8 | * [var 声明与变量提升](#Var-Declarations-and-Hoisting) 9 | * [块级声明](#Block-Level-Declarations) 10 | * [循环中的块级绑定](#Block-Binding-in-Loops) 11 | * [全局块级绑定](#Global-Block-Bindings) 12 | * [块级绑定的最佳实践](#Emerging-Best-Practices-for-Block-Bindings) 13 | * [总结](#Summary) 14 | 15 |
16 | 17 | ### var 声明与变量提升 (Var Declarations and Hoisting) 18 | 19 | 使用 var 关键字声明的变量,不论在何处都会被视作在函数级作用域内顶部的位置发生(如果不包含在函数内则为全局作用域内)。为了说明变量提升到底是什么,查看如下函数定义: 20 | 21 | ```js 22 | function getValue(condition) { 23 | 24 | if (condition) { 25 | var value = "blue"; 26 | 27 | // 其它代码 28 | 29 | return value; 30 | } else { 31 | 32 | // value 可以被访问到,其值为 undefined 33 | 34 | return null; 35 | } 36 | 37 | // 这里也可以访问到 value,值仍为 undefined 38 | } 39 | ``` 40 | 41 | 如果你不太熟悉 JavaScript,或许会认为只有 condition 为 true 时变量 value 才会被创建。实际上,value 总是会被创建。JavaScript 引擎在幕后对 getValue 函数做了调整,可以视为: 42 | 43 | ```js 44 | function getValue(condition) { 45 | 46 | var value; 47 | 48 | if (condition) { 49 | value = "blue"; 50 | 51 | // 其它代码 52 | 53 | return value; 54 | } else { 55 | 56 | return null; 57 | } 58 | } 59 | ``` 60 | 61 | 变量的声明被提升至顶部,但是初始化的位置并没有改变,这意味着在 else 从句内部也能访问 value 变量,但如果真的这么做的话,value 的值会是 undefined,因为它并没有被初始化或赋值。 62 | 63 | 刚开始接触 JavaScript 的开发者总是要花一段时间来习惯变量提升,对该独特概念的陌生也会造成 bug。因此 ECMAScript 6 引入了块级作用域的概念使得变量的生命周期变得更加可控。 64 | 65 |
66 | 67 | ### 块级声明(Block-Level Declarations) 68 | 69 | 块级声明指的是该声明的变量无法被代码块外部访问。块作用域,又被称为词法作用域(lexical scopes),可以在如下的条件下创建: 70 | 71 | * 函数内部 72 | * 在代码块(即 { 和 })内部 73 | 74 | 块级作用域是很多类C语言的工作机制,ECMAScript 6 引入块级声明的目的是增强 JavaScript 的灵活性,同时又能与其它编程语言保持一致。 75 | 76 |
77 | 78 | #### let 声明 79 | 80 | let 声明的语法和 var 完全一致。你可以简单的将所有 var 关键字替换成 let,但是变量的作用域会限制在当前的代码块中(稍后讨论其它细微的差别)。既然 let 声明不会将变量提升至当前作用域的顶部,你或许要把它们手动放到代码块的开头,因为只有这样它们才能被代码块的其它部分访问。举个例子: 81 | 82 | ```js 83 | function getValue(condition) { 84 | 85 | if (condition) { 86 | let value = "blue"; 87 | 88 | // 其它代码 89 | 90 | return value; 91 | } else { 92 | 93 | // value 并不存在(无法访问) 94 | 95 | return null; 96 | } 97 | 98 | // 这里 value 也不存在 99 | } 100 | ``` 101 | 102 | 本次 getValue 函数的写法的默认行为更贴近你脑海中C和其它类C语言的印像。既然变量的声明由 var 替换成了 let,那么它们就不会自动提升到当前函数作用域的顶部,除非执行流程到了 if 从句内部,其它情况时是没有办法对该变量进行访问的。如果 if 从句中 condition 的值为 false,那么 value 变量就不会被声明或者初始化。 103 | 104 |
105 | 106 | #### 禁止重复声明 107 | 108 | 109 | 如果一个标识符在当前作用域里已经存在,那么再用 let 声明相同的标识符或抛出错误 110 | 111 | ```js 112 | var count = 30; 113 | 114 | // 语法错误 115 | let count = 40; 116 | ``` 117 | 118 | 在本例中,count 被声明了两次: 一次是被 var 另一次被 let 。因为 let 不会重新定义已经存在的标识符,所以会抛出一个错误。反过来讲,如果在当前包含的作用域内 let 声明了一个全新的变量,那么就不会有错误抛出。正如以下的代码演示: 119 | 120 | ```js 121 | var count = 30; 122 | 123 | // 不会抛出错误 124 | if (condition) { 125 | 126 | let count = 40; 127 | 128 | // 其它代码 129 | } 130 | ``` 131 | 132 | 本次 let 声明没有抛出错误的原因是 let 声明的变量 count 是在 if 从句的代码块中,并非和 var 声明的 count 处于同一级。在 if 代码块中,这个新声明的 count 会屏蔽掉全局变量 count ,避免在执行流程未跳出 if 之前访问到它。 133 | 134 |
135 | 136 | #### const 声明(Constant Declarations) 137 | 138 | 在 ECMAScript 6 中也可使用常量(const)语法来声明变量。该种方式声明的变量会被视为常量,这意味着它们不能再次被赋值。由于这个原因,所有的 const 声明的变量都必须在声明处初始化。示例如下: 139 | 140 | ```js 141 | // 合法的声明 142 | const maxItems = 30; 143 | 144 | // 语法错误:未初始化 145 | const name; 146 | ``` 147 | 148 | 变量 maxItems 已经被初始化,所以这里不会出现任何问题。至于 name 变量,由于你未对其进行初始化赋值所以在运行时会报错。 149 | 150 |
151 | 152 | ##### const 声明 vs let 声明(Constants vs Let Declarations) 153 | 154 | 155 | const 和 let 都是块级声明,意味着执行流跳出声明所在的代码块后就没有办法在访问它们,同样 const 变量也不会被提升,示例如下: 156 | 157 | ```js 158 | if (condition) { 159 | const maxItems = 5; 160 | 161 | // 其它代码 162 | } 163 | 164 | // maxItems 在这里无法访问 165 | ``` 166 | 167 | 在这段代码中,maxItems 在 if 从句的代码块中作为常量被声明。一旦执行流跳出 if 代码块,外部就无法访问这个变量。 168 | 169 | 另一处和 let 相同的地方是,const 也不能对已存在的标识符重复定义,不论该标识符由 var(全局或函数级作用域)还是 let (块级作用域)定义。例如以下的代码: 170 | 171 | ```js 172 | var message = "Hello!"; 173 | let age = 25; 174 | 175 | // 下面每条语句都会抛出错误 176 | const message = "Goodbye!"; 177 | const age = 30; 178 | 179 | ``` 180 | 181 | 以上两条 const 声明如果单独存在即是合法的,很遗憾的是在本例中前面出现了 var 和 let 声明的相同标识符(变量) 182 | 183 | 尽管 const 和 let 使用起来很相似,但是必须要记住它们的根本性的差异:不管是在严格(strict)模式还是非严格模式(non-strict)模式下,const 变量都不允许被重复赋值。 184 | 185 | ```js 186 | const maxItems = 5; 187 | 188 | maxItems = 6; // 抛出错误 189 | ``` 190 | 191 | 和其它编程语言类似,maxItems 不能被赋予新的值,然而和其它语言不同的是,const 变量的值如果是个对象,那么这个对象本身可以被修改。 192 | 193 |
194 | 195 | ##### 将对象赋值给 const 变量(Declaring Objects with Const) 196 | 197 | const 声明只是阻止变量和值的再次绑定而不是值本身的修改。这意味着 const 不能限制对于值的类型为对象的变量的修改,示例如下: 198 | 199 | ```js 200 | const person = { 201 | name: "Nicholas" 202 | }; 203 | 204 | // 正常 205 | person.name = "Greg"; 206 | 207 | // 抛出错误 208 | person = { 209 | name: "Greg" 210 | }; 211 | ``` 212 | 213 | 在这里,person 变量一开始已经和包含一个属性的对象绑定。修改 person.name 是被允许的因为 person 的值(地址)未发生改变,但是尝试给 person 赋一个新值(代表重新绑定变量和值)的时候会报错。这个微妙之处会导致很多误解。只需记住:const 阻止的是绑定的修改,而不是绑定值的修改。 214 | 215 |
216 | 217 | #### 暂存性死区(The Temporal Dead Zone) 218 | 219 | 220 | let 或 const 声明的变量在声明之前不能被访问。如果执意这么做会出现错误,甚至是 typeof 这种安全调用(safe operations)也不被允许的: 221 | 222 | ```js 223 | if (condition) { 224 | console.log(typeof value); // ReferenceError! 225 | let value = "blue"; 226 | } 227 | ``` 228 | 229 | 在这里,变量 value 由 let 声明并被初始化,但是由于该语句之前抛出了错误导致其从未被执行。这种现象的原因是该语句存在于被 JavaScript 社区泛称为暂存性死区(Temproal Dead Zone, TDZ)之内。ECMAScript 规范中未曾对 TDZ 有过显式命名,不过在描述 let 和 const 声明的变量为何在声明前不可访问时,该术语经常被使用。本章会涵盖在 TDZ 中关于声明位置的一些微妙部分。此外示例虽然全部用的是 let ,但是实际用法和 const 别无二致。 230 | 231 | 当 JavaScript 引擎在作用域中寻找变量声明时,会将变量提升到函数/全局作用域的顶部(var)或是放入 TDZ(let 和const)内部。任何试图访问 TDZ 内部变量的行为都会以抛出运行时(runtime)错误而告终。当执行流达到变量声明的位置时,变量才会被移出 TDZ ,代表它们可以被安全使用。 232 | 233 | 由 let 或 const 声明的变量,如果你想在定义它们之前就使用,那么以上所述的准则都是铁打不变的。正如之前的示例所演示的那样,typeof 操作符都不能幸免。不过,你可以在代码块之外的地方对变量使用 typeof 操作符,但结果可能并不是你想要的。考虑如下的代码: 234 | 235 | ```js 236 | console.log(typeof value); // "undefined" 237 | 238 | if (condition) { 239 | let value = "blue"; 240 | } 241 | ``` 242 | 243 | 当对 value 变量使用 typeof 操作符时它并没有处在 TDZ 内部,因为它的位置在变量声明位置所在的代码块之外。这意味着没有发生任何绑定,所以 typeof 仅返回 “undefined” 244 | 245 | TDZ 只是发生在块级绑定中独特的特设定之一,另一个特殊设定发生在循环中。 246 | 247 |
248 | 249 | ### 循环中的块级绑定(Block Binding in Loops) 250 | 251 | 或许开发者对块级作用域有强烈需求的场景之一就是循环,因为它们不想让循环外部访问到内部的索引计数器。举个例子,以下的代码在 JavaScript 编程中并不罕见: 252 | 253 | ```js 254 | for (var i = 0; i < 10; i++) { 255 | process(items[i]); 256 | } 257 | 258 | // 这里仍然可以访问到 i 259 | console.log(i); // 10 260 | ``` 261 | 262 | 块级作用域在其它语言内部是默认的,以上的代码的执行过程也并无差异,但区别在于变量 i 只能在循环代码块内部使用。然而在 JavaScript中,变量的提升导致块外的部分在循环结束后依然可以访问 i 。若将 var 替换为 let 则更符合预期: 263 | 264 | ```js 265 | for (let i = 0; i < 10; i++) { 266 | process(items[i]); 267 | } 268 | 269 | 270 | // 在这里访问 i 会抛出错误 271 | console.log(i); 272 | ``` 273 | 274 | 在本例中变量 i 只存在于 for 循环代码块中,一旦循环完毕变量 i 将不复存在。 275 | 276 |
277 | 278 | #### 循环中的函数(Functions in Loops) 279 | 280 | 281 | 长久以来 var 声明的特性使得在循环中创建函数问题多多,因为循环中声明的变量在块外也可以被访问,考虑如下的代码: 282 | 283 | ```js 284 | var funcs = []; 285 | 286 | for (var i = 0; i < 10; i++) { 287 | funcs.push(function() { console.log(i); }); 288 | } 289 | 290 | funcs.forEach(function(func) { 291 | func(); // 输出 "10" 共10次 292 | }); 293 | ``` 294 | 295 | 你可能认为这段代码只是普通的输出 0 - 9 这十个数字,但事实上它会连续十次输出 “10”。这是因为每次迭代的过程中 i 是被共享的,意味着循环中创建的函数都保持着对相同变量的引用。当循环结束后 i 的值为 10,于是当 console.log(i)被调用后,该值会被输出。 296 | 297 | 298 | 为了修正这个问题,开发者们在循环内部使用即时调用函数表达式(immediately-invoked function expressions, IIFEs)来迫使每次迭代时创建一份当前索引值的拷贝,示例如下: 299 | 300 | ```js 301 | var funcs = []; 302 | 303 | for (var i = 0; i < 10; i++) { 304 | funcs.push((function(value) { 305 | return function() { 306 | console.log(value); 307 | } 308 | }(i))); 309 | } 310 | 311 | funcs.forEach(function(func) { 312 | func(); // 输出 0,1,2 ... 9 313 | }); 314 | ``` 315 | 316 | 这种写法在循环内部使用了 IIFE,并将变量 i 的值传入 IIFE 以便拷贝索引值并存储起来,这里传入的索引值为同样被当前的迭代所使用,所以循环完毕后每次调用的输出值正如所期待的那样是 0 - 9 。幸运的是,ECMAScript 6 中 let 和 const 的块级绑定对循环代码进行简化。 317 | 318 |
319 | 320 | #### 循环中的 let 声明(Let Declarations in Loops) 321 | 322 | 323 | let 声明通过有效地模仿 上例中 IIFE 的使用方式来简化循环代码。在每次迭代中,一个新的同名变量会被创建并初始化。这意味着你可以抛弃 IIFE 的同时也能获得相同的结果。 324 | 325 | ```js 326 | var funcs = []; 327 | 328 | for (let i = 0; i < 10; i++) { 329 | funcs.push(function() { 330 | console.log(i); 331 | }); 332 | } 333 | 334 | funcs.forEach(function(func) { 335 | func(); // 输出 0,1,2 ... 9 336 | }) 337 | ``` 338 | 339 | 这段循环代码的执行效果完全等同于使用 var 声明和 IIFE,但显然更加简洁。let 声明使得每次迭代都会创建一个变量 i,所以循环内部创建的函数会获得各自的变量 i 的拷贝。每份拷贝都会在每次迭代的开始被创建并被赋值。这同样适用于 for-in 和 for-of 循环,如下所示: 340 | 341 | ```js 342 | var funcs = [], 343 | object = { 344 | a: true, 345 | b: true, 346 | c: true 347 | }; 348 | 349 | for (let key in object) { 350 | funcs.push(function() { 351 | console.log(key); 352 | }); 353 | } 354 | 355 | funcs.forEach(function(func) { 356 | func(); // 输出 "a","b" 和 "c" 357 | }); 358 | 359 | ``` 360 | 361 | 在本例中,for-in 循环的表现和 for 循环相同。每次循环的开始都会创建一个新的变量 key 的绑定,所以每个函数都会有各自 key 变量值的备份。结果就是每个函数都会输出不同的值。如果循环中 key 由 var 声明,那么所有的函数会输出 "c" 。 362 | 363 |
364 | 365 | > 不得不提的是,let 声明在上述循环内部中的表现是在规范中特别定义的,和非变量提升这一特性没有直接的关系。实际上,早期 let 的实现并不会表现中这种效果,它是在后来被添加到规范中的。 366 | 367 |
368 | 369 | #### 循环中的 const 声明(Constant Declarations in Loops) 370 | 371 | 372 | ECMAScript 6 规范中没有明确禁止在循环中使用 const 声明,不过其具体的表现要取决于你使用哪种循环方式。对于普通 的 for 循环你可以在初始化(initializer)语句里使用 const 声明,但当你想要修改该声明变量时循环会报错: 373 | 374 | ```js 375 | var funcs = []; 376 | 377 | // throws an error after one iteration 378 | for (const i = 0; i < 10; i++) { 379 | funcs.push(function() { 380 | console.log(i); 381 | }); 382 | } 383 | ``` 384 | 385 | 在这段代码中,变量 i 被声明为常量。在循环开始迭代,即 i 为 0 的时候,代码是可以运行的,不过当步骤执行到 i++ 的那一刻会发生错误,因为你正在修改一个常量。由此,只有在循环过程中不更改在初始化语句中声明的变量的条件下,才能在里面使用 const 声明。 386 | 387 | 388 | 另外,当使用 for-in 或 for-of 循环时,const 声明的变量的表现和 let 完全一致。所以以下的代码不会出错: 389 | 390 | ```js 391 | var funcs = [], 392 | object = { 393 | a: true, 394 | b: true, 395 | c: true 396 | }; 397 | 398 | // 不会报错 399 | for (const key in object) { 400 | funcs.push(function() { 401 | console.log(key); 402 | }); 403 | } 404 | 405 | funcs.forEach(function(func) { 406 | func(); // 输出 "a","b" 和 "c" 407 | }); 408 | ``` 409 | 410 | 这段代码的作用和 “循环中的 let 声明” 一节的第二个示例相同,唯一的差异是变量 key 的值不能被修改。for-in 和 for-of 循环能正常使用 const 是因为每次迭代都会创建一个新的变量绑定而不是去试图修改已存在的绑定(如上例)。 411 | 412 |
413 | 414 | ### 全局块级绑定(Global Block Bindings) 415 | 416 | 417 | let 与 const 另一处不同体现在全局作用域上。当在全局作用域内使用 var 声明时会创建一个全局变量,同时也是全局对象(浏览器环境下是 window)的一个属性。这意味着全局对象的属性可能会意外地被重写覆盖,例如: 418 | 419 | ```js 420 | // 在浏览器中运行 421 | var RegExp = "Hello!"; 422 | console.log(window.RegExp); // "Hello!" 423 | 424 | var ncz = "Hi!"; 425 | console.log(window.ncz); // "Hi!" 426 | ``` 427 | 428 | 虽然全局 RegExp 对象在 window 上已定义,但 var 声明很容易就能把它覆盖。这个例子就声明了一个新的 RegExp 变量并将原本的 RegExp 替换掉了。同样,ncz 也作为一个全局变量被声明并成为了 window 的属性。这就是 JavaScript 的工作机制。 429 | 430 | 如果你在全局作用域内使用 let 或 const,那么绑定就会发生在全局作用域内,但是不会向全局对象内部添加任何属性。这就意味着你不能使用 let 或 const 重写全局变量,而仅能屏蔽掉它们。如下所示: 431 | 432 | ```js 433 | // 在浏览器中运行 434 | let RegExp = "Hello!"; 435 | console.log(RegExp); // "Hello!" 436 | console.log(window.RegExp === RegExp); // false 437 | 438 | const ncz = "Hi!"; 439 | console.log(ncz); // "Hi!" 440 | console.log("ncz" in window); // false 441 | ``` 442 | 443 | 在这里,let 声明创建了新的 RegExp 绑定并屏蔽掉了全局对象的 RegExp 属性。也就是说 window.RegExp 和 RegExp 并不等同,所以全局作用域并没有被污染。同样 const 声明创建了新的绑定的同时也没有在全局对象内部添加任何属性。这个设定使得在全局作用域内使用 let 或 const 声明要比 var 安全得多,特别是在你不想在全局对象内部添加属性的时候。 444 | 445 |
446 | 447 | > 如果你想让代码可以被全局对象访问,你仍然需要使用 var,特别是当你想要在多个 window 和 frame 之间共享代码的时候 448 | 449 |
450 | 451 | ### 块级绑定的最佳实践(Emerging Best Practices for Block Bindings) 452 | 453 | 454 | 当 ECMAScript 6 还在酝酿中的时候,一个普遍的共识是使用 let 而不是 var 来作为默认的变量声明方式。对大多数 JavaScript 开发者来讲,let 才是 var 该有的表现形式,自然而然这种取代十分合理。在这个理念下,你应该使用 const 声明来保护一些变量不被修改。 455 | 456 | 然而,当越来越多的开发者迁移到 ECMAScript 6 之后,一个新的实践逐渐流行了起来:const 是声明变量的默认方式,仅当你明确哪些变量之后需要修改的情况下再用 let 声明那些变量。这个实践的缘由是大部分变量在初始化之后不应该被修改,因为这样做是造成 bug 的根源之一。这个理念有大批的受众而且在你接纳 ECMAScript 6 之后值得考虑。 457 | 458 |
459 | 460 | ### 总结(Summary) 461 | 462 | let 和 const 块级绑定给 JavaScript 引入了词法作用域的概念。这些声明不会被提升且仅存在于声明它们的代码块中。它们的行为和其它语言更为相似且减少了意外错误的发生,因为变量会在原处被声明。作为副作用之一,你不能在声明之前就使用它们,即使是 typeof 这种安全操作也不被允许。暂存性死区(temproal dead zone, TDZ)中绑定的存在会导致在声明位置之前的访问以失败告终。 463 | 464 | 在大多数情况下,let 和 const 的表现和 var 很相似,但在循环中并不是这样。对于 let 和 const 来讲,每次迭代的开始都会创建新的绑定,意味着循环内部创建的函数可以在迭代时访问到当前的索引值,而不是在整个循环结束之后(即 var 的表现形式)。在 for 中使用 let 声明也同样适用,但 const 声明则会抛出错误。 465 | 466 | 目前关于块级绑定的最佳实践是使用 const 作为默认的声明方式,当变量需要更改时切换为 let 声明。保证代码中最基本的不可变性能防止错误的发生。 467 | 468 |
469 | -------------------------------------------------------------------------------- /chapter_5.md: -------------------------------------------------------------------------------- 1 | # 解构(Destructuring for Easier Data Access) 2 | 3 | 4 | 对象和数组字面量在 JavaScript 中是最常出现的两种表现形式。感谢 JSON 这种数据格式的流行,它们在该门语言中的地位变得举足轻重。事先定义好对象和数组,并在需要的时候系统性地提取出需要的部分,这在编程中十分常见。为了简化从数据结构中获取相关子集的操作,ECMAScript 6 引入了解构(destructuring)。 5 | 6 |
7 | 8 | ### 本章小结 9 | * [解构的实用性在哪里](#Why-is-Destructuring-Useful) 10 | * [对象解构](#Object-Destructuring) 11 | * [数组解构](#Array-Destructuring) 12 | * [混合解构](#Mixed-Destructuring) 13 | * [参数解构](#Destructured-Parameters) 14 | * [总结](#Summary) 15 | 16 |
17 | 18 | ### 解构的实用性在哪里(Why is Destructuring Useful?) 19 | 20 | 21 | 在 ECMAScript 5 或更早的版本中,从对象或数组中获取特定的数据并赋值给本地变量需要书写很多并且相似的代码。例如: 22 | 23 | ```js 24 | let options = { 25 | repeat: true, 26 | save: false 27 | }; 28 | 29 | // 从对象中提取数据 30 | let repeat = options.repeat, 31 | save = options.save; 32 | ``` 33 | 34 | 这段代码反复地提取在 options 上存储地属性值并将它们传递给同名的本地变量。虽然这些看起来不是那么复杂,不过想象一下如果你的一大批变量有着相同的需求,你就只能一个一个地赋值。而且,如果你需要从对象内部嵌套的结构来查找想要的数据,你极有可能为了一小块数据而访问了整个数据结构。 35 | 36 | 这也是 ECMAScript 6 给对象和数组添加解构的原因。当你想要把数据结构分解为更小的部分时,从这些部分中提取数据会更容易些。很多语言都能使用精简的语法来实现解构操作。ECMAScript 6 解构的实际语法或许你已经非常熟悉:对象和数组字面量。 37 | 38 |
39 | 40 | ### 对象解构(Object Destructuring) 41 | 42 | 43 | 对象结构语法在赋值语句的左侧使用对象字面量,例如: 44 | 45 | ```js 46 | let node = { 47 | type: "Identifier", 48 | name: "foo" 49 | }; 50 | 51 | let { type, name } = node; 52 | 53 | console.log(type); // "Identifier" 54 | console.log(name); // "foo" 55 | ``` 56 | 57 | 在该段代码中,node.type 的值由 type 这个本地变量存储,node.name 同理。该语法和第四章中介绍的简写的属性初始化是相同的。type 和 name 标识符具有本地声明变量和 options 对象属性的双重身份。 58 | 59 |
60 | 61 | > ##### 不要忘记初始化(Don’t Forget the Initializer) 62 | > 63 | > 当在解构中使用 var,let 或 const 来声明变量时,必须要由初始化操作。下面的代码会因为未初始化的存在而抛出错误: 64 | 65 | ```js 66 | // 语法错误! 67 | var { type, name }; 68 | 69 | // 语法错误! 70 | let { type, name }; 71 | 72 | // 语法错误! 73 | const { type, name }; 74 | ``` 75 | 76 |
77 | 78 | > when using destructuring.const 即使未使用解构也需要初始化,而 var 和 let 只有在解构的时候才会被强制要求初始化。 79 | 80 |
81 | 82 | ##### 解构赋值表达式(Destructuring Assignment) 83 | 84 | 85 | 目前为止的解构示例使用了变量声明。不过,在表达式中使用赋值也是可疑的。例如,你可以决定在变量声明之后改变它们的值,如下所示: 86 | 87 | ```js 88 | let node = { 89 | type: "Identifier", 90 | name: "foo" 91 | }, 92 | type = "Literal", 93 | name = 5; 94 | 95 | // assign different values using destructuring 96 | ({ type, name } = node); 97 | 98 | console.log(type); // "Identifier" 99 | console.log(name); // "foo" 100 | ``` 101 | 102 | 在本例中,node 对象中的 type 和 name 在声明处初始化,而另一个对同名变量在之后也被不同的值初始化。往下的一行使用了解构赋值表达式将两个变量的值更改为 node 对象对应属性的值。注意你必须在圆括号内才能使用解构表达式。这是因为暴露的花括号会被解析为块声明语句,而该语句不能存在于赋值操作符的左侧。圆括号的存在预示着之后的花括号不是块声明语句而应该被看作表达式,这样它才能正常工作。 103 | 104 | 解构赋值表达式会计算右侧的值(= 右侧)。也就是说你可以在任何期望传值的位置使用表达式。例如,将值传给函数: 105 | 106 | ```js 107 | let node = { 108 | type: "Identifier", 109 | name: "foo" 110 | }, 111 | type = "Literal", 112 | name = 5; 113 | 114 | function outputInfo(value) { 115 | console.log(value === node); // true 116 | } 117 | 118 | outputInfo({ type, name } = node); 119 | 120 | console.log(type); // "Identifier" 121 | console.log(name); // "foo" 122 | ``` 123 | 124 | outputInfo() 函数在调用时被传入解构赋值表达式。表达式计算的结果为 node 是因为它就是右侧的值。type 和 name 的赋值正常进行同时 node 被传给了 outputInfo()。 125 | 126 |
127 | 128 | > **注意**: 当解构赋值表达式的右侧(= 后面的表达式)计算结果为 null 或 undefined 的时候一个错误将被抛出。因为任何读取 null 或 undefined 的操作都会发生运行时错误(runtime error) 129 | 130 |
131 | 132 | ##### 默认值(Default Values) 133 | 134 | 135 | 当你使用解构赋值表达式语句时,如果你定义了一个变量而该变量名在对象中找不到对应的属性名,那么该本地变量的值为 undefined。例如: 136 | 137 | ```js 138 | let node = { 139 | type: "Identifier", 140 | name: "foo" 141 | }; 142 | 143 | let { type, name, value } = node; 144 | 145 | console.log(type); // "Identifier" 146 | console.log(name); // "foo" 147 | console.log(value); // undefined 148 | ``` 149 | 150 | 该段代码额外添加了一个本地变量 value 并试图获取相应的值。然而,node 对象没有对应的属性,所以正如我们所想的那样 value 的值为 undefined。 151 | 152 | 你可以选择定义一个默认值以防对象中不存在对应的属性。想要这么做的方法是在变量后添加等于号(=)并写下默认值,像这样: 153 | 154 | ```js 155 | let node = { 156 | type: "Identifier", 157 | name: "foo" 158 | }; 159 | 160 | let { type, name, value = true } = node; 161 | 162 | console.log(type); // "Identifier" 163 | console.log(name); // "foo" 164 | console.log(value); // true 165 | ``` 166 | 167 | 在该例中,value 变量的默认值为 true。默认值只有在 node 内部没有 value 属性或 value 的属性值为 undefined 的时候才会被使用。既然现在没有 node.value 这个属性,那么 value 变量值就是默认值。这和第三章介绍的 “函数中的默认参数” 一节十分类似。 168 | 169 |
170 | 171 | ##### 赋值给不同的变量名(Assigning to Different Local Variable Names) 172 | 173 | 174 | 到目前为止,每个示例中的解构赋值都使用对象中的属性名做为本地变量的名称;例如,node.type 中的值由 type 变量来存储。在你有意这么做的时候并没有什么问题,但万一你不想呢?ECMAScript 6 对此添加了扩展语法允许你将值赋给不同名字的变量(别名),而且该语法看上去类似于使用非简写方式初始化对象字面量的属性。这里有个示例: 175 | 176 | ```js 177 | let node = { 178 | type: "Identifier", 179 | name: "foo" 180 | }; 181 | 182 | let { type: localType, name: localName } = node; 183 | 184 | console.log(localType); // "Identifier" 185 | console.log(localName); // "foo" 186 | ``` 187 | 188 | 以上的代码使用了解构赋值声明了 localType 和 localName 变量,分别获取 node.type 和 node.name 的值。type: localType 语法的意思是寻找 type 属性并将其值赋给 localType 这个变量。这种语法完全不同于传统的对象字面量语法。后者是属性名在冒号左侧,值为右侧,而前者是属性名在右侧,左侧是要读取的属性值。 189 | 190 | 你也可以在别名后面添加默认值,方式依然是在其后添加等于符号和默认值,例如: 191 | 192 | ```js 193 | let node = { 194 | type: "Identifier" 195 | }; 196 | 197 | let { type: localType, name: localName = "bar" } = node; 198 | 199 | console.log(localType); // "Identifier" 200 | console.log(localName); // "bar" 201 | ``` 202 | 203 | 在这里,localName 变量的默认值是 "bar"。该值会赋给 localName 是因为 node.name 属性是不存在的。 204 | 205 | 到现在你已经知道怎样使用解构来操作对象中名称为原始值(primitive value)的属性。对象解构同样可以在包含嵌套结构的对象中获取数据。 206 | 207 |
208 | 209 | ##### 嵌套的对象解构(Nested Object Destructuring) 210 | 211 | 212 | 使用类似于对象字面量的语法可以在对象嵌套的结构中提取你需要的数据。这里有个示例: 213 | 214 | ```js 215 | let node = { 216 | type: "Identifier", 217 | name: "foo", 218 | loc: { 219 | start: { 220 | line: 1, 221 | column: 1 222 | }, 223 | end: { 224 | line: 1, 225 | column: 4 226 | } 227 | } 228 | }; 229 | 230 | let { loc: { start }} = node; 231 | 232 | console.log(start.line); // 1 233 | console.log(start.column); // 1 234 | ``` 235 | 236 | 该例中使用的解构模式包含了花括号来指示在 node 属性 loc 的内部寻找 start 属性。上一节曾提到不论冒号在何处出现,左面的标识符是查找的参考对象而右侧的标识符是赋值的目标。当冒号的后面出现花括号时,代表寻找的目标在该对象更深的层级中。 237 | 238 | 你可以进一步对嵌套的属性使用别名来声明变量: 239 | 240 | ```js 241 | let node = { 242 | type: "Identifier", 243 | name: "foo", 244 | loc: { 245 | start: { 246 | line: 1, 247 | column: 1 248 | }, 249 | end: { 250 | line: 1, 251 | column: 4 252 | } 253 | } 254 | }; 255 | 256 | // 提取 node.loc.start 257 | let { loc: { start: localStart }} = node; 258 | 259 | console.log(localStart.line); // 1 260 | console.log(localStart.column); // 1 261 | ``` 262 | 263 | 在这种写法的代码中,node.loc.start 的值由新命名的 localStart 变量存储。解构可以在任意的嵌套深度工作,而且每一层级的功能都不会被削减。 264 | 265 | 对象解构十分强大而且形式多样,但数组解构提供了另一些独特的功能用来提取数组中的数据。 266 | 267 |
268 | 269 | > ##### 语法注意(Syntax Gotcha) 270 | > 当在嵌套的数据结构中使用解构时要注意,你可能会无意间创建了无任何意义的语句。在对象解构时空的花括号是允许的,然而它们不会做任何事情。例如: 271 | 272 | ```js 273 | // 无任何变量声明! 274 | let { loc: {} } = node; 275 | ``` 276 | 277 | > 在该声明语句中不存在任何绑定。因为冒号右侧的花括号中无内容,loc 是用来做参考对象而没有创建任何绑定。在该种情况下,该语句的意图应该是使用 = 来声明默认值而不是用 : 来定义参照对象。这种语法在将来可能会被判为违法,但是现在我们只能对它保持警惕。 278 | 279 |
280 | 281 | ### 数组解构(Array Destructuring) 282 | 283 | 284 | 数据解构的语法和对象解构看起来类似,只是将对象字面量替换成了数组字面量,而且解构操作的是数组内部的位置(索引)而不是对象中的命名属性,例如: 285 | 286 | ```js 287 | let colors = [ "red", "green", "blue" ]; 288 | 289 | let [ firstColor, secondColor ] = colors; 290 | 291 | console.log(firstColor); // "red" 292 | console.log(secondColor); // "green" 293 | ``` 294 | 295 | 在这里,数组解构从 colors 数组中找到了 "red" 和 "green" 并将它们赋值给 fristColor 和 secondColor 变量。这些值的选择和它们在数组中的位置有关;实际的变量名称是任意的。任何没有在数组解构语句中显示声明的项都会被忽略掉。注意的是数组本身不会有任何影响: 296 | 297 | 你可以在解构语句忽略一些项而只给你想要的项提供命名。例如,你若只想获取数组中的第三个元素,那么你不必给前两项提供命名。以下示例演示了该情况: 298 | 299 | ```js 300 | let colors = [ "red", "green", "blue" ]; 301 | 302 | let [ , , thirdColor ] = colors; 303 | 304 | console.log(thirdColor); // "blue" 305 | ``` 306 | 307 | 这代码中使用了解构赋值来获取 colors 中的第三个元素。thirdColor 之前的逗号是先前元素的占位符。使用该种方法你可以轻松的获取数组中任意位置的值而不需要给其它项提供命名。 308 | 309 | 和对象解构类似的是,你必须使用 var,let,const 对数组解构进行初始化。 310 | 311 |
312 | 313 | ##### 解构赋值表达式(Destructuring Assignment) 314 | 315 | 316 | 你可以想要赋值的情况下使用数组的解构赋值表达式,但是和对象解构不同,没必要将它们包含在圆括号中,例如: 317 | 318 | ```js 319 | let colors = [ "red", "green", "blue" ], 320 | firstColor = "black", 321 | secondColor = "purple"; 322 | 323 | [ firstColor, secondColor ] = colors; 324 | 325 | console.log(firstColor); // "red" 326 | console.log(secondColor); // "green" 327 | ``` 328 | 329 | 该段代码的解构赋值和上例类似。唯一的区别是 firstColr 和 secondColor 在事先已定义。大部分情况下,上述代码使你足够了解如何去使用数组的解构赋值表达式,但其实它还有一个额外的比较实用的用法。 330 | 331 | 数组中的解构赋值表达式有一个独特的使用场景 —— 对两个变量的值进行交换。在排序算法中值交换操作是非常普遍的,在 ECMAScript 5 中则需要一个第三方变量,如下所示: 332 | 333 | ```js 334 | // 在 ECMAScript 5 中交换变量的值 335 | let a = 1, 336 | b = 2, 337 | tmp; 338 | 339 | tmp = a; 340 | a = b; 341 | b = tmp; 342 | 343 | console.log(a); // 2 344 | console.log(b); // 1 345 | ``` 346 | 347 | 临时变量 tmp 对交换 a 和 b 的值来讲是必要的。不过在使用解构赋值表达式时,它就没有存在的理由了。这里演示了在 ECMAScript 6 中交换变量值的方式: 348 | 349 | ```js 350 | // 在 ECMAScript 6 中交换变量的值 351 | let a = 1, 352 | b = 2; 353 | 354 | [ a, b ] = [ b, a ]; 355 | 356 | console.log(a); // 2 357 | console.log(b); // 1 358 | ``` 359 | 360 | 该例中的解构赋值表达式仿佛在进行镜像操作。表达式的左侧(等于符号之前)和其它数组解构案例别无二致,右侧则是为了交换值而创建的临时数组字面量。解构发生在临时数组上,将 b 和 a 的值分别传给左侧数组第一及第二个元素,效果等同于交换变量值。 361 | 362 | 和对象的解构赋值表达式相同,若表达式右侧的计算值为 null 和 undefined,那么该解构赋值表达式会抛出错误。 363 | 364 |
365 | 366 | ##### 默认值(Default Values) 367 | 368 | 369 | 数组中的解构赋值表达式同样可以在任意位置指定默认值。当某个位置的项未被传值或传入的值为 undefined,那么它的默认值会被使用。例如: 370 | 371 | ```js 372 | let colors = [ "red" ]; 373 | 374 | let [ firstColor, secondColor = "green" ] = colors; 375 | 376 | console.log(firstColor); // "red" 377 | console.log(secondColor); // "green" 378 | ``` 379 | 380 | 在这段代码中,colors 数组只有一个项,所以 secondColor 不会进行值得匹配。既然它被设置了默认值,那么 secondColor 的值即为 "green" 而不是 undefined 。 381 | 382 |
383 | 384 | ##### 嵌套的数组解构(Nested Destructuring) 385 | 386 | 387 | 你可以使用类似于解构嵌套对象的方式来解构嵌套的数组。在数组的内部添加一个数组即可在嵌套数组完成操作。像这样: 388 | 389 | ```js 390 | let colors = [ "red", [ "green", "lightgreen" ], "blue" ]; 391 | 392 | // 之后 393 | 394 | let [ firstColor, [ secondColor ] ] = colors; 395 | 396 | console.log(firstColor); // "red" 397 | console.log(secondColor); // "green" 398 | ``` 399 | 400 | 这在里,secondColor 变量获得是 colors 数组中的 "green"。该项被包含在另一个数组中,所以解构语句中额外的数组添加是必须的。和对象相同,获取任意嵌套深度的值是允许的。 401 | 402 |
403 | 404 | ##### 剩余项(Rest Items) 405 | 406 | 407 | 第三章介绍过剩余参数表达式在函数中的应用,而数组解构中有个类似的概念叫做剩余项。它使用 ... 语法来将剩余的项赋值给一个指定的变量,如下所示: 408 | 409 | ```js 410 | let colors = [ "red", "green", "blue" ]; 411 | 412 | let [ firstColor, ...restColors ] = colors; 413 | 414 | console.log(firstColor); // "red" 415 | console.log(restColors.length); // 2 416 | console.log(restColors[0]); // "green" 417 | console.log(restColors[1]); // "blue" 418 | ``` 419 | 420 | colors 中的首项会被赋值给 firstColor,而其余项会被添加到 restColors 这个新数组中。该数组因此会有两个项:"green 和 "blue"。剩余项在提取数组中特定的项并保持其它项可用的情况下十分好用,而且还有另一种实用的方法。 421 | 422 | 数组的复制在 JavaScript 中是一个容易忽视的问题。在 ECMAScript 5 中,开发者经常使用 concat() 方法来方便的克隆一个数组,例如: 423 | 424 | ```js 425 | // ECMAScript 5 中克隆数组的方法 426 | var colors = [ "red", "green", "blue" ]; 427 | var clonedColors = colors.concat(); 428 | 429 | console.log(clonedColors); //"[red,green,blue]" 430 | ``` 431 | 432 | 虽然 concat() 方法的本意是用来合并两个数组,但是没有给它传递参数的时候他会返回一个该数组的克隆。在 ECMAScript 6 中,你可以使用剩余项来完成同样的任务。实现如下: 433 | 434 | ```js 435 | // ECMAScript 6 中克隆数组的方法 436 | let colors = [ "red", "green", "blue" ]; 437 | let [ ...clonedColors ] = colors; 438 | 439 | console.log(clonedColors); //"[red,green,blue]" 440 | ``` 441 | 442 | 在该例中,剩余项用来将 colors 数组中的项拷贝到 clonedColors 。虽然关于剩余项的复制意图看上去是否比 concat() 方法更为明确的争论是仁者见仁智者见智,但是它依然是个值得了解的使用方法。 443 | 444 | 剩余项必须是解构语句中的最后项并且不能在后面添加逗号,因为该行为会抛出语法错误。 445 | 446 |
447 | 448 | ### 混合解构(Mixed Destructuring) 449 | 450 | 451 | 可以创建更复杂的表达式来混合使用对象和数组解构。这样做你可以精准地获取对象与数组并存的数据结构中的信息。例如: 452 | 453 | ```js 454 | let node = { 455 | type: "Identifier", 456 | name: "foo", 457 | loc: { 458 | start: { 459 | line: 1, 460 | column: 1 461 | }, 462 | end: { 463 | line: 1, 464 | column: 4 465 | } 466 | }, 467 | range: [0, 3] 468 | }; 469 | 470 | let { 471 | loc: { start }, 472 | range: [ startIndex ] 473 | } = node; 474 | 475 | console.log(start.line); // 1 476 | console.log(start.column); // 1 477 | console.log(startIndex); // 0 478 | ``` 479 | 480 | 该段代码分别提取 node.loc.start 和 node.range[0] 的值并赋给 start 和 startIndex 。注意的是解构语句中的 loc: 和 range: 只是 node 对象中参考的对应属性。对 node 对象使用混合解构没有提取不出来的数据。该种实现的特别实用之处在于提取 JSON 中的数据时不必访问整个数据结构。 481 | 482 |
483 | 484 | ### 参数解构(Destructured Parameters) 485 | 486 | 487 | 解构的另一个实用案例发生在传递函数参数的时刻。当 JavaScript 的函数需要接受大量的可选参数时,一个普遍的实践是创建一个带有额外属性的对象用来明确,像这样: 488 | 489 | ```js 490 | // properties on options represent additional parameters 491 | function setCookie(name, value, options) { 492 | 493 | options = options || {}; 494 | 495 | let secure = options.secure, 496 | path = options.path, 497 | domain = options.domain, 498 | expires = options.expires; 499 | 500 | // code to set the cookie 501 | } 502 | 503 | // third argument maps to options 504 | setCookie("type", "js", { 505 | secure: true, 506 | expires: 60000 507 | }); 508 | ``` 509 | 510 | 很多 JavaScript 库都包含类似于上述写法的 setCookie() 函数。在该函数中,name 和 value 必须被传入参数,但是 secure,path,domain 和 expires 并无要求。既然这些可选参数没有顺序限制,与其列出每个参数名倒不如在一个对象中使用它们的命名作为属性。这种方式使用起来不错,但是你无法根据函数定义来确认它到底需要哪些可选参数,只能去查看函数主体。 511 | 512 | 参数解构提供了另一种方案使得函数期望的参数变得明确。它使用对象或数组解构的使用形式取代了命名参数。为了查看参数解构的具体写法,请阅读下面经过重写的 setCookie() 函数: 513 | 514 | ```js 515 | function setCookie(name, value, { secure, path, domain, expires }) { 516 | 517 | // code to set the cookie 518 | } 519 | 520 | setCookie("type", "js", { 521 | secure: true, 522 | expires: 60000 523 | }); 524 | ``` 525 | 526 | 该函数的行为和上例类似,区别在于前者的第三个参数使用解构来获取必要的数据。现在,setCookie() 的必须参数和可选参数变得一样明确。如果要将第三个参数设定为必要参数,那么要传给它的实参类型也是显而易见的。解构的参数和普通参数同样在未被传参的情况下值为 undefined 。 527 | 528 |
529 | 530 | > 参数解构拥有目前为止你在本章见过的其它解构方式的所有能力。你可以使用默认参数,混合对象与数组解构,或者声明和对应属性命名不同的变量。 531 | 532 |
533 | 534 | #### 必选的参数解构(Destructured Parameters are Required) 535 | 536 | 537 | 参数解构有一个怪异之处:在默认情况下,未给参数解构传值会抛出一个错误。例如,上例中的 setCookie() 函数使用下面的方式调用会发生错误: 538 | 539 | ```js 540 | // 错误! 541 | setCookie("type", "js"); 542 | ``` 543 | 544 | 第三个参数未见踪影,所以它的值就是惯例的 undefined 。发生错误的原因是参数解构本质上是解构声明的简写形式,当 setCookie() 函数被调用时,JavaScript 引擎实际上会这么做: 545 | 546 | ```js 547 | function setCookie(name, value, options) { 548 | 549 | let { secure, path, domain, expires } = options; 550 | 551 | // code to set the cookie 552 | } 553 | ``` 554 | 555 | 既然解构会在右侧的表达式计算结果为 null 或 undefined 时抛出错误,那么未给 setCookie() 函数传递第三个参数的结果也是显而易见了。 556 | 557 | 如果你设想的参数解构是必须参数,那么以上的行为不会对你有太大影响。如果你想要将参数解构设定为可选,你可以使用默认参数来作为解决方案,像这样: 558 | 559 | ```js 560 | function setCookie(name, value, { secure, path, domain, expires } = {}) { 561 | 562 | // ... 563 | } 564 | ``` 565 | 566 | 该例向第三个参数提供了一个对象作为默认值。这意味着如果 setCookie() 的未被传入第三个参数,那么 secure,path,domain 和 expires 的值均为 undefined,而且没有错误被抛出。 567 | 568 |
569 | 570 | #### 参数解构的默认值(Default Values for Destructured Parameters) 571 | 572 | 573 | 你可以使用解构赋值表达式来向解构的参数指定默认值,只需在参数后面添加等于符号和做为默认的值。例如: 574 | 575 | ```js 576 | function setCookie(name, value, 577 | { 578 | secure = false, 579 | path = "/", 580 | domain = "example.com", 581 | expires = new Date(Date.now() + 360000000) 582 | } = {} 583 | ) { 584 | 585 | // ... 586 | } 587 | ``` 588 | 589 | 该段代码中,每个解构后的参数都会有默认值,所以你不必对它们进行检查已确认它们是否被传入参数。同样,整个参数解构有一个空的对象做为默认值,于是该参数解构就是可选的。这些设定使得该函数声明看起来比一般的要复杂,但这是为了确保每个参数都有可用的值而做出的必要牺牲。 590 | 591 |
592 | 593 | ### 总结(Summary) 594 | 595 | 解构使得在 JavaScript 中操作对象和数组变得容易。使用熟悉的对象字面量或数组字面量,你可以将数据结构拆分并只获取你感兴趣的信息。对象和数组解构分别允许你从对象和数组中提取信息。 596 | 597 | 对象和数组解构能分别给属性或项设定默认值以便在出现 undefined 的时候修正,在赋值右侧的表达式计算结果为 null 和 undefined 的时候抛出错误。你也可以在深层嵌套的对象和数组中使用对象和数组解构来获取任意层级的数据。 598 | 599 | 使用 var,let 或 const 的解构声明必须要初始化。解构赋值表达式可以用来代替任何赋值操作并且允许你解构对象的属性和使用已经存在的变量名。 600 | 601 | 参数解构使用解构语法使得在函数参数中使用可选对象变得透明化。你实际感兴趣的数据可以使用命名参数详列。参数解构可以是对象形式,数组形式或混合形式,并同时拥有这些形式的全部功能。 602 | 603 |
604 | 605 | 606 | -------------------------------------------------------------------------------- /chapter_13.md: -------------------------------------------------------------------------------- 1 | # 模块(Encapsulating Code With Modules) 2 | 3 | 4 | JavaScript 采用 “共享一切” 的代码加载方式是该语言中最令人迷惑且容易出错的方面之一。其它语言使用包(package)的概念来定义代码的作用范围,然而在 ECMAScript 6 之前,每个 JavaScript 文件中定义的内容都由全局作用域共享。当 web 应用变得复杂并需要书写更多的 JavaScript 代码时,上述加载方式会出现命名冲突或安全方面的问题。ECMAScript 6 的目标之一就是解决作用域的问题并将 JavaScript 应用中的代码整理得更有条理,于是模块应运而生。 5 | 6 |
7 | 8 | ### 本章小结 9 | * [什么是模块?](#What-are-Modules) 10 | * [输出的基本概念](#Basic-Exporting) 11 | * [引入的基本概念](#Basic-Importing) 12 | * [export 和 import 的重命名](#Renaming-Exports-and-Imports) 13 | * [模块中的默认值](#Default-Values-in-Modules) 14 | * [绑定的再输出](#Re-exporting-a-Binding) 15 | * [全局引入](#Importing-Without-Bindings) 16 | * [模块加载](#Loading-Modules) 17 | * [总结](#Summary) 18 | 19 |
20 | 21 | ### 什么是模块?(What are Modules?) 22 | 23 | 24 | 模块是指采取不同于现有加载方式的 JavaScript 文件(与 script 这种传统的加载模式相对)。这种方式很有必要,因为它和 script 使用不同的语义: 25 | 26 | 1. 模块中的代码自动运行在严格模式下,并无任何办法修改为非严格模式。 27 | 2. 模块中的顶级(top level)变量不会被添加到全局作用域中。它们只存在于各自的模块中的顶级作用域。 28 | 3. 模块顶级作用域中的 this 为 undefined 。 29 | 4. 模块不允许存在 HTML 式的注释(JavaScript 历史悠久的遗留特性)。 30 | 5. 模块必须输出可被模块外部代码使用的相关内容。 31 | 6. 模块可能会引入其它模块中的绑定。 32 | 33 | 这些差异刚开始看上去觉得并不是很大,不过它们体现了 JavaScript 关于加载和计算代码的显著变更,本章随后我会解释它们。模块真正的好处在于可以输出和引入需要的绑定,而不是文件中的所有内容。理解输出和引入是领悟模块与 script 之间差异的基础。 34 | 35 |
36 | 37 | ### 引入的基本概念(Basic Exporting) 38 | 39 | 40 | 你可以使用 export 关键字来对外暴露模块中的部分代码。一般情况下,你可以在任何变量,函数或类声明之前添加这个关键字来输出它们,像这样: 41 | 42 | ```js 43 | // 输出变量 44 | export var color = "red"; 45 | export let name = "Nicholas"; 46 | export const magicNumber = 7; 47 | 48 | // 输出函数 49 | export function sum(num1, num2) { 50 | return num1 + num1; 51 | } 52 | 53 | // 输出类 54 | export class Rectangle { 55 | constructor(length, width) { 56 | this.length = length; 57 | this.width = width; 58 | } 59 | } 60 | 61 | // 该函数是模块私有的 62 | function subtract(num1, num2) { 63 | return num1 - num2; 64 | } 65 | 66 | // 定义一个函数... 67 | function multiply(num1, num2) { 68 | return num1 * num2; 69 | } 70 | 71 | // ...并在之后输出它 72 | export { multiply }; 73 | ``` 74 | 75 | 该例中需要注意一些要点。首先,除 export 关键字之外,所有的声明和传统的形式完全一致。每个输出的函数或类都有一个名称;因为名称是必须的。除非你使用了 default 关键字(在 “模块中的默认值” 一节讨论),否则你不能使用该语法来输出匿名函数或类。 76 | 77 | 其次,multiply() 函数并未在定义的时候被输出。这是因为你不必每次都要输出一个声明:你可以输出一个引用。最后,该例中并未输出 subtract() 函数。该函数在外部是不可见的,因为任何未显式输出的变量,函数或类都是模块私有的。 78 | 79 |
80 | 81 | ### 引入的基本概念(Basic Importing) 82 | 83 | 84 | 一旦你有了包含输出内容的模块,你可以在另一个模块内使用 import 关键字来获取它的相关功能。import 语句包含两部分内容,分别为引入的标识符和输出这些标识符的模块。以下是该语句的基本形式: 85 | 86 | ```js 87 | import { identifier1, identifier2 } from "./example.js"; 88 | ``` 89 | 90 | import 之后的花括号表示从模块中引入的绑定。from 关键字表示从哪个模块引入这些绑定。模块由一个包含模块路径的字符串表示(称为模块指示符,module sepcifier)。浏览器中的 ` 447 | 448 | 449 | 456 | ``` 457 | 458 | 本例中的首个 ` 480 | 481 | 482 | 487 | 488 | 489 | 490 | ``` 491 | 492 | 这三个 ` 532 | 533 | ``` 534 | 535 | 该例中异步加载了两个模块。仅仅观察模块中的代码无法确定模块的执行顺序。如果 module1.js 首先下载完毕(包括它引入的资源),那么它会首先执行。module2.js 也是同理。 536 | 537 |
538 | 539 | ##### 在 worker 中加载模块(Loading Modules as Workers) 540 | 541 | 542 | worker,包括 web worker 和 service worker,会在网页之外的上下文中执行 JavaScript 代码。创建一个新的 worker 需要 worker 实例(或类)并传递 JavaScript 文件的位置。worker 默认的加载机制是将文件视为 script,像这样: 543 | 544 | ```js 545 | // 以 script 的方式加载 script.js 546 | let worker = new Worker("script.js"); 547 | ``` 548 | 549 | 为了支持模块的加载,负责 HTML 标准的开发者们给构造函数添加了第二个参数。这个参数是包含 type 属性并且默认值为 script 的对象。你可以将 type 设定为 "module" 来加载模块。 550 | 551 | ```js 552 | // 以模块的方式加载 module.js 553 | let worker = new Worker("module.js", { type: "module" }); 554 | ``` 555 | 556 | 该例将第二个参数中的 type 属性值设定为 "module",因此 module.js 以模块而不是 script 的形式加载(type 属性是对 `