]
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 #