├── LICENSE ├── README.md ├── docs ├── 1. 序言.md ├── 10. 解构赋值.md ├── 11. 新增运算符.md ├── 12. 异步编程.md ├── 13. Set.md ├── 14. Map.md ├── 15. Iterator.md ├── 16. 异步遍历器.md ├── 17. Proxy.md ├── 18. Reflect.md ├── 19. Module 模块化.md ├── 2. 基础重点概览.md ├── 20. 模块加载的实现.md ├── 21. DOM.md ├── 22. DOM 事件.md ├── 23. BOM.md ├── 24. 编程风格.md ├── 25. 正则表达式.md ├── 26. 标准对象库.md ├── 3. 数据类型的检验和转换.md ├── 4. 错误处理机制.md ├── 5. Array.md ├── 6. String.md ├── 7. Object.md ├── 8. Class.md └── 9. Symbol.md └── img ├── ES6Prototype.png ├── EventLoop.gif └── JSPrototype.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Harry Xiong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript 和 ECMAScript 进阶教程 2 | 3 | 4 | 5 | ## 简介 6 | 7 | 本教程系统的归纳和总结了从 JavaScript 到 ECMAScript 的重难点以及高阶使用方法。 8 | 9 | 10 | 11 | ## 目录 12 | 13 | 1. 序言 14 | 2. 基础关注点 15 | 3. 数据类型的检验和转换 16 | 4. 错误处理机制 17 | 5. Array 18 | 6. String 19 | 7. Object 20 | 8. Class 21 | 9. Symbol 22 | 10. 解构赋值 23 | 11. 新增运算符 24 | 12. 异步编程 25 | 13. Set 26 | 14. Map 27 | 15. Iterator 28 | 16. 异步遍历器 29 | 17. Proxy 30 | 18. Reflect 31 | 19. Module 模块化 32 | 20. 模块加载的实现 33 | 21. DOM 34 | 22. DOM 事件 35 | 23. BOM 36 | 24. 编程风格 37 | 25. 正则表达式 38 | 27. 标准对象库 39 | 40 | 41 | 42 | ## 说明 43 | 44 | 本教程不会涉及 JavaScript 和 ECMAScript 基础知识,比如 JavaScript 的基本语法,变量定义,基本类型使用等基础知识,所以本教程适合具备一定 JavaScript 和 ECMAScript 基础知识的读者浏览,初学者建议先从入门教程开始阅读。 45 | 46 | 本教程即可作为教程,也可以作为文档查阅。 47 | 48 | 持续更新中... 49 | 50 | 51 | 52 | ## 参考与引用 53 | 54 | 1. [ECMAScript® 2015 Language Specification](https://262.ecma-international.org/6.0/) 55 | 56 | 2. [JavaScript 教程 MDN版](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript) 57 | 58 | 3. [JavaScript 教程 网道版](https://wangdoc.com/javascript/) 59 | 60 | 4. [ES6 教程 网道版](https://wangdoc.com/es6/index.html) 61 | 62 | 5. [Node](https://nodejs.org/en/docs/) 63 | 64 | 6. [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) 65 | 66 | 67 | 68 | ## License 69 | 70 | Copyright © 2021 [Harry Xiong](https://github.com/HarryXiong24). 71 | 72 | This project is [MIT](https://github.com/HarryXiong24/JavaScript-ECMAScript-Advanced-Guidebook/blob/master/LICENSE) licensed. 73 | 74 | -------------------------------------------------------------------------------- /docs/1. 序言.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarryXiong24/JavaScript-ECMAScript-Advanced-Guidebook/c7b1129bed8d99c8196f9739a3548e2c03c730ca/docs/1. 序言.md -------------------------------------------------------------------------------- /docs/10. 解构赋值.md: -------------------------------------------------------------------------------- 1 | # 解构赋值 2 | 3 | 4 | 5 | ## 数组的解构赋值 6 | 7 | ### 基本用法 8 | 9 | ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。 10 | 11 | 以前,为变量赋值,只能直接指定值。 12 | 13 | ```js 14 | let a = 1; 15 | let b = 2; 16 | let c = 3; 17 | ``` 18 | 19 | ES6 允许写成下面这样。 20 | 21 | ```js 22 | let [a, b, c] = [1, 2, 3]; 23 | ``` 24 | 25 | 上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。 26 | 27 | 本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。 28 | 29 | ```js 30 | let [foo, [[bar], baz]] = [1, [[2], 3]]; 31 | foo // 1 32 | bar // 2 33 | baz // 3 34 | 35 | let [ , , third] = ["foo", "bar", "baz"]; 36 | third // "baz" 37 | 38 | let [x, , y] = [1, 2, 3]; 39 | x // 1 40 | y // 3 41 | 42 | let [head, ...tail] = [1, 2, 3, 4]; 43 | head // 1 44 | tail // [2, 3, 4] 45 | 46 | let [x, y, ...z] = ['a']; 47 | x // "a" 48 | y // undefined 49 | z // [] 50 | ``` 51 | 52 | 如果解构不成功,变量的值就等于`undefined`。 53 | 54 | ```js 55 | let [foo] = []; 56 | let [bar, foo] = [1]; 57 | ``` 58 | 59 | 以上两种情况都属于解构不成功,`foo`的值都会等于`undefined`。 60 | 61 | 另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。 62 | 63 | ```js 64 | let [x, y] = [1, 2, 3]; 65 | x // 1 66 | y // 2 67 | 68 | let [a, [b], d] = [1, [2, 3], 4]; 69 | a // 1 70 | b // 2 71 | d // 4 72 | ``` 73 | 74 | 上面两个例子,都属于不完全解构,但是可以成功。 75 | 76 | 如果等号的右边不是数组(或者严格地说,不是可遍历的结构),那么将会报错。 77 | 78 | ```js 79 | // 报错 80 | let [foo] = 1; 81 | let [foo] = false; 82 | let [foo] = NaN; 83 | let [foo] = undefined; 84 | let [foo] = null; 85 | let [foo] = {}; 86 | ``` 87 | 88 | 上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。 89 | 90 | 对于 Set 结构,也可以使用数组的解构赋值。 91 | 92 | ```js 93 | let [x, y, z] = new Set(['a', 'b', 'c']); 94 | x // "a" 95 | ``` 96 | 97 | **事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。** 98 | 99 | ```js 100 | function* fibs() { 101 | let a = 0; 102 | let b = 1; 103 | while (true) { 104 | yield a; 105 | [a, b] = [b, a + b]; 106 | } 107 | } 108 | 109 | let [first, second, third, fourth, fifth, sixth] = fibs(); 110 | sixth // 5 111 | ``` 112 | 113 | 上面代码中,`fibs`是一个 Generator 函数(参见《Generator 函数》一章),原生具有 Iterator 接口。解构赋值会依次从这个接口获取值。 114 | 115 | ### 默认值 116 | 117 | 解构赋值允许指定默认值。 118 | 119 | ```js 120 | let [foo = true] = []; 121 | foo // true 122 | 123 | let [x, y = 'b'] = ['a']; // x='a', y='b' 124 | let [x, y = 'b'] = ['a', undefined]; // x='a', y='b' 125 | ``` 126 | 127 | 注意,ES6 内部使用严格相等运算符(`===`),判断一个位置是否有值。所以,只有当一个数组成员严格等于`undefined`,默认值才会生效。 128 | 129 | ```js 130 | let [x = 1] = [undefined]; 131 | x // 1 132 | 133 | let [x = 1] = [null]; 134 | x // null 135 | ``` 136 | 137 | 上面代码中,如果一个数组成员是`null`,默认值就不会生效,因为`null`不严格等于`undefined`。 138 | 139 | 如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。 140 | 141 | ```js 142 | function f() { 143 | console.log('aaa'); 144 | } 145 | 146 | let [x = f()] = [1]; 147 | ``` 148 | 149 | 上面代码中,因为`x`能取到值,所以函数`f`根本不会执行。上面的代码其实等价于下面的代码。 150 | 151 | ```js 152 | let x; 153 | if ([1][0] === undefined) { 154 | x = f(); 155 | } else { 156 | x = [1][0]; 157 | } 158 | ``` 159 | 160 | 默认值可以引用解构赋值的其他变量,但该变量必须已经声明。 161 | 162 | ```js 163 | let [x = 1, y = x] = []; // x=1; y=1 164 | let [x = 1, y = x] = [2]; // x=2; y=2 165 | let [x = 1, y = x] = [1, 2]; // x=1; y=2 166 | let [x = y, y = 1] = []; // ReferenceError: y is not defined 167 | ``` 168 | 169 | 上面最后一个表达式之所以会报错,是因为`x`用`y`做默认值时,`y`还没有声明。 170 | 171 | 172 | 173 | ## 对象的解构赋值 174 | 175 | ### 简介 176 | 177 | 解构不仅可以用于数组,还可以用于对象。 178 | 179 | ```js 180 | let { foo, bar } = { foo: 'aaa', bar: 'bbb' }; 181 | foo // "aaa" 182 | bar // "bbb" 183 | ``` 184 | 185 | 对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。 186 | 187 | ```js 188 | let { bar, foo } = { foo: 'aaa', bar: 'bbb' }; 189 | foo // "aaa" 190 | bar // "bbb" 191 | 192 | let { baz } = { foo: 'aaa', bar: 'bbb' }; 193 | baz // undefined 194 | ``` 195 | 196 | 上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于`undefined`。 197 | 198 | 如果解构失败,变量的值等于`undefined`。 199 | 200 | ```js 201 | let {foo} = {bar: 'baz'}; 202 | foo // undefined 203 | ``` 204 | 205 | 上面代码中,等号右边的对象没有`foo`属性,所以变量`foo`取不到值,所以等于`undefined`。 206 | 207 | 对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。 208 | 209 | ```js 210 | // 例一 211 | let { log, sin, cos } = Math; 212 | 213 | // 例二 214 | const { log } = console; 215 | log('hello') // hello 216 | ``` 217 | 218 | 上面代码的例一将`Math`对象的对数、正弦、余弦三个方法,赋值到对应的变量上,使用起来就会方便很多。例二将`console.log`赋值到`log`变量。 219 | 220 | 如果变量名与属性名不一致,必须写成下面这样。 221 | 222 | ```js 223 | let { foo: baz } = { foo: 'aaa', bar: 'bbb' }; 224 | baz // "aaa" 225 | 226 | let obj = { first: 'hello', last: 'world' }; 227 | let { first: f, last: l } = obj; 228 | f // 'hello' 229 | l // 'world' 230 | ``` 231 | 232 | 这实际上说明,对象的解构赋值是下面形式的简写(参见《对象的扩展》一章)。 233 | 234 | ```js 235 | let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' }; 236 | ``` 237 | 238 | 也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。 239 | 240 | ```js 241 | let { foo: baz } = { foo: 'aaa', bar: 'bbb' }; 242 | baz // "aaa" 243 | foo // error: foo is not defined 244 | ``` 245 | 246 | 上面代码中,`foo`是匹配的模式,`baz`才是变量。真正被赋值的是变量`baz`,而不是模式`foo`。 247 | 248 | 与数组一样,解构也可以用于嵌套结构的对象。 249 | 250 | ```js 251 | let obj = { 252 | p: [ 253 | 'Hello', 254 | { y: 'World' } 255 | ] 256 | }; 257 | 258 | let { p: [x, { y }] } = obj; 259 | x // "Hello" 260 | y // "World" 261 | ``` 262 | 263 | 注意,这时`p`是模式,不是变量,因此不会被赋值。如果`p`也要作为变量赋值,可以写成下面这样。 264 | 265 | ```js 266 | let obj = { 267 | p: [ 268 | 'Hello', 269 | { y: 'World' } 270 | ] 271 | }; 272 | 273 | let { p, p: [x, { y }] } = obj; 274 | x // "Hello" 275 | y // "World" 276 | p // ["Hello", {y: "World"}] 277 | ``` 278 | 279 | 下面是另一个例子。 280 | 281 | ```js 282 | const node = { 283 | loc: { 284 | start: { 285 | line: 1, 286 | column: 5 287 | } 288 | } 289 | }; 290 | 291 | let { loc, loc: { start }, loc: { start: { line }} } = node; 292 | line // 1 293 | loc // Object {start: Object} 294 | start // Object {line: 1, column: 5} 295 | ``` 296 | 297 | 上面代码有三次解构赋值,分别是对`loc`、`start`、`line`三个属性的解构赋值。注意,最后一次对`line`属性的解构赋值之中,只有`line`是变量,`loc`和`start`都是模式,不是变量。 298 | 299 | 下面是嵌套赋值的例子。 300 | 301 | ```js 302 | let obj = {}; 303 | let arr = []; 304 | 305 | ({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true }); 306 | 307 | obj // {prop:123} 308 | arr // [true] 309 | ``` 310 | 311 | 如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。 312 | 313 | ```js 314 | // 报错 315 | let {foo: {bar}} = {baz: 'baz'}; 316 | ``` 317 | 318 | 上面代码中,等号左边对象的`foo`属性,对应一个子对象。该子对象的`bar`属性,解构时会报错。原因很简单,因为`foo`这时等于`undefined`,再取子属性就会报错。 319 | 320 | 注意,对象的解构赋值可以取到继承的属性。 321 | 322 | ```js 323 | const obj1 = {}; 324 | const obj2 = { foo: 'bar' }; 325 | Object.setPrototypeOf(obj1, obj2); 326 | 327 | const { foo } = obj1; 328 | foo // "bar" 329 | ``` 330 | 331 | 上面代码中,对象`obj1`的原型对象是`obj2`。`foo`属性不是`obj1`自身的属性,而是继承自`obj2`的属性,解构赋值可以取到这个属性。 332 | 333 | ### 默认值 334 | 335 | 对象的解构也可以指定默认值。 336 | 337 | ```js 338 | var {x = 3} = {}; 339 | x // 3 340 | 341 | var {x, y = 5} = {x: 1}; 342 | x // 1 343 | y // 5 344 | 345 | var {x: y = 3} = {}; 346 | y // 3 347 | 348 | var {x: y = 3} = {x: 5}; 349 | y // 5 350 | 351 | var { message: msg = 'Something went wrong' } = {}; 352 | msg // "Something went wrong" 353 | ``` 354 | 355 | 默认值生效的条件是,对象的属性值严格等于`undefined`。 356 | 357 | ```js 358 | var {x = 3} = {x: undefined}; 359 | x // 3 360 | 361 | var {x = 3} = {x: null}; 362 | x // null 363 | ``` 364 | 365 | 上面代码中,属性`x`等于`null`,因为`null`与`undefined`不严格相等,所以是个有效的赋值,导致默认值`3`不会生效。 366 | 367 | ### 注意点 368 | 369 | **(1)如果要将一个已经声明的变量用于解构赋值,必须非常小心。** 370 | 371 | ```js 372 | // 错误的写法 373 | let x; 374 | {x} = {x: 1}; 375 | // SyntaxError: syntax error 376 | ``` 377 | 378 | 上面代码的写法会报错,因为 JavaScript 引擎会将`{x}`理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。 379 | 380 | ```js 381 | // 正确的写法 382 | let x; 383 | ({x} = {x: 1}); 384 | ``` 385 | 386 | 上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。关于圆括号与解构赋值的关系,参见下文。 387 | 388 | **(2)解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。** 389 | 390 | ```js 391 | ({} = [true, false]); 392 | ({} = 'abc'); 393 | ({} = []); 394 | ``` 395 | 396 | 上面的表达式虽然毫无意义,但是语法是合法的,可以执行。 397 | 398 | **(3)由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。** 399 | 400 | ```js 401 | let arr = [1, 2, 3]; 402 | let {0 : first, [arr.length - 1] : last} = arr; 403 | first // 1 404 | last // 3 405 | ``` 406 | 407 | 上面代码对数组进行对象解构。数组`arr`的`0`键对应的值是`1`,`[arr.length - 1]`就是`2`键,对应的值是`3`。方括号这种写法,属于“属性名表达式”。 408 | 409 | 410 | 411 | ## 字符串的解构赋值 412 | 413 | 字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。 414 | 415 | ```js 416 | const [a, b, c, d, e] = 'hello'; 417 | a // "h" 418 | b // "e" 419 | c // "l" 420 | d // "l" 421 | e // "o" 422 | ``` 423 | 424 | 类似数组的对象都有一个`length`属性,因此还可以对这个属性解构赋值。 425 | 426 | ```js 427 | let {length : len} = 'hello'; 428 | len // 5 429 | ``` 430 | 431 | 432 | 433 | ## 数值和布尔值的解构赋值 434 | 435 | 解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。 436 | 437 | ```js 438 | let {toString: s} = 123; 439 | s === Number.prototype.toString // true 440 | 441 | let {toString: s} = true; 442 | s === Boolean.prototype.toString // true 443 | ``` 444 | 445 | 上面代码中,数值和布尔值的包装对象都有`toString`属性,因此变量`s`都能取到值。 446 | 447 | **解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于`undefined`和`null`无法转为对象,所以对它们进行解构赋值,都会报错。** 448 | 449 | ```js 450 | let { prop: x } = undefined; // TypeError 451 | let { prop: y } = null; // TypeError 452 | ``` 453 | 454 | 455 | 456 | ## 函数参数的解构赋值 457 | 458 | **函数的参数也可以使用解构赋值。** 459 | 460 | ```js 461 | function add([x, y]){ 462 | return x + y; 463 | } 464 | 465 | add([1, 2]); // 3 466 | ``` 467 | 468 | 上面代码中,函数`add`的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量`x`和`y`。对于函数内部的代码来说,它们能感受到的参数就是`x`和`y`。 469 | 470 | 下面是另一个例子。 471 | 472 | ```js 473 | [[1, 2], [3, 4]].map(([a, b]) => a + b); 474 | // [ 3, 7 ] 475 | ``` 476 | 477 | 函数参数的解构也可以使用默认值。 478 | 479 | ```js 480 | function move({x = 0, y = 0} = {}) { 481 | return [x, y]; 482 | } 483 | 484 | move({x: 3, y: 8}); // [3, 8] 485 | move({x: 3}); // [3, 0] 486 | move({}); // [0, 0] 487 | move(); // [0, 0] 488 | ``` 489 | 490 | 上面代码中,函数`move`的参数是一个对象,通过对这个对象进行解构,得到变量`x`和`y`的值。如果解构失败,`x`和`y`等于默认值。 491 | 492 | 注意,下面的写法会得到不一样的结果。 493 | 494 | ```js 495 | function move({x, y} = { x: 0, y: 0 }) { 496 | return [x, y]; 497 | } 498 | 499 | move({x: 3, y: 8}); // [3, 8] 500 | move({x: 3}); // [3, undefined] 501 | move({}); // [undefined, undefined] 502 | move(); // [0, 0] 503 | ``` 504 | 505 | 上面代码是为函数`move`的参数指定默认值,而不是为变量`x`和`y`指定默认值,所以会得到与前一种写法不同的结果。 506 | 507 | `undefined`就会触发函数参数的默认值。 508 | 509 | ```js 510 | [1, undefined, 3].map((x = 'yes') => x); 511 | // [ 1, 'yes', 3 ] 512 | ``` 513 | 514 | 515 | 516 | ## 圆括号问题 517 | 518 | 解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。 519 | 520 | 由此带来的问题是,如果模式中出现圆括号怎么处理。ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号。 521 | 522 | 但是,这条规则实际上不那么容易辨别,处理起来相当麻烦。因此,建议只要有可能,就不要在模式中放置圆括号。 523 | 524 | ### 不能使用圆括号的情况 525 | 526 | 以下三种解构赋值不得使用圆括号。 527 | 528 | #### 变量声明语句 529 | 530 | ```js 531 | // 全部报错 532 | let [(a)] = [1]; 533 | 534 | let {x: (c)} = {}; 535 | let ({x: c}) = {}; 536 | let {(x: c)} = {}; 537 | let {(x): c} = {}; 538 | 539 | let { o: ({ p: p }) } = { o: { p: 2 } }; 540 | ``` 541 | 542 | 上面 6 个语句都会报错,因为它们都是变量声明语句,模式不能使用圆括号。 543 | 544 | #### 函数参数 545 | 546 | 函数参数也属于变量声明,因此不能带有圆括号。 547 | 548 | ```js 549 | // 报错 550 | function f([(z)]) { return z; } 551 | // 报错 552 | function f([z,(x)]) { return x; } 553 | ``` 554 | 555 | #### 赋值语句的模式 556 | 557 | ```js 558 | // 全部报错 559 | ({ p: a }) = { p: 42 }; 560 | ([a]) = [5]; 561 | ``` 562 | 563 | 上面代码将整个模式放在圆括号之中,导致报错。 564 | 565 | ```js 566 | // 报错 567 | [({ p: a }), { x: c }] = [{}, {}]; 568 | ``` 569 | 570 | 上面代码将一部分模式放在圆括号之中,导致报错。 571 | 572 | ### 可以使用圆括号的情况 573 | 574 | 可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。 575 | 576 | ```js 577 | [(b)] = [3]; // 正确 578 | ({ p: (d) } = {}); // 正确 579 | [(parseInt.prop)] = [3]; // 正确 580 | ``` 581 | 582 | 上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是`p`,而不是`d`;第三行语句与第一行语句的性质一致。 583 | 584 | 585 | 586 | ## 用途 587 | 588 | 变量的解构赋值用途很多。 589 | 590 | ### 交换变量的值 591 | 592 | ```js 593 | let x = 1; 594 | let y = 2; 595 | 596 | [x, y] = [y, x]; 597 | ``` 598 | 599 | 上面代码交换变量`x`和`y`的值,这样的写法不仅简洁,而且易读,语义非常清晰。 600 | 601 | ### 函数返回多个值 602 | 603 | 函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。 604 | 605 | ```js 606 | // 返回一个数组 607 | 608 | function example() { 609 | return [1, 2, 3]; 610 | } 611 | let [a, b, c] = example(); 612 | 613 | // 返回一个对象 614 | 615 | function example() { 616 | return { 617 | foo: 1, 618 | bar: 2 619 | }; 620 | } 621 | let { foo, bar } = example(); 622 | ``` 623 | 624 | ### 函数参数的定义 625 | 626 | 解构赋值可以方便地将一组参数与变量名对应起来。 627 | 628 | ```js 629 | // 参数是一组有次序的值 630 | function f([x, y, z]) { ... } 631 | f([1, 2, 3]); 632 | 633 | // 参数是一组无次序的值 634 | function f({x, y, z}) { ... } 635 | f({z: 3, y: 2, x: 1}); 636 | ``` 637 | 638 | ### 提取 JSON 数据 639 | 640 | 解构赋值对提取 JSON 对象中的数据,尤其有用。 641 | 642 | ```js 643 | let jsonData = { 644 | id: 42, 645 | status: "OK", 646 | data: [867, 5309] 647 | }; 648 | 649 | let { id, status, data: number } = jsonData; 650 | 651 | console.log(id, status, number); 652 | // 42, "OK", [867, 5309] 653 | ``` 654 | 655 | 上面代码可以快速提取 JSON 数据的值。 656 | 657 | ### 函数参数的默认值 658 | 659 | ```js 660 | jQuery.ajax = function (url, { 661 | async = true, 662 | beforeSend = function () {}, 663 | cache = true, 664 | complete = function () {}, 665 | crossDomain = false, 666 | global = true, 667 | // ... more config 668 | } = {}) { 669 | // ... do stuff 670 | }; 671 | ``` 672 | 673 | 指定参数的默认值,就避免了在函数体内部再写`var foo = config.foo || 'default foo';`这样的语句。 674 | 675 | ### 遍历 Map 结构 676 | 677 | 任何部署了 Iterator 接口的对象,都可以用`for...of`循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。 678 | 679 | ```js 680 | const map = new Map(); 681 | map.set('first', 'hello'); 682 | map.set('second', 'world'); 683 | 684 | for (let [key, value] of map) { 685 | console.log(key + " is " + value); 686 | } 687 | // first is hello 688 | // second is world 689 | ``` 690 | 691 | 如果只想获取键名,或者只想获取键值,可以写成下面这样。 692 | 693 | ```js 694 | // 获取键名 695 | for (let [key] of map) { 696 | // ... 697 | } 698 | 699 | // 获取键值 700 | for (let [,value] of map) { 701 | // ... 702 | } 703 | ``` 704 | 705 | ### 输入模块的指定方法 706 | 707 | 加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。 708 | 709 | ```js 710 | const { SourceMapConsumer, SourceNode } = require("source-map") 711 | ``` 712 | 713 | 714 | 715 | -------------------------------------------------------------------------------- /docs/11. 新增运算符.md: -------------------------------------------------------------------------------- 1 | # 新增运算符 2 | 3 | 4 | 5 | ## 数组的扩展运算符 6 | 7 | ### 理解 8 | 9 | 扩展运算符(spread)是三个点(`...`)。它好比 rest 参数的逆运算,**将一个数组转为用逗号分隔的参数序列**。 10 | 11 | ```js 12 | console.log(...[1, 2, 3]) 13 | // 1 2 3 14 | 15 | console.log(1, ...[2, 3, 4], 5) 16 | // 1 2 3 4 5 17 | 18 | [...document.querySelectorAll('div')] 19 | // [
,
,
] 20 | ``` 21 | 22 | 更好的理解是,`…args` 就是把`args`中的数据一项一项取出来。 23 | 24 | ```js 25 | function exam(...args) { 26 | console.log(args); // [ 1, 2, 3 ] 27 | console.log(...args); // 1 2 3 28 | } 29 | 30 | exam(1, 2, 3); 31 | ``` 32 | 33 | 这里有一个例子可以很好的理解`...args`。 34 | 35 | 我们传入的参数是`1, 2, 3`,意味着`...args`的等于是`1, 2, 3`。又因为之前说过`…args` 就是把`args`中的数据一项一项取出来,所以反推`args`的值就为 `[ 1, 2, 3 ]`。 36 | 37 | ### 基本用法 38 | 39 | 该运算符主要用于函数调用。 40 | 41 | ```js 42 | function push(array, ...items) { 43 | array.push(...items); 44 | } 45 | 46 | function add(x, y) { 47 | return x + y; 48 | } 49 | 50 | const numbers = [4, 38]; 51 | add(...numbers) // 42 52 | ``` 53 | 54 | 上面代码中,`array.push(...items)`和`add(...numbers)`这两行,都是函数的调用,它们都使用了扩展运算符。该运算符将一个数组,变为参数序列。 55 | 56 | 扩展运算符与正常的函数参数可以结合使用,非常灵活。 57 | 58 | ```js 59 | function f(v, w, x, y, z) { } 60 | const args = [0, 1]; 61 | f(-1, ...args, 2, ...[3]); 62 | ``` 63 | 64 | 扩展运算符后面还可以放置表达式。 65 | 66 | ```js 67 | const arr = [ 68 | ...(x > 0 ? ['a'] : []), 69 | 'b', 70 | ]; 71 | ``` 72 | 73 | 如果扩展运算符后面是一个空数组,则不产生任何效果。 74 | 75 | ```js 76 | [...[], 1] 77 | // [1] 78 | ``` 79 | 80 | 注意,只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错。 81 | 82 | ```js 83 | (...[1, 2]) 84 | // Uncaught SyntaxError: Unexpected number 85 | 86 | console.log((...[1, 2])) 87 | // Uncaught SyntaxError: Unexpected number 88 | 89 | console.log(...[1, 2]) 90 | // 1 2 91 | ``` 92 | 93 | 上面三种情况,扩展运算符都放在圆括号里面,但是前两种情况会报错,因为扩展运算符所在的括号不是函数调用。 94 | 95 | ### 替代函数的 apply 方法 96 | 97 | 由于扩展运算符可以展开数组,所以不再需要`apply`方法,将数组转为函数的参数了。 98 | 99 | ```js 100 | // ES5 的写法 101 | function f(x, y, z) { 102 | // ... 103 | } 104 | var args = [0, 1, 2]; 105 | f.apply(null, args); 106 | 107 | // ES6的写法 108 | function f(x, y, z) { 109 | // ... 110 | } 111 | let args = [0, 1, 2]; 112 | f(...args); 113 | ``` 114 | 115 | 下面是扩展运算符取代`apply`方法的一个实际的例子,应用`Math.max`方法,简化求出一个数组最大元素的写法。 116 | 117 | ```js 118 | // ES5 的写法 119 | Math.max.apply(null, [14, 3, 77]) 120 | 121 | // ES6 的写法 122 | Math.max(...[14, 3, 77]) 123 | 124 | // 等同于 125 | Math.max(14, 3, 77); 126 | ``` 127 | 128 | 上面代码中,由于 JavaScript 不提供求数组最大元素的函数,所以只能套用`Math.max`函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用`Math.max`了。 129 | 130 | 另一个例子是通过`push`函数,将一个数组添加到另一个数组的尾部。 131 | 132 | ```js 133 | // ES5的 写法 134 | var arr1 = [0, 1, 2]; 135 | var arr2 = [3, 4, 5]; 136 | Array.prototype.push.apply(arr1, arr2); 137 | 138 | // ES6 的写法 139 | let arr1 = [0, 1, 2]; 140 | let arr2 = [3, 4, 5]; 141 | arr1.push(...arr2); 142 | ``` 143 | 144 | 上面代码的 ES5 写法中,`push`方法的参数不能是数组,所以只好通过`apply`方法变通使用`push`方法。有了扩展运算符,就可以直接将数组传入`push`方法。 145 | 146 | 下面是另外一个例子。 147 | 148 | ```js 149 | // ES5 150 | new (Date.bind.apply(Date, [null, 2015, 1, 1])) 151 | // ES6 152 | new Date(...[2015, 1, 1]); 153 | ``` 154 | 155 | ### 扩展运算符的应用 156 | 157 | #### 复制数组 158 | 159 | 数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。 160 | 161 | ```js 162 | const a1 = [1, 2]; 163 | const a2 = a1; 164 | 165 | a2[0] = 2; 166 | a1 // [2, 2] 167 | ``` 168 | 169 | 上面代码中,`a2`并不是`a1`的克隆,而是指向同一份数据的另一个指针。修改`a2`,会直接导致`a1`的变化。 170 | 171 | ES5 只能用变通方法来复制数组。 172 | 173 | ```js 174 | const a1 = [1, 2]; 175 | const a2 = a1.concat(); 176 | 177 | a2[0] = 2; 178 | a1 // [1, 2] 179 | ``` 180 | 181 | 上面代码中,`a1`会返回原数组的克隆,再修改`a2`就不会对`a1`产生影响。 182 | 183 | 扩展运算符提供了复制数组的简便写法。 184 | 185 | ```js 186 | const a1 = [1, 2]; 187 | // 写法一 188 | const a2 = [...a1]; 189 | // 写法二 190 | const [...a2] = a1; 191 | ``` 192 | 193 | 上面的两种写法,`a2`都是`a1`的克隆。 194 | 195 | #### 合并数组 196 | 197 | 扩展运算符提供了数组合并的新写法。 198 | 199 | ```js 200 | const arr1 = ['a', 'b']; 201 | const arr2 = ['c']; 202 | const arr3 = ['d', 'e']; 203 | 204 | // ES5 的合并数组 205 | arr1.concat(arr2, arr3); 206 | // [ 'a', 'b', 'c', 'd', 'e' ] 207 | 208 | // ES6 的合并数组 209 | [...arr1, ...arr2, ...arr3] 210 | // [ 'a', 'b', 'c', 'd', 'e' ] 211 | ``` 212 | 213 | 不过,这两种方法都是浅拷贝,使用的时候需要注意。 214 | 215 | ```js 216 | const a1 = [{ foo: 1 }]; 217 | const a2 = [{ bar: 2 }]; 218 | 219 | const a3 = a1.concat(a2); 220 | const a4 = [...a1, ...a2]; 221 | 222 | a3[0] === a1[0] // true 223 | a4[0] === a1[0] // true 224 | ``` 225 | 226 | 上面代码中,`a3`和`a4`是用两种不同方法合并而成的新数组,但是它们的成员都是对原数组成员的引用,这就是浅拷贝。如果修改了引用指向的值,会同步反映到新数组。 227 | 228 | #### 与解构赋值结合 229 | 230 | 扩展运算符可以与解构赋值结合起来,用于生成数组。 231 | 232 | ```js 233 | // ES5 234 | a = list[0], rest = list.slice(1) 235 | // ES6 236 | [a, ...rest] = list 237 | ``` 238 | 239 | 下面是另外一些例子。 240 | 241 | ```js 242 | const [first, ...rest] = [1, 2, 3, 4, 5]; 243 | first // 1 244 | rest // [2, 3, 4, 5] 245 | 246 | const [first, ...rest] = []; 247 | first // undefined 248 | rest // [] 249 | 250 | const [first, ...rest] = ["foo"]; 251 | first // "foo" 252 | rest // [] 253 | ``` 254 | 255 | 如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。 256 | 257 | ```js 258 | const [...butLast, last] = [1, 2, 3, 4, 5]; 259 | // 报错 260 | 261 | const [first, ...middle, last] = [1, 2, 3, 4, 5]; 262 | // 报错 263 | ``` 264 | 265 | #### 字符串 266 | 267 | 扩展运算符还可以将字符串转为真正的数组。 268 | 269 | ```js 270 | [...'hello'] 271 | // [ "h", "e", "l", "l", "o" ] 272 | ``` 273 | 274 | 上面的写法,有一个重要的好处,那就是能够正确识别四个字节的 Unicode 字符。 275 | 276 | ```js 277 | 'x\uD83D\uDE80y'.length // 4 278 | [...'x\uD83D\uDE80y'].length // 3 279 | ``` 280 | 281 | 上面代码的第一种写法,JavaScript 会将四个字节的 Unicode 字符,识别为 2 个字符,采用扩展运算符就没有这个问题。因此,正确返回字符串长度的函数,可以像下面这样写。 282 | 283 | ```js 284 | function length(str) { 285 | return [...str].length; 286 | } 287 | 288 | length('x\uD83D\uDE80y') // 3 289 | ``` 290 | 291 | 凡是涉及到操作四个字节的 Unicode 字符的函数,都有这个问题。因此,最好都用扩展运算符改写。 292 | 293 | ```js 294 | let str = 'x\uD83D\uDE80y'; 295 | 296 | str.split('').reverse().join('') 297 | // 'y\uDE80\uD83Dx' 298 | 299 | [...str].reverse().join('') 300 | // 'y\uD83D\uDE80x' 301 | ``` 302 | 303 | 上面代码中,如果不用扩展运算符,字符串的`reverse`操作就不正确。 304 | 305 | #### 实现了 Iterator 接口的对象 306 | 307 | 任何定义了遍历器(Iterator)接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组。 308 | 309 | ```js 310 | let nodeList = document.querySelectorAll('div'); 311 | let array = [...nodeList]; 312 | ``` 313 | 314 | 上面代码中,`querySelectorAll`方法返回的是一个`NodeList`对象。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于`NodeList`对象实现了 Iterator 。 315 | 316 | ```js 317 | Number.prototype[Symbol.iterator] = function*() { 318 | let i = 0; 319 | let num = this.valueOf(); 320 | while (i < num) { 321 | yield i++; 322 | } 323 | } 324 | 325 | console.log([...5]) // [0, 1, 2, 3, 4] 326 | ``` 327 | 328 | 上面代码中,先定义了`Number`对象的遍历器接口,扩展运算符将`5`自动转成`Number`实例以后,就会调用这个接口,就会返回自定义的结果。 329 | 330 | 对于那些没有部署 Iterator 接口的类似数组的对象,扩展运算符就无法将其转为真正的数组。 331 | 332 | ```js 333 | let arrayLike = { 334 | '0': 'a', 335 | '1': 'b', 336 | '2': 'c', 337 | length: 3 338 | }; 339 | 340 | // TypeError: Cannot spread non-iterable object. 341 | let arr = [...arrayLike]; 342 | ``` 343 | 344 | 上面代码中,`arrayLike`是一个类似数组的对象,但是没有部署 Iterator 接口,扩展运算符就会报错。这时,可以改为使用`Array.from`方法将`arrayLike`转为真正的数组。 345 | 346 | #### Map 和 Set 结构,Generator 函数 347 | 348 | 扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 Map 结构。 349 | 350 | ```js 351 | let map = new Map([ 352 | [1, 'one'], 353 | [2, 'two'], 354 | [3, 'three'], 355 | ]); 356 | 357 | let arr = [...map.keys()]; // [1, 2, 3] 358 | ``` 359 | 360 | Generator 函数运行后,返回一个遍历器对象,因此也可以使用扩展运算符。 361 | 362 | ```js 363 | const go = function*(){ 364 | yield 1; 365 | yield 2; 366 | yield 3; 367 | }; 368 | 369 | [...go()] // [1, 2, 3] 370 | ``` 371 | 372 | 上面代码中,**变量`go`是一个 Generator 函数,执行后返回的是一个遍历器对象**,**对这个遍历器对象执行扩展运算符,就会将内部遍历得到的值,转为一个数组**。 373 | 374 | 如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错。 375 | 376 | ```js 377 | const obj = {a: 1, b: 2}; 378 | let arr = [...obj]; // TypeError: Cannot spread non-iterable object 379 | ``` 380 | 381 | 382 | 383 | ## 对象的扩展运算符 384 | 385 | 之前已经介绍过扩展运算符(`...`)。ES2018 将这个运算符[引入](https://github.com/sebmarkbage/ecmascript-rest-spread)了对象。 386 | 387 | ### 理解 388 | 389 | 可以与数组的扩展运算符有相同的理解,`…arg` 就是把`args`中的数据一项一项取出来。 390 | 391 | ```js 392 | let obj = { a: 1, b: 2 }; 393 | let { ...x } = obj; 394 | 395 | console.log(x); // { a: 1, b: 2 } 396 | console.log(...x); // 报错 397 | ``` 398 | 399 | 这是一个对象拷贝的例子,可以很好的进行理解。 400 | 401 | 首先打印变量`x`,结果是`{ a: 1, b: 2 }`,意味着`...x`可以看作是`a: 1, b: 2`,注意,这里和数组不同,这里是只是可以看作,因为属性脱离了对象并不能存在,所以打印`...x`,会立马报错。 402 | 403 | 因此,在赋值的时候,我们需要在`...x`外面在包括一层`{}`,使得属性存在与对象之中。 404 | 405 | 这看起来似乎没有意义,是多此一举,还不如直接: 406 | 407 | ```js 408 | let obj = { a: 1, b: 2 }; 409 | let x = obj; 410 | ``` 411 | 412 | 但是一旦需要获得一个对象中的部分属性的时候,对象的扩展运算符就显得十分好用。 413 | 414 | ```js 415 | let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; 416 | x // 1 417 | y // 2 418 | z // { a: 3, b: 4 } 419 | ``` 420 | 421 | 上面的例子中,我们可以轻松的让变量`z`取到`{ a: 3, b: 4 }`这两个属性。 422 | 423 | 否则我们需要这么写: 424 | 425 | ```js 426 | let obj = { x: 1, y: 2, a: 3, b: 4 }; 427 | let newObj = {}; 428 | newObj.a = obj.a; 429 | newObj.b = obj.b; 430 | 431 | // 或者 432 | let obj = { x: 1, y: 2, a: 3, b: 4 }; 433 | let newObj = obj; 434 | Reflect.deleteProperty(newObj, 'x'); 435 | Reflect.deleteProperty(newObj, 'y'); 436 | ``` 437 | 438 | 不论哪一种方法,都是很麻烦的。 439 | 440 | 441 | 442 | ### 基本用法 443 | 444 | **对象的扩展运算符(`...`)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。** 445 | 446 | ```js 447 | let z = { a: 3, b: 4 }; 448 | let n = { ...z }; 449 | n // { a: 3, b: 4 } 450 | ``` 451 | 452 | 由于数组是特殊的对象,所以对象的扩展运算符也可以用于数组。 453 | 454 | ```js 455 | let foo = { ...['a', 'b', 'c'] }; 456 | foo 457 | // {0: "a", 1: "b", 2: "c"} 458 | ``` 459 | 460 | 如果扩展运算符后面是一个空对象,则没有任何效果。 461 | 462 | ```js 463 | {...{}, a: 1} 464 | // { a: 1 } 465 | ``` 466 | 467 | 如果扩展运算符后面不是对象,则会自动将其转为对象。 468 | 469 | ```js 470 | // 等同于 {...Object(1)} 471 | {...1} // {} 472 | ``` 473 | 474 | 上面代码中,扩展运算符后面是整数`1`,会自动转为数值的包装对象`Number{1}`。由于该对象没有自身属性,所以返回一个空对象。 475 | 476 | 下面的例子都是类似的道理。 477 | 478 | ```js 479 | // 等同于 {...Object(true)} 480 | {...true} // {} 481 | 482 | // 等同于 {...Object(undefined)} 483 | {...undefined} // {} 484 | 485 | // 等同于 {...Object(null)} 486 | {...null} // {} 487 | ``` 488 | 489 | 但是,如果扩展运算符后面是字符串,它会自动转成一个类似数组的对象,因此返回的不是空对象。 490 | 491 | ```js 492 | {...'hello'} 493 | // {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"} 494 | ``` 495 | 496 | **对象的扩展运算符等同于使用`Object.assign()`方法。** 497 | 498 | ```js 499 | let aClone = { ...a }; 500 | // 等同于 501 | let aClone = Object.assign({}, a); 502 | ``` 503 | 504 | 上面的例子只是拷贝了对象实例的属性,如果想完整克隆一个对象,还拷贝对象原型的属性,可以采用下面的写法。 505 | 506 | ```js 507 | // 写法一 508 | const clone1 = { 509 | __proto__: Object.getPrototypeOf(obj), 510 | ...obj 511 | }; 512 | 513 | // 写法二 514 | const clone2 = Object.assign( 515 | Object.create(Object.getPrototypeOf(obj)), 516 | obj 517 | ); 518 | 519 | // 写法三 520 | const clone3 = Object.create( 521 | Object.getPrototypeOf(obj), 522 | Object.getOwnPropertyDescriptors(obj) 523 | ) 524 | ``` 525 | 526 | 上面代码中,写法一的`__proto__`属性在非浏览器的环境不一定部署,因此推荐使用写法二和写法三。 527 | 528 | 扩展运算符可以用于合并两个对象。 529 | 530 | ```js 531 | let ab = { ...a, ...b }; 532 | // 等同于 533 | let ab = Object.assign({}, a, b); 534 | ``` 535 | 536 | 如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。 537 | 538 | ```js 539 | let aWithOverrides = { ...a, x: 1, y: 2 }; 540 | // 等同于 541 | let aWithOverrides = { ...a, ...{ x: 1, y: 2 } }; 542 | // 等同于 543 | let x = 1, y = 2, aWithOverrides = { ...a, x, y }; 544 | // 等同于 545 | let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 }); 546 | ``` 547 | 548 | 上面代码中,`a`对象的`x`属性和`y`属性,拷贝到新对象后会被覆盖掉。 549 | 550 | 这用来修改现有对象部分的属性就很方便了。 551 | 552 | ```js 553 | let newVersion = { 554 | ...previousVersion, 555 | name: 'New Name' // Override the name property 556 | }; 557 | ``` 558 | 559 | 上面代码中,`newVersion`对象自定义了`name`属性,其他属性全部复制自`previousVersion`对象。 560 | 561 | 如果把自定义属性放在扩展运算符前面,就变成了设置新对象的默认属性值。 562 | 563 | ```js 564 | let aWithDefaults = { x: 1, y: 2, ...a }; 565 | // 等同于 566 | let aWithDefaults = Object.assign({}, { x: 1, y: 2 }, a); 567 | // 等同于 568 | let aWithDefaults = Object.assign({ x: 1, y: 2 }, a); 569 | ``` 570 | 571 | 与数组的扩展运算符一样,对象的扩展运算符后面可以跟表达式。 572 | 573 | ```js 574 | const obj = { 575 | ...(x > 1 ? {a: 1} : {}), 576 | b: 2, 577 | }; 578 | ``` 579 | 580 | 扩展运算符的参数对象之中,如果有取值函数`get`,这个函数是会执行的。 581 | 582 | ```js 583 | let a = { 584 | get x() { 585 | throw new Error('not throw yet'); 586 | } 587 | } 588 | 589 | let aWithXGetter = { ...a }; // 报错 590 | ``` 591 | 592 | 上面例子中,取值函数`get`在扩展`a`对象时会自动执行,导致报错。 593 | 594 | ### 解构赋值中运用 595 | 596 | 对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。 597 | 598 | ```js 599 | let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; 600 | x // 1 601 | y // 2 602 | z // { a: 3, b: 4 } 603 | ``` 604 | 605 | 上面代码中,变量`z`是解构赋值所在的对象。它获取等号右边的所有尚未读取的键(`a`和`b`),将它们连同值一起拷贝过来。 606 | 607 | 由于解构赋值要求等号右边是一个对象,所以如果等号右边是`undefined`或`null`,就会报错,因为它们无法转为对象。 608 | 609 | ```js 610 | let { ...z } = null; // 运行时错误 611 | let { ...z } = undefined; // 运行时错误 612 | ``` 613 | 614 | 解构赋值必须是最后一个参数,否则会报错。 615 | 616 | ```js 617 | let { ...x, y, z } = someObject; // 句法错误 618 | let { x, ...y, ...z } = someObject; // 句法错误 619 | ``` 620 | 621 | 上面代码中,解构赋值不是最后一个参数,所以会报错。 622 | 623 | 注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。 624 | 625 | ```js 626 | let obj = { a: { b: 1 } }; 627 | let { ...x } = obj; 628 | obj.a.b = 2; 629 | x.a.b // 2 630 | ``` 631 | 632 | 上面代码中,`x`是解构赋值所在的对象,拷贝了对象`obj`的`a`属性。`a`属性引用了一个对象,修改这个对象的值,会影响到解构赋值对它的引用。 633 | 634 | **另外,扩展运算符的解构赋值,不能复制继承自原型对象的属性。** 635 | 636 | ```js 637 | let o1 = { a: 1 }; 638 | let o2 = { b: 2 }; 639 | o2.__proto__ = o1; 640 | let { ...o3 } = o2; 641 | o3 // { b: 2 } 642 | o3.a // undefined 643 | ``` 644 | 645 | 上面代码中,对象`o3`复制了`o2`,但是只复制了`o2`自身的属性,没有复制它的原型对象`o1`的属性。 646 | 647 | 下面是另一个例子。 648 | 649 | ```js 650 | const o = Object.create({ x: 1, y: 2 }); 651 | o.z = 3; 652 | 653 | let { x, ...newObj } = o; 654 | let { y, z } = newObj; 655 | x // 1 656 | y // undefined 657 | z // 3 658 | ``` 659 | 660 | 上面代码中,变量`x`是单纯的解构赋值,所以可以读取对象`o`继承的属性;变量`y`和`z`是扩展运算符的解构赋值,只能读取对象`o`自身的属性,所以变量`z`可以赋值成功,变量`y`取不到值。 661 | 662 | **ES6 规定,变量声明语句之中,如果使用解构赋值,扩展运算符后面必须是一个变量名,而不能是一个解构赋值表达式,所以上面代码引入了中间变量`newObj`,如果写成下面这样会报错。** 663 | 664 | ``` 665 | let { x, ...{ y, z } } = o; 666 | // SyntaxError: ... must be followed by an identifier in declaration contexts 667 | ``` 668 | 669 | 解构赋值的一个用处,是扩展某个函数的参数,引入其他操作。 670 | 671 | ```js 672 | function baseFunction({ a, b }) { 673 | // ... 674 | } 675 | function wrapperFunction({ x, y, ...restConfig }) { 676 | // 使用 x 和 y 参数进行操作 677 | // 其余参数传给原始函数 678 | return baseFunction(restConfig); 679 | } 680 | ``` 681 | 682 | 上面代码中,原始函数`baseFunction`接受`a`和`b`作为参数,函数`wrapperFunction`在`baseFunction`的基础上进行了扩展,能够接受多余的参数,并且保留原始函数的行为。 683 | 684 | 685 | 686 | ## 链判断运算符 687 | 688 | ### 设计目的 689 | 690 | 编程中,如果读取对象内部的某个属性,往往需要判断一下该对象是否存在。比如,要读取`message.body.user.firstName`,安全的写法是写成下面这样。 691 | 692 | ```js 693 | // 错误的写法 694 | const firstName = message.body.user.firstName; 695 | 696 | // 正确的写法 697 | const firstName = (message 698 | && message.body 699 | && message.body.user 700 | && message.body.user.firstName) || 'default'; 701 | ``` 702 | 703 | 上面例子中,`firstName`属性在对象的第四层,所以需要判断四次,每一层是否有值。 704 | 705 | 三元运算符`?:`也常用于判断对象是否存在。 706 | 707 | ```js 708 | const fooInput = myForm.querySelector('input[name=foo]') 709 | const fooValue = fooInput ? fooInput.value : undefined 710 | ``` 711 | 712 | 上面例子中,必须先判断`fooInput`是否存在,才能读取`fooInput.value`。 713 | 714 | **这样的层层判断非常麻烦,因此 [ES2020](https://github.com/tc39/proposal-optional-chaining) 引入了“链判断运算符”(optional chaining operator)`?.`,简化上面的写法。** 715 | 716 | ```js 717 | const firstName = message?.body?.user?.firstName || 'default'; 718 | const fooValue = myForm.querySelector('input[name=foo]')?.value 719 | ``` 720 | 721 | 上面代码使用了`?.`运算符,直接在链式调用的时候判断,左侧的对象是否为`null`或`undefined`。如果是的,就不再往下运算,而是返回`undefined`。 722 | 723 | ### 基本用法 724 | 725 | 下面是判断对象方法是否存在,如果存在就立即执行的例子。 726 | 727 | ```js 728 | iterator.return?.() 729 | ``` 730 | 731 | 上面代码中,`iterator.return`如果有定义,就会调用该方法,否则`iterator.return`直接返回`undefined`,不再执行`?.`后面的部分。 732 | 733 | 对于那些可能没有实现的方法,这个运算符尤其有用。 734 | 735 | ```js 736 | if (myForm.checkValidity?.() === false) { 737 | // 表单校验失败 738 | return; 739 | } 740 | ``` 741 | 742 | 上面代码中,老式浏览器的表单可能没有`checkValidity`这个方法,这时`?.`运算符就会返回`undefined`,判断语句就变成了`undefined === false`,所以就会跳过下面的代码。 743 | 744 | **链判断运算符有三种用法:** 745 | 746 | - **`obj?.prop` // 对象属性** 747 | - **`obj?.[expr]` // 同上** 748 | - **`func?.(...args)` // 函数或对象方法的调用** 749 | 750 | 下面是`obj?.[expr]`用法的一个例子。 751 | 752 | ```js 753 | let hex = "#C0FFEE".match(/#([A-Z]+)/i)?.[1]; 754 | ``` 755 | 756 | 上面例子中,字符串的`match()`方法,如果没有发现匹配会返回`null`,如果发现匹配会返回一个数组,`?.`运算符起到了判断作用。 757 | 758 | 下面是`?.`运算符常见形式,以及不使用该运算符时的等价形式。 759 | 760 | ```js 761 | a?.b 762 | // 等同于 763 | a == null ? undefined : a.b 764 | 765 | a?.[x] 766 | // 等同于 767 | a == null ? undefined : a[x] 768 | 769 | a?.b() 770 | // 等同于 771 | a == null ? undefined : a.b() 772 | 773 | a?.() 774 | // 等同于 775 | a == null ? undefined : a() 776 | ``` 777 | 778 | 上面代码中,特别注意后两种形式,如果`a?.b()`里面的`a.b`不是函数,不可调用,那么`a?.b()`是会报错的。`a?.()`也是如此,如果`a`不是`null`或`undefined`,但也不是函数,那么`a?.()`会报错。 779 | 780 | ### 注意点 781 | 782 | 使用这个运算符,有几个注意点: 783 | 784 | #### 短路机制 785 | 786 | `?.`运算符相当于一种短路机制,只要不满足条件,就不再往下执行。 787 | 788 | ```js 789 | a?.[++x] 790 | // 等同于 791 | a == null ? undefined : a[++x] 792 | ``` 793 | 794 | 上面代码中,如果`a`是`undefined`或`null`,那么`x`不会进行递增运算。也就是说,链判断运算符一旦为真,右侧的表达式就不再求值。 795 | 796 | #### delete 运算符 797 | 798 | ```js 799 | delete a?.b 800 | // 等同于 801 | a == null ? undefined : delete a.b 802 | ``` 803 | 804 | 上面代码中,如果`a`是`undefined`或`null`,会直接返回`undefined`,而不会进行`delete`运算。 805 | 806 | #### 括号的影响 807 | 808 | 如果属性链有圆括号,链判断运算符对圆括号外部没有影响,只对圆括号内部有影响。 809 | 810 | ```js 811 | (a?.b).c 812 | // 等价于 813 | (a == null ? undefined : a.b).c 814 | ``` 815 | 816 | 上面代码中,`?.`对圆括号外部没有影响,不管`a`对象是否存在,圆括号后面的`.c`总是会执行。 817 | 818 | **一般来说,使用`?.`运算符的场合,不应该使用圆括号。** 819 | 820 | #### 报错场合 821 | 822 | 以下写法是禁止的,会报错。 823 | 824 | ```js 825 | // 构造函数 826 | new a?.() 827 | new a?.b() 828 | 829 | // 链判断运算符的右侧有模板字符串 830 | a?.`{b}` 831 | a?.b`{c}` 832 | 833 | // 链判断运算符的左侧是 super 834 | super?.() 835 | super?.foo 836 | 837 | // 链运算符用于赋值运算符左侧 838 | a?.b = c 839 | ``` 840 | 841 | #### 右侧不得为十进制数值 842 | 843 | 为了保证兼容以前的代码,允许`foo?.3:0`被解析成`foo ? .3 : 0`,因此规定如果`?.`后面紧跟一个十进制数字,那么`?.`不再被看成是一个完整的运算符,而会按照三元运算符进行处理,也就是说,那个小数点会归属于后面的十进制数字,形成一个小数。 844 | 845 | 846 | 847 | ## Null 判断运算符 848 | 849 | ### 设计目的 850 | 851 | 读取对象属性的时候,如果某个属性的值是`null`或`undefined`,有时候需要为它们指定默认值。常见做法是通过`||`运算符指定默认值。 852 | 853 | ```js 854 | const headerText = response.settings.headerText || 'Hello, world!'; 855 | const animationDuration = response.settings.animationDuration || 300; 856 | const showSplashScreen = response.settings.showSplashScreen || true; 857 | ``` 858 | 859 | 上面的三行代码都通过`||`运算符指定默认值,但是这样写是错的。开发者的原意是,只要属性的值为`null`或`undefined`,默认值就会生效,但是属性的值如果为空字符串或`false`或`0`,默认值也会生效。 860 | 861 | **为了避免这种情况,[ES2020](https://github.com/tc39/proposal-nullish-coalescing) 引入了一个新的 Null 判断运算符`??`。它的行为类似`||`,但是只有运算符左侧的值为`null`或`undefined`时,才会返回右侧的值。** 862 | 863 | ```js 864 | const headerText = response.settings.headerText ?? 'Hello, world!'; 865 | const animationDuration = response.settings.animationDuration ?? 300; 866 | const showSplashScreen = response.settings.showSplashScreen ?? true; 867 | ``` 868 | 869 | 上面代码中,默认值只有在左侧属性值为`null`或`undefined`时,才会生效。 870 | 871 | ### 基本用法 872 | 873 | 这个运算符的一个目的,就是跟链判断运算符`?.`配合使用,为`null`或`undefined`的值设置默认值。 874 | 875 | ```js 876 | const animationDuration = response.settings?.animationDuration ?? 300; 877 | ``` 878 | 879 | 上面代码中,如果`response.settings`是`null`或`undefined`,或者`response.settings.animationDuration`是`null`或`undefined`,就会返回默认值300。也就是说,这一行代码包括了两级属性的判断。 880 | 881 | 这个运算符很适合判断函数参数是否赋值。 882 | 883 | ```js 884 | function Component(props) { 885 | const enable = props.enabled ?? true; 886 | // … 887 | } 888 | ``` 889 | 890 | 上面代码判断`props`参数的`enabled`属性是否赋值,基本等同于下面的写法。 891 | 892 | ```js 893 | function Component(props) { 894 | const { 895 | enabled: enable = true, 896 | } = props; 897 | // … 898 | } 899 | ``` 900 | 901 | ### 优先级问题 902 | 903 | **`??`有一个运算优先级问题,它与`&&`和`||`的优先级孰高孰低。现在的规则是,如果多个逻辑运算符一起使用,必须用括号表明优先级,否则会报错。** 904 | 905 | ```js 906 | // 报错 907 | lhs && middle ?? rhs 908 | lhs ?? middle && rhs 909 | lhs || middle ?? rhs 910 | lhs ?? middle || rhs 911 | ``` 912 | 913 | 上面四个表达式都会报错,必须加入表明优先级的括号。 914 | 915 | ```js 916 | (lhs && middle) ?? rhs; 917 | lhs && (middle ?? rhs); 918 | 919 | (lhs ?? middle) && rhs; 920 | lhs ?? (middle && rhs); 921 | 922 | (lhs || middle) ?? rhs; 923 | lhs || (middle ?? rhs); 924 | 925 | (lhs ?? middle) || rhs; 926 | lhs ?? (middle || rhs); 927 | ``` 928 | 929 | -------------------------------------------------------------------------------- /docs/13. Set.md: -------------------------------------------------------------------------------- 1 | # Set 2 | 3 | 4 | 5 | ## 概念 6 | 7 | ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。 8 | 9 | 10 | 11 | ## 基本语法 12 | 13 | ### 构造函数 14 | 15 | `Set`本身是一个构造函数,用来生成 Set 数据结构。 16 | 17 | ```js 18 | const s = new Set(); 19 | 20 | [2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x)); 21 | 22 | for (let i of s) { 23 | console.log(i); 24 | } 25 | // 2 3 5 4 26 | ``` 27 | 28 | 上面代码通过`add()`方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。 29 | 30 | `Set`函数可以接受一个数组(**或者具有 Iterable 接口的其他数据结构**)作为参数,用来初始化。 31 | 32 | ```js 33 | // 例一 34 | const set = new Set([1, 2, 3, 4, 4]); 35 | [...set] 36 | // [1, 2, 3, 4] 37 | 38 | // 例二 39 | const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]); 40 | items.size // 5 41 | 42 | // 例三 43 | const set = new Set(document.querySelectorAll('div')); 44 | set.size // 56 45 | 46 | // 类似于 47 | const set = new Set(); 48 | document 49 | .querySelectorAll('div') 50 | .forEach(div => set.add(div)); 51 | set.size // 56 52 | ``` 53 | 54 | 上面代码中,例一和例二都是`Set`函数接受数组作为参数,例三是接受类似数组的对象作为参数。 55 | 56 | ### 注意点 57 | 58 | **向 Set 加入值的时候,不会发生类型转换,所以`5`和`"5"`是两个不同的值。** 59 | 60 | Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(`===`),主要的区别是向 Set 加入值时认为`NaN`等于自身,而精确相等运算符认为`NaN`不等于自身。 61 | 62 | ```js 63 | let set = new Set(); 64 | let a = NaN; 65 | let b = NaN; 66 | set.add(a); 67 | set.add(b); 68 | set // Set {NaN} 69 | ``` 70 | 71 | 上面代码向 Set 实例添加了两次`NaN`,但是只会加入一个。这表明,在 Set 内部,两个`NaN`是相等的。 72 | 73 | 另外,两个对象总是不相等的。 74 | 75 | ```js 76 | let set = new Set(); 77 | 78 | set.add({}); 79 | set.size // 1 80 | 81 | set.add({}); 82 | set.size // 2 83 | ``` 84 | 85 | 上面代码表示,由于两个空对象不相等,所以它们被视为两个值。 86 | 87 | 88 | 89 | ## 实例的属性和方法 90 | 91 | Set 结构的实例有以下属性。 92 | 93 | - `Set.prototype.constructor`:构造函数,默认就是`Set`函数。 94 | - `Set.prototype.size`:返回`Set`实例的成员总数。 95 | 96 | Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。 97 | 98 | - `Set.prototype.add(value)`:添加某个值,返回 Set 结构本身。 99 | - `Set.prototype.delete(value)`:删除某个值,返回一个布尔值,表示删除是否成功。 100 | - `Set.prototype.has(value)`:返回一个布尔值,表示该值是否为`Set`的成员。 101 | - `Set.prototype.clear()`:清除所有成员,没有返回值。 102 | 103 | 上面这些属性和方法的实例如下。 104 | 105 | ```js 106 | s.add(1).add(2).add(2); 107 | // 注意2被加入了两次 108 | 109 | s.size // 2 110 | 111 | s.has(1) // true 112 | s.has(2) // true 113 | s.has(3) // false 114 | 115 | s.delete(2); 116 | s.has(2) // false 117 | ``` 118 | 119 | 下面是一个对比,看看在判断是否包括一个键上面,`Object`结构和`Set`结构的写法不同。 120 | 121 | ```js 122 | // 对象的写法 123 | const properties = { 124 | 'width': 1, 125 | 'height': 1 126 | }; 127 | 128 | if (properties[someName]) { 129 | // do something 130 | } 131 | 132 | // Set的写法 133 | const properties = new Set(); 134 | 135 | properties.add('width'); 136 | properties.add('height'); 137 | 138 | if (properties.has(someName)) { 139 | // do something 140 | } 141 | ``` 142 | 143 | 144 | 145 | ## Set 与 Array 的相互转化 146 | 147 | `Array.from`方法可以将 Set 结构转为数组。 148 | 149 | ```js 150 | const items = new Set([1, 2, 3, 4, 5]); 151 | const array = Array.from(items); 152 | ``` 153 | 154 | 这就提供了去除数组重复成员的另一种方法。 155 | 156 | ```js 157 | function dedupe(array) { 158 | return Array.from(new Set(array)); 159 | } 160 | 161 | dedupe([1, 1, 2, 3]) // [1, 2, 3] 162 | ``` 163 | 164 | 而将数组转化成 Set,只需要使用 Set 的 构造函数即可。 165 | 166 | ```js 167 | const array = [1, 1, 2, 3]; 168 | 169 | const set = new Set(array); 170 | 171 | // Set(3) { 1, 2, 3 } 172 | ``` 173 | 174 | 上面代码也展示了一种去除数组重复成员的方法。 175 | 176 | ```js 177 | // 去除数组的重复成员 178 | [...new Set(array)] 179 | ``` 180 | 181 | 上面的方法也可以用于,去除字符串里面的重复字符。 182 | 183 | ```js 184 | [...new Set('ababbc')].join('') 185 | // "abc" 186 | ``` 187 | 188 | 189 | 190 | ## 遍历操作 191 | 192 | ### 概述 193 | 194 | Set 结构的实例有四个遍历方法,可以用于遍历成员。 195 | 196 | - `Set.prototype.keys()`:返回键名的遍历器 197 | - `Set.prototype.values()`:返回键值的遍历器 198 | - `Set.prototype.entries()`:返回键值对的遍历器 199 | - `Set.prototype.forEach()`:使用回调函数遍历每个成员 200 | 201 | 需要特别指出的是,`Set`的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。 202 | 203 | ### keys()&values()&entries() 204 | 205 | `keys`方法、`values`方法、`entries`方法返回的都是遍历器对象。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以`keys`方法和`values`方法的行为完全一致。 206 | 207 | ```js 208 | let set = new Set(['red', 'green', 'blue']); 209 | 210 | for (let item of set.keys()) { 211 | console.log(item); 212 | } 213 | // red 214 | // green 215 | // blue 216 | 217 | for (let item of set.values()) { 218 | console.log(item); 219 | } 220 | // red 221 | // green 222 | // blue 223 | 224 | for (let item of set.entries()) { 225 | console.log(item); 226 | } 227 | // ["red", "red"] 228 | // ["green", "green"] 229 | // ["blue", "blue"] 230 | ``` 231 | 232 | 上面代码中,`entries`方法返回的遍历器,同时包括键名和键值,所以每次输出一个数组,它的两个成员完全相等。 233 | 234 | Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的`values`方法。 235 | 236 | ```js 237 | Set.prototype[Symbol.iterator] === Set.prototype.values 238 | // true 239 | ``` 240 | 241 | 这意味着,可以省略`values`方法,直接用`for...of`循环遍历 Set。 242 | 243 | ```js 244 | let set = new Set(['red', 'green', 'blue']); 245 | 246 | for (let x of set) { 247 | console.log(x); 248 | } 249 | // red 250 | // green 251 | // blue 252 | ``` 253 | 254 | ### forEach() 255 | 256 | **Set 结构的实例与数组一样,也拥有`forEach`方法,用于对每个成员执行某种操作,没有返回值。** 257 | 258 | ```js 259 | let set = new Set([1, 4, 9]); 260 | set.forEach((value, key) => console.log(key + ' : ' + value)) 261 | // 1 : 1 262 | // 4 : 4 263 | // 9 : 9 264 | ``` 265 | 266 | 上面代码说明,`forEach`方法的参数就是一个处理函数。该函数的参数与数组的`forEach`一致,依次为键值、键名、集合本身(上例省略了该参数)。这里需要注意,Set 结构的键名就是键值(两者是同一个值),因此第一个参数与第二个参数的值永远都是一样的。 267 | 268 | **另外,`forEach`方法还可以有第二个参数,表示绑定处理函数内部的`this`对象。** 269 | 270 | ### 遍历的应用 271 | 272 | 扩展运算符(`...`)内部使用`for...of`循环,所以也可以用于 Set 结构。 273 | 274 | ```js 275 | let set = new Set(['red', 'green', 'blue']); 276 | let arr = [...set]; 277 | // ['red', 'green', 'blue'] 278 | ``` 279 | 280 | 扩展运算符和 Set 结构相结合,就可以去除数组的重复成员。 281 | 282 | ```js 283 | let arr = [3, 5, 2, 2, 5, 5]; 284 | let unique = [...new Set(arr)]; 285 | // [3, 5, 2] 286 | ``` 287 | 288 | 而且,数组的`map`和`filter`方法也可以间接用于 Set 了。 289 | 290 | ```js 291 | let set = new Set([1, 2, 3]); 292 | set = new Set([...set].map(x => x * 2)); 293 | // 返回Set结构:{2, 4, 6} 294 | 295 | let set = new Set([1, 2, 3, 4, 5]); 296 | set = new Set([...set].filter(x => (x % 2) == 0)); 297 | // 返回Set结构:{2, 4} 298 | ``` 299 | 300 | **因此使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。** 301 | 302 | ```js 303 | let a = new Set([1, 2, 3]); 304 | let b = new Set([4, 3, 2]); 305 | 306 | // 并集 307 | let union = new Set([...a, ...b]); 308 | // Set {1, 2, 3, 4} 309 | 310 | // 交集 311 | let intersect = new Set([...a].filter(x => b.has(x))); 312 | // set {2, 3} 313 | 314 | // (a 相对于 b 的)差集 315 | let difference = new Set([...a].filter(x => !b.has(x))); 316 | // Set {1} 317 | ``` 318 | 319 | 如果想在遍历操作中,同步改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。一种是利用原 Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构;另一种是利用`Array.from`方法。 320 | 321 | ```js 322 | // 方法一 323 | let set = new Set([1, 2, 3]); 324 | set = new Set([...set].map(val => val * 2)); 325 | // set的值是2, 4, 6 326 | 327 | // 方法二 328 | let set = new Set([1, 2, 3]); 329 | set = new Set(Array.from(set, val => val * 2)); 330 | // set的值是2, 4, 6 331 | ``` 332 | 333 | 上面代码提供了两种方法,直接在遍历操作中改变原来的 Set 结构。 334 | 335 | 336 | 337 | ## WeakSet 338 | 339 | ### 含义 340 | 341 | WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。 342 | 343 | **首先,WeakSet 的成员只能是对象,而不能是其他类型的值。** 344 | 345 | ```js 346 | const ws = new WeakSet(); 347 | ws.add(1) 348 | // TypeError: Invalid value used in weak set 349 | ws.add(Symbol()) 350 | // TypeError: invalid value used in weak set 351 | ``` 352 | 353 | **上面代码试图向 WeakSet 添加一个数值和`Symbol`值,结果报错,因为 WeakSet 只能放置对象。** 354 | 355 | 其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。 356 | 357 | 这是因为垃圾回收机制依赖引用计数,如果一个值的引用次数不为`0`,垃圾回收机制就不会释放这块内存。结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。WeakSet 里面的引用,都不计入垃圾回收机制,所以就不存在这个问题。因此,WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。 358 | 359 | 由于上面这个特点,WeakSet 的成员是不适合引用的,因为它会随时消失。另外,由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。 360 | 361 | 这些特点同样适用于本章后面要介绍的 WeakMap 结构。 362 | 363 | ### 语法 364 | 365 | WeakSet 是一个构造函数,可以使用`new`命令,创建 WeakSet 数据结构。 366 | 367 | ```js 368 | const ws = new WeakSet(); 369 | ``` 370 | 371 | 作为构造函数,WeakSet 可以接受一个数组或类似数组的对象作为参数。(实际上,任何具有 Iterable 接口的对象,都可以作为 WeakSet 的参数。)该数组的所有成员,都会自动成为 WeakSet 实例对象的成员。 372 | 373 | ```js 374 | const a = [[1, 2], [3, 4]]; 375 | const ws = new WeakSet(a); 376 | // WeakSet {[1, 2], [3, 4]} 377 | ``` 378 | 379 | 上面代码中,`a`是一个数组,它有两个成员,也都是数组。将`a`作为 WeakSet 构造函数的参数,`a`的成员会自动成为 WeakSet 的成员。 380 | 381 | 注意,是`a`数组的成员成为 WeakSet 的成员,而不是`a`数组本身。这意味着,数组的成员只能是对象。 382 | 383 | ```js 384 | const b = [3, 4]; 385 | const ws = new WeakSet(b); 386 | // Uncaught TypeError: Invalid value used in weak set(…) 387 | ``` 388 | 389 | 上面代码中,数组`b`的成员不是对象,加入 WeakSet 就会报错。 390 | 391 | WeakSet 结构有以下三个方法。 392 | 393 | - **WeakSet.prototype.add(value)**:向 WeakSet 实例添加一个新成员。 394 | - **WeakSet.prototype.delete(value)**:清除 WeakSet 实例的指定成员。 395 | - **WeakSet.prototype.has(value)**:返回一个布尔值,表示某个值是否在 WeakSet 实例之中。 396 | 397 | 下面是一个例子。 398 | 399 | ```js 400 | const ws = new WeakSet(); 401 | const obj = {}; 402 | const foo = {}; 403 | 404 | ws.add(window); 405 | ws.add(obj); 406 | 407 | ws.has(window); // true 408 | ws.has(foo); // false 409 | 410 | ws.delete(window); 411 | ws.has(window); // false 412 | ``` 413 | 414 | **WeakSet 没有`size`属性,没有办法遍历它的成员。** 415 | 416 | ```js 417 | ws.size // undefined 418 | ws.forEach // undefined 419 | 420 | ws.forEach(function(item){ console.log('WeakSet has ' + item)}) 421 | // TypeError: undefined is not a function 422 | ``` 423 | 424 | 上面代码试图获取`size`和`forEach`属性,结果都不能成功。 425 | 426 | WeakSet 不能遍历,是因为成员都是弱引用,随时可能消失,遍历机制无法保证成员的存在,很可能刚刚遍历结束,成员就取不到了。WeakSet 的一个用处,是储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏。 427 | 428 | 下面是 WeakSet 的另一个例子。 429 | 430 | ```js 431 | const foos = new WeakSet() 432 | class Foo { 433 | constructor() { 434 | foos.add(this) 435 | } 436 | method () { 437 | if (!foos.has(this)) { 438 | throw new TypeError('Foo.prototype.method 只能在Foo的实例上调用!'); 439 | } 440 | } 441 | } 442 | ``` 443 | 444 | 上面代码保证了`Foo`的实例方法,只能在`Foo`的实例上调用。这里使用 WeakSet 的好处是,`foos`对实例的引用,不会被计入内存回收机制,所以删除实例的时候,不用考虑`foos`,也不会出现内存泄漏。 445 | 446 | -------------------------------------------------------------------------------- /docs/14. Map.md: -------------------------------------------------------------------------------- 1 | # Map 2 | 3 | 4 | 5 | ## 概念 6 | 7 | JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。 8 | 9 | ```js 10 | const data = {}; 11 | const element = document.getElementById('myDiv'); 12 | 13 | data[element] = 'metadata'; 14 | data['[object HTMLDivElement]'] // "metadata" 15 | ``` 16 | 17 | 上面代码原意是将一个 DOM 节点作为对象`data`的键,但是由于对象只接受字符串作为键名,所以`element`被自动转为字符串`[object HTMLDivElement]`。 18 | 19 | 为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。 20 | 21 | ```js 22 | const m = new Map(); 23 | const o = {p: 'Hello World'}; 24 | 25 | m.set(o, 'content') 26 | m.get(o) // "content" 27 | 28 | m.has(o) // true 29 | m.delete(o) // true 30 | m.has(o) // false 31 | ``` 32 | 33 | 上面代码使用 Map 结构的`set`方法,将对象`o`当作`m`的一个键,然后又使用`get`方法读取这个键,接着使用`delete`方法删除了这个键。 34 | 35 | 36 | 37 | ## 基本用法 38 | 39 | ### 构造函数 40 | 41 | **作为构造函数,Map 也可以接受一个数组作为参数。** 42 | 43 | **该数组的成员是一个个表示键值对的数组。** 44 | 45 | ```js 46 | const map = new Map([ 47 | ['name', '张三'], 48 | ['title', 'Author'] 49 | ]); 50 | 51 | map.size // 2 52 | map.has('name') // true 53 | map.get('name') // "张三" 54 | map.has('title') // true 55 | map.get('title') // "Author" 56 | ``` 57 | 58 | 上面代码在新建 Map 实例时,就指定了两个键`name`和`title`。 59 | 60 | **`Map`构造函数接受数组作为参数,实际上执行的是下面的算法。** 61 | 62 | ```js 63 | const items = [ 64 | ['name', '张三'], 65 | ['title', 'Author'] 66 | ]; 67 | 68 | const map = new Map(); 69 | 70 | items.forEach( 71 | ([key, value]) => map.set(key, value) 72 | ); 73 | ``` 74 | 75 | 事实上,不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构(详见《Iterator》一章)都可以当作`Map`构造函数的参数。这就是说,`Set`和`Map`都可以用来生成新的 Map。 76 | 77 | ```js 78 | const set = new Set([ 79 | ['foo', 1], 80 | ['bar', 2] 81 | ]); 82 | const m1 = new Map(set); 83 | m1.get('foo') // 1 84 | 85 | const m2 = new Map([['baz', 3]]); 86 | const m3 = new Map(m2); 87 | m3.get('baz') // 3 88 | ``` 89 | 90 | 上面代码中,我们分别使用 Set 对象和 Map 对象,当作`Map`构造函数的参数,结果都生成了新的 Map 对象。 91 | 92 | ### 注意点 93 | 94 | **如果对同一个键多次赋值,后面的值将覆盖前面的值。** 95 | 96 | ```js 97 | const map = new Map(); 98 | 99 | map 100 | .set(1, 'aaa') 101 | .set(1, 'bbb'); 102 | 103 | map.get(1) // "bbb" 104 | ``` 105 | 106 | 上面代码对键`1`连续赋值两次,后一次的值覆盖前一次的值。 107 | 108 | **如果读取一个未知的键,则返回`undefined`。** 109 | 110 | ```js 111 | new Map().get('asfddfsasadf') 112 | // undefined 113 | ``` 114 | 115 | 注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。 116 | 117 | ```js 118 | const map = new Map(); 119 | 120 | map.set(['a'], 555); 121 | map.get(['a']) // undefined 122 | ``` 123 | 124 | 上面代码的`set`和`get`方法,表面是针对同一个键,但实际上这是两个不同的数组实例,内存地址是不一样的,因此`get`方法无法读取该键,返回`undefined`。 125 | 126 | **同理,同样的值的两个实例,在 Map 结构中被视为两个键。** 127 | 128 | ```js 129 | const map = new Map(); 130 | 131 | const k1 = ['a']; 132 | const k2 = ['a']; 133 | 134 | map 135 | .set(k1, 111) 136 | .set(k2, 222); 137 | 138 | map.get(k1) // 111 139 | map.get(k2) // 222 140 | ``` 141 | 142 | 上面代码中,变量`k1`和`k2`的值是一样的,但是它们在 Map 结构中被视为两个键。 143 | 144 | ### 键的本质 145 | 146 | **由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。** 147 | 148 | 如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如`0`和`-0`就是一个键,布尔值`true`和字符串`true`则是两个不同的键。另外,`undefined`和`null`也是两个不同的键。虽然`NaN`不严格相等于自身,但 Map 将其视为同一个键。 149 | 150 | ```js 151 | let map = new Map(); 152 | 153 | map.set(-0, 123); 154 | map.get(+0) // 123 155 | 156 | map.set(true, 1); 157 | map.set('true', 2); 158 | map.get(true) // 1 159 | 160 | map.set(undefined, 3); 161 | map.set(null, 4); 162 | map.get(undefined) // 3 163 | 164 | map.set(NaN, 123); 165 | map.get(NaN) // 123 166 | ``` 167 | 168 | 169 | 170 | ## 实例的属性和操作方法 171 | 172 | Map 结构的实例有以下属性和操作方法。 173 | 174 | ### size 属性 175 | 176 | `size`属性返回 Map 结构的成员总数。 177 | 178 | ```js 179 | const map = new Map(); 180 | map.set('foo', true); 181 | map.set('bar', false); 182 | 183 | map.size // 2 184 | ``` 185 | 186 | ### Map.prototype.set(key, value) 187 | 188 | `set`方法设置键名`key`对应的键值为`value`,然后返回整个 Map 结构。如果`key`已经有值,则键值会被更新,否则就新生成该键。 189 | 190 | ```js 191 | const m = new Map(); 192 | 193 | m.set('edition', 6) // 键是字符串 194 | m.set(262, 'standard') // 键是数值 195 | m.set(undefined, 'nah') // 键是 undefined 196 | ``` 197 | 198 | **`set`方法返回的是当前的`Map`对象,因此可以采用链式写法。** 199 | 200 | ```js 201 | let map = new Map() 202 | .set(1, 'a') 203 | .set(2, 'b') 204 | .set(3, 'c'); 205 | ``` 206 | 207 | ### Map.prototype.get(key) 208 | 209 | `get`方法读取`key`对应的键值,如果找不到`key`,返回`undefined`。 210 | 211 | ```js 212 | const m = new Map(); 213 | 214 | const hello = function() {console.log('hello');}; 215 | m.set(hello, 'Hello ES6!') // 键是函数 216 | 217 | m.get(hello) // Hello ES6! 218 | ``` 219 | 220 | ### Map.prototype.has(key) 221 | 222 | `has`方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。 223 | 224 | ```js 225 | const m = new Map(); 226 | 227 | m.set('edition', 6); 228 | m.set(262, 'standard'); 229 | m.set(undefined, 'nah'); 230 | 231 | m.has('edition') // true 232 | m.has('years') // false 233 | m.has(262) // true 234 | m.has(undefined) // true 235 | ``` 236 | 237 | ### Map.prototype.delete(key) 238 | 239 | `delete`方法删除某个键,返回`true`。如果删除失败,返回`false`。 240 | 241 | ```js 242 | const m = new Map(); 243 | m.set(undefined, 'nah'); 244 | m.has(undefined) // true 245 | 246 | m.delete(undefined) 247 | m.has(undefined) // false 248 | ``` 249 | 250 | ### Map.prototype.clear() 251 | 252 | `clear`方法清除所有成员,没有返回值。 253 | 254 | ```js 255 | let map = new Map(); 256 | map.set('foo', true); 257 | map.set('bar', false); 258 | 259 | map.size // 2 260 | map.clear() 261 | map.size // 0 262 | ``` 263 | 264 | 265 | 266 | ## 遍历方法 267 | 268 | ### 概述 269 | 270 | Map 结构原生提供三个遍历器生成函数和一个遍历方法。 271 | 272 | - `Map.prototype.keys()`:返回键名的遍历器。 273 | - `Map.prototype.values()`:返回键值的遍历器。 274 | - `Map.prototype.entries()`:返回所有成员的遍历器。 275 | - `Map.prototype.forEach()`:遍历 Map 的所有成员。 276 | 277 | **需要特别注意的是,Map 的遍历顺序就是插入顺序。** 278 | 279 | ```js 280 | const map = new Map([ 281 | ['F', 'no'], 282 | ['T', 'yes'], 283 | ]); 284 | 285 | for (let key of map.keys()) { 286 | console.log(key); 287 | } 288 | // "F" 289 | // "T" 290 | 291 | for (let value of map.values()) { 292 | console.log(value); 293 | } 294 | // "no" 295 | // "yes" 296 | 297 | for (let item of map.entries()) { 298 | console.log(item[0], item[1]); 299 | } 300 | // "F" "no" 301 | // "T" "yes" 302 | 303 | // 或者 304 | for (let [key, value] of map.entries()) { 305 | console.log(key, value); 306 | } 307 | // "F" "no" 308 | // "T" "yes" 309 | 310 | // 等同于使用map.entries() 311 | for (let [key, value] of map) { 312 | console.log(key, value); 313 | } 314 | // "F" "no" 315 | // "T" "yes" 316 | ``` 317 | 318 | 上面代码最后的那个例子,表示 Map 结构的默认遍历器接口(`Symbol.iterator`属性),就是`entries`方法。 319 | 320 | ``` 321 | map[Symbol.iterator] === map.entries 322 | // true 323 | ``` 324 | 325 | 此外,Map 还有一个`forEach`方法,与数组的`forEach`方法类似,也可以实现遍历。 326 | 327 | ``` 328 | map.forEach(function(value, key, map) { 329 | console.log("Key: %s, Value: %s", key, value); 330 | }); 331 | ``` 332 | 333 | `forEach`方法还可以接受第二个参数,用来绑定`this`。 334 | 335 | ```js 336 | const reporter = { 337 | report: function(key, value) { 338 | console.log("Key: %s, Value: %s", key, value); 339 | } 340 | }; 341 | 342 | map.forEach(function(value, key, map) { 343 | this.report(key, value); 344 | }, reporter); 345 | ``` 346 | 347 | 上面代码中,`forEach`方法的回调函数的`this`,就指向`reporter`。 348 | 349 | 350 | 351 | ### 遍历的应用 352 | 353 | Map 结构转为数组结构,比较快速的方法是使用扩展运算符(`...`)。 354 | 355 | ```js 356 | const map = new Map([ 357 | [1, 'one'], 358 | [2, 'two'], 359 | [3, 'three'], 360 | ]); 361 | 362 | [...map.keys()] 363 | // [1, 2, 3] 364 | 365 | [...map.values()] 366 | // ['one', 'two', 'three'] 367 | 368 | [...map.entries()] 369 | // [[1,'one'], [2, 'two'], [3, 'three']] 370 | 371 | [...map] 372 | // [[1,'one'], [2, 'two'], [3, 'three']] 373 | ``` 374 | 375 | 结合数组的`map`方法、`filter`方法,可以实现 Map 的遍历和过滤(Map 本身没有`map`和`filter`方法)。 376 | 377 | ```js 378 | const map0 = new Map() 379 | .set(1, 'a') 380 | .set(2, 'b') 381 | .set(3, 'c'); 382 | 383 | const map1 = new Map( 384 | [...map0].filter(([k, v]) => k < 3) 385 | ); 386 | // 产生 Map 结构 {1 => 'a', 2 => 'b'} 387 | 388 | const map2 = new Map( 389 | [...map0].map(([k, v]) => [k * 2, '_' + v]) 390 | ); 391 | // 产生 Map 结构 {2 => '_a', 4 => '_b', 6 => '_c'} 392 | ``` 393 | 394 | 395 | 396 | ## 与其他数据结构的互相转换 397 | 398 | ### Map 转为数组 399 | 400 | 前面已经提过,Map 转为数组最方便的方法,就是使用扩展运算符(`...`)。 401 | 402 | ```js 403 | const myMap = new Map() 404 | .set(true, 7) 405 | .set({foo: 3}, ['abc']); 406 | [...myMap] 407 | // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ] 408 | ``` 409 | 410 | ### 数组 转为 Map 411 | 412 | 将数组传入 Map 构造函数,就可以转为 Map。 413 | 414 | ```js 415 | new Map([ 416 | [true, 7], 417 | [{foo: 3}, ['abc']] 418 | ]) 419 | // Map { 420 | // true => 7, 421 | // Object {foo: 3} => ['abc'] 422 | // } 423 | ``` 424 | 425 | ### Map 转为对象 426 | 427 | 如果所有 Map 的键都是字符串,它可以无损地转为对象。 428 | 429 | ```js 430 | function strMapToObj(strMap) { 431 | let obj = Object.create(null); 432 | for (let [k,v] of strMap) { 433 | obj[k] = v; 434 | } 435 | return obj; 436 | } 437 | 438 | const myMap = new Map() 439 | .set('yes', true) 440 | .set('no', false); 441 | strMapToObj(myMap) 442 | // { yes: true, no: false } 443 | ``` 444 | 445 | 如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。 446 | 447 | ### 对象转为 Map 448 | 449 | 对象转为 Map 可以通过`Object.entries()`。 450 | 451 | ```js 452 | let obj = {"a":1, "b":2}; 453 | let map = new Map(Object.entries(obj)); 454 | ``` 455 | 456 | 此外,也可以自己实现一个转换函数。 457 | 458 | ```js 459 | function objToStrMap(obj) { 460 | let strMap = new Map(); 461 | for (let k of Object.keys(obj)) { 462 | strMap.set(k, obj[k]); 463 | } 464 | return strMap; 465 | } 466 | 467 | objToStrMap({yes: true, no: false}) 468 | // Map {"yes" => true, "no" => false} 469 | ``` 470 | 471 | ### Map 转为 JSON 472 | 473 | Map 转为 JSON 要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON。 474 | 475 | ```js 476 | function strMapToJson(strMap) { 477 | return JSON.stringify(strMapToObj(strMap)); 478 | } 479 | 480 | let myMap = new Map().set('yes', true).set('no', false); 481 | strMapToJson(myMap) 482 | // '{"yes":true,"no":false}' 483 | ``` 484 | 485 | 另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。 486 | 487 | ```js 488 | function mapToArrayJson(map) { 489 | return JSON.stringify([...map]); 490 | } 491 | 492 | let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']); 493 | mapToArrayJson(myMap) 494 | // '[[true,7],[{"foo":3},["abc"]]]' 495 | ``` 496 | 497 | ### JSON 转为 Map 498 | 499 | JSON 转为 Map,正常情况下,所有键名都是字符串。 500 | 501 | ```js 502 | function jsonToStrMap(jsonStr) { 503 | return objToStrMap(JSON.parse(jsonStr)); 504 | } 505 | 506 | jsonToStrMap('{"yes": true, "no": false}') 507 | // Map {'yes' => true, 'no' => false} 508 | ``` 509 | 510 | 但是,有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 JSON 的逆操作。 511 | 512 | ```js 513 | function jsonToMap(jsonStr) { 514 | return new Map(JSON.parse(jsonStr)); 515 | } 516 | 517 | jsonToMap('[[true,7],[{"foo":3},["abc"]]]') 518 | // Map {true => 7, Object {foo: 3} => ['abc']} 519 | ``` 520 | 521 | 522 | 523 | ## WeakMap 524 | 525 | ### 含义 526 | 527 | `WeakMap`结构与`Map`结构类似,也是用于生成键值对的集合。 528 | 529 | ```js 530 | // WeakMap 可以使用 set 方法添加成员 531 | const wm1 = new WeakMap(); 532 | const key = {foo: 1}; 533 | wm1.set(key, 2); 534 | wm1.get(key) // 2 535 | 536 | // WeakMap 也可以接受一个数组, 537 | // 作为构造函数的参数 538 | const k1 = [1, 2, 3]; 539 | const k2 = [4, 5, 6]; 540 | const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]); 541 | wm2.get(k2) // "bar" 542 | ``` 543 | 544 | `WeakMap`与`Map`的区别有两点。 545 | 546 | 首先,`WeakMap`只接受对象作为键名(`null`除外),不接受其他类型的值作为键名。 547 | 548 | ```js 549 | const map = new WeakMap(); 550 | map.set(1, 2) 551 | // TypeError: 1 is not an object! 552 | map.set(Symbol(), 2) 553 | // TypeError: Invalid value used as weak map key 554 | map.set(null, 2) 555 | // TypeError: Invalid value used as weak map key 556 | ``` 557 | 558 | 上面代码中,如果将数值`1`和`Symbol`值作为 WeakMap 的键名,都会报错。 559 | 560 | 其次,`WeakMap`的键名所指向的对象,不计入垃圾回收机制。 561 | 562 | `WeakMap`的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。请看下面的例子。 563 | 564 | ```js 565 | const e1 = document.getElementById('foo'); 566 | const e2 = document.getElementById('bar'); 567 | const arr = [ 568 | [e1, 'foo 元素'], 569 | [e2, 'bar 元素'], 570 | ]; 571 | ``` 572 | 573 | 上面代码中,`e1`和`e2`是两个对象,我们通过`arr`数组对这两个对象添加一些文字说明。这就形成了`arr`对`e1`和`e2`的引用。 574 | 575 | 一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放`e1`和`e2`占用的内存。 576 | 577 | ```js 578 | // 不需要 e1 和 e2 的时候 579 | // 必须手动删除引用 580 | arr [0] = null; 581 | arr [1] = null; 582 | ``` 583 | 584 | 上面这样的写法显然很不方便。一旦忘了写,就会造成内存泄露。 585 | 586 | WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。 587 | 588 | 基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。一个典型应用场景是,在网页的 DOM 元素上添加数据,就可以使用`WeakMap`结构。当该 DOM 元素被清除,其所对应的`WeakMap`记录就会自动被移除。 589 | 590 | ```js 591 | const wm = new WeakMap(); 592 | 593 | const element = document.getElementById('example'); 594 | 595 | wm.set(element, 'some information'); 596 | wm.get(element) // "some information" 597 | ``` 598 | 599 | 上面代码中,先新建一个 Weakmap 实例。然后,将一个 DOM 节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在 WeakMap 里面。这时,WeakMap 里面对`element`的引用就是弱引用,不会被计入垃圾回收机制。 600 | 601 | 也就是说,上面的 DOM 节点对象的引用计数是`1`,而不是`2`。这时,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。Weakmap 保存的这个键值对,也会自动消失。 602 | 603 | 总之,`WeakMap`的专用场合就是,它的键所对应的对象,可能会在将来消失。`WeakMap`结构有助于防止内存泄漏。 604 | 605 | 注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。 606 | 607 | ```js 608 | const wm = new WeakMap(); 609 | let key = {}; 610 | let obj = {foo: 1}; 611 | 612 | wm.set(key, obj); 613 | obj = null; 614 | wm.get(key) 615 | // Object {foo: 1} 616 | ``` 617 | 618 | 上面代码中,键值`obj`是正常引用。所以,即使在 WeakMap 外部消除了`obj`的引用,WeakMap 内部的引用依然存在。 619 | 620 | ### WeakMap 的语法 621 | 622 | WeakMap 与 Map 在 API 上的区别主要是两个,一是没有遍历操作(即没有`keys()`、`values()`和`entries()`方法),也没有`size`属性。因为没有办法列出所有键名,某个键名是否存在完全不可预测,跟垃圾回收机制是否运行相关。这一刻可以取到键名,下一刻垃圾回收机制突然运行了,这个键名就没了,为了防止出现不确定性,就统一规定不能取到键名。二是无法清空,即不支持`clear`方法。因此,`WeakMap`只有四个方法可用:`get()`、`set()`、`has()`、`delete()`。 623 | 624 | ```js 625 | const wm = new WeakMap(); 626 | 627 | // size、forEach、clear 方法都不存在 628 | wm.size // undefined 629 | wm.forEach // undefined 630 | wm.clear // undefined 631 | ``` 632 | 633 | ### WeakMap 的示例 634 | 635 | WeakMap 的例子很难演示,因为无法观察它里面的引用会自动消失。此时,其他引用都解除了,已经没有引用指向 WeakMap 的键名了,导致无法证实那个键名是不是存在。 636 | 637 | 贺师俊老师[提示](https://github.com/ruanyf/es6tutorial/issues/362#issuecomment-292109104),如果引用所指向的值占用特别多的内存,就可以通过 Node 的`process.memoryUsage`方法看出来。根据这个思路,网友[vtxf](https://github.com/ruanyf/es6tutorial/issues/362#issuecomment-292451925)补充了下面的例子。 638 | 639 | 首先,打开 Node 命令行。 640 | 641 | ```bash 642 | $ node --expose-gc 643 | ``` 644 | 645 | 上面代码中,`--expose-gc`参数表示允许手动执行垃圾回收机制。 646 | 647 | 然后,执行下面的代码。 648 | 649 | ```js 650 | // 手动执行一次垃圾回收,保证获取的内存使用状态准确 651 | > global.gc(); 652 | undefined 653 | 654 | // 查看内存占用的初始状态,heapUsed 为 4M 左右 655 | > process.memoryUsage(); 656 | { rss: 21106688, 657 | heapTotal: 7376896, 658 | heapUsed: 4153936, 659 | external: 9059 } 660 | 661 | > let wm = new WeakMap(); 662 | undefined 663 | 664 | // 新建一个变量 key,指向一个 5*1024*1024 的数组 665 | > let key = new Array(5 * 1024 * 1024); 666 | undefined 667 | 668 | // 设置 WeakMap 实例的键名,也指向 key 数组 669 | // 这时,key 数组实际被引用了两次, 670 | // 变量 key 引用一次,WeakMap 的键名引用了第二次 671 | // 但是,WeakMap 是弱引用,对于引擎来说,引用计数还是1 672 | > wm.set(key, 1); 673 | WeakMap {} 674 | 675 | > global.gc(); 676 | undefined 677 | 678 | // 这时内存占用 heapUsed 增加到 45M 了 679 | > process.memoryUsage(); 680 | { rss: 67538944, 681 | heapTotal: 7376896, 682 | heapUsed: 45782816, 683 | external: 8945 } 684 | 685 | // 清除变量 key 对数组的引用, 686 | // 但没有手动清除 WeakMap 实例的键名对数组的引用 687 | > key = null; 688 | null 689 | 690 | // 再次执行垃圾回收 691 | > global.gc(); 692 | undefined 693 | 694 | // 内存占用 heapUsed 变回 4M 左右, 695 | // 可以看到 WeakMap 的键名引用没有阻止 gc 对内存的回收 696 | > process.memoryUsage(); 697 | { rss: 20639744, 698 | heapTotal: 8425472, 699 | heapUsed: 3979792, 700 | external: 8956 } 701 | ``` 702 | 703 | 上面代码中,只要外部的引用消失,WeakMap 内部的引用,就会自动被垃圾回收清除。由此可见,有了 WeakMap 的帮助,解决内存泄漏就会简单很多。 704 | 705 | Chrome 浏览器的 Dev Tools 的 Memory 面板,有一个垃圾桶的按钮,可以强制垃圾回收(garbage collect)。这个按钮也能用来观察 WeakMap 里面的引用是否消失。 706 | 707 | ### WeakMap 的用途 708 | 709 | 前文说过,WeakMap 应用的典型场合就是 DOM 节点作为键名。下面是一个例子。 710 | 711 | ```js 712 | let myWeakmap = new WeakMap(); 713 | 714 | myWeakmap.set( 715 | document.getElementById('logo'), 716 | {timesClicked: 0}) 717 | ; 718 | 719 | document.getElementById('logo').addEventListener('click', function() { 720 | let logoData = myWeakmap.get(document.getElementById('logo')); 721 | logoData.timesClicked++; 722 | }, false); 723 | ``` 724 | 725 | 上面代码中,`document.getElementById('logo')`是一个 DOM 节点,每当发生`click`事件,就更新一下状态。我们将这个状态作为键值放在 WeakMap 里,对应的键名就是这个节点对象。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险。 726 | 727 | WeakMap 的另一个用处是部署私有属性。 728 | 729 | ```js 730 | const _counter = new WeakMap(); 731 | const _action = new WeakMap(); 732 | 733 | class Countdown { 734 | constructor(counter, action) { 735 | _counter.set(this, counter); 736 | _action.set(this, action); 737 | } 738 | dec() { 739 | let counter = _counter.get(this); 740 | if (counter < 1) return; 741 | counter--; 742 | _counter.set(this, counter); 743 | if (counter === 0) { 744 | _action.get(this)(); 745 | } 746 | } 747 | } 748 | 749 | const c = new Countdown(2, () => console.log('DONE')); 750 | 751 | c.dec() 752 | c.dec() 753 | // DONE 754 | ``` 755 | 756 | 上面代码中,`Countdown`类的两个内部属性`_counter`和`_action`,是实例的弱引用,所以如果删除实例,它们也就随之消失,不会造成内存泄漏。 757 | 758 | -------------------------------------------------------------------------------- /docs/16. 异步遍历器.md: -------------------------------------------------------------------------------- 1 | # 异步遍历器 2 | 3 | 4 | 5 | ## 同步遍历器的问题 6 | 7 | Iterator 接口是一种数据遍历的协议,只要调用遍历器对象的`next`方法,就会得到一个对象,表示当前遍历指针所在的那个位置的信息。`next`方法返回的对象的结构是`{value, done}`,其中`value`表示当前的数据的值,`done`是一个布尔值,表示遍历是否结束。 8 | 9 | ```js 10 | function idMaker() { 11 | let index = 0; 12 | 13 | return { 14 | next: function() { 15 | return { value: index++, done: false }; 16 | } 17 | }; 18 | } 19 | 20 | const it = idMaker(); 21 | 22 | it.next().value // 0 23 | it.next().value // 1 24 | it.next().value // 2 25 | // ... 26 | ``` 27 | 28 | 上面代码中,变量`it`是一个遍历器(iterator)。每次调用`it.next()`方法,就返回一个对象,表示当前遍历位置的信息。 29 | 30 | **这里隐含着一个规定,`it.next()`方法必须是同步的,只要调用就必须立刻返回值。也就是说,一旦执行`it.next()`方法,就必须同步地得到`value`和`done`这两个属性。如果遍历指针正好指向同步操作,当然没有问题,但对于异步操作,就不太合适了。** 31 | 32 | ```js 33 | function idMaker() { 34 | let index = 0; 35 | 36 | return { 37 | next: function() { 38 | return new Promise(function (resolve, reject) { 39 | setTimeout(() => { 40 | resolve({ value: index++, done: false }); 41 | }, 1000); 42 | }); 43 | } 44 | }; 45 | } 46 | ``` 47 | 48 | **上面代码中,`next()`方法返回的是一个 Promise 对象,这样就不行,不符合 Iterator 协议,只要代码里面包含异步操作都不行。也就是说,Iterator 协议里面`next()`方法只能包含同步操作。** 49 | 50 | 目前的解决方法是,将异步操作包装成 Thunk 函数或者 Promise 对象,即`next()`方法返回值的`value`属性是一个 Thunk 函数或者 Promise 对象,等待以后返回真正的值,而`done`属性则还是同步产生的。 51 | 52 | ```js 53 | function idMaker() { 54 | let index = 0; 55 | 56 | return { 57 | next: function() { 58 | return { 59 | value: new Promise(resolve => setTimeout(() => resolve(index++), 1000)), 60 | done: false 61 | }; 62 | } 63 | }; 64 | } 65 | 66 | const it = idMaker(); 67 | 68 | it.next().value.then(o => console.log(o)) // 0 69 | it.next().value.then(o => console.log(o)) // 1 70 | it.next().value.then(o => console.log(o)) // 2 71 | // ... 72 | ``` 73 | 74 | 上面代码中,`value`属性的返回值是一个 Promise 对象,用来放置异步操作。但是这样写很麻烦,不太符合直觉,语义也比较绕。 75 | 76 | ES2018 [引入](https://github.com/tc39/proposal-async-iteration)了“异步遍历器”(Async Iterator),为异步操作提供原生的遍历器接口,即`value`和`done`这两个属性都是异步产生。 77 | 78 | 79 | 80 | ## 异步遍历的接口 81 | 82 | 异步遍历器的最大的语法特点,就是调用遍历器的`next`方法,返回的是一个 Promise 对象。 83 | 84 | ```js 85 | asyncIterator 86 | .next() 87 | .then( 88 | ({ value, done }) => /* ... */ 89 | ); 90 | ``` 91 | 92 | 上面代码中,`asyncIterator`是一个异步遍历器,调用`next`方法以后,返回一个 Promise 对象。因此,可以使用`then`方法指定,这个 Promise 对象的状态变为`resolve`以后的回调函数。回调函数的参数,则是一个具有`value`和`done`两个属性的对象,这个跟同步遍历器是一样的。 93 | 94 | 我们知道,一个对象的同步遍历器的接口,部署在`Symbol.iterator`属性上面。同样地,对象的异步遍历器接口,部署在`Symbol.asyncIterator`属性上面。不管是什么样的对象,只要它的`Symbol.asyncIterator`属性有值,就表示应该对它进行异步遍历。 95 | 96 | 下面是一个异步遍历器的例子。 97 | 98 | ```js 99 | const asyncIterable = createAsyncIterable(['a', 'b']); 100 | const asyncIterator = asyncIterable[Symbol.asyncIterator](); 101 | 102 | asyncIterator 103 | .next() 104 | .then(iterResult1 => { 105 | console.log(iterResult1); // { value: 'a', done: false } 106 | return asyncIterator.next(); 107 | }) 108 | .then(iterResult2 => { 109 | console.log(iterResult2); // { value: 'b', done: false } 110 | return asyncIterator.next(); 111 | }) 112 | .then(iterResult3 => { 113 | console.log(iterResult3); // { value: undefined, done: true } 114 | }); 115 | ``` 116 | 117 | 上面代码中,异步遍历器其实返回了两次值。第一次调用的时候,返回一个 Promise 对象;等到 Promise 对象`resolve`了,再返回一个表示当前数据成员信息的对象。这就是说,异步遍历器与同步遍历器最终行为是一致的,只是会先返回 Promise 对象,作为中介。 118 | 119 | 由于异步遍历器的`next`方法,返回的是一个 Promise 对象。因此,可以把它放在`await`命令后面。 120 | 121 | ```js 122 | async function f() { 123 | const asyncIterable = createAsyncIterable(['a', 'b']); 124 | const asyncIterator = asyncIterable[Symbol.asyncIterator](); 125 | console.log(await asyncIterator.next()); 126 | // { value: 'a', done: false } 127 | console.log(await asyncIterator.next()); 128 | // { value: 'b', done: false } 129 | console.log(await asyncIterator.next()); 130 | // { value: undefined, done: true } 131 | } 132 | ``` 133 | 134 | 上面代码中,`next`方法用`await`处理以后,就不必使用`then`方法了。整个流程已经很接近同步处理了。 135 | 136 | 注意,异步遍历器的`next`方法是可以连续调用的,不必等到上一步产生的 Promise 对象`resolve`以后再调用。这种情况下,`next`方法会累积起来,自动按照每一步的顺序运行下去。下面是一个例子,把所有的`next`方法放在`Promise.all`方法里面。 137 | 138 | ```js 139 | const asyncIterable = createAsyncIterable(['a', 'b']); 140 | const asyncIterator = asyncIterable[Symbol.asyncIterator](); 141 | const [{value: v1}, {value: v2}] = await Promise.all([ 142 | asyncIterator.next(), asyncIterator.next() 143 | ]); 144 | 145 | console.log(v1, v2); // a b 146 | ``` 147 | 148 | 另一种用法是一次性调用所有的`next`方法,然后`await`最后一步操作。 149 | 150 | ```js 151 | async function runner() { 152 | const writer = openFile('someFile.txt'); 153 | writer.next('hello'); 154 | writer.next('world'); 155 | await writer.return(); 156 | } 157 | 158 | runner(); 159 | ``` 160 | 161 | 162 | 163 | ## for await...of 164 | 165 | 前面介绍过,`for...of`循环用于遍历同步的 Iterator 接口。新引入的`for await...of`循环,则是用于遍历异步的 Iterator 接口。 166 | 167 | ```js 168 | async function f() { 169 | for await (const x of createAsyncIterable(['a', 'b'])) { 170 | console.log(x); 171 | } 172 | } 173 | // a 174 | // b 175 | ``` 176 | 177 | 上面代码中,`createAsyncIterable()`返回一个拥有异步遍历器接口的对象,`for...of`循环自动调用这个对象的异步遍历器的`next`方法,会得到一个 Promise 对象。`await`用来处理这个 Promise 对象,一旦`resolve`,就把得到的值(`x`)传入`for...of`的循环体。 178 | 179 | `for await...of`循环的一个用途,是部署了 asyncIterable 操作的异步接口,可以直接放入这个循环。 180 | 181 | ```js 182 | let body = ''; 183 | 184 | async function f() { 185 | for await(const data of req) body += data; 186 | const parsed = JSON.parse(body); 187 | console.log('got', parsed); 188 | } 189 | ``` 190 | 191 | 上面代码中,`req`是一个 asyncIterable 对象,用来异步读取数据。可以看到,使用`for await...of`循环以后,代码会非常简洁。 192 | 193 | 如果`next`方法返回的 Promise 对象被`reject`,`for await...of`就会报错,要用`try...catch`捕捉。 194 | 195 | ```js 196 | async function () { 197 | try { 198 | for await (const x of createRejectingIterable()) { 199 | console.log(x); 200 | } 201 | } catch (e) { 202 | console.error(e); 203 | } 204 | } 205 | ``` 206 | 207 | 注意,`for await...of`循环也可以用于同步遍历器。 208 | 209 | ```js 210 | (async function () { 211 | for await (const x of ['a', 'b']) { 212 | console.log(x); 213 | } 214 | })(); 215 | // a 216 | // b 217 | ``` 218 | 219 | Node v10 支持异步遍历器,Stream 就部署了这个接口。下面是读取文件的传统写法与异步遍历器写法的差异。 220 | 221 | ```js 222 | // 传统写法 223 | function main(inputFilePath) { 224 | const readStream = fs.createReadStream( 225 | inputFilePath, 226 | { encoding: 'utf8', highWaterMark: 1024 } 227 | ); 228 | readStream.on('data', (chunk) => { 229 | console.log('>>> '+chunk); 230 | }); 231 | readStream.on('end', () => { 232 | console.log('### DONE ###'); 233 | }); 234 | } 235 | 236 | // 异步遍历器写法 237 | async function main(inputFilePath) { 238 | const readStream = fs.createReadStream( 239 | inputFilePath, 240 | { encoding: 'utf8', highWaterMark: 1024 } 241 | ); 242 | 243 | for await (const chunk of readStream) { 244 | console.log('>>> '+chunk); 245 | } 246 | console.log('### DONE ###'); 247 | } 248 | ``` 249 | 250 | 251 | 252 | ## 异步 Generator 函数 253 | 254 | 就像 Generator 函数返回一个同步遍历器对象一样,异步 Generator 函数的作用,是返回一个异步遍历器对象。 255 | 256 | 在语法上,异步 Generator 函数就是`async`函数与 Generator 函数的结合。 257 | 258 | ```js 259 | async function* gen() { 260 | yield 'hello'; 261 | } 262 | const genObj = gen(); 263 | genObj.next().then(x => console.log(x)); 264 | // { value: 'hello', done: false } 265 | ``` 266 | 267 | 上面代码中,`gen`是一个异步 Generator 函数,执行后返回一个异步 Iterator 对象。对该对象调用`next`方法,返回一个 Promise 对象。 268 | 269 | 异步遍历器的设计目的之一,就是 Generator 函数处理同步操作和异步操作时,能够使用同一套接口。 270 | 271 | ```js 272 | // 同步 Generator 函数 273 | function* map(iterable, func) { 274 | const iter = iterable[Symbol.iterator](); 275 | while (true) { 276 | const {value, done} = iter.next(); 277 | if (done) break; 278 | yield func(value); 279 | } 280 | } 281 | 282 | // 异步 Generator 函数 283 | async function* map(iterable, func) { 284 | const iter = iterable[Symbol.asyncIterator](); 285 | while (true) { 286 | const {value, done} = await iter.next(); 287 | if (done) break; 288 | yield func(value); 289 | } 290 | } 291 | ``` 292 | 293 | 上面代码中,`map`是一个 Generator 函数,第一个参数是可遍历对象`iterable`,第二个参数是一个回调函数`func`。`map`的作用是将`iterable`每一步返回的值,使用`func`进行处理。上面有两个版本的`map`,前一个处理同步遍历器,后一个处理异步遍历器,可以看到两个版本的写法基本上是一致的。 294 | 295 | 下面是另一个异步 Generator 函数的例子。 296 | 297 | ```js 298 | async function* readLines(path) { 299 | let file = await fileOpen(path); 300 | 301 | try { 302 | while (!file.EOF) { 303 | yield await file.readLine(); 304 | } 305 | } finally { 306 | await file.close(); 307 | } 308 | } 309 | ``` 310 | 311 | 上面代码中,异步操作前面使用`await`关键字标明,即`await`后面的操作,应该返回 Promise 对象。凡是使用`yield`关键字的地方,就是`next`方法停下来的地方,它后面的表达式的值(即`await file.readLine()`的值),会作为`next()`返回对象的`value`属性,这一点是与同步 Generator 函数一致的。 312 | 313 | 异步 Generator 函数内部,能够同时使用`await`和`yield`命令。可以这样理解,`await`命令用于将外部操作产生的值输入函数内部,`yield`命令用于将函数内部的值输出。 314 | 315 | 上面代码定义的异步 Generator 函数的用法如下。 316 | 317 | ```js 318 | (async function () { 319 | for await (const line of readLines(filePath)) { 320 | console.log(line); 321 | } 322 | })() 323 | ``` 324 | 325 | 异步 Generator 函数可以与`for await...of`循环结合起来使用。 326 | 327 | ```js 328 | async function* prefixLines(asyncIterable) { 329 | for await (const line of asyncIterable) { 330 | yield '> ' + line; 331 | } 332 | } 333 | ``` 334 | 335 | 异步 Generator 函数的返回值是一个异步 Iterator,即每次调用它的`next`方法,会返回一个 Promise 对象,也就是说,跟在`yield`命令后面的,应该是一个 Promise 对象。如果像上面那个例子那样,`yield`命令后面是一个字符串,会被自动包装成一个 Promise 对象。 336 | 337 | ```js 338 | function fetchRandom() { 339 | const url = 'https://www.random.org/decimal-fractions/' 340 | + '?num=1&dec=10&col=1&format=plain&rnd=new'; 341 | return fetch(url); 342 | } 343 | 344 | async function* asyncGenerator() { 345 | console.log('Start'); 346 | const result = await fetchRandom(); // (A) 347 | yield 'Result: ' + await result.text(); // (B) 348 | console.log('Done'); 349 | } 350 | 351 | const ag = asyncGenerator(); 352 | ag.next().then(({value, done}) => { 353 | console.log(value); 354 | }) 355 | ``` 356 | 357 | 上面代码中,`ag`是`asyncGenerator`函数返回的异步遍历器对象。调用`ag.next()`以后,上面代码的执行顺序如下。 358 | 359 | 1. `ag.next()`立刻返回一个 Promise 对象。 360 | 2. `asyncGenerator`函数开始执行,打印出`Start`。 361 | 3. `await`命令返回一个 Promise 对象,`asyncGenerator`函数停在这里。 362 | 4. A 处变成 fulfilled 状态,产生的值放入`result`变量,`asyncGenerator`函数继续往下执行。 363 | 5. 函数在 B 处的`yield`暂停执行,一旦`yield`命令取到值,`ag.next()`返回的那个 Promise 对象变成 fulfilled 状态。 364 | 6. `ag.next()`后面的`then`方法指定的回调函数开始执行。该回调函数的参数是一个对象`{value, done}`,其中`value`的值是`yield`命令后面的那个表达式的值,`done`的值是`false`。 365 | 366 | A 和 B 两行的作用类似于下面的代码。 367 | 368 | ```js 369 | return new Promise((resolve, reject) => { 370 | fetchRandom() 371 | .then(result => result.text()) 372 | .then(result => { 373 | resolve({ 374 | value: 'Result: ' + result, 375 | done: false, 376 | }); 377 | }); 378 | }); 379 | ``` 380 | 381 | 如果异步 Generator 函数抛出错误,会导致 Promise 对象的状态变为`reject`,然后抛出的错误被`catch`方法捕获。 382 | 383 | ```js 384 | async function* asyncGenerator() { 385 | throw new Error('Problem!'); 386 | } 387 | 388 | asyncGenerator() 389 | .next() 390 | .catch(err => console.log(err)); // Error: Problem! 391 | ``` 392 | 393 | 注意,普通的 async 函数返回的是一个 Promise 对象,而异步 Generator 函数返回的是一个异步 Iterator 对象。可以这样理解,async 函数和异步 Generator 函数,是封装异步操作的两种方法,都用来达到同一种目的。区别在于,前者自带执行器,后者通过`for await...of`执行,或者自己编写执行器。下面就是一个异步 Generator 函数的执行器。 394 | 395 | ```js 396 | async function takeAsync(asyncIterable, count = Infinity) { 397 | const result = []; 398 | const iterator = asyncIterable[Symbol.asyncIterator](); 399 | while (result.length < count) { 400 | const {value, done} = await iterator.next(); 401 | if (done) break; 402 | result.push(value); 403 | } 404 | return result; 405 | } 406 | ``` 407 | 408 | 上面代码中,异步 Generator 函数产生的异步遍历器,会通过`while`循环自动执行,每当`await iterator.next()`完成,就会进入下一轮循环。一旦`done`属性变为`true`,就会跳出循环,异步遍历器执行结束。 409 | 410 | 下面是这个自动执行器的一个使用实例。 411 | 412 | ```js 413 | async function f() { 414 | async function* gen() { 415 | yield 'a'; 416 | yield 'b'; 417 | yield 'c'; 418 | } 419 | 420 | return await takeAsync(gen()); 421 | } 422 | 423 | f().then(function (result) { 424 | console.log(result); // ['a', 'b', 'c'] 425 | }) 426 | ``` 427 | 428 | 异步 Generator 函数出现以后,JavaScript 就有了四种函数形式:普通函数、async 函数、Generator 函数和异步 Generator 函数。请注意区分每种函数的不同之处。基本上,如果是一系列按照顺序执行的异步操作(比如读取文件,然后写入新内容,再存入硬盘),可以使用 async 函数;如果是一系列产生相同数据结构的异步操作(比如一行一行读取文件),可以使用异步 Generator 函数。 429 | 430 | 异步 Generator 函数也可以通过`next`方法的参数,接收外部传入的数据。 431 | 432 | ```js 433 | const writer = openFile('someFile.txt'); 434 | writer.next('hello'); // 立即执行 435 | writer.next('world'); // 立即执行 436 | await writer.return(); // 等待写入结束 437 | ``` 438 | 439 | 上面代码中,`openFile`是一个异步 Generator 函数。`next`方法的参数,向该函数内部的操作传入数据。每次`next`方法都是同步执行的,最后的`await`命令用于等待整个写入操作结束。 440 | 441 | 最后,同步的数据结构,也可以使用异步 Generator 函数。 442 | 443 | ```js 444 | async function* createAsyncIterable(syncIterable) { 445 | for (const elem of syncIterable) { 446 | yield elem; 447 | } 448 | } 449 | ``` 450 | 451 | 上面代码中,由于没有异步操作,所以也就没有使用`await`关键字。 452 | 453 | 454 | 455 | ## yield* 语句 456 | 457 | `yield*`语句也可以跟一个异步遍历器。 458 | 459 | ```js 460 | async function* gen1() { 461 | yield 'a'; 462 | yield 'b'; 463 | return 2; 464 | } 465 | 466 | async function* gen2() { 467 | // result 最终会等于 2 468 | const result = yield* gen1(); 469 | } 470 | ``` 471 | 472 | 上面代码中,`gen2`函数里面的`result`变量,最后的值是`2`。 473 | 474 | 与同步 Generator 函数一样,`for await...of`循环会展开`yield*`。 475 | 476 | ```js 477 | (async function () { 478 | for await (const x of gen2()) { 479 | console.log(x); 480 | } 481 | })(); 482 | // a 483 | // b 484 | ``` 485 | 486 | -------------------------------------------------------------------------------- /docs/18. Reflect.md: -------------------------------------------------------------------------------- 1 | # Reflect 2 | 3 | 4 | 5 | ## 设计目的 6 | 7 | `Reflect`对象与`Proxy`对象一样,也是 ES6 为了操作对象而提供的新 API。`Reflect`对象的设计目的有这样几个: 8 | 9 | (1) **将`Object`对象的一些明显属于语言内部的方法(比如`Object.defineProperty`),放到`Reflect`对象上。**现阶段,某些方法同时在`Object`和`Reflect`对象上部署,未来的新方法将只部署在`Reflect`对象上。也就是说,从`Reflect`对象上可以拿到语言内部的方法。 10 | 11 | (2) **修改某些`Object`方法的返回结果,让其变得更合理。**比如,`Object.defineProperty(obj, name, desc)`在无法定义属性时,会抛出一个错误,而`Reflect.defineProperty(obj, name, desc)`则会返回`false`。 12 | 13 | ```js 14 | // 老写法 15 | try { 16 | Object.defineProperty(target, property, attributes); 17 | // success 18 | } catch (e) { 19 | // failure 20 | } 21 | 22 | // 新写法 23 | if (Reflect.defineProperty(target, property, attributes)) { 24 | // success 25 | } else { 26 | // failure 27 | } 28 | ``` 29 | 30 | (3) **让`Object`操作都变成函数行为。**某些`Object`操作是命令式,比如`name in obj`和`delete obj[name]`,而`Reflect.has(obj, name)`和`Reflect.deleteProperty(obj, name)`让它们变成了函数行为。 31 | 32 | ``` 33 | // 老写法 34 | 'assign' in Object // true 35 | 36 | // 新写法 37 | Reflect.has(Object, 'assign') // true 38 | ``` 39 | 40 | (4)**`Reflect`对象的方法与`Proxy`对象的方法一一对应,只要是`Proxy`对象的方法,就能在`Reflect`对象上找到对应的方法。**这就让`Proxy`对象可以方便地调用对应的`Reflect`方法,完成默认行为,作为修改行为的基础。也就是说,不管`Proxy`怎么修改默认行为,你总可以在`Reflect`上获取默认行为。 41 | 42 | ```js 43 | Proxy(target, { 44 | set: function(target, name, value, receiver) { 45 | var success = Reflect.set(target, name, value, receiver); 46 | if (success) { 47 | console.log('property ' + name + ' on ' + target + ' set to ' + value); 48 | } 49 | return success; 50 | } 51 | }); 52 | ``` 53 | 54 | 上面代码中,`Proxy`方法拦截`target`对象的属性赋值行为。它采用`Reflect.set`方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能。 55 | 56 | 下面是另一个例子。 57 | 58 | ```js 59 | var loggedObj = new Proxy(obj, { 60 | get(target, name) { 61 | console.log('get', target, name); 62 | return Reflect.get(target, name); 63 | }, 64 | deleteProperty(target, name) { 65 | console.log('delete' + name); 66 | return Reflect.deleteProperty(target, name); 67 | }, 68 | has(target, name) { 69 | console.log('has' + name); 70 | return Reflect.has(target, name); 71 | } 72 | }); 73 | ``` 74 | 75 | 上面代码中,每一个`Proxy`对象的拦截操作(`get`、`delete`、`has`),内部都调用对应的`Reflect`方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。 76 | 77 | 有了`Reflect`对象以后,很多操作会更易读。 78 | 79 | ```js 80 | // 老写法 81 | Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1 82 | 83 | // 新写法 84 | Reflect.apply(Math.floor, undefined, [1.75]) // 1 85 | ``` 86 | 87 | ## 静态方法 88 | 89 | ### 概述 90 | 91 | `Reflect`对象一共有 13 个静态方法。 92 | 93 | - Reflect.apply(target, thisArg, args) 94 | - Reflect.construct(target, args) 95 | - Reflect.get(target, name, receiver) 96 | - Reflect.set(target, name, value, receiver) 97 | - Reflect.defineProperty(target, name, desc) 98 | - Reflect.deleteProperty(target, name) 99 | - Reflect.has(target, name) 100 | - Reflect.ownKeys(target) 101 | - Reflect.isExtensible(target) 102 | - Reflect.preventExtensions(target) 103 | - Reflect.getOwnPropertyDescriptor(target, name) 104 | - Reflect.getPrototypeOf(target) 105 | - Reflect.setPrototypeOf(target, prototype) 106 | 107 | 上面这些方法的作用,大部分与`Object`对象的同名方法的作用都是相同的,而且它与`Proxy`对象的方法是一一对应的。 108 | 109 | ### Reflect.get(target, name, receiver) 110 | 111 | `Reflect.get`方法查找并返回`target`对象的`name`属性,如果没有该属性,则返回`undefined`。 112 | 113 | ```js 114 | var myObject = { 115 | foo: 1, 116 | bar: 2, 117 | get baz() { 118 | return this.foo + this.bar; 119 | }, 120 | } 121 | 122 | Reflect.get(myObject, 'foo') // 1 123 | Reflect.get(myObject, 'bar') // 2 124 | Reflect.get(myObject, 'baz') // 3 125 | ``` 126 | 127 | 如果`name`属性部署了读取函数(getter),则读取函数的`this`绑定`receiver`。 128 | 129 | ```js 130 | var myObject = { 131 | foo: 1, 132 | bar: 2, 133 | get baz() { 134 | return this.foo + this.bar; 135 | }, 136 | }; 137 | 138 | var myReceiverObject = { 139 | foo: 4, 140 | bar: 4, 141 | }; 142 | 143 | Reflect.get(myObject, 'baz', myReceiverObject) // 8 144 | ``` 145 | 146 | 如果第一个参数不是对象,`Reflect.get`方法会报错。 147 | 148 | ```js 149 | Reflect.get(1, 'foo') // 报错 150 | Reflect.get(false, 'foo') // 报错 151 | ``` 152 | 153 | ### Reflect.set(target, name, value, receiver) 154 | 155 | `Reflect.set`方法设置`target`对象的`name`属性等于`value`。 156 | 157 | ```js 158 | var myObject = { 159 | foo: 1, 160 | set bar(value) { 161 | return this.foo = value; 162 | }, 163 | } 164 | 165 | myObject.foo // 1 166 | 167 | Reflect.set(myObject, 'foo', 2); 168 | myObject.foo // 2 169 | 170 | Reflect.set(myObject, 'bar', 3) 171 | myObject.foo // 3 172 | ``` 173 | 174 | 如果`name`属性设置了赋值函数,则赋值函数的`this`绑定`receiver`。 175 | 176 | ```js 177 | var myObject = { 178 | foo: 4, 179 | set bar(value) { 180 | return this.foo = value; 181 | }, 182 | }; 183 | 184 | var myReceiverObject = { 185 | foo: 0, 186 | }; 187 | 188 | Reflect.set(myObject, 'bar', 1, myReceiverObject); 189 | myObject.foo // 4 190 | myReceiverObject.foo // 1 191 | ``` 192 | 193 | 注意,如果 `Proxy`对象和 `Reflect`对象联合使用,前者拦截赋值操作,后者完成赋值的默认行为,而且传入了`receiver`,那么`Reflect.set`会触发`Proxy.defineProperty`拦截。 194 | 195 | ```js 196 | let p = { 197 | a: 'a' 198 | }; 199 | 200 | let handler = { 201 | set(target, key, value, receiver) { 202 | console.log('set'); 203 | Reflect.set(target, key, value, receiver) 204 | }, 205 | defineProperty(target, key, attribute) { 206 | console.log('defineProperty'); 207 | Reflect.defineProperty(target, key, attribute); 208 | } 209 | }; 210 | 211 | let obj = new Proxy(p, handler); 212 | obj.a = 'A'; 213 | // set 214 | // defineProperty 215 | ``` 216 | 217 | 上面代码中,`Proxy.set`拦截里面使用了`Reflect.set`,而且传入了`receiver`,导致触发`Proxy.defineProperty`拦截。这是因为`Proxy.set`的`receiver`参数总是指向当前的 `Proxy`实例(即上例的`obj`),而`Reflect.set`一旦传入`receiver`,就会将属性赋值到`receiver`上面(即`obj`),导致触发`defineProperty`拦截。如果`Reflect.set`没有传入`receiver`,那么就不会触发`defineProperty`拦截。 218 | 219 | ```js 220 | let p = { 221 | a: 'a' 222 | }; 223 | 224 | let handler = { 225 | set(target, key, value, receiver) { 226 | console.log('set'); 227 | Reflect.set(target, key, value) 228 | }, 229 | defineProperty(target, key, attribute) { 230 | console.log('defineProperty'); 231 | Reflect.defineProperty(target, key, attribute); 232 | } 233 | }; 234 | 235 | let obj = new Proxy(p, handler); 236 | obj.a = 'A'; 237 | // set 238 | ``` 239 | 240 | 如果第一个参数不是对象,`Reflect.set`会报错。 241 | 242 | ```js 243 | Reflect.set(1, 'foo', {}) // 报错 244 | Reflect.set(false, 'foo', {}) // 报错 245 | ``` 246 | 247 | ### Reflect.has(obj, name) 248 | 249 | `Reflect.has`方法对应`name in obj`里面的`in`运算符。 250 | 251 | ```js 252 | var myObject = { 253 | foo: 1, 254 | }; 255 | 256 | // 旧写法 257 | 'foo' in myObject // true 258 | 259 | // 新写法 260 | Reflect.has(myObject, 'foo') // true 261 | ``` 262 | 263 | 如果`Reflect.has()`方法的第一个参数不是对象,会报错。 264 | 265 | ### Reflect.deleteProperty(obj, name) 266 | 267 | `Reflect.deleteProperty`方法等同于`delete obj[name]`,用于删除对象的属性。 268 | 269 | ```js 270 | const myObj = { foo: 'bar' }; 271 | 272 | // 旧写法 273 | delete myObj.foo; 274 | 275 | // 新写法 276 | Reflect.deleteProperty(myObj, 'foo'); 277 | ``` 278 | 279 | 该方法返回一个布尔值。如果删除成功,或者被删除的属性不存在,返回`true`;删除失败,被删除的属性依然存在,返回`false`。 280 | 281 | 如果`Reflect.deleteProperty()`方法的第一个参数不是对象,会报错。 282 | 283 | ### Reflect.construct(target, args) 284 | 285 | `Reflect.construct`方法等同于`new target(...args)`,这提供了一种不使用`new`,来调用构造函数的方法。 286 | 287 | ```js 288 | function Greeting(name) { 289 | this.name = name; 290 | } 291 | 292 | // new 的写法 293 | const instance = new Greeting('张三'); 294 | 295 | // Reflect.construct 的写法 296 | const instance = Reflect.construct(Greeting, ['张三']); 297 | ``` 298 | 299 | 如果`Reflect.construct()`方法的第一个参数不是函数,会报错。 300 | 301 | ### Reflect.getPrototypeOf(obj) 302 | 303 | `Reflect.getPrototypeOf`方法用于读取对象的`__proto__`属性,对应`Object.getPrototypeOf(obj)`。 304 | 305 | ```js 306 | const myObj = new FancyThing(); 307 | 308 | // 旧写法 309 | Object.getPrototypeOf(myObj) === FancyThing.prototype; 310 | 311 | // 新写法 312 | Reflect.getPrototypeOf(myObj) === FancyThing.prototype; 313 | ``` 314 | 315 | `Reflect.getPrototypeOf`和`Object.getPrototypeOf`的一个区别是,如果参数不是对象,`Object.getPrototypeOf`会将这个参数转为对象,然后再运行,而`Reflect.getPrototypeOf`会报错。 316 | 317 | ```js 318 | Object.getPrototypeOf(1) // Number {[[PrimitiveValue]]: 0} 319 | Reflect.getPrototypeOf(1) // 报错 320 | ``` 321 | 322 | ### Reflect.setPrototypeOf(obj, newProto) 323 | 324 | `Reflect.setPrototypeOf`方法用于设置目标对象的原型(prototype),对应`Object.setPrototypeOf(obj, newProto)`方法。它返回一个布尔值,表示是否设置成功。 325 | 326 | ```js 327 | const myObj = {}; 328 | 329 | // 旧写法 330 | Object.setPrototypeOf(myObj, Array.prototype); 331 | 332 | // 新写法 333 | Reflect.setPrototypeOf(myObj, Array.prototype); 334 | 335 | myObj.length // 0 336 | ``` 337 | 338 | 如果无法设置目标对象的原型(比如,目标对象禁止扩展),`Reflect.setPrototypeOf`方法返回`false`。 339 | 340 | ```js 341 | Reflect.setPrototypeOf({}, null) 342 | // true 343 | Reflect.setPrototypeOf(Object.freeze({}), null) 344 | // false 345 | ``` 346 | 347 | 如果第一个参数不是对象,`Object.setPrototypeOf`会返回第一个参数本身,而`Reflect.setPrototypeOf`会报错。 348 | 349 | ```js 350 | Object.setPrototypeOf(1, {}) 351 | // 1 352 | 353 | Reflect.setPrototypeOf(1, {}) 354 | // TypeError: Reflect.setPrototypeOf called on non-object 355 | ``` 356 | 357 | 如果第一个参数是`undefined`或`null`,`Object.setPrototypeOf`和`Reflect.setPrototypeOf`都会报错。 358 | 359 | ```js 360 | Object.setPrototypeOf(null, {}) 361 | // TypeError: Object.setPrototypeOf called on null or undefined 362 | 363 | Reflect.setPrototypeOf(null, {}) 364 | // TypeError: Reflect.setPrototypeOf called on non-object 365 | ``` 366 | 367 | ### Reflect.apply(func, thisArg, args) 368 | 369 | `Reflect.apply`方法等同于`Function.prototype.apply.call(func, thisArg, args)`,用于绑定`this`对象后执行给定函数。 370 | 371 | 一般来说,如果要绑定一个函数的`this`对象,可以这样写`fn.apply(obj, args)`,但是如果函数定义了自己的`apply`方法,就只能写成`Function.prototype.apply.call(fn, obj, args)`,采用`Reflect`对象可以简化这种操作。 372 | 373 | ```js 374 | const ages = [11, 33, 12, 54, 18, 96]; 375 | 376 | // 旧写法 377 | const youngest = Math.min.apply(Math, ages); 378 | const oldest = Math.max.apply(Math, ages); 379 | const type = Object.prototype.toString.call(youngest); 380 | 381 | // 新写法 382 | const youngest = Reflect.apply(Math.min, Math, ages); 383 | const oldest = Reflect.apply(Math.max, Math, ages); 384 | const type = Reflect.apply(Object.prototype.toString, youngest, []); 385 | ``` 386 | 387 | ### Reflect.defineProperty(target, propertyKey, attributes) 388 | 389 | `Reflect.defineProperty`方法基本等同于`Object.defineProperty`,用来为对象定义属性。未来,后者会被逐渐废除,请从现在开始就使用`Reflect.defineProperty`代替它。 390 | 391 | ```js 392 | function MyDate() { 393 | /*…*/ 394 | } 395 | 396 | // 旧写法 397 | Object.defineProperty(MyDate, 'now', { 398 | value: () => Date.now() 399 | }); 400 | 401 | // 新写法 402 | Reflect.defineProperty(MyDate, 'now', { 403 | value: () => Date.now() 404 | }); 405 | ``` 406 | 407 | 如果`Reflect.defineProperty`的第一个参数不是对象,就会抛出错误,比如`Reflect.defineProperty(1, 'foo')`。 408 | 409 | 这个方法可以与`Proxy.defineProperty`配合使用。 410 | 411 | ```js 412 | const p = new Proxy({}, { 413 | defineProperty(target, prop, descriptor) { 414 | console.log(descriptor); 415 | return Reflect.defineProperty(target, prop, descriptor); 416 | } 417 | }); 418 | 419 | p.foo = 'bar'; 420 | // {value: "bar", writable: true, enumerable: true, configurable: true} 421 | 422 | p.foo // "bar" 423 | ``` 424 | 425 | 上面代码中,`Proxy.defineProperty`对属性赋值设置了拦截,然后使用`Reflect.defineProperty`完成了赋值。 426 | 427 | ### Reflect.getOwnPropertyDescriptor(target, propertyKey) 428 | 429 | `Reflect.getOwnPropertyDescriptor`基本等同于`Object.getOwnPropertyDescriptor`,用于得到指定属性的描述对象,将来会替代掉后者。 430 | 431 | ```js 432 | var myObject = {}; 433 | Object.defineProperty(myObject, 'hidden', { 434 | value: true, 435 | enumerable: false, 436 | }); 437 | 438 | // 旧写法 439 | var theDescriptor = Object.getOwnPropertyDescriptor(myObject, 'hidden'); 440 | 441 | // 新写法 442 | var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden'); 443 | ``` 444 | 445 | `Reflect.getOwnPropertyDescriptor`和`Object.getOwnPropertyDescriptor`的一个区别是,如果第一个参数不是对象,`Object.getOwnPropertyDescriptor(1, 'foo')`不报错,返回`undefined`,而`Reflect.getOwnPropertyDescriptor(1, 'foo')`会抛出错误,表示参数非法。 446 | 447 | ### Reflect.isExtensible (target) 448 | 449 | `Reflect.isExtensible`方法对应`Object.isExtensible`,返回一个布尔值,表示当前对象是否可扩展。 450 | 451 | ```js 452 | const myObject = {}; 453 | 454 | // 旧写法 455 | Object.isExtensible(myObject) // true 456 | 457 | // 新写法 458 | Reflect.isExtensible(myObject) // true 459 | ``` 460 | 461 | 如果参数不是对象,`Object.isExtensible`会返回`false`,因为非对象本来就是不可扩展的,而`Reflect.isExtensible`会报错。 462 | 463 | ```js 464 | Object.isExtensible(1) // false 465 | Reflect.isExtensible(1) // 报错 466 | ``` 467 | 468 | ### Reflect.preventExtensions(target) 469 | 470 | `Reflect.preventExtensions`对应`Object.preventExtensions`方法,用于让一个对象变为不可扩展。它返回一个布尔值,表示是否操作成功。 471 | 472 | ```js 473 | var myObject = {}; 474 | 475 | // 旧写法 476 | Object.preventExtensions(myObject) // Object {} 477 | 478 | // 新写法 479 | Reflect.preventExtensions(myObject) // true 480 | ``` 481 | 482 | 如果参数不是对象,`Object.preventExtensions`在 ES5 环境报错,在 ES6 环境返回传入的参数,而`Reflect.preventExtensions`会报错。 483 | 484 | ```js 485 | // ES5 环境 486 | Object.preventExtensions(1) // 报错 487 | 488 | // ES6 环境 489 | Object.preventExtensions(1) // 1 490 | 491 | // 新写法 492 | Reflect.preventExtensions(1) // 报错 493 | ``` 494 | 495 | ### Reflect.ownKeys (target) 496 | 497 | `Reflect.ownKeys`方法用于返回对象的所有属性,基本等同于`Object.getOwnPropertyNames`与`Object.getOwnPropertySymbols`之和。 498 | 499 | ```js 500 | var myObject = { 501 | foo: 1, 502 | bar: 2, 503 | [Symbol.for('baz')]: 3, 504 | [Symbol.for('bing')]: 4, 505 | }; 506 | 507 | // 旧写法 508 | Object.getOwnPropertyNames(myObject) 509 | // ['foo', 'bar'] 510 | 511 | Object.getOwnPropertySymbols(myObject) 512 | //[Symbol(baz), Symbol(bing)] 513 | 514 | // 新写法 515 | Reflect.ownKeys(myObject) 516 | // ['foo', 'bar', Symbol(baz), Symbol(bing)] 517 | ``` 518 | 519 | 如果`Reflect.ownKeys()`方法的第一个参数不是对象,会报错。 520 | 521 | -------------------------------------------------------------------------------- /docs/19. Module 模块化.md: -------------------------------------------------------------------------------- 1 | # Module 模块化 2 | 3 | 4 | 5 | ## 设计原因 6 | 7 | 历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的`require`、Python 的`import`,甚至就连 CSS 都有`@import`,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。 8 | 9 | 在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。 10 | 11 | **ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。**CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。 12 | 13 | ```js 14 | // CommonJS模块 15 | let { stat, exists, readfile } = require('fs'); 16 | 17 | // 等同于 18 | let _fs = require('fs'); 19 | let stat = _fs.stat; 20 | let exists = _fs.exists; 21 | let readfile = _fs.readfile; 22 | ``` 23 | 24 | 上面代码的实质是整体加载`fs`模块(即加载`fs`的所有方法),生成一个对象(`_fs`),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。 25 | 26 | ES6 模块不是对象,而是通过`export`命令显式指定输出的代码,再通过`import`命令输入。 27 | 28 | ```js 29 | // ES6模块 30 | import { stat, exists, readFile } from 'fs'; 31 | ``` 32 | 33 | 上面代码的实质是从`fs`模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。 34 | 35 | **由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。** 36 | 37 | 除了静态加载带来的各种好处,ES6 模块还有以下好处。 38 | 39 | - 不再需要`UMD`模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。 40 | - 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者`navigator`对象的属性。 41 | - 不再需要对象作为命名空间(比如`Math`对象),未来这些功能可以通过模块提供。 42 | 43 | 44 | 45 | ## 严格模式 46 | 47 | ES6 的模块自动采用严格模式,不管你有没有在模块头部加上`"use strict";`。 48 | 49 | 严格模式主要有以下限制。 50 | 51 | - 变量必须声明后再使用 52 | - 函数的参数不能有同名属性,否则报错 53 | - 不能使用`with`语句 54 | - 不能对只读属性赋值,否则报错 55 | - 不能使用前缀 0 表示八进制数,否则报错 56 | - 不能删除不可删除的属性,否则报错 57 | - 不能删除变量`delete prop`,会报错,只能删除属性`delete global[prop]` 58 | - `eval`不会在它的外层作用域引入变量 59 | - `eval`和`arguments`不能被重新赋值 60 | - `arguments`不会自动反映函数参数的变化 61 | - 不能使用`arguments.callee` 62 | - 不能使用`arguments.caller` 63 | - 禁止`this`指向全局对象 64 | - 不能使用`fn.caller`和`fn.arguments`获取函数调用的堆栈 65 | - 增加了保留字(比如`protected`、`static`和`interface`) 66 | 67 | 上面这些限制,模块都必须遵守。由于严格模式是 ES5 引入的,不属于 ES6,所以请参阅相关 ES5 书籍,本书不再详细介绍了。 68 | 69 | 其中,尤其需要注意`this`的限制。ES6 模块之中,顶层的`this`指向`undefined`,即不应该在顶层代码使用`this`。 70 | 71 | 72 | 73 | ## 基本命令 74 | 75 | 模块功能主要由两个命令构成:`export`和`import`。`export`命令用于规定模块的对外接口,`import`命令用于输入其他模块提供的功能。 76 | 77 | ### export 命令 78 | 79 | #### 可以导出的内容 80 | 81 | 一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。**如果你希望外部能够读取模块内部的某个变量,就必须使用`export`关键字输出该变量。下面是一个 JS 文件,里面使用`export`命令输出变量。** 82 | 83 | ```js 84 | // profile.js 85 | export var firstName = 'Michael'; 86 | export var lastName = 'Jackson'; 87 | export var year = 1958; 88 | ``` 89 | 90 | 上面代码是`profile.js`文件,保存了用户信息。ES6 将其视为一个模块,里面用`export`命令对外部输出了三个变量。 91 | 92 | `export`的写法,除了像上面这样,还有另外一种。 93 | 94 | ```js 95 | // profile.js 96 | var firstName = 'Michael'; 97 | var lastName = 'Jackson'; 98 | var year = 1958; 99 | 100 | export { firstName, lastName, year }; 101 | ``` 102 | 103 | 上面代码在`export`命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在`var`语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。 104 | 105 | **`export`命令除了输出变量,还可以输出函数或类(class)。** 106 | 107 | ```js 108 | export function multiply(x, y) { 109 | return x * y; 110 | }; 111 | ``` 112 | 113 | 上面代码对外输出一个函数`multiply`。 114 | 115 | #### 模块重命名 116 | 117 | **通常情况下,`export`输出的变量就是本来的名字,但是可以使用`as`关键字重命名。** 118 | 119 | ```js 120 | function v1() { ... } 121 | function v2() { ... } 122 | 123 | export { 124 | v1 as streamV1, 125 | v2 as streamV2, 126 | v2 as streamLatestVersion 127 | }; 128 | ``` 129 | 130 | 上面代码使用`as`关键字,重命名了函数`v1`和`v2`的对外接口。重命名后,`v2`可以用不同的名字输出两次。 131 | 132 | #### 注意点 133 | 134 | **需要特别注意的是,`export`命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。** 135 | 136 | ```js 137 | // 报错 138 | export 1; 139 | 140 | // 报错 141 | var m = 1; 142 | export m; 143 | ``` 144 | 145 | 上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出 1,第二种写法通过变量`m`,还是直接输出 1。`1`只是一个值,不是接口。正确的写法是下面这样。 146 | 147 | ```js 148 | // 写法一 149 | export var m = 1; 150 | 151 | // 写法二 152 | var m = 1; 153 | export {m}; 154 | 155 | // 写法三 156 | var n = 1; 157 | export {n as m}; 158 | ``` 159 | 160 | 上面三种写法都是正确的,规定了对外的接口`m`。其他脚本可以通过这个接口,取到值`1`。它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。 161 | 162 | 同样的,`function`和`class`的输出,也必须遵守这样的写法。 163 | 164 | ```js 165 | // 报错 166 | function f() {} 167 | export f; 168 | 169 | // 正确 170 | export function f() {}; 171 | 172 | // 正确 173 | function f() {} 174 | export {f}; 175 | ``` 176 | 177 | 另外,`export`语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。 178 | 179 | ```js 180 | export var foo = 'bar'; 181 | setTimeout(() => foo = 'baz', 500); 182 | ``` 183 | 184 | 上面代码输出变量`foo`,值为`bar`,500 毫秒之后变成`baz`。 185 | 186 | **这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新。** 187 | 188 | **最后,`export`命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的`import`命令也是如此。**这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。 189 | 190 | ```js 191 | function foo() { 192 | export default 'bar' // SyntaxError 193 | } 194 | foo() 195 | ``` 196 | 197 | 上面代码中,`export`语句放在函数之中,结果报错。 198 | 199 | ### import 命令 200 | 201 | #### 基本用法 202 | 203 | 使用`export`命令定义了模块的对外接口以后,其他 JS 文件就可以通过`import`命令加载这个模块。 204 | 205 | ```js 206 | // main.js 207 | import { firstName, lastName, year } from './profile.js'; 208 | 209 | function setName(element) { 210 | element.textContent = firstName + ' ' + lastName; 211 | } 212 | ``` 213 | 214 | 上面代码的`import`命令,用于加载`profile.js`文件,并从中输入变量。`import`命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(`profile.js`)对外接口的名称相同。 215 | 216 | 如果想为输入的变量重新取一个名字,`import`命令要使用`as`关键字,将输入的变量重命名。 217 | 218 | ```js 219 | import { lastName as surname } from './profile.js'; 220 | ``` 221 | 222 | #### 导入模块只读性 223 | 224 | **`import`命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。** 225 | 226 | ```js 227 | import {a} from './xxx.js' 228 | 229 | a = {}; // Syntax Error : 'a' is read-only; 230 | ``` 231 | 232 | 上面代码中,脚本加载了变量`a`,对其重新赋值就会报错,因为`a`是一个只读的接口。 233 | 234 | **但是,如果`a`是一个对象,改写`a`的属性是允许的。** 235 | 236 | ```js 237 | import {a} from './xxx.js' 238 | 239 | a.foo = 'hello'; // 合法操作 240 | ``` 241 | 242 | 上面代码中,`a`的属性可以成功改写,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,**建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。** 243 | 244 | `import`后面的`from`指定模块文件的位置,可以是相对路径,也可以是绝对路径。如果不带有路径,只是一个模块名,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。 245 | 246 | ```js 247 | import { myMethod } from 'util'; 248 | ``` 249 | 250 | 上面代码中,`util`是模块文件名,由于不带有路径,必须通过配置,告诉引擎怎么取到这个模块。 251 | 252 | #### 导入模块提升 253 | 254 | **注意,`import`命令具有提升效果,会提升到整个模块的头部,首先执行。** 255 | 256 | ```js 257 | foo(); 258 | 259 | import { foo } from 'my_module'; 260 | ``` 261 | 262 | 上面的代码不会报错,因为`import`的执行早于`foo`的调用。这种行为的本质是,**`import`命令是编译阶段执行的,在代码运行之前**。 263 | 264 | #### 注意点 265 | 266 | **由于`import`是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。** 267 | 268 | ```js 269 | // 报错 270 | import { 'f' + 'oo' } from 'my_module'; 271 | 272 | // 报错 273 | let module = 'my_module'; 274 | import { foo } from module; 275 | 276 | // 报错 277 | if (x === 1) { 278 | import { foo } from 'module1'; 279 | } else { 280 | import { foo } from 'module2'; 281 | } 282 | ``` 283 | 284 | 上面三种写法都会报错,因为它们用到了表达式、变量和`if`结构。在静态分析阶段,这些语法都是没法得到值的。 285 | 286 | #### 加载所有模块 287 | 288 | 最后,`import`语句会执行所加载的模块,因此可以有下面的写法。 289 | 290 | ```js 291 | import 'lodash'; 292 | ``` 293 | 294 | 上面代码仅仅执行`lodash`模块,但是不输入任何值。 295 | 296 | 如果多次重复执行同一句`import`语句,那么只会执行一次,而不会执行多次。 297 | 298 | ```js 299 | import 'lodash'; 300 | import 'lodash'; 301 | ``` 302 | 303 | 上面代码加载了两次`lodash`,但是只会执行一次。 304 | 305 | ```js 306 | import { foo } from 'my_module'; 307 | import { bar } from 'my_module'; 308 | 309 | // 等同于 310 | import { foo, bar } from 'my_module'; 311 | ``` 312 | 313 | 上面代码中,虽然`foo`和`bar`在两个语句中加载,但是它们对应的是同一个`my_module`模块。也就是说,`import`语句是 Singleton 模式。 314 | 315 | **目前阶段,通过 Babel 转码,CommonJS 模块的`require`命令和 ES6 模块的`import`命令,可以写在同一个模块里面,但是最好不要这样做。因为`import`在静态解析阶段执行,所以它是一个模块之中最早执行的。下面的代码可能不会得到预期结果。** 316 | 317 | ```js 318 | require('core-js/modules/es6.symbol'); 319 | require('core-js/modules/es6.promise'); 320 | import React from 'React'; 321 | ``` 322 | 323 | 324 | 325 | 326 | 327 | ## 跨模块常量 328 | 329 | 本书介绍`const`命令的时候说过,`const`声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法。 330 | 331 | ```js 332 | // constants.js 模块 333 | export const A = 1; 334 | export const B = 3; 335 | export const C = 4; 336 | 337 | // test1.js 模块 338 | import * as constants from './constants'; 339 | console.log(constants.A); // 1 340 | console.log(constants.B); // 3 341 | 342 | // test2.js 模块 343 | import {A, B} from './constants'; 344 | console.log(A); // 1 345 | console.log(B); // 3 346 | ``` 347 | 348 | 如果要使用的常量非常多,可以建一个专门的`constants`目录,将各种常量写在不同的文件里面,保存在该目录下。 349 | 350 | ```js 351 | // constants/db.js 352 | export const db = { 353 | url: 'http://my.couchdbserver.local:5984', 354 | admin_username: 'admin', 355 | admin_password: 'admin password' 356 | }; 357 | 358 | // constants/user.js 359 | export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator']; 360 | ``` 361 | 362 | 然后,将这些文件输出的常量,合并在`index.js`里面。 363 | 364 | ```js 365 | // constants/index.js 366 | export {db} from './db'; 367 | export {users} from './users'; 368 | ``` 369 | 370 | 使用的时候,直接加载`index.js`就可以了。 371 | 372 | ```js 373 | // script.js 374 | import {db, users} from './constants/index'; 375 | ``` 376 | 377 | 378 | 379 | ## 模块的整体加载 380 | 381 | **除了指定加载某个输出值,还可以使用整体加载,即用星号(`*`)指定一个对象,所有输出值都加载在这个对象上面。** 382 | 383 | 下面是一个`circle.js`文件,它输出两个方法`area`和`circumference`。 384 | 385 | ```js 386 | // circle.js 387 | 388 | export function area(radius) { 389 | return Math.PI * radius * radius; 390 | } 391 | 392 | export function circumference(radius) { 393 | return 2 * Math.PI * radius; 394 | } 395 | ``` 396 | 397 | 现在,加载这个模块。 398 | 399 | ```js 400 | // main.js 401 | 402 | import { area, circumference } from './circle'; 403 | 404 | console.log('圆面积:' + area(4)); 405 | console.log('圆周长:' + circumference(14)); 406 | ``` 407 | 408 | 上面写法是逐一指定要加载的方法,整体加载的写法如下。 409 | 410 | ```js 411 | import * as circle from './circle'; 412 | 413 | console.log('圆面积:' + circle.area(4)); 414 | console.log('圆周长:' + circle.circumference(14)); 415 | ``` 416 | 417 | 注意,模块整体加载所在的那个对象(上例是`circle`),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。 418 | 419 | ```js 420 | import * as circle from './circle'; 421 | 422 | // 下面两行都是不允许的 423 | circle.foo = 'hello'; 424 | circle.area = function () {}; 425 | ``` 426 | 427 | 428 | 429 | ## export default 430 | 431 | #### 基本使用 432 | 433 | 从前面的例子可以看出,使用`import`命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。 434 | 435 | 为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到`export default`命令,为模块指定默认输出。 436 | 437 | ```js 438 | // export-default.js 439 | export default function () { 440 | console.log('foo'); 441 | } 442 | ``` 443 | 444 | 上面代码是一个模块文件`export-default.js`,它的默认输出是一个函数。 445 | 446 | 其他模块加载该模块时,`import`命令可以为该匿名函数指定任意名字。 447 | 448 | ```js 449 | // import-default.js 450 | import customName from './export-default'; 451 | customName(); // 'foo' 452 | ``` 453 | 454 | 上面代码的`import`命令,可以用任意名称指向`export-default.js`输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时`import`命令后面,不使用大括号。 455 | 456 | `export default`命令用在非匿名函数前,也是可以的。 457 | 458 | ```js 459 | // export-default.js 460 | export default function foo() { 461 | console.log('foo'); 462 | } 463 | 464 | // 或者写成 465 | 466 | function foo() { 467 | console.log('foo'); 468 | } 469 | 470 | export default foo; 471 | ``` 472 | 473 | 上面代码中,`foo`函数的函数名`foo`,在模块外部是无效的。加载的时候,视同匿名函数加载。 474 | 475 | 下面比较一下默认输出和正常输出。 476 | 477 | ```js 478 | // 第一组 479 | export default function crc32() { // 输出 480 | // ... 481 | } 482 | 483 | import crc32 from 'crc32'; // 输入 484 | 485 | // 第二组 486 | export function crc32() { // 输出 487 | // ... 488 | }; 489 | 490 | import {crc32} from 'crc32'; // 输入 491 | ``` 492 | 493 | 上面代码的两组写法,第一组是使用`export default`时,对应的`import`语句不需要使用大括号;第二组是不使用`export default`时,对应的`import`语句需要使用大括号。 494 | 495 | #### 唯一性 496 | 497 | **`export default`命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此`export default`命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应`export default`命令。** 498 | 499 | #### 本质 500 | 501 | 本质上,`export default`就是输出一个叫做`default`的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。 502 | 503 | ```js 504 | // modules.js 505 | function add(x, y) { 506 | return x * y; 507 | } 508 | export {add as default}; 509 | // 等同于 510 | // export default add; 511 | 512 | // app.js 513 | import { default as foo } from 'modules'; 514 | // 等同于 515 | // import foo from 'modules'; 516 | ``` 517 | 518 | 正是因为`export default`命令其实只是输出一个叫做`default`的变量,所以它后面不能跟变量声明语句。 519 | 520 | ```js 521 | // 正确 522 | export var a = 1; 523 | 524 | // 正确 525 | var a = 1; 526 | export default a; 527 | 528 | // 错误 529 | export default var a = 1; 530 | ``` 531 | 532 | 上面代码中,`export default a`的含义是将变量`a`的值赋给变量`default`。所以,最后一种写法会报错。 533 | 534 | 同样地,因为`export default`命令的本质是将后面的值,赋给`default`变量,所以可以直接将一个值写在`export default`之后。 535 | 536 | ```js 537 | // 正确 538 | export default 42; 539 | 540 | // 报错 541 | export 42; 542 | ``` 543 | 544 | 上面代码中,后一句报错是因为没有指定对外的接口,而前一句指定对外接口为`default`。 545 | 546 | 有了`export default`命令,输入模块时就非常直观了,以输入 lodash 模块为例。 547 | 548 | ```js 549 | import _ from 'lodash'; 550 | ``` 551 | 552 | 如果想在一条`import`语句中,同时输入默认方法和其他接口,可以写成下面这样。 553 | 554 | ```js 555 | import _, { each, forEach } from 'lodash'; 556 | ``` 557 | 558 | 对应上面代码的`export`语句如下。 559 | 560 | ```js 561 | export default function (obj) { 562 | // ··· 563 | } 564 | 565 | export function each(obj, iterator, context) { 566 | // ··· 567 | } 568 | 569 | export { each as forEach }; 570 | ``` 571 | 572 | 上面代码的最后一行的意思是,暴露出`forEach`接口,默认指向`each`接口,即`forEach`和`each`指向同一个方法。 573 | 574 | `export default`也可以用来输出类。 575 | 576 | ```js 577 | // MyClass.js 578 | export default class { ... } 579 | 580 | // main.js 581 | import MyClass from 'MyClass'; 582 | let o = new MyClass(); 583 | ``` 584 | 585 | 586 | 587 | ## export 与 import 的复合写法 588 | 589 | 如果在一个模块之中,先输入后输出同一个模块,`import`语句可以与`export`语句写在一起。 590 | 591 | ```js 592 | export { foo, bar } from 'my_module'; 593 | 594 | // 可以简单理解为 595 | import { foo, bar } from 'my_module'; 596 | export { foo, bar }; 597 | ``` 598 | 599 | 上面代码中,`export`和`import`语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,`foo`和`bar`实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用`foo`和`bar`。 600 | 601 | 模块的接口改名和整体输出,也可以采用这种写法。 602 | 603 | ```js 604 | // 接口改名 605 | export { foo as myFoo } from 'my_module'; 606 | 607 | // 整体输出 608 | export * from 'my_module'; 609 | ``` 610 | 611 | 默认接口的写法如下。 612 | 613 | ```js 614 | export { default } from 'foo'; 615 | ``` 616 | 617 | 具名接口改为默认接口的写法如下。 618 | 619 | ```js 620 | export { es6 as default } from './someModule'; 621 | 622 | // 等同于 623 | import { es6 } from './someModule'; 624 | export default es6; 625 | ``` 626 | 627 | 同样地,默认接口也可以改名为具名接口。 628 | 629 | ```js 630 | export { default as es6 } from './someModule'; 631 | ``` 632 | 633 | ES2020 之前,有一种`import`语句,没有对应的复合写法。 634 | 635 | ```js 636 | import * as someIdentifier from "someModule"; 637 | ``` 638 | 639 | [ES2020](https://github.com/tc39/proposal-export-ns-from)补上了这个写法。 640 | 641 | ```js 642 | export * as ns from "mod"; 643 | 644 | // 等同于 645 | import * as ns from "mod"; 646 | export {ns}; 647 | ``` 648 | 649 | 650 | 651 | ## 模块的继承 652 | 653 | 模块之间也可以继承。 654 | 655 | 假设有一个`circleplus`模块,继承了`circle`模块。 656 | 657 | ```js 658 | // circleplus.js 659 | 660 | export * from 'circle'; 661 | export var e = 2.71828182846; 662 | export default function(x) { 663 | return Math.exp(x); 664 | } 665 | ``` 666 | 667 | 上面代码中的`export *`,表示再输出`circle`模块的所有属性和方法。注意,`export *`命令会忽略`circle`模块的`default`方法。然后,上面代码又输出了自定义的`e`变量和默认方法。 668 | 669 | 这时,也可以将`circle`的属性或方法,改名后再输出。 670 | 671 | ```js 672 | // circleplus.js 673 | 674 | export { area as circleArea } from 'circle'; 675 | ``` 676 | 677 | 上面代码表示,只输出`circle`模块的`area`方法,且将其改名为`circleArea`。 678 | 679 | 加载上面模块的写法如下。 680 | 681 | ```js 682 | // main.js 683 | 684 | import * as math from 'circleplus'; 685 | import exp from 'circleplus'; 686 | console.log(exp(math.e)); 687 | ``` 688 | 689 | 上面代码中的`import exp`表示,将`circleplus`模块的默认方法加载为`exp`方法。 690 | 691 | 692 | 693 | ## import() 694 | 695 | ### 简介 696 | 697 | 前面介绍过,`import`命令会被 JavaScript 引擎静态分析,先于模块内的其他语句执行(`import`命令叫做“连接” binding 其实更合适)。所以,下面的代码会报错。 698 | 699 | ```js 700 | // 报错 701 | if (x === 2) { 702 | import MyModual from './myModual'; 703 | } 704 | ``` 705 | 706 | 上面代码中,引擎处理`import`语句是在编译时,这时不会去分析或执行`if`语句,所以`import`语句放在`if`代码块之中毫无意义,因此会报句法错误,而不是执行时错误。也就是说,`import`和`export`命令只能在模块的顶层,不能在代码块之中(比如,在`if`代码块之中,或在函数之中)。 707 | 708 | 这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。在语法上,条件加载就不可能实现。如果`import`命令要取代 Node 的`require`方法,这就形成了一个障碍。因为`require`是运行时加载模块,`import`命令无法取代`require`的动态加载功能。 709 | 710 | ```js 711 | const path = './' + fileName; 712 | const myModual = require(path); 713 | ``` 714 | 715 | 上面的语句就是动态加载,`require`到底加载哪一个模块,只有运行时才知道。`import`命令做不到这一点。 716 | 717 | [ES2020提案](https://github.com/tc39/proposal-dynamic-import) 引入`import()`函数,支持动态加载模块。 718 | 719 | ```js 720 | import(specifier) 721 | ``` 722 | 723 | 上面代码中,`import`函数的参数`specifier`,指定所要加载的模块的位置。`import`命令能够接受什么参数,`import()`函数就能接受什么参数,两者区别主要是后者为动态加载。 724 | 725 | **`import()`返回一个 Promise 对象。下面是一个例子。** 726 | 727 | ```js 728 | const main = document.querySelector('main'); 729 | 730 | import(`./section-modules/${someVariable}.js`) 731 | .then(module => { 732 | module.loadPageInto(main); 733 | }) 734 | .catch(err => { 735 | main.textContent = err.message; 736 | }); 737 | ``` 738 | 739 | `import()`函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,`import()`函数与所加载的模块没有静态连接关系,这点也是与`import`语句不相同。`import()`类似于 Node 的`require`方法,区别主要是前者是异步加载,后者是同步加载。 740 | 741 | ### 适用场合 742 | 743 | 下面是`import()`的一些适用场合。 744 | 745 | **(1)按需加载** 746 | 747 | `import()`可以在需要的时候,再加载某个模块。 748 | 749 | ```js 750 | button.addEventListener('click', event => { 751 | import('./dialogBox.js') 752 | .then(dialogBox => { 753 | dialogBox.open(); 754 | }) 755 | .catch(error => { 756 | /* Error handling */ 757 | }) 758 | }); 759 | ``` 760 | 761 | 上面代码中,`import()`方法放在`click`事件的监听函数之中,只有用户点击了按钮,才会加载这个模块。 762 | 763 | **(2)条件加载** 764 | 765 | `import()`可以放在`if`代码块,根据不同的情况,加载不同的模块。 766 | 767 | ```js 768 | if (condition) { 769 | import('moduleA').then(...); 770 | } else { 771 | import('moduleB').then(...); 772 | } 773 | ``` 774 | 775 | 上面代码中,如果满足条件,就加载模块 A,否则加载模块 B。 776 | 777 | **(3)动态的模块路径** 778 | 779 | `import()`允许模块路径动态生成。 780 | 781 | ```js 782 | import(f()) 783 | .then(...); 784 | ``` 785 | 786 | 上面代码中,根据函数`f`的返回结果,加载不同的模块。 787 | 788 | ### 注意点 789 | 790 | `import()`加载模块成功以后,这个模块会作为一个对象,当作`then`方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。 791 | 792 | ```js 793 | import('./myModule.js') 794 | .then(({export1, export2}) => { 795 | // ...· 796 | }); 797 | ``` 798 | 799 | 上面代码中,`export1`和`export2`都是`myModule.js`的输出接口,可以解构获得。 800 | 801 | 如果模块有`default`输出接口,可以用参数直接获得。 802 | 803 | ```js 804 | import('./myModule.js') 805 | .then(myModule => { 806 | console.log(myModule.default); 807 | }); 808 | ``` 809 | 810 | 上面的代码也可以使用具名输入的形式。 811 | 812 | ```js 813 | import('./myModule.js') 814 | .then(({default: theDefault}) => { 815 | console.log(theDefault); 816 | }); 817 | ``` 818 | 819 | 如果想同时加载多个模块,可以采用下面的写法。 820 | 821 | ```js 822 | Promise.all([ 823 | import('./module1.js'), 824 | import('./module2.js'), 825 | import('./module3.js'), 826 | ]) 827 | .then(([module1, module2, module3]) => { 828 | ··· 829 | }); 830 | ``` 831 | 832 | `import()`也可以用在 async 函数之中。 833 | 834 | ```js 835 | async function main() { 836 | const myModule = await import('./myModule.js'); 837 | const {export1, export2} = await import('./myModule.js'); 838 | const [module1, module2, module3] = 839 | await Promise.all([ 840 | import('./module1.js'), 841 | import('./module2.js'), 842 | import('./module3.js'), 843 | ]); 844 | } 845 | main(); 846 | ``` 847 | 848 | -------------------------------------------------------------------------------- /docs/2. 基础重点概览.md: -------------------------------------------------------------------------------- 1 | # 基础关注点 2 | 3 | 4 | 5 | ## 几个概念 6 | 7 | 8 | 9 | ### 语句 10 | 11 | JavaScript 程序的执行单位为行(line),也就是一行一行地执行。一般情况下,每一行就是一个语句。 12 | 13 | **语句(statement)是为了完成某种任务而进行的操作,比如下面就是一行赋值语句。** 14 | 15 | ```js 16 | var a = 1 + 3; 17 | ``` 18 | 19 | 这条语句先用`var`命令,声明了变量`a`,然后将`1 + 3`的运算结果赋值给变量`a`。 20 | 21 | `1 + 3`叫做表达式(expression),指一个为了得到返回值的计算式。语句和表达式的区别在于,前者主要为了进行某种操作,一般情况下不需要返回值;后者则是为了得到返回值,一定会返回一个值。凡是 JavaScript 语言中预期为值的地方,都可以使用表达式。比如,赋值语句的等号右边,预期是一个值,因此可以放置各种表达式。 22 | 23 | 语句以分号结尾,一个分号就表示一个语句结束。多个语句可以写在一行内。 24 | 25 | ```js 26 | var a = 1 + 3 ; var b = 'abc'; 27 | ``` 28 | 29 | 分号前面可以没有任何内容,JavaScript 引擎将其视为空语句。 30 | 31 | ```js 32 | ;;; 33 | ``` 34 | 35 | 上面的代码就表示3个空语句。 36 | 37 | **表达式不需要分号结尾。一旦在表达式后面添加分号,则 JavaScript 引擎就将表达式视为语句,这样会产生一些没有任何意义的语句。** 38 | 39 | ```js 40 | 1 + 3; 41 | 'abc'; 42 | ``` 43 | 44 | 上面两行语句只是单纯地产生一个值,并没有任何实际的意义。 45 | 46 | 47 | 48 | ### 区块 49 | 50 | 如果循环和判断的代码体只有一行,JavaScript 允许该区块(block)省略大括号。 51 | 52 | ```js 53 | if (a) 54 | b(); 55 | c(); 56 | ``` 57 | 58 | 上面代码的原意可能是下面这样。 59 | 60 | ```js 61 | if (a) { 62 | b(); 63 | c(); 64 | } 65 | ``` 66 | 67 | 但是,实际效果却是下面这样。 68 | 69 | ```js 70 | if (a) { 71 | b(); 72 | } 73 | c(); 74 | ``` 75 | 76 | 因此,建议总是使用大括号表示区块。 77 | 78 | 另外,区块起首的大括号的位置,有许多不同的写法。最流行的有两种,一种是起首的大括号另起一行。 79 | 80 | ```js 81 | block 82 | { 83 | // ... 84 | } 85 | ``` 86 | 87 | 另一种是起首的大括号跟在关键字的后面。 88 | 89 | ```js 90 | block { 91 | // ... 92 | } 93 | ``` 94 | 95 | 一般来说,这两种写法都可以接受。但是,JavaScript 要使用后一种,因为 JavaScript 会自动添加句末的分号,导致一些难以察觉的错误。 96 | 97 | ```js 98 | return 99 | { 100 | key: value 101 | }; 102 | 103 | // 相当于 104 | return; 105 | { 106 | key: value 107 | }; 108 | ``` 109 | 110 | 上面的代码的原意,是要返回一个对象,但实际上返回的是`undefined`,因为 JavaScript 自动在`return`语句后面添加了分号。为了避免这一类错误,需要写成下面这样。 111 | 112 | ```js 113 | return { 114 | key : value 115 | }; 116 | ``` 117 | 118 | 因此,表示区块起首的大括号,不要另起一行。 119 | 120 | 121 | 122 | ### 圆括号 123 | 124 | 圆括号(parentheses)在 JavaScript 中有两种作用,一种表示函数的调用,另一种表示表达式的组合(grouping)。 125 | 126 | ```js 127 | // 圆括号表示函数的调用 128 | console.log('abc'); 129 | 130 | // 圆括号表示表达式的组合 131 | (1 + 2) * 3 132 | ``` 133 | 134 | 建议可以用空格,区分这两种不同的括号。 135 | 136 | > 1. 表示函数调用时,函数名与左括号之间没有空格。 137 | > 2. 表示函数定义时,函数名与左括号之间没有空格。 138 | > 3. 其他情况时,前面位置的语法元素与左括号之间,都有一个空格。 139 | 140 | 按照上面的规则,下面的写法都是不规范的。 141 | 142 | ```js 143 | foo (bar) 144 | return(a+b); 145 | if(a === 0) {...} 146 | function foo (b) {...} 147 | function(x) {...} 148 | ``` 149 | 150 | 上面代码的最后一行是一个匿名函数,`function`是语法关键字,不是函数名,所以与左括号之间应该要有一个空格。 151 | 152 | 圆括号(`()`)也可以用来提高运算的优先级,因为它的优先级是最高的,即圆括号中的表达式会第一个运算。 153 | 154 | ```js 155 | (4 + 5) * 6 // 54 156 | ``` 157 | 158 | 上面代码中,由于使用了圆括号,加法会先于乘法执行。 159 | 160 | 运算符的优先级别十分繁杂,且都是硬性规定,因此建议总是使用圆括号,保证运算顺序清晰可读,这对代码的维护和除错至关重要。 161 | 162 | 顺便说一下,圆括号不是运算符,而是一种语法结构。它一共有两种用法:一种是把表达式放在圆括号之中,提升运算的优先级;另一种是跟在函数的后面,作用是调用函数。 163 | 164 | 注意,因为圆括号不是运算符,所以不具有求值作用,只改变运算的优先级。 165 | 166 | ```js 167 | var x = 1; 168 | (x) = 2; 169 | ``` 170 | 171 | 上面代码的第二行,如果圆括号具有求值作用,那么就会变成`1 = 2`,这是会报错了。但是,上面的代码可以运行,这验证了圆括号只改变优先级,不会求值。 172 | 173 | 这也意味着,如果整个表达式都放在圆括号之中,那么不会有任何效果。 174 | 175 | ```js 176 | (expression) 177 | // 等同于 178 | expression 179 | ``` 180 | 181 | 函数放在圆括号中,会返回函数本身。如果圆括号紧跟在函数的后面,就表示调用函数。 182 | 183 | ```js 184 | function f() { 185 | return 1; 186 | } 187 | 188 | (f) // function f(){return 1;} 189 | f() // 1 190 | ``` 191 | 192 | 上面代码中,函数放在圆括号之中会返回函数本身,圆括号跟在函数后面则是调用函数。 193 | 194 | **圆括号之中,只能放置表达式,如果将语句放在圆括号之中,就会报错。** 195 | 196 | 197 | 198 | ### 行尾的分号 199 | 200 | 分号表示一条语句的结束。JavaScript 允许省略行尾的分号。事实上,确实有一些开发者行尾从来不写分号。但是,由于下面要讨论的原因,建议还是不要省略这个分号。 201 | 202 | #### 不使用分号的情况 203 | 204 | 首先,以下三种情况,语法规定本来就不需要在结尾添加分号。 205 | 206 | **(1)for 和 while 循环** 207 | 208 | ```js 209 | for ( ; ; ) { 210 | } // 没有分号 211 | 212 | while (true) { 213 | } // 没有分号 214 | ``` 215 | 216 | 注意,`do...while`循环是有分号的。 217 | 218 | ```js 219 | do { 220 | a--; 221 | } while(a > 0); // 分号不能省略 222 | ``` 223 | 224 | **(2)分支语句:if,switch,try** 225 | 226 | ```js 227 | if (true) { 228 | } // 没有分号 229 | 230 | switch () { 231 | } // 没有分号 232 | 233 | try { 234 | } catch { 235 | } // 没有分号 236 | ``` 237 | 238 | **(3)函数的声明语句** 239 | 240 | ```js 241 | function f() { 242 | } // 没有分号 243 | ``` 244 | 245 | 注意,函数表达式仍然要使用分号。 246 | 247 | ```js 248 | var f = function f() { 249 | }; 250 | ``` 251 | 252 | 以上三种情况,如果使用了分号,并不会出错。因为,解释引擎会把这个分号解释为空语句。 253 | 254 | #### 分号的自动添加 255 | 256 | 除了上一节的三种情况,所有语句都应该使用分号。但是,如果没有使用分号,大多数情况下,JavaScript 会自动添加。 257 | 258 | ```js 259 | var a = 1 260 | // 等同于 261 | var a = 1; 262 | ``` 263 | 264 | 这种语法特性被称为“分号的自动添加”(Automatic Semicolon Insertion,简称 ASI)。 265 | 266 | 因此,有人提倡省略句尾的分号。麻烦的是,如果下一行的开始可以与本行的结尾连在一起解释,JavaScript 就不会自动添加分号。 267 | 268 | ```js 269 | // 等同于 var a = 3 270 | var 271 | a 272 | = 273 | 3 274 | 275 | // 等同于 'abc'.length 276 | 'abc' 277 | .length 278 | 279 | // 等同于 return a + b; 280 | return a + 281 | b; 282 | 283 | // 等同于 obj.foo(arg1, arg2); 284 | obj.foo(arg1, 285 | arg2); 286 | 287 | // 等同于 3 * 2 + 10 * (27 / 6) 288 | 3 * 2 289 | + 290 | 10 * (27 / 6) 291 | ``` 292 | 293 | 上面代码都会多行放在一起解释,不会每一行自动添加分号。这些例子还是比较容易看出来的,但是下面这个例子就不那么容易看出来了。 294 | 295 | ```js 296 | x = y 297 | (function () { 298 | // ... 299 | })(); 300 | 301 | // 等同于 302 | x = y(function () {...})(); 303 | ``` 304 | 305 | 下面是更多不会自动添加分号的例子。 306 | 307 | ```js 308 | // 引擎解释为 c(d+e) 309 | var a = b + c 310 | (d+e).toString(); 311 | 312 | // 引擎解释为 a = b/hi/g.exec(c).map(d) 313 | // 正则表达式的斜杠,会当作除法运算符 314 | a = b 315 | /hi/g.exec(c).map(d); 316 | 317 | // 解释为'b'['red', 'green'], 318 | // 即把字符串当作一个数组,按索引取值 319 | var a = 'b' 320 | ['red', 'green'].forEach(function (c) { 321 | console.log(c); 322 | }) 323 | 324 | // 解释为 function (x) { return x }(a++) 325 | // 即调用匿名函数,结果f等于0 326 | var a = 0; 327 | var f = function (x) { return x } 328 | (a++) 329 | ``` 330 | 331 | 只有下一行的开始与本行的结尾,无法放在一起解释,JavaScript 引擎才会自动添加分号。 332 | 333 | ```js 334 | if (a < 0) a = 0 335 | console.log(a) 336 | 337 | // 等同于下面的代码, 338 | // 因为 0console 没有意义 339 | if (a < 0) a = 0; 340 | console.log(a) 341 | ``` 342 | 343 | 另外,如果一行的起首是“自增”(`++`)或“自减”(`--`)运算符,则它们的前面会自动添加分号。 344 | 345 | ```js 346 | a = b = c = 1 347 | 348 | a 349 | ++ 350 | b 351 | -- 352 | c 353 | 354 | console.log(a, b, c) 355 | // 1 2 0 356 | ``` 357 | 358 | 上面代码之所以会得到`1 2 0`的结果,原因是自增和自减运算符前,自动加上了分号。上面的代码实际上等同于下面的形式。 359 | 360 | ```js 361 | a = b = c = 1; 362 | a; 363 | ++b; 364 | --c; 365 | ``` 366 | 367 | 如果`continue`、`break`、`return`和`throw`这四个语句后面,直接跟换行符,则会自动添加分号。这意味着,如果`return`语句返回的是一个对象的字面量,起首的大括号一定要写在同一行,否则得不到预期结果。 368 | 369 | ```js 370 | return 371 | { first: 'Jane' }; 372 | 373 | // 解释成 374 | return; 375 | { first: 'Jane' }; 376 | ``` 377 | 378 | 由于解释引擎自动添加分号的行为难以预测,因此编写代码的时候不应该省略行尾的分号。 379 | 380 | 不应该省略结尾的分号,还有一个原因。有些 JavaScript 代码压缩器(uglifier)不会自动添加分号,因此遇到没有分号的结尾,就会让代码保持原状,而不是压缩成一行,使得压缩无法得到最优的结果。 381 | 382 | 另外,不写结尾的分号,可能会导致脚本合并出错。所以,有的代码库在第一行语句开始前,会加上一个分号。 383 | 384 | ```js 385 | ;var a = 1; 386 | // ... 387 | ``` 388 | 389 | 上面这种写法就可以避免与其他脚本合并时,排在前面的脚本最后一行语句没有分号,导致运行出错的问题。 390 | 391 | 392 | 393 | ## eval 命令 394 | 395 | ### 基本用法 396 | 397 | `eval`命令接受一个字符串作为参数,并将这个字符串当作语句执行。 398 | 399 | ```js 400 | eval('var a = 1;'); 401 | a // 1 402 | ``` 403 | 404 | 上面代码将字符串当作语句运行,生成了变量`a`。 405 | 406 | 如果参数字符串无法当作语句运行,那么就会报错。 407 | 408 | ```js 409 | eval('3x') // Uncaught SyntaxError: Invalid or unexpected token 410 | ``` 411 | 412 | 放在`eval`中的字符串,应该有独自存在的意义,不能用来与`eval`以外的命令配合使用。举例来说,下面的代码将会报错。 413 | 414 | ```js 415 | eval('return;'); // Uncaught SyntaxError: Illegal return statement 416 | ``` 417 | 418 | 上面代码会报错,因为`return`不能单独使用,必须在函数中使用。 419 | 420 | 如果`eval`的参数不是字符串,那么会原样返回。 421 | 422 | ```js 423 | eval(123) // 123 424 | ``` 425 | 426 | `eval`没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安全问题。 427 | 428 | ```js 429 | var a = 1; 430 | eval('a = 2'); 431 | 432 | a // 2 433 | ``` 434 | 435 | 上面代码中,`eval`命令修改了外部变量`a`的值。由于这个原因,`eval`有安全风险。 436 | 437 | 为了防止这种风险,JavaScript 规定,如果使用严格模式,`eval`内部声明的变量,不会影响到外部作用域。 438 | 439 | ```js 440 | (function f() { 441 | 'use strict'; 442 | eval('var foo = 123'); 443 | console.log(foo); // ReferenceError: foo is not defined 444 | })() 445 | ``` 446 | 447 | 上面代码中,函数`f`内部是严格模式,这时`eval`内部声明的`foo`变量,就不会影响到外部。 448 | 449 | 不过,即使在严格模式下,`eval`依然可以读写当前作用域的变量。 450 | 451 | ```js 452 | (function f() { 453 | 'use strict'; 454 | var foo = 1; 455 | eval('foo = 2'); 456 | console.log(foo); // 2 457 | })() 458 | ``` 459 | 460 | 上面代码中,严格模式下,`eval`内部还是改写了外部变量,可见安全风险依然存在。 461 | 462 | 总之,`eval`的本质是在当前作用域之中,注入代码。由于安全风险和不利于 JavaScript 引擎优化执行速度,所以一般不推荐使用。通常情况下,`eval`最常见的场合是解析 JSON 数据的字符串,不过正确的做法应该是使用原生的`JSON.parse`方法。 463 | 464 | ### eval 的别名调用 465 | 466 | 前面说过`eval`不利于引擎优化执行速度。更麻烦的是,还有下面这种情况,引擎在静态代码分析的阶段,根本无法分辨执行的是`eval`。 467 | 468 | ```js 469 | var m = eval; 470 | m('var x = 1'); 471 | x // 1 472 | ``` 473 | 474 | 上面代码中,变量`m`是`eval`的别名。静态代码分析阶段,引擎分辨不出`m('var x = 1')`执行的是`eval`命令。 475 | 476 | 为了保证`eval`的别名不影响代码优化,JavaScript 的标准规定,凡是使用别名执行`eval`,`eval`内部一律是全局作用域。 477 | 478 | ```js 479 | var a = 1; 480 | 481 | function f() { 482 | var a = 2; 483 | var e = eval; 484 | e('console.log(a)'); 485 | } 486 | 487 | f() // 1 488 | ``` 489 | 490 | 上面代码中,`eval`是别名调用,所以即使它是在函数中,它的作用域还是全局作用域,因此输出的`a`为全局变量。这样的话,引擎就能确认`e()`不会对当前的函数作用域产生影响,优化的时候就可以把这一行排除掉。 491 | 492 | `eval`的别名调用的形式五花八门,只要不是直接调用,都属于别名调用,因为引擎只能分辨`eval()`这一种形式是直接调用。 493 | 494 | ```js 495 | eval.call(null, '...') 496 | window.eval('...') 497 | (1, eval)('...') 498 | (eval, eval)('...') 499 | ``` 500 | 501 | 上面这些形式都是`eval`的别名调用,作用域都是全局作用域。 502 | 503 | 504 | 505 | ## 几种特殊运算符 506 | 507 | ### 对象相加, valueOf, toString() 508 | 509 | 如果运算子是对象,必须先转成原始类型的值,然后再相加。 510 | 511 | ```js 512 | var obj = { p: 1 }; 513 | obj + 2 // "[object Object]2" 514 | ``` 515 | 516 | 上面代码中,对象`obj`转成原始类型的值是`[object Object]`,再加`2`就得到了上面的结果。 517 | 518 | 对象转成原始类型的值,规则如下。 519 | 520 | 首先,自动调用对象的`valueOf`方法。 521 | 522 | ```js 523 | var obj = { p: 1 }; 524 | obj.valueOf() // { p: 1 } 525 | ``` 526 | 527 | 一般来说,对象的`valueOf`方法总是返回对象自身,这时再自动调用对象的`toString`方法,将其转为字符串。 528 | 529 | ```js 530 | var obj = { p: 1 }; 531 | obj.valueOf().toString() // "[object Object]" 532 | ``` 533 | 534 | 对象的`toString`方法默认返回`[object Object]`,所以就得到了最前面那个例子的结果。 535 | 536 | 知道了这个规则以后,就可以自己定义`valueOf`方法或`toString`方法,得到想要的结果。 537 | 538 | 539 | 540 | ### 三元条件表达式 541 | 542 | 通常来说,三元条件表达式与`if...else`语句具有同样表达效果,前者可以表达的,后者也能表达。但是两者具有一个重大差别,`if...else`是语句,没有返回值;三元条件表达式是表达式,具有返回值。所以,在需要返回值的场合,只能使用三元条件表达式,而不能使用`if..else`。 543 | 544 | 545 | 546 | ### void 运算符 547 | 548 | `void`运算符的作用是执行一个表达式,然后不返回任何值,或者说返回`undefined`。 549 | 550 | ```js 551 | void 0 // undefined 552 | void(0) // undefined 553 | void func() 554 | void(func()) 555 | ``` 556 | 557 | **上面是`void`运算符的两种写法,都正确。建议采用后一种形式,即总是使用圆括号。因为`void`运算符的优先性很高,如果不使用括号,容易造成错误的结果。**比如,`void 4 + 7`实际上等同于`(void 4) + 7`。 558 | 559 | 下面是`void`运算符的一个例子。 560 | 561 | ```js 562 | var x = 3; 563 | void (x = 5) //undefined 564 | x // 5 565 | ``` 566 | 567 | 这个运算符的主要用途是浏览器的书签工具(Bookmarklet),以及在超级链接中插入代码防止网页跳转。 568 | 569 | 请看下面的代码。 570 | 571 | ```js 572 | 577 | 点击 578 | ``` 579 | 580 | 上面代码中,点击链接后,会先执行`onclick`的代码,由于`onclick`返回`false`,所以浏览器不会跳转到 example.com。 581 | 582 | `void`运算符可以取代上面的写法。 583 | 584 | ```js 585 | 文字 586 | ``` 587 | 588 | 下面是一个更实际的例子,用户点击链接提交表单,但是不产生页面跳转。 589 | 590 | ```js 591 | 592 | 提交 593 | 594 | ``` 595 | 596 | 597 | 598 | ## 严格模式 599 | 600 | 除了正常的运行模式,JavaScript 还有第二种运行模式:严格模式(strict mode)。顾名思义,这种模式采用更加严格的 JavaScript 语法。 601 | 602 | 同样的代码,在正常模式和严格模式中,可能会有不一样的运行结果。一些在正常模式下可以运行的语句,在严格模式下将不能运行。 603 | 604 | ### 设计目的 605 | 606 | 早期的 JavaScript 语言有很多设计不合理的地方,但是为了兼容以前的代码,又不能改变老的语法,只能不断添加新的语法,引导程序员使用新语法。 607 | 608 | 严格模式是从 ES5 进入标准的,主要目的有以下几个。 609 | 610 | - 明确禁止一些不合理、不严谨的语法,减少 JavaScript 语言的一些怪异行为。 611 | - 增加更多报错的场合,消除代码运行的一些不安全之处,保证代码运行的安全。 612 | - 提高编译器效率,增加运行速度。 613 | - 为未来新版本的 JavaScript 语法做好铺垫。 614 | 615 | 总之,严格模式体现了 JavaScript 更合理、更安全、更严谨的发展方向。 616 | 617 | #### 启用方法 618 | 619 | 进入严格模式的标志,是一行字符串`use strict`。 620 | 621 | ```js 622 | 'use strict'; 623 | ``` 624 | 625 | 老版本的引擎会把它当作一行普通字符串,加以忽略。新版本的引擎就会进入严格模式。 626 | 627 | 严格模式可以用于整个脚本,也可以只用于单个函数。 628 | 629 | **(1) 整个脚本文件** 630 | 631 | `use strict`放在脚本文件的第一行,整个脚本都将以严格模式运行。如果这行语句不在第一行就无效,整个脚本会以正常模式运行。(严格地说,只要前面不是产生实际运行结果的语句,`use strict`可以不在第一行,比如直接跟在一个空的分号后面,或者跟在注释后面。) 632 | 633 | ```js 634 | 638 | 639 | 642 | ``` 643 | 644 | 上面代码中,一个网页文件依次有两段 JavaScript 代码。前一个` 653 | ``` 654 | 655 | **(2)单个函数** 656 | 657 | `use strict`放在函数体的第一行,则整个函数以严格模式运行。 658 | 659 | ```js 660 | function strict() { 661 | 'use strict'; 662 | return '这是严格模式'; 663 | } 664 | 665 | function strict2() { 666 | 'use strict'; 667 | function f() { 668 | return '这也是严格模式'; 669 | } 670 | return f(); 671 | } 672 | 673 | function notStrict() { 674 | return '这是正常模式'; 675 | } 676 | ``` 677 | 678 | 有时,需要把不同的脚本合并在一个文件里面。如果一个脚本是严格模式,另一个脚本不是,它们的合并就可能出错。严格模式的脚本在前,则合并后的脚本都是严格模式;如果正常模式的脚本在前,则合并后的脚本都是正常模式。这两种情况下,合并后的结果都是不正确的。这时可以考虑把整个脚本文件放在一个立即执行的匿名函数之中。 679 | 680 | ```js 681 | (function () { 682 | 'use strict'; 683 | // some code here 684 | })(); 685 | ``` 686 | 687 | ### 显式报错 688 | 689 | 严格模式使得 JavaScript 的语法变得更严格,更多的操作会显式报错。其中有些操作,在正常模式下只会默默地失败,不会报错。 690 | 691 | #### 只读属性不可写 692 | 693 | 严格模式下,设置字符串的`length`属性,会报错。 694 | 695 | ```js 696 | 'use strict'; 697 | 'abc'.length = 5; 698 | // TypeError: Cannot assign to read only property 'length' of string 'abc' 699 | ``` 700 | 701 | 上面代码报错,因为`length`是只读属性,严格模式下不可写。正常模式下,改变`length`属性是无效的,但不会报错。 702 | 703 | 严格模式下,对只读属性赋值,或者删除不可配置(non-configurable)属性都会报错。 704 | 705 | ```js 706 | // 对只读属性赋值会报错 707 | 'use strict'; 708 | Object.defineProperty({}, 'a', { 709 | value: 37, 710 | writable: false 711 | }); 712 | obj.a = 123; 713 | // TypeError: Cannot assign to read only property 'a' of object # 714 | 715 | // 删除不可配置的属性会报错 716 | 'use strict'; 717 | var obj = Object.defineProperty({}, 'p', { 718 | value: 1, 719 | configurable: false 720 | }); 721 | delete obj.p 722 | // TypeError: Cannot delete property 'p' of # 723 | ``` 724 | 725 | #### 只设置了取值器的属性不可写 726 | 727 | 严格模式下,对一个只有取值器(getter)、没有存值器(setter)的属性赋值,会报错。 728 | 729 | ```js 730 | 'use strict'; 731 | var obj = { 732 | get v() { return 1; } 733 | }; 734 | obj.v = 2; 735 | // Uncaught TypeError: Cannot set property v of # which has only a getter 736 | ``` 737 | 738 | 上面代码中,`obj.v`只有取值器,没有存值器,对它进行赋值就会报错。 739 | 740 | #### 禁止扩展的对象不可扩展 741 | 742 | 严格模式下,对禁止扩展的对象添加新属性,会报错。 743 | 744 | ```js 745 | 'use strict'; 746 | var obj = {}; 747 | Object.preventExtensions(obj); 748 | obj.v = 1; 749 | // Uncaught TypeError: Cannot add property v, object is not extensible 750 | ``` 751 | 752 | 上面代码中,`obj`对象禁止扩展,添加属性就会报错。 753 | 754 | #### eval、arguments 不可用作标识名 755 | 756 | 严格模式下,使用`eval`或者`arguments`作为标识名,将会报错。下面的语句都会报错。 757 | 758 | ```js 759 | 'use strict'; 760 | var eval = 17; 761 | var arguments = 17; 762 | var obj = { set p(arguments) { } }; 763 | try { } catch (arguments) { } 764 | function x(eval) { } 765 | function arguments() { } 766 | var y = function eval() { }; 767 | var f = new Function('arguments', "'use strict'; return 17;"); 768 | // SyntaxError: Unexpected eval or arguments in strict mode 769 | ``` 770 | 771 | #### 函数不能有重名的参数 772 | 773 | 正常模式下,如果函数有多个重名的参数,可以用`arguments[i]`读取。严格模式下,这属于语法错误。 774 | 775 | ```js 776 | function f(a, a, b) { 777 | 'use strict'; 778 | return a + b; 779 | } 780 | // Uncaught SyntaxError: Duplicate parameter name not allowed in this context 781 | ``` 782 | 783 | #### 禁止八进制的前缀0表示法 784 | 785 | 正常模式下,整数的第一位如果是`0`,表示这是八进制数,比如`0100`等于十进制的64。严格模式禁止这种表示法,整数第一位为`0`,将报错。 786 | 787 | ```js 788 | 'use strict'; 789 | var n = 0100; 790 | // Uncaught SyntaxError: Octal literals are not allowed in strict mode. 791 | ``` 792 | 793 | ### 增强的安全措施 794 | 795 | 严格模式增强了安全保护,从语法上防止了一些不小心会出现的错误。 796 | 797 | #### 全局变量显式声明 798 | 799 | 正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,全局变量必须显式声明。 800 | 801 | ```js 802 | 'use strict'; 803 | 804 | v = 1; // 报错,v未声明 805 | 806 | for (i = 0; i < 2; i++) { // 报错,i 未声明 807 | // ... 808 | } 809 | 810 | function f() { 811 | x = 123; 812 | } 813 | f() // 报错,未声明就创建一个全局变量 814 | ``` 815 | 816 | 因此,严格模式下,变量都必须先声明,然后再使用。 817 | 818 | #### 禁止 this 关键字指向全局对象 819 | 820 | 正常模式下,函数内部的`this`可能会指向全局对象,严格模式禁止这种用法,避免无意间创造全局变量。 821 | 822 | ```js 823 | // 正常模式 824 | function f() { 825 | console.log(this === window); 826 | } 827 | f() // true 828 | 829 | // 严格模式 830 | function f() { 831 | 'use strict'; 832 | console.log(this === undefined); 833 | } 834 | f() // true 835 | ``` 836 | 837 | 上面代码中,严格模式的函数体内部`this`是`undefined`。 838 | 839 | 这种限制对于构造函数尤其有用。使用构造函数时,有时忘了加`new`,这时`this`不再指向全局对象,而是报错。 840 | 841 | ```js 842 | function f() { 843 | 'use strict'; 844 | this.a = 1; 845 | }; 846 | 847 | f();// 报错,this 未定义 848 | ``` 849 | 850 | 严格模式下,函数直接调用时(不使用`new`调用),函数内部的`this`表示`undefined`(未定义),因此可以用`call`、`apply`和`bind`方法,将任意值绑定在`this`上面。正常模式下,`this`指向全局对象,如果绑定的值是非对象,将被自动转为对象再绑定上去,而`null`和`undefined`这两个无法转成对象的值,将被忽略。 851 | 852 | ```js 853 | // 正常模式 854 | function fun() { 855 | return this; 856 | } 857 | 858 | fun() // window 859 | fun.call(2) // Number {2} 860 | fun.call(true) // Boolean {true} 861 | fun.call(null) // window 862 | fun.call(undefined) // window 863 | 864 | // 严格模式 865 | 'use strict'; 866 | function fun() { 867 | return this; 868 | } 869 | 870 | fun() //undefined 871 | fun.call(2) // 2 872 | fun.call(true) // true 873 | fun.call(null) // null 874 | fun.call(undefined) // undefined 875 | ``` 876 | 877 | 上面代码中,可以把任意类型的值,绑定在`this`上面。 878 | 879 | #### 禁止使用 fn.callee、fn.caller 880 | 881 | 函数内部不得使用`fn.caller`、`fn.arguments`,否则会报错。这意味着不能在函数内部得到调用栈了。 882 | 883 | ```js 884 | function f1() { 885 | 'use strict'; 886 | f1.caller; // 报错 887 | f1.arguments; // 报错 888 | } 889 | 890 | f1(); 891 | ``` 892 | 893 | #### 禁止使用 arguments.callee、arguments.caller 894 | 895 | `arguments.callee`和`arguments.caller`是两个历史遗留的变量,从来没有标准化过,现在已经取消了。正常模式下调用它们没有什么作用,但是不会报错。严格模式明确规定,函数内部使用`arguments.callee`、`arguments.caller`将会报错。 896 | 897 | ```js 898 | 'use strict'; 899 | var f = function () { 900 | return arguments.callee; 901 | }; 902 | 903 | f(); // 报错 904 | ``` 905 | 906 | #### 禁止删除变量 907 | 908 | 严格模式下无法删除变量,如果使用`delete`命令删除一个变量,会报错。只有对象的属性,且属性的描述对象的`configurable`属性设置为`true`,才能被`delete`命令删除。 909 | 910 | ```js 911 | 'use strict'; 912 | var x; 913 | delete x; // 语法错误 914 | 915 | var obj = Object.create(null, { 916 | x: { 917 | value: 1, 918 | configurable: true 919 | } 920 | }); 921 | delete obj.x; // 删除成功 922 | ``` 923 | 924 | ### 静态绑定 925 | 926 | JavaScript 语言的一个特点,就是允许“动态绑定”,即某些属性和方法到底属于哪一个对象,不是在编译时确定的,而是在运行时(runtime)确定的。 927 | 928 | 严格模式对动态绑定做了一些限制。某些情况下,只允许静态绑定。也就是说,属性和方法到底归属哪个对象,必须在编译阶段就确定。这样做有利于编译效率的提高,也使得代码更容易阅读,更少出现意外。 929 | 930 | 具体来说,涉及以下几个方面。 931 | 932 | #### 禁止使用 with 语句 933 | 934 | 严格模式下,使用`with`语句将报错。因为`with`语句无法在编译时就确定,某个属性到底归属哪个对象,从而影响了编译效果。 935 | 936 | ```js 937 | 'use strict'; 938 | var v = 1; 939 | var obj = {}; 940 | 941 | with (obj) { 942 | v = 2; 943 | } 944 | // Uncaught SyntaxError: Strict mode code may not include a with statement 945 | ``` 946 | 947 | #### 创设 eval 作用域 948 | 949 | 正常模式下,JavaScript 语言有两种变量作用域(scope):全局作用域和函数作用域。严格模式创设了第三种作用域:`eval`作用域。 950 | 951 | 正常模式下,`eval`语句的作用域,取决于它处于全局作用域,还是函数作用域。严格模式下,`eval`语句本身就是一个作用域,不再能够在其所运行的作用域创设新的变量了,也就是说,`eval`所生成的变量只能用于`eval`内部。 952 | 953 | ```js 954 | (function () { 955 | 'use strict'; 956 | var x = 2; 957 | console.log(eval('var x = 5; x')) // 5 958 | console.log(x) // 2 959 | })() 960 | ``` 961 | 962 | 上面代码中,由于`eval`语句内部是一个独立作用域,所以内部的变量`x`不会泄露到外部。 963 | 964 | 注意,如果希望`eval`语句也使用严格模式,有两种方式。 965 | 966 | ```js 967 | // 方式一 968 | function f1(str){ 969 | 'use strict'; 970 | return eval(str); 971 | } 972 | f1('undeclared_variable = 1'); // 报错 973 | 974 | // 方式二 975 | function f2(str){ 976 | return eval(str); 977 | } 978 | f2('"use strict";undeclared_variable = 1') // 报错 979 | ``` 980 | 981 | 上面两种写法,`eval`内部使用的都是严格模式。 982 | 983 | #### arguments 不再追踪参数的变化 984 | 985 | 变量`arguments`代表函数的参数。严格模式下,函数内部改变参数与`arguments`的联系被切断了,两者不再存在联动关系。 986 | 987 | ```js 988 | function f(a) { 989 | a = 2; 990 | return [a, arguments[0]]; 991 | } 992 | f(1); // 正常模式为[2, 2] 993 | 994 | function f(a) { 995 | 'use strict'; 996 | a = 2; 997 | return [a, arguments[0]]; 998 | } 999 | f(1); // 严格模式为[2, 1] 1000 | ``` 1001 | 1002 | 上面代码中,改变函数的参数,不会反应到`arguments`对象上来。 1003 | 1004 | ### 向下一个版本的 JavaScript 过渡 1005 | 1006 | JavaScript 语言的下一个版本是 ECMAScript 6,为了平稳过渡,严格模式引入了一些 ES6 语法。 1007 | 1008 | #### 非函数代码块不得声明函数 1009 | 1010 | ES6 会引入块级作用域。为了与新版本接轨,ES5 的严格模式只允许在全局作用域或函数作用域声明函数。也就是说,不允许在非函数的代码块内声明函数。 1011 | 1012 | ```js 1013 | 'use strict'; 1014 | if (true) { 1015 | function f1() { } // 语法错误 1016 | } 1017 | 1018 | for (var i = 0; i < 5; i++) { 1019 | function f2() { } // 语法错误 1020 | } 1021 | ``` 1022 | 1023 | 上面代码在`if`代码块和`for`代码块中声明了函数,ES5 环境会报错。 1024 | 1025 | 注意,如果是 ES6 环境,上面的代码不会报错,因为 ES6 允许在代码块之中声明函数。 1026 | 1027 | #### 保留字 1028 | 1029 | 为了向将来 JavaScript 的新版本过渡,严格模式新增了一些保留字(implements、interface、let、package、private、protected、public、static、yield等)。使用这些词作为变量名将会报错。 1030 | 1031 | ```js 1032 | function package(protected) { // 语法错误 1033 | 'use strict'; 1034 | var implements; // 语法错误 1035 | } 1036 | ``` 1037 | 1038 | -------------------------------------------------------------------------------- /docs/20. 模块加载的实现.md: -------------------------------------------------------------------------------- 1 | # 模块加载的实现 2 | 3 | 4 | 5 | ## ESM VS CJS 6 | 7 | 它们有三个重大差异。 8 | 9 | - CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。 10 | - CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。 11 | - CommonJS 模块的`require()`是同步加载模块,ES6 模块的`import`命令是异步加载,有一个独立的模块依赖的解析阶段。 12 | 13 | 第二个差异是因为 CommonJS 加载的是一个对象(即`module.exports`属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。 14 | 15 | 下面重点解释第一个差异。 16 | 17 | CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件`lib.js`的例子。 18 | 19 | ```js 20 | // lib.js 21 | var counter = 3; 22 | function incCounter() { 23 | counter++; 24 | } 25 | module.exports = { 26 | counter: counter, 27 | incCounter: incCounter, 28 | }; 29 | ``` 30 | 31 | 上面代码输出内部变量`counter`和改写这个变量的内部方法`incCounter`。然后,在`main.js`里面加载这个模块。 32 | 33 | ```js 34 | // main.js 35 | var mod = require('./lib'); 36 | 37 | console.log(mod.counter); // 3 38 | mod.incCounter(); 39 | console.log(mod.counter); // 3 40 | ``` 41 | 42 | 上面代码说明,`lib.js`模块加载以后,它的内部变化就影响不到输出的`mod.counter`了。这是因为`mod.counter`是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。 43 | 44 | ```js 45 | // lib.js 46 | var counter = 3; 47 | function incCounter() { 48 | counter++; 49 | } 50 | module.exports = { 51 | get counter() { 52 | return counter 53 | }, 54 | incCounter: incCounter, 55 | }; 56 | ``` 57 | 58 | 上面代码中,输出的`counter`属性实际上是一个取值器函数。现在再执行`main.js`,就可以正确读取内部变量`counter`的变动了。 59 | 60 | ```bash 61 | $ node main.js 62 | 3 63 | 4 64 | ``` 65 | 66 | ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令`import`,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的`import`有点像 Unix 系统的“符号连接”,原始值变了,`import`加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。 67 | 68 | 还是举上面的例子。 69 | 70 | ```js 71 | // lib.js 72 | export let counter = 3; 73 | export function incCounter() { 74 | counter++; 75 | } 76 | 77 | // main.js 78 | import { counter, incCounter } from './lib'; 79 | console.log(counter); // 3 80 | incCounter(); 81 | console.log(counter); // 4 82 | ``` 83 | 84 | 上面代码说明,ES6 模块输入的变量`counter`是活的,完全反应其所在模块`lib.js`内部的变化。 85 | 86 | 再举一个出现在`export`一节中的例子。 87 | 88 | ```js 89 | // m1.js 90 | export var foo = 'bar'; 91 | setTimeout(() => foo = 'baz', 500); 92 | 93 | // m2.js 94 | import {foo} from './m1.js'; 95 | console.log(foo); 96 | setTimeout(() => console.log(foo), 500); 97 | ``` 98 | 99 | 上面代码中,`m1.js`的变量`foo`,在刚加载时等于`bar`,过了 500 毫秒,又变为等于`baz`。 100 | 101 | 让我们看看,`m2.js`能否正确读取这个变化。 102 | 103 | ```bash 104 | $ babel-node m2.js 105 | 106 | bar 107 | baz 108 | ``` 109 | 110 | 上面代码表明,ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。 111 | 112 | 由于 ES6 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。 113 | 114 | ```js 115 | // lib.js 116 | export let obj = {}; 117 | 118 | // main.js 119 | import { obj } from './lib'; 120 | 121 | obj.prop = 123; // OK 122 | obj = {}; // TypeError 123 | ``` 124 | 125 | 上面代码中,`main.js`从`lib.js`输入变量`obj`,可以对`obj`添加属性,但是重新赋值就会报错。因为变量`obj`指向的地址是只读的,不能重新赋值,这就好比`main.js`创造了一个名为`obj`的`const`变量。 126 | 127 | **最后,`export`通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。** 128 | 129 | ```js 130 | // mod.js 131 | function C() { 132 | this.sum = 0; 133 | this.add = function () { 134 | this.sum += 1; 135 | }; 136 | this.show = function () { 137 | console.log(this.sum); 138 | }; 139 | } 140 | 141 | export let c = new C(); 142 | ``` 143 | 144 | 上面的脚本`mod.js`,输出的是一个`C`的实例。不同的脚本加载这个模块,得到的都是同一个实例。 145 | 146 | ```js 147 | // x.js 148 | import {c} from './mod'; 149 | c.add(); 150 | 151 | // y.js 152 | import {c} from './mod'; 153 | c.show(); 154 | 155 | // main.js 156 | import './x'; 157 | import './y'; 158 | ``` 159 | 160 | 现在执行`main.js`,输出的是`1`。 161 | 162 | ```bash 163 | $ babel-node main.js 164 | 1 165 | ``` 166 | 167 | 这就证明了`x.js`和`y.js`加载的都是`C`的同一个实例。 168 | 169 | 170 | 171 | ## 浏览器加载 172 | 173 | ### 模块加载规则 174 | 175 | HTML 网页中,浏览器通过` 182 | 183 | 184 | 186 | ``` 187 | 188 | 上面代码中,由于浏览器脚本的默认语言是 JavaScript,因此`type="application/javascript"`可以省略。 189 | 190 | 默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到` 196 | 197 | ``` 198 | 199 | 上面代码中,` 215 | ``` 216 | 217 | 上面代码在网页中插入一个模块`foo.js`,由于`type`属性设为`module`,所以浏览器知道这是一个 ES6 模块。 218 | 219 | 浏览器对于带有`type="module"`的` 223 | 224 | 225 | ``` 226 | 227 | 如果网页有多个` 233 | ``` 234 | 235 | 一旦使用了`async`属性,` 245 | ``` 246 | 247 | 举例来说,jQuery 就支持模块加载。 248 | 249 | ```html 250 | 254 | ``` 255 | 256 | 对于外部的模块脚本(上例是`foo.js`),有几点需要注意。 257 | 258 | - 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。 259 | - 模块脚本自动采用严格模式,不管有没有声明`use strict`。 260 | - 模块之中,可以使用`import`命令加载其他模块(`.js`后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用`export`命令输出对外接口。 261 | - 模块之中,顶层的`this`关键字返回`undefined`,而不是指向`window`。也就是说,在模块顶层使用`this`关键字,是无意义的。 262 | - 同一个模块如果加载多次,将只执行一次。 263 | 264 | 下面是一个示例模块。 265 | 266 | ```js 267 | import utils from 'https://example.com/js/utils.js'; 268 | 269 | const x = 1; 270 | 271 | console.log(x === window.x); //false 272 | console.log(this === undefined); // true 273 | ``` 274 | 275 | 利用顶层的`this`等于`undefined`这个语法点,可以侦测当前代码是否在 ES6 模块之中。 276 | 277 | ```js 278 | const isNotModuleScript = this !== undefined; 279 | ``` 280 | 281 | 282 | 283 | ## Node.js 的模块加载方法 284 | 285 | ### 模块引用 286 | 287 | JavaScript 现在有两种模块。一种是 ES6 模块,简称 ESM;另一种是 CommonJS 模块,简称 CJS。 288 | 289 | CommonJS 模块是 Node.js 专用的,与 ES6 模块不兼容。语法上面,两者最明显的差异是,CommonJS 模块使用`require()`和`module.exports`,ES6 模块使用`import`和`export`。 290 | 291 | 它们采用不同的加载方案。从 Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。 292 | 293 | Node.js 要求 ES6 模块采用`.mjs`后缀文件名。也就是说,只要脚本文件里面使用`import`或者`export`命令,那么就必须采用`.mjs`后缀名。Node.js 遇到`.mjs`文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定`"use strict"`。 294 | 295 | 如果不希望将后缀名改成`.mjs`,可以在项目的`package.json`文件中,指定`type`字段为`module`。 296 | 297 | ```json 298 | { 299 | "type": "module" 300 | } 301 | ``` 302 | 303 | 一旦设置了以后,该目录里面的 JS 脚本,就被解释用 ES6 模块。 304 | 305 | ```bash 306 | # 解释成 ES6 模块 307 | $ node my-app.js 308 | ``` 309 | 310 | 如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成`.cjs`。如果没有`type`字段,或者`type`字段为`commonjs`,则`.js`脚本会被解释成 CommonJS 模块。 311 | 312 | 总结为一句话:`.mjs`文件总是以 ES6 模块加载,`.cjs`文件总是以 CommonJS 模块加载,`.js`文件的加载取决于`package.json`里面`type`字段的设置。 313 | 314 | 注意,ES6 模块与 CommonJS 模块尽量不要混用。`require`命令不能加载`.mjs`文件,会报错,只有`import`命令才可以加载`.mjs`文件。反过来,`.mjs`文件里面也不能使用`require`命令,必须使用`import`。 315 | 316 | ### package.json文件 317 | 318 | #### main 字段 319 | 320 | `package.json`文件有两个字段可以指定模块的入口文件:`main`和`exports`。比较简单的模块,可以只使用`main`字段,指定模块加载的入口文件。 321 | 322 | ```json 323 | // ./node_modules/es-module-package/package.json 324 | { 325 | "type": "module", 326 | "main": "./src/index.js" 327 | } 328 | ``` 329 | 330 | 上面代码指定项目的入口脚本为`./src/index.js`,它的格式为 ES6 模块。如果没有`type`字段,`index.js`就会被解释为 CommonJS 模块。 331 | 332 | 然后,`import`命令就可以加载这个模块。 333 | 334 | ```js 335 | // ./my-app.mjs 336 | 337 | import { something } from 'es-module-package'; 338 | // 实际加载的是 ./node_modules/es-module-package/src/index.js 339 | ``` 340 | 341 | 上面代码中,运行该脚本以后,Node.js 就会到`./node_modules`目录下面,寻找`es-module-package`模块,然后根据该模块`package.json`的`main`字段去执行入口文件。 342 | 343 | 这时,如果用 CommonJS 模块的`require()`命令去加载`es-module-package`模块会报错,因为 CommonJS 模块不能处理`export`命令。 344 | 345 | #### exports 字段 346 | 347 | `exports`字段的优先级高于`main`字段。它有多种用法。 348 | 349 | **(1)子目录别名** 350 | 351 | `package.json`文件的`exports`字段可以指定脚本或子目录的别名。 352 | 353 | ```json 354 | // ./node_modules/es-module-package/package.json 355 | { 356 | "exports": { 357 | "./submodule": "./src/submodule.js" 358 | } 359 | } 360 | ``` 361 | 362 | 上面的代码指定`src/submodule.js`别名为`submodule`,然后就可以从别名加载这个文件。 363 | 364 | ```js 365 | import submodule from 'es-module-package/submodule'; 366 | // 加载 ./node_modules/es-module-package/src/submodule.js 367 | ``` 368 | 369 | 下面是子目录别名的例子。 370 | 371 | ```js 372 | // ./node_modules/es-module-package/package.json 373 | { 374 | "exports": { 375 | "./features/": "./src/features/" 376 | } 377 | } 378 | 379 | import feature from 'es-module-package/features/x.js'; 380 | // 加载 ./node_modules/es-module-package/src/features/x.js 381 | ``` 382 | 383 | 如果没有指定别名,就不能用“模块+脚本名”这种形式加载脚本。 384 | 385 | ```js 386 | // 报错 387 | import submodule from 'es-module-package/private-module.js'; 388 | 389 | // 不报错 390 | import submodule from './node_modules/es-module-package/private-module.js'; 391 | ``` 392 | 393 | **(2)main 的别名** 394 | 395 | `exports`字段的别名如果是`.`,就代表模块的主入口,优先级高于`main`字段,并且可以直接简写成`exports`字段的值。 396 | 397 | ```js 398 | { 399 | "exports": { 400 | ".": "./main.js" 401 | } 402 | } 403 | 404 | // 等同于 405 | { 406 | "exports": "./main.js" 407 | } 408 | ``` 409 | 410 | 由于`exports`字段只有支持 ES6 的 Node.js 才认识,所以可以用来兼容旧版本的 Node.js。 411 | 412 | ```js 413 | { 414 | "main": "./main-legacy.cjs", 415 | "exports": { 416 | ".": "./main-modern.cjs" 417 | } 418 | } 419 | ``` 420 | 421 | 上面代码中,老版本的 Node.js (不支持 ES6 模块)的入口文件是`main-legacy.cjs`,新版本的 Node.js 的入口文件是`main-modern.cjs`。 422 | 423 | **(3)条件加载** 424 | 425 | 利用`.`这个别名,可以为 ES6 模块和 CommonJS 指定不同的入口。目前,这个功能需要在 Node.js 运行的时候,打开`--experimental-conditional-exports`标志。 426 | 427 | ```js 428 | { 429 | "type": "module", 430 | "exports": { 431 | ".": { 432 | "require": "./main.cjs", 433 | "default": "./main.js" 434 | } 435 | } 436 | } 437 | ``` 438 | 439 | 上面代码中,别名`.`的`require`条件指定`require()`命令的入口文件(即 CommonJS 的入口),`default`条件指定其他情况的入口(即 ES6 的入口)。 440 | 441 | 上面的写法可以简写如下。 442 | 443 | ```js 444 | { 445 | "exports": { 446 | "require": "./main.cjs", 447 | "default": "./main.js" 448 | } 449 | } 450 | ``` 451 | 452 | 注意,如果同时还有其他别名,就不能采用简写,否则或报错。 453 | 454 | ```js 455 | { 456 | // 报错 457 | "exports": { 458 | "./feature": "./lib/feature.js", 459 | "require": "./main.cjs", 460 | "default": "./main.js" 461 | } 462 | } 463 | ``` 464 | 465 | ### CommonJS 模块加载 ES6 模块 466 | 467 | CommonJS 的`require()`命令不能加载 ES6 模块,会报错,只能使用`import()`这个方法加载。 468 | 469 | ```js 470 | (async () => { 471 | await import('./my-app.mjs'); 472 | })(); 473 | ``` 474 | 475 | 上面代码可以在 CommonJS 模块中运行。 476 | 477 | `require()`不支持 ES6 模块的一个原因是,它是同步加载,而 ES6 模块内部可以使用顶层`await`命令,导致无法被同步加载。 478 | 479 | ### ES6 模块加载 CommonJS 模块 480 | 481 | **ES6 模块的`import`命令可以加载 CommonJS 模块,但是只能整体加载,不能只加载单一的输出项。** 482 | 483 | ```js 484 | // 正确 485 | import packageMain from 'commonjs-package'; 486 | 487 | // 报错 488 | import { method } from 'commonjs-package'; 489 | ``` 490 | 491 | 这是因为 ES6 模块需要支持静态代码分析,而 CommonJS 模块的输出接口是`module.exports`,是一个对象,无法被静态分析,所以只能整体加载。 492 | 493 | 加载单一的输出项,可以写成下面这样。 494 | 495 | ```js 496 | import packageMain from 'commonjs-package'; 497 | const { method } = packageMain; 498 | ``` 499 | 500 | **还有一种变通的加载方法,就是使用 Node.js 内置的`module.createRequire()`方法。** 501 | 502 | ```js 503 | // cjs.cjs 504 | module.exports = 'cjs'; 505 | 506 | // esm.mjs 507 | import { createRequire } from 'module'; 508 | 509 | const require = createRequire(import.meta.url); 510 | 511 | const cjs = require('./cjs.cjs'); 512 | cjs === 'cjs'; // true 513 | ``` 514 | 515 | 上面代码中,ES6 模块通过`module.createRequire()`方法可以加载 CommonJS 模块。**但是,这种写法等于将 ES6 和 CommonJS 混在一起了,所以不建议使用。** 516 | 517 | ### 同时支持两种格式的模块 518 | 519 | 一个模块同时要支持 CommonJS 和 ES6 两种格式,也很容易。 520 | 521 | 如果原始模块是 ES6 格式,那么需要给出一个整体输出接口,比如`export default obj`,使得 CommonJS 可以用`import()`进行加载。 522 | 523 | 如果原始模块是 CommonJS 格式,那么可以加一个包装层。 524 | 525 | ```js 526 | import cjsModule from '../index.js'; 527 | export const foo = cjsModule.foo; 528 | ``` 529 | 530 | 上面代码先整体输入 CommonJS 模块,然后再根据需要输出具名接口。 531 | 532 | 你可以把这个文件的后缀名改为`.mjs`,或者将它放在一个子目录,再在这个子目录里面放一个单独的`package.json`文件,指明`{ type: "module" }`。 533 | 534 | 另一种做法是在`package.json`文件的`exports`字段,指明两种格式模块各自的加载入口。 535 | 536 | ```js 537 | "exports":{ 538 | "require": "./index.js", 539 | "import": "./esm/wrapper.js" 540 | } 541 | ``` 542 | 543 | 上面代码指定`require()`和`import`,加载该模块会自动切换到不一样的入口文件。 544 | 545 | ### Node.js 的内置模块 546 | 547 | Node.js 的内置模块可以整体加载,也可以加载指定的输出项。 548 | 549 | ```js 550 | // 整体加载 551 | import EventEmitter from 'events'; 552 | const e = new EventEmitter(); 553 | 554 | // 加载指定的输出项 555 | import { readFile } from 'fs'; 556 | readFile('./foo.txt', (err, source) => { 557 | if (err) { 558 | console.error(err); 559 | } else { 560 | console.log(source); 561 | } 562 | }); 563 | ``` 564 | 565 | ### 加载路径 566 | 567 | ES6 模块的加载路径必须给出脚本的完整路径,不能省略脚本的后缀名。`import`命令和`package.json`文件的`main`字段如果省略脚本的后缀名,会报错。 568 | 569 | ```js 570 | // ES6 模块中将报错 571 | import { something } from './index'; 572 | ``` 573 | 574 | 为了与浏览器的`import`加载规则相同,Node.js 的`.mjs`文件支持 URL 路径。 575 | 576 | ```js 577 | import './foo.mjs?query=1'; // 加载 ./foo 传入参数 ?query=1 578 | ``` 579 | 580 | 上面代码中,脚本路径带有参数`?query=1`,Node 会按 URL 规则解读。同一个脚本只要参数不同,就会被加载多次,并且保存成不同的缓存。由于这个原因,只要文件名中含有`:`、`%`、`#`、`?`等特殊字符,最好对这些字符进行转义。 581 | 582 | 目前,Node.js 的`import`命令只支持加载本地模块(`file:`协议)和`data:`协议,不支持加载远程模块。另外,脚本路径只支持相对路径,不支持绝对路径(即以`/`或`//`开头的路径)。 583 | 584 | ### 内部变量 585 | 586 | ES6 模块应该是通用的,同一个模块不用修改,就可以用在浏览器环境和服务器环境。为了达到这个目标,Node.js 规定 ES6 模块之中不能使用 CommonJS 模块的特有的一些内部变量。 587 | 588 | 首先,就是`this`关键字。ES6 模块之中,顶层的`this`指向`undefined`;CommonJS 模块的顶层`this`指向当前模块,这是两者的一个重大差异。 589 | 590 | 其次,以下这些顶层变量在 ES6 模块之中都是不存在的。 591 | 592 | - `arguments` 593 | - `require` 594 | - `module` 595 | - `exports` 596 | - `__filename` 597 | - `__dirname` 598 | 599 | 600 | 601 | ## 循环加载 602 | 603 | ### 概念 604 | 605 | “循环加载”(circular dependency)指的是,`a`脚本的执行依赖`b`脚本,而`b`脚本的执行又依赖`a`脚本。 606 | 607 | ```js 608 | // a.js 609 | var b = require('b'); 610 | 611 | // b.js 612 | var a = require('a'); 613 | ``` 614 | 615 | 通常,“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。 616 | 617 | 但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目,很容易出现`a`依赖`b`,`b`依赖`c`,`c`又依赖`a`这样的情况。这意味着,模块加载机制必须考虑“循环加载”的情况。 618 | 619 | 对于 JavaScript 语言来说,目前最常见的两种模块格式 CommonJS 和 ES6,处理“循环加载”的方法是不一样的,返回的结果也不一样。 620 | 621 | ### CommonJS 模块的加载原理 622 | 623 | 介绍 ES6 如何处理“循环加载”之前,先介绍目前最流行的 CommonJS 模块格式的加载原理。 624 | 625 | CommonJS 的一个模块,就是一个脚本文件。`require`命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。 626 | 627 | ```js 628 | { 629 | id: '...', 630 | exports: { ... }, 631 | loaded: true, 632 | ... 633 | } 634 | ``` 635 | 636 | 上面代码就是 Node 内部加载模块后生成的一个对象。该对象的`id`属性是模块名,`exports`属性是模块输出的各个接口,`loaded`属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。 637 | 638 | 以后需要用到这个模块的时候,就会到`exports`属性上面取值。即使再次执行`require`命令,也不会再次执行该模块,而是到缓存之中取值。也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。 639 | 640 | ### CommonJS 模块的循环加载 641 | 642 | CommonJS 模块的重要特性是加载时执行,即脚本代码在`require`的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。 643 | 644 | 让我们来看,Node [官方文档](https://nodejs.org/api/modules.html#modules_cycles)里面的例子。脚本文件`a.js`代码如下。 645 | 646 | ```js 647 | exports.done = false; 648 | var b = require('./b.js'); 649 | console.log('在 a.js 之中,b.done = %j', b.done); 650 | exports.done = true; 651 | console.log('a.js 执行完毕'); 652 | ``` 653 | 654 | 上面代码之中,`a.js`脚本先输出一个`done`变量,然后加载另一个脚本文件`b.js`。注意,此时`a.js`代码就停在这里,等待`b.js`执行完毕,再往下执行。 655 | 656 | 再看`b.js`的代码。 657 | 658 | ```js 659 | exports.done = false; 660 | var a = require('./a.js'); 661 | console.log('在 b.js 之中,a.done = %j', a.done); 662 | exports.done = true; 663 | console.log('b.js 执行完毕'); 664 | ``` 665 | 666 | 上面代码之中,`b.js`执行到第二行,就会去加载`a.js`,这时,就发生了“循环加载”。系统会去`a.js`模块对应对象的`exports`属性取值,可是因为`a.js`还没有执行完,从`exports`属性只能取回已经执行的部分,而不是最后的值。 667 | 668 | `a.js`已经执行的部分,只有一行。 669 | 670 | ```js 671 | exports.done = false; 672 | ``` 673 | 674 | 因此,对于`b.js`来说,它从`a.js`只输入一个变量`done`,值为`false`。 675 | 676 | 然后,`b.js`接着往下执行,等到全部执行完毕,再把执行权交还给`a.js`。于是,`a.js`接着往下执行,直到执行完毕。我们写一个脚本`main.js`,验证这个过程。 677 | 678 | ```js 679 | var a = require('./a.js'); 680 | var b = require('./b.js'); 681 | console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done); 682 | ``` 683 | 684 | 执行`main.js`,运行结果如下。 685 | 686 | ```js 687 | $ node main.js 688 | 689 | 在 b.js 之中,a.done = false 690 | b.js 执行完毕 691 | 在 a.js 之中,b.done = true 692 | a.js 执行完毕 693 | 在 main.js 之中, a.done=true, b.done=true 694 | ``` 695 | 696 | 上面的代码证明了两件事。一是,在`b.js`之中,`a.js`没有执行完毕,只执行了第一行。二是,`main.js`执行到第二行时,不会再次执行`b.js`,而是输出缓存的`b.js`的执行结果,即它的第四行。 697 | 698 | ```js 699 | exports.done = true; 700 | ``` 701 | 702 | 总之,CommonJS 输入的是被输出值的拷贝,不是引用。 703 | 704 | 另外,由于 CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。 705 | 706 | ```js 707 | var a = require('a'); // 安全的写法 708 | var foo = require('a').foo; // 危险的写法 709 | 710 | exports.good = function (arg) { 711 | return a.foo('good', arg); // 使用的是 a.foo 的最新值 712 | }; 713 | 714 | exports.bad = function (arg) { 715 | return foo('bad', arg); // 使用的是一个部分加载时的值 716 | }; 717 | ``` 718 | 719 | 上面代码中,如果发生循环加载,`require('a').foo`的值很可能后面会被改写,改用`require('a')`会更保险一点。 720 | 721 | ### ES6 模块的循环加载 722 | 723 | ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用,如果使用`import`从一个模块加载变量(即`import foo from 'foo'`),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。 724 | 725 | 请看下面这个例子。 726 | 727 | ```js 728 | // a.mjs 729 | import {bar} from './b'; 730 | console.log('a.mjs'); 731 | console.log(bar); 732 | export let foo = 'foo'; 733 | 734 | // b.mjs 735 | import {foo} from './a'; 736 | console.log('b.mjs'); 737 | console.log(foo); 738 | export let bar = 'bar'; 739 | ``` 740 | 741 | 上面代码中,`a.mjs`加载`b.mjs`,`b.mjs`又加载`a.mjs`,构成循环加载。执行`a.mjs`,结果如下。 742 | 743 | ```bash 744 | $ node --experimental-modules a.mjs 745 | b.mjs 746 | ReferenceError: foo is not defined 747 | ``` 748 | 749 | 上面代码中,执行`a.mjs`以后会报错,`foo`变量未定义,这是为什么? 750 | 751 | 让我们一行行来看,ES6 循环加载是怎么处理的。首先,执行`a.mjs`以后,引擎发现它加载了`b.mjs`,因此会优先执行`b.mjs`,然后再执行`a.mjs`。接着,执行`b.mjs`的时候,已知它从`a.mjs`输入了`foo`接口,这时不会去执行`a.mjs`,而是认为这个接口已经存在了,继续往下执行。执行到第三行`console.log(foo)`的时候,才发现这个接口根本没定义,因此报错。 752 | 753 | 解决这个问题的方法,就是让`b.mjs`运行的时候,`foo`已经有定义了。这可以通过将`foo`写成函数来解决。 754 | 755 | ```js 756 | // a.mjs 757 | import {bar} from './b'; 758 | console.log('a.mjs'); 759 | console.log(bar()); 760 | function foo() { return 'foo' } 761 | export {foo}; 762 | 763 | // b.mjs 764 | import {foo} from './a'; 765 | console.log('b.mjs'); 766 | console.log(foo()); 767 | function bar() { return 'bar' } 768 | export {bar}; 769 | ``` 770 | 771 | 这时再执行`a.mjs`就可以得到预期结果。 772 | 773 | ```bash 774 | $ node --experimental-modules a.mjs 775 | b.mjs 776 | foo 777 | a.mjs 778 | bar 779 | ``` 780 | 781 | 这是因为函数具有提升作用,在执行`import {bar} from './b'`时,函数`foo`就已经有定义了,所以`b.mjs`加载的时候不会报错。这也意味着,如果把函数`foo`改写成函数表达式,也会报错。 782 | 783 | ```js 784 | // a.mjs 785 | import {bar} from './b'; 786 | console.log('a.mjs'); 787 | console.log(bar()); 788 | const foo = () => 'foo'; 789 | export {foo}; 790 | ``` 791 | 792 | 上面代码的第四行,改成了函数表达式,就不具有提升作用,执行就会报错。 793 | 794 | 我们再来看 ES6 模块加载器[SystemJS](https://github.com/ModuleLoader/es6-module-loader/blob/master/docs/circular-references-bindings.md)给出的一个例子。 795 | 796 | ```js 797 | // even.js 798 | import { odd } from './odd' 799 | export var counter = 0; 800 | export function even(n) { 801 | counter++; 802 | return n === 0 || odd(n - 1); 803 | } 804 | 805 | // odd.js 806 | import { even } from './even'; 807 | export function odd(n) { 808 | return n !== 0 && even(n - 1); 809 | } 810 | ``` 811 | 812 | 上面代码中,`even.js`里面的函数`even`有一个参数`n`,只要不等于 0,就会减去 1,传入加载的`odd()`。`odd.js`也会做类似操作。 813 | 814 | 运行上面这段代码,结果如下。 815 | 816 | ```bash 817 | $ babel-node 818 | > import * as m from './even.js'; 819 | > m.even(10); 820 | true 821 | > m.counter 822 | 6 823 | > m.even(20) 824 | true 825 | > m.counter 826 | 17 827 | ``` 828 | 829 | 上面代码中,参数`n`从 10 变为 0 的过程中,`even()`一共会执行 6 次,所以变量`counter`等于 6。第二次调用`even()`时,参数`n`从 20 变为 0,`even()`一共会执行 11 次,加上前面的 6 次,所以变量`counter`等于 17。 830 | 831 | 这个例子要是改写成 CommonJS,就根本无法执行,会报错。 832 | 833 | ```js 834 | // even.js 835 | var odd = require('./odd'); 836 | var counter = 0; 837 | exports.counter = counter; 838 | exports.even = function (n) { 839 | counter++; 840 | return n == 0 || odd(n - 1); 841 | } 842 | 843 | // odd.js 844 | var even = require('./even').even; 845 | module.exports = function (n) { 846 | return n != 0 && even(n - 1); 847 | } 848 | ``` 849 | 850 | 上面代码中,`even.js`加载`odd.js`,而`odd.js`又去加载`even.js`,形成“循环加载”。这时,执行引擎就会输出`even.js`已经执行的部分(不存在任何结果),所以在`odd.js`之中,变量`even`等于`undefined`,等到后面调用`even(n - 1)`就会报错。 851 | 852 | ```js 853 | $ node 854 | > var m = require('./even'); 855 | > m.even(10) 856 | TypeError: even is not a function 857 | ``` 858 | 859 | -------------------------------------------------------------------------------- /docs/22. DOM 事件.md: -------------------------------------------------------------------------------- 1 | # DOM 事件 2 | 3 | 4 | 5 | ## EventTarget 接口 6 | 7 | ### 概述 8 | 9 | DOM 的事件操作(监听和触发),都定义在`EventTarget`接口。所有节点对象都部署了这个接口,其他一些需要事件通信的浏览器内置对象(比如,`XMLHttpRequest`、`AudioNode`、`AudioContext`)也部署了这个接口。 10 | 11 | 该接口主要提供三个实例方法。 12 | 13 | - `addEventListener`:绑定事件的监听函数 14 | - `removeEventListener`:移除事件的监听函数 15 | - `dispatchEvent`:触发事件 16 | 17 | ### EventTarget.addEventListener() 18 | 19 | `EventTarget.addEventListener()`用于在当前节点或对象上,定义一个特定事件的监听函数。一旦这个事件发生,就会执行监听函数。该方法没有返回值。 20 | 21 | ```js 22 | target.addEventListener(type, listener[, useCapture]); 23 | ``` 24 | 25 | 该方法接受三个参数。 26 | 27 | - `type`:事件名称,大小写敏感。 28 | - `listener`:监听函数。事件发生时,会调用该监听函数。 29 | - `useCapture`:布尔值,表示监听函数是否在捕获阶段(capture)触发(参见后文《事件的传播》部分),默认为`false`(监听函数只在冒泡阶段被触发)。该参数可选。 30 | 31 | 下面是一个例子。 32 | 33 | ```js 34 | function hello() { 35 | console.log('Hello world'); 36 | } 37 | 38 | var button = document.getElementById('btn'); 39 | button.addEventListener('click', hello, false); 40 | ``` 41 | 42 | 上面代码中,`button`节点的`addEventListener`方法绑定`click`事件的监听函数`hello`,该函数只在冒泡阶段触发。 43 | 44 | 关于参数,有两个地方需要注意。 45 | 46 | 首先,第二个参数除了监听函数,还可以是一个具有`handleEvent`方法的对象。 47 | 48 | ```js 49 | buttonElement.addEventListener('click', { 50 | handleEvent: function (event) { 51 | console.log('click'); 52 | } 53 | }); 54 | ``` 55 | 56 | 上面代码中,`addEventListener`方法的第二个参数,就是一个具有`handleEvent`方法的对象。 57 | 58 | 其次,第三个参数除了布尔值`useCapture`,还可以是一个属性配置对象。该对象有以下属性。 59 | 60 | > - `capture`:布尔值,表示该事件是否在`捕获阶段`触发监听函数。 61 | > - `once`:布尔值,表示监听函数是否只触发一次,然后就自动移除。 62 | > - `passive`:布尔值,表示监听函数不会调用事件的`preventDefault`方法。如果监听函数调用了,浏览器将忽略这个要求,并在监控台输出一行警告。 63 | 64 | 如果希望事件监听函数只执行一次,可以打开属性配置对象的`once`属性。 65 | 66 | ```js 67 | element.addEventListener('click', function (event) { 68 | // 只执行一次的代码 69 | }, {once: true}); 70 | ``` 71 | 72 | `addEventListener`方法可以为针对当前对象的同一个事件,添加多个不同的监听函数。这些函数按照添加顺序触发,即先添加先触发。如果为同一个事件多次添加同一个监听函数,该函数只会执行一次,多余的添加将自动被去除(不必使用`removeEventListener`方法手动去除)。 73 | 74 | ```js 75 | function hello() { 76 | console.log('Hello world'); 77 | } 78 | 79 | document.addEventListener('click', hello, false); 80 | document.addEventListener('click', hello, false); 81 | ``` 82 | 83 | 执行上面代码,点击文档只会输出一行`Hello world`。 84 | 85 | 如果希望向监听函数传递参数,可以用匿名函数包装一下监听函数。 86 | 87 | ```js 88 | function print(x) { 89 | console.log(x); 90 | } 91 | 92 | var el = document.getElementById('div1'); 93 | el.addEventListener('click', function () { print('Hello'); }, false); 94 | ``` 95 | 96 | 上面代码通过匿名函数,向监听函数`print`传递了一个参数。 97 | 98 | 监听函数内部的`this`,指向当前事件所在的那个对象。 99 | 100 | ```js 101 | // HTML 代码如下 102 | //

Hello

103 | var para = document.getElementById('para'); 104 | para.addEventListener('click', function (e) { 105 | console.log(this.nodeName); // "P" 106 | }, false); 107 | ``` 108 | 109 | 上面代码中,监听函数内部的`this`指向事件所在的对象`para`。 110 | 111 | ### EventTarget.removeEventListener() 112 | 113 | `EventTarget.removeEventListener`方法用来移除`addEventListener`方法添加的事件监听函数。该方法没有返回值。 114 | 115 | ```js 116 | div.addEventListener('click', listener, false); 117 | div.removeEventListener('click', listener, false); 118 | ``` 119 | 120 | `removeEventListener`方法的参数,与`addEventListener`方法完全一致。它的第一个参数“事件类型”,大小写敏感。 121 | 122 | 注意,`removeEventListener`方法移除的监听函数,必须是`addEventListener`方法添加的那个监听函数,而且必须在同一个元素节点,否则无效。 123 | 124 | ```js 125 | div.addEventListener('click', function (e) {}, false); 126 | div.removeEventListener('click', function (e) {}, false); 127 | ``` 128 | 129 | 上面代码中,`removeEventListener`方法无效,因为监听函数不是同一个匿名函数。 130 | 131 | ```js 132 | element.addEventListener('mousedown', handleMouseDown, true); 133 | element.removeEventListener("mousedown", handleMouseDown, false); 134 | ``` 135 | 136 | 上面代码中,`removeEventListener`方法也是无效的,因为第三个参数不一样。 137 | 138 | ### EventTarget.dispatchEvent() 139 | 140 | `EventTarget.dispatchEvent`方法在当前节点上触发指定事件,从而触发监听函数的执行。该方法返回一个布尔值,只要有一个监听函数调用了`Event.preventDefault()`,则返回值为`false`,否则为`true`。 141 | 142 | ```js 143 | target.dispatchEvent(event) 144 | ``` 145 | 146 | `dispatchEvent`方法的参数是一个`Event`对象的实例(详见《Event 对象》章节)。 147 | 148 | ```js 149 | para.addEventListener('click', hello, false); 150 | var event = new Event('click'); 151 | para.dispatchEvent(event); 152 | ``` 153 | 154 | 上面代码在当前节点触发了`click`事件。 155 | 156 | 如果`dispatchEvent`方法的参数为空,或者不是一个有效的事件对象,将报错。 157 | 158 | 下面代码根据`dispatchEvent`方法的返回值,判断事件是否被取消了。 159 | 160 | ```js 161 | var canceled = !cb.dispatchEvent(event); 162 | if (canceled) { 163 | console.log('事件取消'); 164 | } else { 165 | console.log('事件未取消'); 166 | } 167 | ``` 168 | 169 | 170 | 171 | ## 事件模型 172 | 173 | ### 监听函数 174 | 175 | 浏览器的事件模型,就是通过监听函数(listener)对事件做出反应。事件发生后,浏览器监听到了这个事件,就会执行对应的监听函数。这是事件驱动编程模式(event-driven)的主要编程方式。 176 | 177 | JavaScript 有三种方法,可以为事件绑定监听函数。 178 | 179 | #### HTML 的 on- 属性 180 | 181 | HTML 语言允许在元素的属性中,直接定义某些事件的监听代码。 182 | 183 | ```html 184 | 185 |
186 | ``` 187 | 188 | 上面代码为`body`节点的`load`事件、`div`节点的`click`事件,指定了监听代码。一旦事件发生,就会执行这段代码。 189 | 190 | 元素的事件监听属性,都是`on`加上事件名,比如`onload`就是`on + load`,表示`load`事件的监听代码。 191 | 192 | 注意,这些属性的值是将会执行的代码,而不是一个函数。 193 | 194 | ```html 195 | 196 | 197 | 198 | 199 | 200 | ``` 201 | 202 | 一旦指定的事件发生,`on-`属性的值是原样传入 JavaScript 引擎执行。因此如果要执行函数,不要忘记加上一对圆括号。 203 | 204 | 使用这个方法指定的监听代码,只会在冒泡阶段触发。 205 | 206 | ```html 207 |
208 | 209 |
210 | ``` 211 | 212 | 上面代码中,` 266 | ``` 267 | 268 | 执行上面代码,点击后会输出`btn`。 269 | 270 | 其他两种监听函数的写法,`this`的指向也是如此。 271 | 272 | ```js 273 | // HTML 代码如下 274 | // 275 | var btn = document.getElementById('btn'); 276 | 277 | // 写法一 278 | btn.onclick = function () { 279 | console.log(this.id); 280 | }; 281 | 282 | // 写法二 283 | btn.addEventListener( 284 | 'click', 285 | function (e) { 286 | console.log(this.id); 287 | }, 288 | false 289 | ); 290 | ``` 291 | 292 | 上面两种写法,点击按钮以后也是输出`btn`。 293 | 294 | ### 事件的传播 295 | 296 | 一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。 297 | 298 | - **第一阶段**:从`window`对象传导到目标节点(上层传到底层),称为“捕获阶段”(capture phase)。 299 | - **第二阶段**:在目标节点上触发,称为“目标阶段”(target phase)。 300 | - **第三阶段**:从目标节点传导回`window`对象(从底层传回上层),称为“冒泡阶段”(bubbling phase)。 301 | 302 | 这种三阶段的传播模型,使得同一个事件会在多个节点上触发。 303 | 304 | ```html 305 |
306 |

点击

307 |
308 | ``` 309 | 310 | 上面代码中,`
`节点之中有一个`

`节点。 311 | 312 | 如果对这两个节点,都设置`click`事件的监听函数(每个节点的捕获阶段和冒泡阶段,各设置一个监听函数),共计设置四个监听函数。然后,对`

`点击,`click`事件会触发四次。 313 | 314 | ```js 315 | var phases = { 316 | 1: 'capture', 317 | 2: 'target', 318 | 3: 'bubble' 319 | }; 320 | 321 | var div = document.querySelector('div'); 322 | var p = document.querySelector('p'); 323 | 324 | div.addEventListener('click', callback, true); 325 | p.addEventListener('click', callback, true); 326 | div.addEventListener('click', callback, false); 327 | p.addEventListener('click', callback, false); 328 | 329 | function callback(event) { 330 | var tag = event.currentTarget.tagName; 331 | var phase = phases[event.eventPhase]; 332 | console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'"); 333 | } 334 | 335 | // 点击以后的结果 336 | // Tag: 'DIV'. EventPhase: 'capture' 337 | // Tag: 'P'. EventPhase: 'target' 338 | // Tag: 'P'. EventPhase: 'target' 339 | // Tag: 'DIV'. EventPhase: 'bubble' 340 | ``` 341 | 342 | 上面代码表示,`click`事件被触发了四次:`

`节点的捕获阶段和冒泡阶段各1次,`

`节点的目标阶段触发了2次。 343 | 344 | 1. 捕获阶段:事件从`

`向`

`传播时,触发`

`的`click`事件; 345 | 2. 目标阶段:事件从`
`到达`

`时,触发`

`的`click`事件; 346 | 3. 冒泡阶段:事件从`

`传回`

`时,再次触发`
`的`click`事件。 347 | 348 | 其中,`

`节点有两个监听函数(`addEventListener`方法第三个参数的不同,会导致绑定两个监听函数),因此它们都会因为`click`事件触发一次。所以,`

`会在`target`阶段有两次输出。 349 | 350 | 注意,浏览器总是假定`click`事件的目标节点,就是点击位置嵌套最深的那个节点(本例是`

`节点里面的`

`节点)。所以,`

`节点的捕获阶段和冒泡阶段,都会显示为`target`阶段。 351 | 352 | 事件传播的最上层对象是`window`,接着依次是`document`,`html`(`document.documentElement`)和`body`(`document.body`)。也就是说,上例的事件传播顺序,在捕获阶段依次为`window`、`document`、`html`、`body`、`div`、`p`,在冒泡阶段依次为`p`、`div`、`body`、`html`、`document`、`window`。 353 | 354 | ### 事件的代理 355 | 356 | 由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。 357 | 358 | ```js 359 | var ul = document.querySelector('ul'); 360 | 361 | ul.addEventListener('click', function (event) { 362 | if (event.target.tagName.toLowerCase() === 'li') { 363 | // some code 364 | } 365 | }); 366 | ``` 367 | 368 | 上面代码中,`click`事件的监听函数定义在`

    `节点,但是实际上,它处理的是子节点`
  • `的`click`事件。这样做的好处是,只要定义一个监听函数,就能处理多个子节点的事件,而不用在每个`
  • `节点上定义监听函数。而且以后再添加子节点,监听函数依然有效。 369 | 370 | 如果希望事件到某个节点为止,不再传播,可以使用事件对象的`stopPropagation`方法。 371 | 372 | ```js 373 | // 事件传播到 p 元素后,就不再向下传播了 374 | p.addEventListener('click', function (event) { 375 | event.stopPropagation(); 376 | }, true); 377 | 378 | // 事件冒泡到 p 元素后,就不再向上冒泡了 379 | p.addEventListener('click', function (event) { 380 | event.stopPropagation(); 381 | }, false); 382 | ``` 383 | 384 | 上面代码中,`stopPropagation`方法分别在捕获阶段和冒泡阶段,阻止了事件的传播。 385 | 386 | 但是,`stopPropagation`方法只会阻止事件的传播,不会阻止该事件触发`

    `节点的其他`click`事件的监听函数。也就是说,不是彻底取消`click`事件。 387 | 388 | ```js 389 | p.addEventListener('click', function (event) { 390 | event.stopPropagation(); 391 | console.log(1); 392 | }); 393 | 394 | p.addEventListener('click', function(event) { 395 | // 会触发 396 | console.log(2); 397 | }); 398 | ``` 399 | 400 | 上面代码中,`p`元素绑定了两个`click`事件的监听函数。`stopPropagation`方法只能阻止这个事件的传播,不能取消这个事件,因此,第二个监听函数会触发。输出结果会先是1,然后是2。 401 | 402 | 如果想要彻底取消该事件,不再触发后面所有`click`的监听函数,可以使用`stopImmediatePropagation`方法。 403 | 404 | ```js 405 | p.addEventListener('click', function (event) { 406 | event.stopImmediatePropagation(); 407 | console.log(1); 408 | }); 409 | 410 | p.addEventListener('click', function(event) { 411 | // 不会被触发 412 | console.log(2); 413 | }); 414 | ``` 415 | 416 | 上面代码中,`stopImmediatePropagation`方法可以彻底取消这个事件,使得后面绑定的所有`click`监听函数都不再触发。所以,只会输出1,不会输出2。 417 | 418 | 419 | 420 | ## Event 对象 421 | 422 | ### 概述 423 | 424 | 事件发生以后,会产生一个事件对象,作为参数传给监听函数。浏览器原生提供一个`Event`对象,所有的事件都是这个对象的实例,或者说继承了`Event.prototype`对象。 425 | 426 | `Event`对象本身就是一个构造函数,可以用来生成新的实例。 427 | 428 | ```js 429 | event = new Event(type, options); 430 | ``` 431 | 432 | `Event`构造函数接受两个参数。第一个参数`type`是字符串,表示事件的名称;第二个参数`options`是一个对象,表示事件对象的配置。该对象主要有下面两个属性。 433 | 434 | - `bubbles`:布尔值,可选,默认为`false`,表示事件对象是否冒泡。 435 | - `cancelable`:布尔值,可选,默认为`false`,表示事件是否可以被取消,即能否用`Event.preventDefault()`取消这个事件。一旦事件被取消,就好像从来没有发生过,不会触发浏览器对该事件的默认行为。 436 | 437 | ```js 438 | var ev = new Event( 439 | 'look', 440 | { 441 | 'bubbles': true, 442 | 'cancelable': false 443 | } 444 | ); 445 | document.dispatchEvent(ev); 446 | ``` 447 | 448 | 上面代码新建一个`look`事件实例,然后使用`dispatchEvent`方法触发该事件。 449 | 450 | 注意,如果不是显式指定`bubbles`属性为`true`,生成的事件就只能在“捕获阶段”触发监听函数。 451 | 452 | ```js 453 | // HTML 代码为 454 | //

    Hello

    455 | var div = document.querySelector('div'); 456 | var p = document.querySelector('p'); 457 | 458 | function callback(event) { 459 | var tag = event.currentTarget.tagName; 460 | console.log('Tag: ' + tag); // 没有任何输出 461 | } 462 | 463 | div.addEventListener('click', callback, false); 464 | 465 | var click = new Event('click'); 466 | p.dispatchEvent(click); 467 | ``` 468 | 469 | 上面代码中,`p`元素发出一个`click`事件,该事件默认不会冒泡。`div.addEventListener`方法指定在冒泡阶段监听,因此监听函数不会触发。如果写成`div.addEventListener('click', callback, true)`,那么在“捕获阶段”可以监听到这个事件。 470 | 471 | 另一方面,如果这个事件在`div`元素上触发。 472 | 473 | ```js 474 | div.dispatchEvent(click); 475 | ``` 476 | 477 | 那么,不管`div`元素是在冒泡阶段监听,还是在捕获阶段监听,都会触发监听函数。因为这时`div`元素是事件的目标,不存在是否冒泡的问题,`div`元素总是会接收到事件,因此导致监听函数生效。 478 | 479 | ### 实例属性 480 | 481 | #### Event.bubbles,Event.eventPhase 482 | 483 | `Event.bubbles`属性返回一个布尔值,表示当前事件是否会冒泡。该属性为只读属性,一般用来了解 Event 实例是否可以冒泡。前面说过,除非显式声明,`Event`构造函数生成的事件,默认是不冒泡的。 484 | 485 | `Event.eventPhase`属性返回一个整数常量,表示事件目前所处的阶段。该属性只读。 486 | 487 | ```js 488 | var phase = event.eventPhase; 489 | ``` 490 | 491 | `Event.eventPhase`的返回值有四种可能。 492 | 493 | - 0,事件目前没有发生。 494 | - 1,事件目前处于捕获阶段,即处于从祖先节点向目标节点的传播过程中。 495 | - 2,事件到达目标节点,即`Event.target`属性指向的那个节点。 496 | - 3,事件处于冒泡阶段,即处于从目标节点向祖先节点的反向传播过程中。 497 | 498 | #### Event.cancelable,Event.cancelBubble,event.defaultPrevented 499 | 500 | `Event.cancelable`属性返回一个布尔值,表示事件是否可以取消。该属性为只读属性,一般用来了解 Event 实例的特性。 501 | 502 | 大多数浏览器的原生事件是可以取消的。比如,取消`click`事件,点击链接将无效。但是除非显式声明,`Event`构造函数生成的事件,默认是不可以取消的。 503 | 504 | ```js 505 | var evt = new Event('foo'); 506 | evt.cancelable // false 507 | ``` 508 | 509 | 当`Event.cancelable`属性为`true`时,调用`Event.preventDefault()`就可以取消这个事件,阻止浏览器对该事件的默认行为。 510 | 511 | 如果事件不能取消,调用`Event.preventDefault()`会没有任何效果。所以使用这个方法之前,最好用`Event.cancelable`属性判断一下是否可以取消。 512 | 513 | ```js 514 | function preventEvent(event) { 515 | if (event.cancelable) { 516 | event.preventDefault(); 517 | } else { 518 | console.warn('This event couldn\'t be canceled.'); 519 | console.dir(event); 520 | } 521 | } 522 | ``` 523 | 524 | `Event.cancelBubble`属性是一个布尔值,如果设为`true`,相当于执行`Event.stopPropagation()`,可以阻止事件的传播。 525 | 526 | `Event.defaultPrevented`属性返回一个布尔值,表示该事件是否调用过`Event.preventDefault`方法。该属性只读。 527 | 528 | ```js 529 | if (event.defaultPrevented) { 530 | console.log('该事件已经取消了'); 531 | } 532 | ``` 533 | 534 | #### Event.currentTarget,Event.target 535 | 536 | 事件发生以后,会经过捕获和冒泡两个阶段,依次通过多个 DOM 节点。因此,任意事件都有两个与事件相关的节点,一个是事件的原始触发节点(`Event.target`),另一个是事件当前正在通过的节点(`Event.currentTarget`)。前者通常是后者的后代节点。 537 | 538 | `Event.currentTarget`属性返回事件当前所在的节点,即事件当前正在通过的节点,也就是当前正在执行的监听函数所在的那个节点。随着事件的传播,这个属性的值会变。 539 | 540 | `Event.target`属性返回原始触发事件的那个节点,即事件最初发生的节点。这个属性不会随着事件的传播而改变。 541 | 542 | 事件传播过程中,不同节点的监听函数内部的`Event.target`与`Event.currentTarget`属性的值是不一样的。 543 | 544 | ```js 545 | // HTML 代码为 546 | //

    Hello World

    547 | function hide(e) { 548 | // 不管点击 Hello 或 World,总是返回 true 549 | console.log(this === e.currentTarget); 550 | 551 | // 点击 Hello,返回 true 552 | // 点击 World,返回 false 553 | console.log(this === e.target); 554 | } 555 | 556 | document.getElementById('para').addEventListener('click', hide, false); 557 | ``` 558 | 559 | 上面代码中,``是`

    `的子节点,点击``或者点击`

    `,都会导致监听函数执行。这时,`e.target`总是指向原始点击位置的那个节点,而`e.currentTarget`指向事件传播过程中正在经过的那个节点。由于监听函数只有事件经过时才会触发,所以`e.currentTarget`总是等同于监听函数内部的`this`。 560 | 561 | #### Event.type 562 | 563 | `Event.type`属性返回一个字符串,表示事件类型。事件的类型是在生成事件的时候指定的。该属性只读。 564 | 565 | ```js 566 | var evt = new Event('foo'); 567 | evt.type // "foo" 568 | ``` 569 | 570 | #### Event.timeStamp 571 | 572 | `Event.timeStamp`属性返回一个毫秒时间戳,表示事件发生的时间。它是相对于网页加载成功开始计算的。 573 | 574 | ```js 575 | var evt = new Event('foo'); 576 | evt.timeStamp // 3683.6999999995896 577 | ``` 578 | 579 | 它的返回值有可能是整数,也有可能是小数(高精度时间戳),取决于浏览器的设置。 580 | 581 | 下面是一个计算鼠标移动速度的例子,显示每秒移动的像素数量。 582 | 583 | ```js 584 | var previousX; 585 | var previousY; 586 | var previousT; 587 | 588 | window.addEventListener('mousemove', function(event) { 589 | if ( 590 | previousX !== undefined && 591 | previousY !== undefined && 592 | previousT !== undefined 593 | ) { 594 | var deltaX = event.screenX - previousX; 595 | var deltaY = event.screenY - previousY; 596 | var deltaD = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); 597 | 598 | var deltaT = event.timeStamp - previousT; 599 | console.log(deltaD / deltaT * 1000); 600 | } 601 | 602 | previousX = event.screenX; 603 | previousY = event.screenY; 604 | previousT = event.timeStamp; 605 | }); 606 | ``` 607 | 608 | #### Event.isTrusted 609 | 610 | `Event.isTrusted`属性返回一个布尔值,表示该事件是否由真实的用户行为产生。比如,用户点击链接会产生一个`click`事件,该事件是用户产生的;`Event`构造函数生成的事件,则是脚本产生的。 611 | 612 | ```js 613 | var evt = new Event('foo'); 614 | evt.isTrusted // false 615 | ``` 616 | 617 | 上面代码中,`evt`对象是脚本产生的,所以`isTrusted`属性返回`false`。 618 | 619 | #### Event.detail 620 | 621 | `Event.detail`属性只有浏览器的 UI (用户界面)事件才具有。该属性返回一个数值,表示事件的某种信息。具体含义与事件类型相关。比如,对于`click`和`dblclick`事件,`Event.detail`是鼠标按下的次数(`1`表示单击,`2`表示双击,`3`表示三击);对于鼠标滚轮事件,`Event.detail`是滚轮正向滚动的距离,负值就是负向滚动的距离,返回值总是3的倍数。 622 | 623 | ```js 624 | // HTML 代码如下 625 | //

    Hello

    626 | function giveDetails(e) { 627 | console.log(e.detail); 628 | } 629 | 630 | document.querySelector('p').onclick = giveDetails; 631 | ``` 632 | 633 | ### 实例方法 634 | 635 | #### Event.preventDefault() 636 | 637 | `Event.preventDefault`方法取消浏览器对当前事件的默认行为。比如点击链接后,浏览器默认会跳转到另一个页面,使用这个方法以后,就不会跳转了;再比如,按一下空格键,页面向下滚动一段距离,使用这个方法以后也不会滚动了。该方法生效的前提是,事件对象的`cancelable`属性为`true`,如果为`false`,调用该方法没有任何效果。 638 | 639 | 注意,该方法只是取消事件对当前元素的默认影响,不会阻止事件的传播。如果要阻止传播,可以使用`stopPropagation()`或`stopImmediatePropagation()`方法。 640 | 641 | ```js 642 | // HTML 代码为 643 | // 644 | var cb = document.getElementById('my-checkbox'); 645 | 646 | cb.addEventListener( 647 | 'click', 648 | function (e){ e.preventDefault(); }, 649 | false 650 | ); 651 | ``` 652 | 653 | 上面代码中,浏览器的默认行为是单击会选中单选框,取消这个行为,就导致无法选中单选框。 654 | 655 | 利用这个方法,可以为文本输入框设置校验条件。如果用户的输入不符合条件,就无法将字符输入文本框。 656 | 657 | ```js 658 | // HTML 代码为 659 | // 660 | var input = document.getElementById('my-input'); 661 | input.addEventListener('keypress', checkName, false); 662 | 663 | function checkName(e) { 664 | if (e.charCode < 97 || e.charCode > 122) { 665 | e.preventDefault(); 666 | } 667 | } 668 | ``` 669 | 670 | 上面代码为文本框的`keypress`事件设定监听函数后,将只能输入小写字母,否则输入事件的默认行为(写入文本框)将被取消,导致不能向文本框输入内容。 671 | 672 | #### Event.stopPropagation() 673 | 674 | `stopPropagation`方法阻止事件在 DOM 中继续传播,防止再触发定义在别的节点上的监听函数,但是不包括在当前节点上其他的事件监听函数。 675 | 676 | ```js 677 | function stopEvent(e) { 678 | e.stopPropagation(); 679 | } 680 | 681 | el.addEventListener('click', stopEvent, false); 682 | ``` 683 | 684 | 上面代码中,`click`事件将不会进一步冒泡到`el`节点的父节点。 685 | 686 | #### Event.stopImmediatePropagation() 687 | 688 | `Event.stopImmediatePropagation`方法阻止同一个事件的其他监听函数被调用,不管监听函数定义在当前节点还是其他节点。也就是说,该方法阻止事件的传播,比`Event.stopPropagation()`更彻底。 689 | 690 | 如果同一个节点对于同一个事件指定了多个监听函数,这些函数会根据添加的顺序依次调用。只要其中有一个监听函数调用了`Event.stopImmediatePropagation`方法,其他的监听函数就不会再执行了。 691 | 692 | ```js 693 | function l1(e){ 694 | e.stopImmediatePropagation(); 695 | } 696 | 697 | function l2(e){ 698 | console.log('hello world'); 699 | } 700 | 701 | el.addEventListener('click', l1, false); 702 | el.addEventListener('click', l2, false); 703 | ``` 704 | 705 | 上面代码在`el`节点上,为`click`事件添加了两个监听函数`l1`和`l2`。由于`l1`调用了`event.stopImmediatePropagation`方法,所以`l2`不会被调用。 706 | 707 | #### Event.composedPath() 708 | 709 | `Event.composedPath()`返回一个数组,成员是事件的最底层节点和依次冒泡经过的所有上层节点。 710 | 711 | ```js 712 | // HTML 代码如下 713 | //
    714 | //

    Hello

    715 | //
    716 | var div = document.querySelector('div'); 717 | var p = document.querySelector('p'); 718 | 719 | div.addEventListener('click', function (e) { 720 | console.log(e.composedPath()); 721 | }, false); 722 | // [p, div, body, html, document, Window] 723 | ``` 724 | 725 | 上面代码中,`click`事件的最底层节点是`p`,向上依次是`div`、`body`、`html`、`document`、`Window`。 726 | 727 | -------------------------------------------------------------------------------- /docs/24. 编程风格.md: -------------------------------------------------------------------------------- 1 | # 编程风格 2 | 3 | 4 | 5 | ## 概述 6 | 7 | “编程风格”(programming style)指的是编写代码的样式规则。不同的程序员,往往有不同的编程风格。 8 | 9 | 有人说,编译器的规范叫做“语法规则”(grammar),这是程序员必须遵守的;而编译器忽略的部分,就叫“编程风格”(programming style),这是程序员可以自由选择的。这种说法不完全正确,程序员固然可以自由选择编程风格,但是好的编程风格有助于写出质量更高、错误更少、更易于维护的程序。 10 | 11 | 所以,编程风格的选择不应该基于个人爱好、熟悉程度、打字量等因素,而要考虑如何尽量使代码清晰易读、减少出错。你选择的,不是你喜欢的风格,而是一种能够清晰表达你的意图的风格。这一点,对于 JavaScript 这种语法自由度很高的语言尤其重要。 12 | 13 | 必须牢记的一点是,如果你选定了一种“编程风格”,就应该坚持遵守,切忌多种风格混用。如果你加入他人的项目,就应该遵守现有的风格。 14 | 15 | ## 缩进 16 | 17 | 行首的空格和 Tab 键,都可以产生代码缩进效果(indent)。 18 | 19 | Tab 键可以节省击键次数,但不同的文本编辑器对 Tab 的显示不尽相同,有的显示四个空格,有的显示两个空格,所以有人觉得,空格键可以使得显示效果更统一。 20 | 21 | 无论你选择哪一种方法,都是可以接受的,要做的就是始终坚持这一种选择。不要一会使用 Tab 键,一会使用空格键。 22 | 23 | ## 区块 24 | 25 | 如果循环和判断的代码体只有一行,JavaScript 允许该区块(block)省略大括号。 26 | 27 | ```js 28 | if (a) 29 | b(); 30 | c(); 31 | ``` 32 | 33 | 上面代码的原意可能是下面这样。 34 | 35 | ```js 36 | if (a) { 37 | b(); 38 | c(); 39 | } 40 | ``` 41 | 42 | 但是,实际效果却是下面这样。 43 | 44 | ```js 45 | if (a) { 46 | b(); 47 | } 48 | c(); 49 | ``` 50 | 51 | 因此,建议总是使用大括号表示区块。 52 | 53 | 另外,区块起首的大括号的位置,有许多不同的写法。最流行的有两种,一种是起首的大括号另起一行。 54 | 55 | ```js 56 | block 57 | { 58 | // ... 59 | } 60 | ``` 61 | 62 | 另一种是起首的大括号跟在关键字的后面。 63 | 64 | ```js 65 | block { 66 | // ... 67 | } 68 | ``` 69 | 70 | 一般来说,这两种写法都可以接受。但是,JavaScript 要使用后一种,因为 JavaScript 会自动添加句末的分号,导致一些难以察觉的错误。 71 | 72 | ```js 73 | return 74 | { 75 | key: value 76 | }; 77 | 78 | // 相当于 79 | return; 80 | { 81 | key: value 82 | }; 83 | ``` 84 | 85 | 上面的代码的原意,是要返回一个对象,但实际上返回的是`undefined`,因为 JavaScript 自动在`return`语句后面添加了分号。为了避免这一类错误,需要写成下面这样。 86 | 87 | ```js 88 | return { 89 | key : value 90 | }; 91 | ``` 92 | 93 | 因此,表示区块起首的大括号,不要另起一行。 94 | 95 | ## 圆括号 96 | 97 | 圆括号(parentheses)在 JavaScript 中有两种作用,一种表示函数的调用,另一种表示表达式的组合(grouping)。 98 | 99 | ```js 100 | // 圆括号表示函数的调用 101 | console.log('abc'); 102 | 103 | // 圆括号表示表达式的组合 104 | (1 + 2) * 3 105 | ``` 106 | 107 | 建议可以用空格,区分这两种不同的括号。 108 | 109 | > 1. 表示函数调用时,函数名与左括号之间没有空格。 110 | > 2. 表示函数定义时,函数名与左括号之间没有空格。 111 | > 3. 其他情况时,前面位置的语法元素与左括号之间,都有一个空格。 112 | 113 | 按照上面的规则,下面的写法都是不规范的。 114 | 115 | ```js 116 | foo (bar) 117 | return(a+b); 118 | if(a === 0) {...} 119 | function foo (b) {...} 120 | function(x) {...} 121 | ``` 122 | 123 | 上面代码的最后一行是一个匿名函数,`function`是语法关键字,不是函数名,所以与左括号之间应该要有一个空格。 124 | 125 | ## 行尾的分号 126 | 127 | 分号表示一条语句的结束。JavaScript 允许省略行尾的分号。事实上,确实有一些开发者行尾从来不写分号。但是,由于下面要讨论的原因,建议还是不要省略这个分号。 128 | 129 | ### 不使用分号的情况 130 | 131 | 首先,以下三种情况,语法规定本来就不需要在结尾添加分号。 132 | 133 | **(1)for 和 while 循环** 134 | 135 | ```js 136 | for ( ; ; ) { 137 | } // 没有分号 138 | 139 | while (true) { 140 | } // 没有分号 141 | ``` 142 | 143 | 注意,`do...while`循环是有分号的。 144 | 145 | ```js 146 | do { 147 | a--; 148 | } while(a > 0); // 分号不能省略 149 | ``` 150 | 151 | **(2)分支语句:if,switch,try** 152 | 153 | ```js 154 | if (true) { 155 | } // 没有分号 156 | 157 | switch () { 158 | } // 没有分号 159 | 160 | try { 161 | } catch { 162 | } // 没有分号 163 | ``` 164 | 165 | **(3)函数的声明语句** 166 | 167 | ```js 168 | function f() { 169 | } // 没有分号 170 | ``` 171 | 172 | 注意,函数表达式仍然要使用分号。 173 | 174 | ```js 175 | var f = function f() { 176 | }; 177 | ``` 178 | 179 | 以上三种情况,如果使用了分号,并不会出错。因为,解释引擎会把这个分号解释为空语句。 180 | 181 | ### 分号的自动添加 182 | 183 | 除了上一节的三种情况,所有语句都应该使用分号。但是,如果没有使用分号,大多数情况下,JavaScript 会自动添加。 184 | 185 | ```js 186 | var a = 1 187 | // 等同于 188 | var a = 1; 189 | ``` 190 | 191 | 这种语法特性被称为“分号的自动添加”(Automatic Semicolon Insertion,简称 ASI)。 192 | 193 | 因此,有人提倡省略句尾的分号。麻烦的是,如果下一行的开始可以与本行的结尾连在一起解释,JavaScript 就不会自动添加分号。 194 | 195 | ```js 196 | // 等同于 var a = 3 197 | var 198 | a 199 | = 200 | 3 201 | 202 | // 等同于 'abc'.length 203 | 'abc' 204 | .length 205 | 206 | // 等同于 return a + b; 207 | return a + 208 | b; 209 | 210 | // 等同于 obj.foo(arg1, arg2); 211 | obj.foo(arg1, 212 | arg2); 213 | 214 | // 等同于 3 * 2 + 10 * (27 / 6) 215 | 3 * 2 216 | + 217 | 10 * (27 / 6) 218 | ``` 219 | 220 | 上面代码都会多行放在一起解释,不会每一行自动添加分号。这些例子还是比较容易看出来的,但是下面这个例子就不那么容易看出来了。 221 | 222 | ```js 223 | x = y 224 | (function () { 225 | // ... 226 | })(); 227 | 228 | // 等同于 229 | x = y(function () {...})(); 230 | ``` 231 | 232 | 下面是更多不会自动添加分号的例子。 233 | 234 | ```js 235 | // 引擎解释为 c(d+e) 236 | var a = b + c 237 | (d+e).toString(); 238 | 239 | // 引擎解释为 a = b/hi/g.exec(c).map(d) 240 | // 正则表达式的斜杠,会当作除法运算符 241 | a = b 242 | /hi/g.exec(c).map(d); 243 | 244 | // 解释为'b'['red', 'green'], 245 | // 即把字符串当作一个数组,按索引取值 246 | var a = 'b' 247 | ['red', 'green'].forEach(function (c) { 248 | console.log(c); 249 | }) 250 | 251 | // 解释为 function (x) { return x }(a++) 252 | // 即调用匿名函数,结果f等于0 253 | var a = 0; 254 | var f = function (x) { return x } 255 | (a++) 256 | ``` 257 | 258 | 只有下一行的开始与本行的结尾,无法放在一起解释,JavaScript 引擎才会自动添加分号。 259 | 260 | ```js 261 | if (a < 0) a = 0 262 | console.log(a) 263 | 264 | // 等同于下面的代码, 265 | // 因为 0console 没有意义 266 | if (a < 0) a = 0; 267 | console.log(a) 268 | ``` 269 | 270 | 另外,如果一行的起首是“自增”(`++`)或“自减”(`--`)运算符,则它们的前面会自动添加分号。 271 | 272 | ```js 273 | a = b = c = 1 274 | 275 | a 276 | ++ 277 | b 278 | -- 279 | c 280 | 281 | console.log(a, b, c) 282 | // 1 2 0 283 | ``` 284 | 285 | 上面代码之所以会得到`1 2 0`的结果,原因是自增和自减运算符前,自动加上了分号。上面的代码实际上等同于下面的形式。 286 | 287 | ```js 288 | a = b = c = 1; 289 | a; 290 | ++b; 291 | --c; 292 | ``` 293 | 294 | 如果`continue`、`break`、`return`和`throw`这四个语句后面,直接跟换行符,则会自动添加分号。这意味着,如果`return`语句返回的是一个对象的字面量,起首的大括号一定要写在同一行,否则得不到预期结果。 295 | 296 | ```js 297 | return 298 | { first: 'Jane' }; 299 | 300 | // 解释成 301 | return; 302 | { first: 'Jane' }; 303 | ``` 304 | 305 | 由于解释引擎自动添加分号的行为难以预测,因此编写代码的时候不应该省略行尾的分号。 306 | 307 | 不应该省略结尾的分号,还有一个原因。有些 JavaScript 代码压缩器(uglifier)不会自动添加分号,因此遇到没有分号的结尾,就会让代码保持原状,而不是压缩成一行,使得压缩无法得到最优的结果。 308 | 309 | 另外,不写结尾的分号,可能会导致脚本合并出错。所以,有的代码库在第一行语句开始前,会加上一个分号。 310 | 311 | ```js 312 | ;var a = 1; 313 | // ... 314 | ``` 315 | 316 | 上面这种写法就可以避免与其他脚本合并时,排在前面的脚本最后一行语句没有分号,导致运行出错的问题。 317 | 318 | ## 全局变量 319 | 320 | JavaScript 最大的语法缺点,可能就是全局变量对于任何一个代码块,都是可读可写。这对代码的模块化和重复使用,非常不利。 321 | 322 | 因此,建议避免使用全局变量。如果不得不使用,可以考虑用大写字母表示变量名,这样更容易看出这是全局变量,比如`UPPER_CASE`。 323 | 324 | ## 变量声明 325 | 326 | JavaScript 会自动将变量声明“提升”(hoist)到代码块(block)的头部。 327 | 328 | ```js 329 | if (!x) { 330 | var x = {}; 331 | } 332 | 333 | // 等同于 334 | var x; 335 | if (!x) { 336 | x = {}; 337 | } 338 | ``` 339 | 340 | 这意味着,变量`x`是`if`代码块之前就存在了。为了避免可能出现的问题,最好把变量声明都放在代码块的头部。 341 | 342 | ```js 343 | for (var i = 0; i < 10; i++) { 344 | // ... 345 | } 346 | 347 | // 写成 348 | var i; 349 | for (i = 0; i < 10; i++) { 350 | // ... 351 | } 352 | ``` 353 | 354 | 上面这样的写法,就容易看出存在一个全局的循环变量`i`。 355 | 356 | 另外,所有函数都应该在使用之前定义。函数内部的变量声明,都应该放在函数的头部。 357 | 358 | ## with 语句 359 | 360 | `with`可以减少代码的书写,但是会造成混淆。 361 | 362 | ```js 363 | with (o) { 364 |  foo = bar; 365 | } 366 | ``` 367 | 368 | 上面的代码,可以有四种运行结果: 369 | 370 | ```js 371 | o.foo = bar; 372 | o.foo = o.bar; 373 | foo = bar; 374 | foo = o.bar; 375 | ``` 376 | 377 | 这四种结果都可能发生,取决于不同的变量是否有定义。因此,不要使用`with`语句。 378 | 379 | ## 相等和严格相等 380 | 381 | JavaScript 有两个表示相等的运算符:“相等”(`==`)和“严格相等”(`===`)。 382 | 383 | 相等运算符会自动转换变量类型,造成很多意想不到的情况。 384 | 385 | ```js 386 | 0 == ''// true 387 | 1 == true // true 388 | 2 == true // false 389 | 0 == '0' // true 390 | false == 'false' // false 391 | false == '0' // true 392 | ' \t\r\n ' == 0 // true 393 | ``` 394 | 395 | 因此,建议不要使用相等运算符(`==`),只使用严格相等运算符(`===`)。 396 | 397 | ## 语句的合并 398 | 399 | 有些程序员追求简洁,喜欢合并不同目的的语句。比如,原来的语句是 400 | 401 | ```js 402 | a = b; 403 | if (a) { 404 | // ... 405 | } 406 | ``` 407 | 408 | 他喜欢写成下面这样。 409 | 410 | ```js 411 | if (a = b) { 412 | // ... 413 | } 414 | ``` 415 | 416 | 虽然语句少了一行,但是可读性大打折扣,而且会造成误读,让别人误解这行代码的意思是下面这样。 417 | 418 | ```js 419 | if (a === b){ 420 | // ... 421 | } 422 | ``` 423 | 424 | 建议不要将不同目的的语句,合并成一行。 425 | 426 | ## 自增和自减运算符 427 | 428 | 自增(`++`)和自减(`--`)运算符,放在变量的前面或后面,返回的值不一样,很容易发生错误。事实上,所有的`++`运算符都可以用`+= 1`代替。 429 | 430 | ```js 431 | ++x 432 | // 等同于 433 | x += 1; 434 | ``` 435 | 436 | 改用`+= 1`,代码变得更清晰了。 437 | 438 | 建议自增(`++`)和自减(`--`)运算符尽量使用`+=`和`-=`代替。 439 | 440 | ## switch...case 结构 441 | 442 | `switch...case`结构要求,在每一个`case`的最后一行必须是`break`语句,否则会接着运行下一个`case`。这样不仅容易忘记,还会造成代码的冗长。 443 | 444 | 而且,`switch...case`不使用大括号,不利于代码形式的统一。此外,这种结构类似于`goto`语句,容易造成程序流程的混乱,使得代码结构混乱不堪,不符合面向对象编程的原则。 445 | 446 | ```js 447 | function doAction(action) { 448 | switch (action) { 449 | case 'hack': 450 | return 'hack'; 451 | case 'slash': 452 | return 'slash'; 453 | case 'run': 454 | return 'run'; 455 | default: 456 | throw new Error('Invalid action.'); 457 | } 458 | } 459 | ``` 460 | 461 | 上面的代码建议改写成对象结构。 462 | 463 | ```js 464 | function doAction(action) { 465 | var actions = { 466 | 'hack': function () { 467 | return 'hack'; 468 | }, 469 | 'slash': function () { 470 | return 'slash'; 471 | }, 472 | 'run': function () { 473 | return 'run'; 474 | } 475 | }; 476 | 477 | if (typeof actions[action] !== 'function') { 478 | throw new Error('Invalid action.'); 479 | } 480 | 481 | return actions[action](); 482 | } 483 | ``` 484 | 485 | 因此,建议`switch...case`结构可以用对象结构代替。 486 | 487 | ## 块级作用域 488 | 489 | 从本节开始,将探讨如何将 ES6 的新语法,运用到编码实践之中,与传统的 JavaScript 语法结合在一起,写出合理的、易于阅读和维护的代码。 490 | 491 | 多家公司和组织已经公开了它们的风格规范,下面的内容主要参考了 [Airbnb](https://github.com/airbnb/javascript) 公司的 JavaScript 风格规范。 492 | 493 | ### let 取代 var 494 | 495 | ES6 提出了两个新的声明变量的命令:`let`和`const`。其中,`let`完全可以取代`var`,因为两者语义相同,而且`let`没有副作用。 496 | 497 | ```js 498 | 'use strict'; 499 | 500 | if (true) { 501 | let x = 'hello'; 502 | } 503 | 504 | for (let i = 0; i < 10; i++) { 505 | console.log(i); 506 | } 507 | ``` 508 | 509 | 上面代码如果用`var`替代`let`,实际上就声明了两个全局变量,这显然不是本意。变量应该只在其声明的代码块内有效,`var`命令做不到这一点。 510 | 511 | `var`命令存在变量提升效用,`let`命令没有这个问题。 512 | 513 | ```js 514 | 'use strict'; 515 | 516 | if (true) { 517 | console.log(x); // ReferenceError 518 | let x = 'hello'; 519 | } 520 | ``` 521 | 522 | 上面代码如果使用`var`替代`let`,`console.log`那一行就不会报错,而是会输出`undefined`,因为变量声明提升到代码块的头部。这违反了变量先声明后使用的原则。 523 | 524 | 所以,建议不再使用`var`命令,而是使用`let`命令取代。 525 | 526 | ### 全局常量和线程安全 527 | 528 | 在`let`和`const`之间,建议优先使用`const`,尤其是在全局环境,不应该设置变量,只应设置常量。 529 | 530 | `const`优于`let`有几个原因。一个是`const`可以提醒阅读程序的人,这个变量不应该改变;另一个是`const`比较符合函数式编程思想,运算不改变值,只是新建值,而且这样也有利于将来的分布式运算;最后一个原因是 JavaScript 编译器会对`const`进行优化,所以多使用`const`,有利于提高程序的运行效率,也就是说`let`和`const`的本质区别,其实是编译器内部的处理不同。 531 | 532 | ```js 533 | // bad 534 | var a = 1, b = 2, c = 3; 535 | 536 | // good 537 | const a = 1; 538 | const b = 2; 539 | const c = 3; 540 | 541 | // best 542 | const [a, b, c] = [1, 2, 3]; 543 | ``` 544 | 545 | `const`声明常量还有两个好处,一是阅读代码的人立刻会意识到不应该修改这个值,二是防止了无意间修改变量值所导致的错误。 546 | 547 | 所有的函数都应该设置为常量。 548 | 549 | 长远来看,JavaScript 可能会有多线程的实现(比如 Intel 公司的 River Trail 那一类的项目),这时`let`表示的变量,只应出现在单线程运行的代码中,不能是多线程共享的,这样有利于保证线程安全。 550 | 551 | 552 | 553 | ## 字符串 554 | 555 | 静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。 556 | 557 | ```js 558 | // bad 559 | const a = "foobar"; 560 | const b = 'foo' + a + 'bar'; 561 | 562 | // acceptable 563 | const c = `foobar`; 564 | 565 | // good 566 | const a = 'foobar'; 567 | const b = `foo${a}bar`; 568 | ``` 569 | 570 | 571 | 572 | ## 解构赋值 573 | 574 | 使用数组成员对变量赋值时,优先使用解构赋值。 575 | 576 | ```js 577 | const arr = [1, 2, 3, 4]; 578 | 579 | // bad 580 | const first = arr[0]; 581 | const second = arr[1]; 582 | 583 | // good 584 | const [first, second] = arr; 585 | ``` 586 | 587 | 函数的参数如果是对象的成员,优先使用解构赋值。 588 | 589 | ```js 590 | // bad 591 | function getFullName(user) { 592 | const firstName = user.firstName; 593 | const lastName = user.lastName; 594 | } 595 | 596 | // good 597 | function getFullName(obj) { 598 | const { firstName, lastName } = obj; 599 | } 600 | 601 | // best 602 | function getFullName({ firstName, lastName }) { 603 | } 604 | ``` 605 | 606 | 如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。这样便于以后添加返回值,以及更改返回值的顺序。 607 | 608 | ```js 609 | // bad 610 | function processInput(input) { 611 | return [left, right, top, bottom]; 612 | } 613 | 614 | // good 615 | function processInput(input) { 616 | return { left, right, top, bottom }; 617 | } 618 | 619 | const { left, right } = processInput(input); 620 | ``` 621 | 622 | 623 | 624 | ## 对象 625 | 626 | 单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。 627 | 628 | ```js 629 | // bad 630 | const a = { k1: v1, k2: v2, }; 631 | const b = { 632 | k1: v1, 633 | k2: v2 634 | }; 635 | 636 | // good 637 | const a = { k1: v1, k2: v2 }; 638 | const b = { 639 | k1: v1, 640 | k2: v2, 641 | }; 642 | ``` 643 | 644 | 对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用`Object.assign`方法。 645 | 646 | ```js 647 | // bad 648 | const a = {}; 649 | a.x = 3; 650 | 651 | // if reshape unavoidable 652 | const a = {}; 653 | Object.assign(a, { x: 3 }); 654 | 655 | // good 656 | const a = { x: null }; 657 | a.x = 3; 658 | ``` 659 | 660 | 如果对象的属性名是动态的,可以在创造对象的时候,使用属性表达式定义。 661 | 662 | ```js 663 | // bad 664 | const obj = { 665 | id: 5, 666 | name: 'San Francisco', 667 | }; 668 | obj[getKey('enabled')] = true; 669 | 670 | // good 671 | const obj = { 672 | id: 5, 673 | name: 'San Francisco', 674 | [getKey('enabled')]: true, 675 | }; 676 | ``` 677 | 678 | 上面代码中,对象`obj`的最后一个属性名,需要计算得到。这时最好采用属性表达式,在新建`obj`的时候,将该属性与其他属性定义在一起。这样一来,所有属性就在一个地方定义了。 679 | 680 | 另外,对象的属性和方法,尽量采用简洁表达法,这样易于描述和书写。 681 | 682 | ```js 683 | var ref = 'some value'; 684 | 685 | // bad 686 | const atom = { 687 | ref: ref, 688 | 689 | value: 1, 690 | 691 | addValue: function (value) { 692 | return atom.value + value; 693 | }, 694 | }; 695 | 696 | // good 697 | const atom = { 698 | ref, 699 | 700 | value: 1, 701 | 702 | addValue(value) { 703 | return atom.value + value; 704 | }, 705 | }; 706 | ``` 707 | 708 | 709 | 710 | ## 数组 711 | 712 | 使用扩展运算符(...)拷贝数组。 713 | 714 | ```js 715 | // bad 716 | const len = items.length; 717 | const itemsCopy = []; 718 | let i; 719 | 720 | for (i = 0; i < len; i++) { 721 | itemsCopy[i] = items[i]; 722 | } 723 | 724 | // good 725 | const itemsCopy = [...items]; 726 | ``` 727 | 728 | 使用 Array.from 方法,将类似数组的对象转为数组。 729 | 730 | ```js 731 | const foo = document.querySelectorAll('.foo'); 732 | const nodes = Array.from(foo); 733 | ``` 734 | 735 | 736 | 737 | ## 函数 738 | 739 | 立即执行函数可以写成箭头函数的形式。 740 | 741 | ```js 742 | (() => { 743 | console.log('Welcome to the Internet.'); 744 | })(); 745 | ``` 746 | 747 | 那些使用匿名函数当作参数的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了 this。 748 | 749 | ```js 750 | // bad 751 | [1, 2, 3].map(function (x) { 752 | return x * x; 753 | }); 754 | 755 | // good 756 | [1, 2, 3].map((x) => { 757 | return x * x; 758 | }); 759 | 760 | // best 761 | [1, 2, 3].map(x => x * x); 762 | ``` 763 | 764 | 箭头函数取代`Function.prototype.bind`,不应再用 self/_this/that 绑定 this。 765 | 766 | ```js 767 | // bad 768 | const self = this; 769 | const boundMethod = function(...params) { 770 | return method.apply(self, params); 771 | } 772 | 773 | // acceptable 774 | const boundMethod = method.bind(this); 775 | 776 | // best 777 | const boundMethod = (...params) => method.apply(this, params); 778 | ``` 779 | 780 | 简单的、单行的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法。 781 | 782 | 所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数。 783 | 784 | ```js 785 | // bad 786 | function divide(a, b, option = false ) { 787 | } 788 | 789 | // good 790 | function divide(a, b, { option = false } = {}) { 791 | } 792 | ``` 793 | 794 | 不要在函数体内使用 arguments 变量,使用 rest 运算符(...)代替。因为 rest 运算符显式表明你想要获取参数,而且 arguments 是一个类似数组的对象,而 rest 运算符可以提供一个真正的数组。 795 | 796 | ```js 797 | // bad 798 | function concatenateAll() { 799 | const args = Array.prototype.slice.call(arguments); 800 | return args.join(''); 801 | } 802 | 803 | // good 804 | function concatenateAll(...args) { 805 | return args.join(''); 806 | } 807 | ``` 808 | 809 | 使用默认值语法设置函数参数的默认值。 810 | 811 | ```js 812 | // bad 813 | function handleThings(opts) { 814 | opts = opts || {}; 815 | } 816 | 817 | // good 818 | function handleThings(opts = {}) { 819 | // ... 820 | } 821 | ``` 822 | 823 | 824 | 825 | ## Map 结构 826 | 827 | 注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要`key: value`的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。 828 | 829 | ```js 830 | let map = new Map(arr); 831 | 832 | for (let key of map.keys()) { 833 | console.log(key); 834 | } 835 | 836 | for (let value of map.values()) { 837 | console.log(value); 838 | } 839 | 840 | for (let item of map.entries()) { 841 | console.log(item[0], item[1]); 842 | } 843 | ``` 844 | 845 | 846 | 847 | ## Class 848 | 849 | 总是用 Class,取代需要 prototype 的操作。因为 Class 的写法更简洁,更易于理解。 850 | 851 | ```js 852 | // bad 853 | function Queue(contents = []) { 854 | this._queue = [...contents]; 855 | } 856 | Queue.prototype.pop = function() { 857 | const value = this._queue[0]; 858 | this._queue.splice(0, 1); 859 | return value; 860 | } 861 | 862 | // good 863 | class Queue { 864 | constructor(contents = []) { 865 | this._queue = [...contents]; 866 | } 867 | pop() { 868 | const value = this._queue[0]; 869 | this._queue.splice(0, 1); 870 | return value; 871 | } 872 | } 873 | ``` 874 | 875 | 使用`extends`实现继承,因为这样更简单,不会有破坏`instanceof`运算的危险。 876 | 877 | ```js 878 | // bad 879 | const inherits = require('inherits'); 880 | function PeekableQueue(contents) { 881 | Queue.apply(this, contents); 882 | } 883 | inherits(PeekableQueue, Queue); 884 | PeekableQueue.prototype.peek = function() { 885 | return this._queue[0]; 886 | } 887 | 888 | // good 889 | class PeekableQueue extends Queue { 890 | peek() { 891 | return this._queue[0]; 892 | } 893 | } 894 | ``` 895 | 896 | 897 | 898 | ## 模块 899 | 900 | 首先,Module 语法是 JavaScript 模块的标准写法,坚持使用这种写法。使用`import`取代`require`。 901 | 902 | ```js 903 | // bad 904 | const moduleA = require('moduleA'); 905 | const func1 = moduleA.func1; 906 | const func2 = moduleA.func2; 907 | 908 | // good 909 | import { func1, func2 } from 'moduleA'; 910 | ``` 911 | 912 | 使用`export`取代`module.exports`。 913 | 914 | ```js 915 | // commonJS的写法 916 | var React = require('react'); 917 | 918 | var Breadcrumbs = React.createClass({ 919 | render() { 920 | return