1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-24 18:32:22 +00:00

Merge branch 'gh-pages' into gh-pages

This commit is contained in:
zhangbao 2018-01-01 05:15:48 -06:00 committed by GitHub
commit e3f7e972e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 7802 additions and 5227 deletions

23
.gitignore vendored
View File

@ -1 +1,22 @@
npm-debug.log
git # OS X
Icon?
._*
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
# Linux
.directory
*~
# npm
node_modules
dist
*.gz
# webstorm
.idea/

0
.nojekyll Normal file
View File

View File

@ -2,20 +2,21 @@
《ECMAScript 6 入门》是一本开源的 JavaScript 语言教程,全面介绍 ECMAScript 6 新引入的语法特性。
[![cover](images/cover_thumbnail.jpg)](images/cover-2nd.jpg)
[![cover](images/cover_thumbnail_3rd.jpg)](images/cover-3rd.jpg)
本书覆盖 ES6/ES7 与 ES5 的所有不同之处,对涉及的语法知识给予详细介绍,并给出大量简洁易懂的示例代码。
本书覆盖 ES6 与上一个版本 ES5 的所有不同之处,对涉及的语法知识给予详细介绍,并给出大量简洁易懂的示例代码。
本书为中级难度,适合已经掌握 ES5 的读者,用来了解这门语言的最新发展;也可当作参考手册,查寻新增的语法点。
全书已由电子工业出版社出版,目前是第二书名为《ES6 标准入门》。纸版是基于网站内容排版印刷的。
全书已由电子工业出版社出版,2017年9月推出了第三书名为《ES6 标准入门》。纸版是基于网站内容排版印刷的。
感谢张春雨编辑支持我将全书开源的做法。如果您认可这本书,建议购买纸版。这样可以使出版社不因出版开源书籍而亏钱,进而鼓励更多的作者开源自己的书籍。
感谢张春雨编辑支持我将全书开源的做法。如果您认可这本书,建议购买纸版。这样可以使出版社不因出版开源书籍而亏钱,进而鼓励更多的作者开源自己的书籍。下面是第三版的购买地址。
- [京东](http://item.jd.com/11849235.html)
- [当当](http://product.dangdang.com/23840431.html)
- [亚马逊](http://www.amazon.cn/ES6-%E6%A0%87%E5%87%86%E5%85%A5%E9%97%A8-%E9%98%AE%E4%B8%80%E5%B3%B0/dp/B01A18WWAG/)
- [China-pub](http://product.china-pub.com/4904712)
- [淘宝](https://s.taobao.com/search?q=ES6%E6%A0%87%E5%87%86%E5%85%A5%E9%97%A8+%E7%AC%AC3%E7%89%88)
- [京东](https://search.jd.com/Search?keyword=ES6%E6%A0%87%E5%87%86%E5%85%A5%E9%97%A8%20%E7%AC%AC3%E7%89%88&enc=utf-8&wq=ES6%E6%A0%87%E5%87%86%E5%85%A5%E9%97%A8%20%E7%AC%AC3%E7%89%88)
- [当当](http://product.dangdang.com/25156888.html)
- [亚马逊](https://www.amazon.cn/ES6%E6%A0%87%E5%87%86%E5%85%A5%E9%97%A8-%E9%98%AE%E4%B8%80%E5%B3%B0/dp/B0755547ZZ)
- [China-pub](http://product.china-pub.com/6504650)
### 版权许可

View File

@ -120,6 +120,7 @@ input[type=search] {
height: 18px;
text-align: left;
border: none;
outline: none;
}
input.searchButton {

View File

@ -1,5 +1,325 @@
# 数组的扩展
## 扩展运算符
### 含义
扩展运算符spread是三个点`...`)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
```javascript
console.log(...[1, 2, 3])
// 1 2 3
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]
```
该运算符主要用于函数调用。
```javascript
function push(array, ...items) {
array.push(...items);
}
function add(x, y) {
return x + y;
}
const numbers = [4, 38];
add(...numbers) // 42
```
上面代码中,`array.push(...items)``add(...numbers)`这两行,都是函数的调用,它们的都使用了扩展运算符。该运算符将一个数组,变为参数序列。
扩展运算符与正常的函数参数可以结合使用,非常灵活。
```javascript
function f(v, w, x, y, z) { }
const args = [0, 1];
f(-1, ...args, 2, ...[3]);
```
扩展运算符后面还可以放置表达式。
```javascript
const arr = [
...(x > 0 ? ['a'] : []),
'b',
];
```
如果扩展运算符后面是一个空数组,则不产生任何效果。
```javascript
[...[], 1]
// [1]
```
### 替代数组的 apply 方法
由于扩展运算符可以展开数组,所以不再需要`apply`方法,将数组转为函数的参数了。
```javascript
// ES5 的写法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f.apply(null, args);
// ES6的写法
function f(x, y, z) {
// ...
}
let args = [0, 1, 2];
f(...args);
```
下面是扩展运算符取代`apply`方法的一个实际的例子,应用`Math.max`方法,简化求出一个数组最大元素的写法。
```javascript
// ES5 的写法
Math.max.apply(null, [14, 3, 77])
// ES6 的写法
Math.max(...[14, 3, 77])
// 等同于
Math.max(14, 3, 77);
```
上面代码中,由于 JavaScript 不提供求数组最大元素的函数,所以只能套用`Math.max`函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用`Math.max`了。
另一个例子是通过`push`函数,将一个数组添加到另一个数组的尾部。
```javascript
// ES5的 写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);
// ES6 的写法
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1.push(...arr2);
```
上面代码的 ES5 写法中,`push`方法的参数不能是数组,所以只好通过`apply`方法变通使用`push`方法。有了扩展运算符,就可以直接将数组传入`push`方法。
下面是另外一个例子。
```javascript
// ES5
new (Date.bind.apply(Date, [null, 2015, 1, 1]))
// ES6
new Date(...[2015, 1, 1]);
```
### 扩展运算符的应用
**1复制数组**
数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。
```javascript
const a1 = [1, 2];
const a2 = a1;
a2[0] = 2;
a1 // [2, 2]
```
上面代码中,`a2`并不是`a1`的克隆,而是指向同一份数据的另一个指针。修改`a2`,会直接导致`a1`的变化。
ES5 只能用变通方法来复制数组。
```javascript
const a1 = [1, 2];
const a2 = a1.concat();
a2[0] = 2;
a1 // [1, 2]
```
上面代码中,`a1`会返回原数组的克隆,再修改`a2`就不会对`a1`产生影响。
扩展运算符提供了复制数组的简便写法。
```javascript
const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;
```
上面的两种写法,`a2`都是`a1`的克隆。
**2合并数组**
扩展运算符提供了数组合并的新写法。
```javascript
// ES5
[1, 2].concat(more)
// ES6
[1, 2, ...more]
var arr1 = ['a', 'b'];
var arr2 = ['c'];
var arr3 = ['d', 'e'];
// ES5的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]
// ES6的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
```
**3与解构赋值结合**
扩展运算符可以与解构赋值结合起来,用于生成数组。
```javascript
// ES5
a = list[0], rest = list.slice(1)
// ES6
[a, ...rest] = list
```
下面是另外一些例子。
```javascript
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest // [2, 3, 4, 5]
const [first, ...rest] = [];
first // undefined
rest // []
const [first, ...rest] = ["foo"];
first // "foo"
rest // []
```
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
```javascript
const [...butLast, last] = [1, 2, 3, 4, 5];
// 报错
const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 报错
```
**4字符串**
扩展运算符还可以将字符串转为真正的数组。
```javascript
[...'hello']
// [ "h", "e", "l", "l", "o" ]
```
上面的写法,有一个重要的好处,那就是能够正确识别四个字节的 Unicode 字符。
```javascript
'x\uD83D\uDE80y'.length // 4
[...'x\uD83D\uDE80y'].length // 3
```
上面代码的第一种写法JavaScript 会将四个字节的 Unicode 字符,识别为 2 个字符,采用扩展运算符就没有这个问题。因此,正确返回字符串长度的函数,可以像下面这样写。
```javascript
function length(str) {
return [...str].length;
}
length('x\uD83D\uDE80y') // 3
```
凡是涉及到操作四个字节的 Unicode 字符的函数,都有这个问题。因此,最好都用扩展运算符改写。
```javascript
let str = 'x\uD83D\uDE80y';
str.split('').reverse().join('')
// 'y\uDE80\uD83Dx'
[...str].reverse().join('')
// 'y\uD83D\uDE80x'
```
上面代码中,如果不用扩展运算符,字符串的`reverse`操作就不正确。
**5实现了 Iterator 接口的对象**
任何 Iterator 接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组。
```javascript
let nodeList = document.querySelectorAll('div');
let array = [...nodeList];
```
上面代码中,`querySelectorAll`方法返回的是一个`nodeList`对象。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于`NodeList`对象实现了 Iterator 。
对于那些没有部署 Iterator 接口的类似数组的对象,扩展运算符就无法将其转为真正的数组。
```javascript
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// TypeError: Cannot spread non-iterable object.
let arr = [...arrayLike];
```
上面代码中,`arrayLike`是一个类似数组的对象,但是没有部署 Iterator 接口,扩展运算符就会报错。这时,可以改为使用`Array.from`方法将`arrayLike`转为真正的数组。
**6Map 和 Set 结构Generator 函数**
扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 Map 结构。
```javascript
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
let arr = [...map.keys()]; // [1, 2, 3]
```
Generator 函数运行后,返回一个遍历器对象,因此也可以使用扩展运算符。
```javascript
const go = function*(){
yield 1;
yield 2;
yield 3;
};
[...go()] // [1, 2, 3]
```
上面代码中,变量`go`是一个 Generator 函数,执行后返回的是一个遍历器对象,对这个遍历器对象执行扩展运算符,就会将内部遍历得到的值,转为一个数组。
如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错。
```javascript
const obj = {a: 1, b: 2};
let arr = [...obj]; // TypeError: Cannot spread non-iterable object
```
## Array.from()
`Array.from`方法用于将两类对象转为真正的数组类似数组的对象array-like object和可遍历iterable的对象包括 ES6 新增的数据结构 Set 和 Map
@ -26,8 +346,8 @@ let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
```javascript
// NodeList对象
let ps = document.querySelectorAll('p');
Array.from(ps).forEach(function (p) {
console.log(p);
Array.from(ps).filter(p => {
return p.textContent.length > 100;
});
// arguments对象
@ -37,7 +357,7 @@ function foo() {
}
```
上面代码中,`querySelectorAll`方法返回的是一个类似数组的对象,只有将这个对象转为真正的数组,才能使用`forEach`方法。
上面代码中,`querySelectorAll`方法返回的是一个类似数组的对象,可以将这个对象转为真正的数组,再使用`forEach`方法。
只要是部署了 Iterator 接口的数据结构,`Array.from`都能将其转为数组。
@ -63,14 +383,14 @@ Array.from([1, 2, 3])
```javascript
// arguments对象
function foo() {
var args = [...arguments];
const args = [...arguments];
}
// NodeList对象
[...document.querySelectorAll('div')]
```
扩展运算符背后调用的是遍历器接口(`Symbol.iterator`),如果一个对象没有部署这个接口,就无法转换。`Array.from`方法则是还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有`length`属性。因此,任何有`length`属性的对象,都可以通过`Array.from`方法转为数组,而此时扩展运算符就无法转换。
扩展运算符背后调用的是遍历器接口(`Symbol.iterator`),如果一个对象没有部署这个接口,就无法转换。`Array.from`方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有`length`属性。因此,任何有`length`属性的对象,都可以通过`Array.from`方法转为数组,而此时扩展运算符就无法转换。
```javascript
Array.from({ length: 3 });
@ -224,7 +544,7 @@ Array.prototype.copyWithin(target, start = 0, end = this.length)
// {0: 1, 3: 1, length: 5}
// 将2号位到数组结束复制到0号位
var i32a = new Int32Array([1, 2, 3, 4, 5]);
let i32a = new Int32Array([1, 2, 3, 4, 5]);
i32a.copyWithin(0, 2);
// Int32Array [3, 4, 5, 4, 5]
@ -263,7 +583,7 @@ i32a.copyWithin(0, 2);
这两个方法都可以接受第二个参数,用来绑定回调函数的`this`对象。
另外,这两个方法都可以发现`NaN`,弥补了数组的`IndexOf`方法的不足。
另外,这两个方法都可以发现`NaN`,弥补了数组的`indexOf`方法的不足。
```javascript
[NaN].indexOf(NaN)
@ -334,15 +654,15 @@ console.log(entries.next().value); // [2, 'c']
## 数组实例的 includes()
`Array.prototype.includes`方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的`includes`方法类似。该方法属于ES7但Babel转码器已经支持
`Array.prototype.includes`方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的`includes`方法类似。ES2016 引入了该方法。
```javascript
[1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false
[1, 2, NaN].includes(NaN); // true
[1, 2, 3].includes(2) // true
[1, 2, 3].includes(4) // false
[1, 2, NaN].includes(NaN) // true
```
该方法的第二个参数表示搜索的起始位置默认为0。如果第二个参数为负数则表示倒数的位置如果这时它大于数组长度比如第二个参数为-4但数组长度为3则会重置为从0开始。
该方法的第二个参数表示搜索的起始位置,默认为`0`。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为`-4`,但数组长度为`3`),则会重置为从`0`开始。
```javascript
[1, 2, 3].includes(3, 3); // false
@ -357,7 +677,7 @@ if (arr.indexOf(el) !== -1) {
}
```
`indexOf`方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1表达起来不够直观。二是它内部使用严格相当运算符(===)进行判断,这会导致对`NaN`的误判。
`indexOf`方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于`-1`,表达起来不够直观。二是,它内部使用严格相等运算符(`===`)进行判断,这会导致对`NaN`的误判。
```javascript
[NaN].indexOf(NaN)
@ -379,7 +699,7 @@ const contains = (() =>
? (arr, value) => arr.includes(value)
: (arr, value) => arr.some(el => el === value)
)();
contains(["foo", "bar"], "baz"); // => false
contains(['foo', 'bar'], 'baz'); // => false
```
另外Map 和 Set 数据结构有一个`has`方法,需要注意与`includes`区分。
@ -408,7 +728,7 @@ Array(3) // [, , ,]
ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。
- `forEach()`, `filter()`, `every()` 和`some()`都会跳过空位。
- `forEach()`, `filter()`, `reduce()`, `every()` 和`some()`都会跳过空位。
- `map()`会跳过空位,但会保留这个值
- `join()``toString()`会将空位视为`undefined`,而`undefined``null`会被处理成空字符串。
@ -422,6 +742,9 @@ ES5对空位的处理已经很不一致了大多数情况下会忽略空
// every方法
[,'a'].every(x => x==='a') // true
// reduce方法
[1,,2].reduce((x,y) => return x+y) // 3
// some方法
[,'a'].some(x => x !== 'a') // false
@ -496,4 +819,3 @@ for (let i of arr) {
```
由于空位的处理规则非常不统一,所以建议避免出现空位。

View File

@ -1,6 +1,6 @@
# 二进制数组
# ArrayBuffer
二进制数组(`ArrayBuffer`对象、TypedArray视图和`DataView`视图是JavaScript操作二进制数据的一个接口。这些对象早就存在属于独立的规格2011年2月发布ES6将它们纳入了ECMAScript规格并且增加了新的方法。
`ArrayBuffer`对象、`TypedArray`视图和`DataView`视图是 JavaScript 操作二进制数据的一个接口。这些对象早就存在属于独立的规格2011 2 月发布ES6 将它们纳入了 ECMAScript 规格,并且增加了新的方法。它们都是以数组的语法处理二进制数据,所以统称为二进制数组。
这个接口的原始设计目的,与 WebGL 项目有关。所谓 WebGL就是指浏览器与显卡之间的通信接口为了满足 JavaScript 与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式。文本格式传递一个 32 位整数,两端的 JavaScript 脚本与显卡都要进行格式转化,将非常耗时。这时要是存在一种机制,可以像 C 语言那样,直接操作字节,将 4 个字节的 32 位整数,以二进制形式原封不动地送入显卡,脚本的性能就会大幅提升。
@ -10,7 +10,7 @@
**1`ArrayBuffer`对象**:代表内存之中的一段二进制数据,可以通过“视图”进行操作。“视图”部署了数组接口,这意味着,可以用数组的方法操作内存。
**2TypedArray视图**共包括9种类型的视图比如`Uint8Array`无符号8位整数数组视图, `Int16Array`16位整数数组视图, `Float32Array`32位浮点数数组视图等等。
**2`TypedArray`视图**:共包括 9 种类型的视图,比如`Uint8Array`(无符号 8 位整数)数组视图, `Int16Array`16 位整数)数组视图, `Float32Array`32 位浮点数)数组视图等等。
**3`DataView`视图**:可以自定义复合格式的视图,比如第一个字节是 Uint8无符号 8 位整数)、第二、三个字节是 Int1616 位整数)、第四个字节开始是 Float3232 位浮点数)等等,此外还可以自定义字节序。
@ -18,17 +18,17 @@
TypedArray 视图支持的数据类型一共有 9 种(`DataView`视图支持除`Uint8C`以外的其他 8 种)。
数据类型 | 字节长度 | 含义 | 对应的C语言类型
--------|--------|----|---------------
Int8|1|8位带符号整数|signed char
Uint8|1|8位不带符号整数|unsigned char
Uint8C|1|8位不带符号整数自动过滤溢出|unsigned char
Int16|2|16位带符号整数|short
Uint16|2|16位不带符号整数|unsigned short
Int32|4|32位带符号整数|int
Uint32|4|32位不带符号的整数|unsigned int
Float32|4|32位浮点数|float
Float64|8|64位浮点数|double
| 数据类型 | 字节长度 | 含义 | 对应的 C 语言类型 |
| -------- | -------- | -------------------------------- | ----------------- |
| Int8 | 1 | 8 位带符号整数 | signed char |
| Uint8 | 1 | 8 位不带符号整数 | unsigned char |
| Uint8C | 1 | 8 位不带符号整数(自动过滤溢出) | unsigned char |
| Int16 | 2 | 16 位带符号整数 | short |
| Uint16 | 2 | 16 位不带符号整数 | unsigned short |
| Int32 | 4 | 32 位带符号整数 | int |
| Uint32 | 4 | 32 位不带符号的整数 | unsigned int |
| Float32 | 4 | 32 位浮点数 | float |
| Float64 | 8 | 64 位浮点数 | double |
注意,二进制数组并不是真正的数组,而是类似数组的对象。
@ -44,12 +44,12 @@ Float64|8|64位浮点数|double
### 概述
`ArrayBuffer`对象代表储存二进制数据的一段内存它不能直接读写只能通过视图TypedArray视图和`DataView`视图)来读写,视图的作用是以指定格式解读二进制数据。
`ArrayBuffer`对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(`TypedArray`视图和`DataView`视图)来读写,视图的作用是以指定格式解读二进制数据。
`ArrayBuffer`也是一个构造函数,可以分配一段可以存放数据的连续内存区域。
```javascript
var buf = new ArrayBuffer(32);
const buf = new ArrayBuffer(32);
```
上面代码生成了一段 32 字节的内存区域,每个字节的值默认都是 0。可以看到`ArrayBuffer`构造函数的参数是所需要的内存大小(单位字节)。
@ -57,21 +57,21 @@ var buf = new ArrayBuffer(32);
为了读写这段内容,需要为它指定视图。`DataView`视图的创建,需要提供`ArrayBuffer`对象实例作为参数。
```javascript
var buf = new ArrayBuffer(32);
var dataView = new DataView(buf);
const buf = new ArrayBuffer(32);
const dataView = new DataView(buf);
dataView.getUint8(0) // 0
```
上面代码对一段32字节的内存建立`DataView`视图,然后以不带符号的8位整数格式读取第一个元素结果得到0因为原始内存的`ArrayBuffer`对象默认所有位都是0。
上面代码对一段 32 字节的内存,建立`DataView`视图,然后以不带符号的 8 位整数格式,从头读取 8 位二进制数据,结果得到 0因为原始内存的`ArrayBuffer`对象,默认所有位都是 0。
另一种 TypedArray 视图,与`DataView`视图的一个区别是,它不是一个构造函数,而是一组构造函数,代表不同的数据格式。
```javascript
var buffer = new ArrayBuffer(12);
const buffer = new ArrayBuffer(12);
var x1 = new Int32Array(buffer);
const x1 = new Int32Array(buffer);
x1[0] = 1;
var x2 = new Uint8Array(buffer);
const x2 = new Uint8Array(buffer);
x2[0] = 2;
x1[0] // 2
@ -82,7 +82,7 @@ x1[0] // 2
TypedArray 视图的构造函数,除了接受`ArrayBuffer`实例作为参数,还可以接受普通数组作为参数,直接分配内存生成底层的`ArrayBuffer`实例,并同时完成对这段内存的赋值。
```javascript
var typedArray = new Uint8Array([0,1,2]);
const typedArray = new Uint8Array([0,1,2]);
typedArray.length // 3
typedArray[0] = 5;
@ -96,7 +96,7 @@ typedArray // [5, 1, 2]
`ArrayBuffer`实例的`byteLength`属性,返回所分配的内存区域的字节长度。
```javascript
var buffer = new ArrayBuffer(32);
const buffer = new ArrayBuffer(32);
buffer.byteLength
// 32
```
@ -116,8 +116,8 @@ if (buffer.byteLength === n) {
`ArrayBuffer`实例有一个`slice`方法,允许将内存区域的一部分,拷贝生成一个新的`ArrayBuffer`对象。
```javascript
var buffer = new ArrayBuffer(8);
var newBuffer = buffer.slice(0, 3);
const buffer = new ArrayBuffer(8);
const newBuffer = buffer.slice(0, 3);
```
上面代码拷贝`buffer`对象的前 3 个字节(从 0 开始,到第 3 个字节前面结束),生成一个新的`ArrayBuffer`对象。`slice`方法其实包含两步,第一步是先分配一段新内存,第二步是将原来那个`ArrayBuffer`对象拷贝过去。
@ -131,10 +131,10 @@ var newBuffer = buffer.slice(0, 3);
`ArrayBuffer`有一个静态方法`isView`,返回一个布尔值,表示参数是否为`ArrayBuffer`的视图实例。这个方法大致相当于判断参数,是否为 TypedArray 实例或`DataView`实例。
```javascript
var buffer = new ArrayBuffer(8);
const buffer = new ArrayBuffer(8);
ArrayBuffer.isView(buffer) // false
var v = new Int32Array(buffer);
const v = new Int32Array(buffer);
ArrayBuffer.isView(v) // true
```
@ -175,16 +175,16 @@ TypedArray数组提供9种构造函数用来生成相应类型的数组实例
```javascript
// 创建一个8字节的ArrayBuffer
var b = new ArrayBuffer(8);
const b = new ArrayBuffer(8);
// 创建一个指向b的Int32视图开始于字节0直到缓冲区的末尾
var v1 = new Int32Array(b);
const v1 = new Int32Array(b);
// 创建一个指向b的Uint8视图开始于字节2直到缓冲区的末尾
var v2 = new Uint8Array(b, 2);
const v2 = new Uint8Array(b, 2);
// 创建一个指向b的Int16视图开始于字节2长度为2
var v3 = new Int16Array(b, 2, 2);
const v3 = new Int16Array(b, 2, 2);
```
上面代码在一段长度为 8 个字节的内存(`b`)之上,生成了三个视图:`v1``v2``v3`
@ -200,8 +200,8 @@ var v3 = new Int16Array(b, 2, 2);
注意,`byteOffset`必须与所要建立的数据类型一致,否则会报错。
```javascript
var buffer = new ArrayBuffer(8);
var i16 = new Int16Array(buffer, 1);
const buffer = new ArrayBuffer(8);
const i16 = new Int16Array(buffer, 1);
// Uncaught RangeError: start offset of Int16Array should be a multiple of 2
```
@ -214,7 +214,7 @@ var i16 = new Int16Array(buffer, 1);
视图还可以不通过`ArrayBuffer`对象,直接分配内存而生成。
```javascript
var f64a = new Float64Array(8);
const f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];
@ -227,7 +227,7 @@ f64a[2] = f64a[0] + f64a[1];
TypedArray 数组的构造函数,可以接受另一个 TypedArray 实例作为参数。
```javascript
var typedArray = new Int8Array(new Uint8Array(4));
const typedArray = new Int8Array(new Uint8Array(4));
```
上面代码中,`Int8Array`构造函数接受一个`Uint8Array`实例作为参数。
@ -235,8 +235,8 @@ var typedArray = new Int8Array(new Uint8Array(4));
注意,此时生成的新数组,只是复制了参数数组的值,对应的底层内存是不一样的。新数组会开辟一段新的内存储存数据,不会在原数组的内存之上建立视图。
```javascript
var x = new Int8Array([1, 1]);
var y = new Int8Array(x);
const x = new Int8Array([1, 1]);
const y = new Int8Array(x);
x[0] // 1
y[0] // 1
@ -249,8 +249,8 @@ y[0] // 1
如果想基于同一段内存,构造不同的视图,可以采用下面的写法。
```javascript
var x = new Int8Array([1, 1]);
var y = new Int8Array(x.buffer);
const x = new Int8Array([1, 1]);
const y = new Int8Array(x.buffer);
x[0] // 1
y[0] // 1
@ -263,7 +263,7 @@ y[0] // 2
构造函数的参数也可以是一个普通数组,然后直接生成 TypedArray 实例。
```javascript
var typedArray = new Uint8Array([1, 2, 3, 4]);
const typedArray = new Uint8Array([1, 2, 3, 4]);
```
注意,这时 TypedArray 视图会重新开辟内存,不会在原数组的内存上建立视图。
@ -273,7 +273,11 @@ var typedArray = new Uint8Array([1, 2, 3, 4]);
TypedArray 数组也可以转换回普通数组。
```javascript
var normalArray = Array.prototype.slice.call(typedArray);
const normalArray = [...typedArray];
// or
const normalArray = Array.from(typedArray);
// or
const normalArray = Array.prototype.slice.call(typedArray);
```
### 数组方法
@ -343,10 +347,10 @@ for (let byte of ui8) {
字节序指的是数值在内存中的表示方式。
```javascript
var buffer = new ArrayBuffer(16);
var int32View = new Int32Array(buffer);
const buffer = new ArrayBuffer(16);
const int32View = new Int32Array(buffer);
for (var i = 0; i < int32View.length; i++) {
for (let i = 0; i < int32View.length; i++) {
int32View[i] = i * 2;
}
```
@ -356,9 +360,9 @@ for (var i = 0; i < int32View.length; i++) {
如果在这段数据上接着建立一个 16 位整数的视图,则可以读出完全不一样的结果。
```javascript
var int16View = new Int16Array(buffer);
const int16View = new Int16Array(buffer);
for (var i = 0; i < int16View.length; i++) {
for (let i = 0; i < int16View.length; i++) {
console.log("Entry " + i + ": " + int16View[i]);
}
// Entry 0: 0
@ -381,14 +385,14 @@ for (var i = 0; i < int16View.length; i++) {
```javascript
// 假定某段buffer包含如下字节 [0x02, 0x01, 0x03, 0x07]
var buffer = new ArrayBuffer(4);
var v1 = new Uint8Array(buffer);
const buffer = new ArrayBuffer(4);
const v1 = new Uint8Array(buffer);
v1[0] = 2;
v1[1] = 1;
v1[2] = 3;
v1[3] = 7;
var uInt16View = new Uint16Array(buffer);
const uInt16View = new Uint16Array(buffer);
// 计算机采用小端字节序
// 所以头两个字节等于258
@ -448,14 +452,16 @@ Float64Array.BYTES_PER_ELEMENT // 8
```javascript
// ArrayBuffer 转为字符串,参数为 ArrayBuffer 对象
function ab2str(buf) {
// 注意,如果是大型二进制数组,为了避免溢出,
// 必须一个一个字符地转
return String.fromCharCode.apply(null, new Uint16Array(buf));
}
// 字符串转为 ArrayBuffer 对象,参数为字符串
function str2ab(str) {
var buf = new ArrayBuffer(str.length * 2); // 每个字符占用2个字节
var bufView = new Uint16Array(buf);
for (var i = 0, strLen = str.length; i < strLen; i++) {
const buf = new ArrayBuffer(str.length * 2); // 每个字符占用2个字节
const bufView = new Uint16Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
@ -469,7 +475,7 @@ function str2ab(str) {
TypedArray 数组的溢出处理规则,简单来说,就是抛弃溢出的位,然后按照视图类型进行解释。
```javascript
var uint8 = new Uint8Array(1);
const uint8 = new Uint8Array(1);
uint8[0] = 256;
uint8[0] // 0
@ -487,10 +493,19 @@ uint8[0] // 255
- 正向溢出overflow当输入值大于当前数据类型的最大值结果等于当前数据类型的最小值加上余值再减去 1。
- 负向溢出underflow当输入值小于当前数据类型的最小值结果等于当前数据类型的最大值减去余值再加上 1。
上面的“余值”就是模运算的结果,即 JavaScript 里面的`%`运算符的结果。
```javascript
12 % 4 // 0
12 % 5 // 2
```
上面代码中12 除以 4 是没有余值的,而除以 5 会得到余值 2。
请看下面的例子。
```javascript
var int8 = new Int8Array(1);
const int8 = new Int8Array(1);
int8[0] = 128;
int8[0] // -128
@ -499,12 +514,12 @@ int8[0] = -129;
int8[0] // 127
```
上面例子中,`int8`是一个带符号的8位整数视图它的最大值是127最小值是-128。输入值为`128`时,相当于正向溢出`1`根据“最小值加上余值再减去1”的规则就会返回`-128`;输入值为`-129`时,相当于负向溢出`1`根据“最大值减去余值再加上1”的规则就会返回`127`
上面例子中,`int8`是一个带符号的 8 位整数视图,它的最大值是 127最小值是-128。输入值为`128`时,相当于正向溢出`1`,根据“最小值加上余值128 除以 127 的余值是 1,再减去 1”的规则就会返回`-128`;输入值为`-129`时,相当于负向溢出`1`,根据“最大值减去余值-129 除以-128 的余值是 1,再加上 1”的规则就会返回`127`
`Uint8ClampedArray`视图的溢出规则,与上面的规则不同。它规定,凡是发生正向溢出,该值一律等于当前数据类型的最大值,即 255如果发生负向溢出该值一律等于当前数据类型的最小值即 0。
```javascript
var uint8c = new Uint8ClampedArray(1);
const uint8c = new Uint8ClampedArray(1);
uint8c[0] = 256;
uint8c[0] // 255
@ -520,8 +535,8 @@ uint8c[0] // 0
TypedArray 实例的`buffer`属性,返回整段内存区域对应的`ArrayBuffer`对象。该属性为只读属性。
```javascript
var a = new Float32Array(64);
var b = new Uint8Array(a.buffer);
const a = new Float32Array(64);
const b = new Uint8Array(a.buffer);
```
上面代码的`a`视图对象和`b`视图对象,对应同一个`ArrayBuffer`对象,即同一段内存。
@ -531,11 +546,11 @@ var b = new Uint8Array(a.buffer);
`byteLength`属性返回 TypedArray 数组占据的内存长度,单位为字节。`byteOffset`属性返回 TypedArray 数组从底层`ArrayBuffer`对象的哪个字节开始。这两个属性都是只读属性。
```javascript
var b = new ArrayBuffer(8);
const b = new ArrayBuffer(8);
var v1 = new Int32Array(b);
var v2 = new Uint8Array(b, 2);
var v3 = new Int16Array(b, 2, 2);
const v1 = new Int32Array(b);
const v2 = new Uint8Array(b, 2);
const v3 = new Int16Array(b, 2, 2);
v1.byteLength // 8
v2.byteLength // 6
@ -551,7 +566,7 @@ v3.byteOffset // 2
`length`属性表示 TypedArray 数组含有多少个成员。注意将`byteLength`属性和`length`属性区分,前者是字节长度,后者是成员长度。
```javascript
var a = new Int16Array(8);
const a = new Int16Array(8);
a.length // 8
a.byteLength // 16
@ -562,8 +577,8 @@ a.byteLength // 16
TypedArray 数组的`set`方法用于复制数组(普通数组或 TypedArray 数组),也就是将一段内容完全复制到另一段内存。
```javascript
var a = new Uint8Array(8);
var b = new Uint8Array(8);
const a = new Uint8Array(8);
const b = new Uint8Array(8);
b.set(a);
```
@ -573,8 +588,8 @@ b.set(a);
`set`方法还可以接受第二个参数,表示从`b`对象的哪一个成员开始复制`a`对象。
```javascript
var a = new Uint16Array(8);
var b = new Uint16Array(10);
const a = new Uint16Array(8);
const b = new Uint16Array(10);
b.set(a, 2)
```
@ -586,8 +601,8 @@ b.set(a, 2)
`subarray`方法是对于 TypedArray 数组的一部分,再建立一个新的视图。
```javascript
var a = new Uint16Array(8);
var b = a.subarray(2,3);
const a = new Uint16Array(8);
const b = a.subarray(2,3);
a.byteLength // 16
b.byteLength // 2
@ -646,7 +661,7 @@ Uint16Array.from([0, 1, 2])
这个方法还可以将一种 TypedArray 实例,转为另一种。
```javascript
var ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2));
const ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2));
ui16 instanceof Uint16Array // true
```
@ -667,11 +682,11 @@ Int16Array.from(Int8Array.of(127, 126, 125), x => 2 * x)
由于视图的构造函数可以指定起始位置和长度,所以在同一段内存之中,可以依次存放不同类型的数据,这叫做“复合视图”。
```javascript
var buffer = new ArrayBuffer(24);
const buffer = new ArrayBuffer(24);
var idView = new Uint32Array(buffer, 0, 1);
var usernameView = new Uint8Array(buffer, 4, 16);
var amountDueView = new Float32Array(buffer, 20, 1);
const idView = new Uint32Array(buffer, 0, 1);
const usernameView = new Uint8Array(buffer, 4, 16);
const amountDueView = new Float32Array(buffer, 20, 1);
```
上面代码将一个 24 字节长度的`ArrayBuffer`对象,分成三个部分:
@ -705,8 +720,8 @@ DataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]);
下面是一个例子。
```javascript
var buffer = new ArrayBuffer(24);
var dv = new DataView(buffer);
const buffer = new ArrayBuffer(24);
const dv = new DataView(buffer);
```
`DataView`实例有以下属性,含义与 TypedArray 实例的同名方法相同。
@ -729,17 +744,17 @@ var dv = new DataView(buffer);
这一系列`get`方法的参数都是一个字节序号(不能是负数,否则会报错),表示从哪个字节开始读取。
```javascript
var buffer = new ArrayBuffer(24);
var dv = new DataView(buffer);
const buffer = new ArrayBuffer(24);
const dv = new DataView(buffer);
// 从第1个字节读取一个8位无符号整数
var v1 = dv.getUint8(0);
const v1 = dv.getUint8(0);
// 从第2个字节读取一个16位无符号整数
var v2 = dv.getUint16(1);
const v2 = dv.getUint16(1);
// 从第4个字节读取一个16位无符号整数
var v3 = dv.getUint16(3);
const v3 = dv.getUint16(3);
```
上面代码读取了`ArrayBuffer`对象的前 5 个字节,其中有一个 8 位整数和两个十六位整数。
@ -748,13 +763,13 @@ var v3 = dv.getUint16(3);
```javascript
// 小端字节序
var v1 = dv.getUint16(1, true);
const v1 = dv.getUint16(1, true);
// 大端字节序
var v2 = dv.getUint16(3, false);
const v2 = dv.getUint16(3, false);
// 大端字节序
var v3 = dv.getUint16(3);
const v3 = dv.getUint16(3);
```
DataView 视图提供 8 个方法写入内存。
@ -784,8 +799,8 @@ dv.setFloat32(8, 2.5, true);
如果不确定正在使用的计算机的字节序,可以采用下面的判断方式。
```javascript
var littleEndian = (function() {
var buffer = new ArrayBuffer(2);
const littleEndian = (function() {
const buffer = new ArrayBuffer(2);
new DataView(buffer).setInt16(0, 256, true);
return new Int16Array(buffer)[0] === 256;
})();
@ -802,7 +817,7 @@ var littleEndian = (function() {
传统上,服务器通过 AJAX 操作只能返回文本数据,即`responseType`属性默认为`text``XMLHttpRequest`第二版`XHR2`允许服务器返回二进制数据,这时分成两种情况。如果明确知道返回的二进制数据类型,可以把返回类型(`responseType`)设为`arraybuffer`;如果不知道,就设为`blob`
```javascript
var xhr = new XMLHttpRequest();
let xhr = new XMLHttpRequest();
xhr.open('GET', someUrl);
xhr.responseType = 'arraybuffer';
@ -819,9 +834,9 @@ xhr.send();
```javascript
xhr.onreadystatechange = function () {
if (req.readyState === 4 ) {
var arrayResponse = xhr.response;
var dataView = new DataView(arrayResponse);
var ints = new Uint32Array(dataView.byteLength / 4);
const arrayResponse = xhr.response;
const dataView = new DataView(arrayResponse);
const ints = new Uint32Array(dataView.byteLength / 4);
xhrDiv.style.backgroundColor = "#00FF00";
xhrDiv.innerText = "Array is " + ints.length + "uints long";
@ -834,11 +849,11 @@ xhr.onreadystatechange = function () {
网页`Canvas`元素输出的二进制像素数据,就是 TypedArray 数组。
```javascript
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var uint8ClampedArray = imageData.data;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const uint8ClampedArray = imageData.data;
```
需要注意的是,上面代码的`uint8ClampedArray`虽然是一个 TypedArray 数组,但是它的视图类型是一种针对`Canvas`元素的专有类型`Uint8ClampedArray`。这个视图类型的特点,就是专门针对颜色,把每个字节解读为无符号的 8 位整数,即只能取值 0 255而且发生运算的时候自动过滤高位溢出。这为图像处理带来了巨大的方便。
@ -862,19 +877,19 @@ pixels[i] *= gamma;
`WebSocket`可以通过`ArrayBuffer`,发送或接收二进制数据。
```javascript
var socket = new WebSocket('ws://127.0.0.1:8081');
let socket = new WebSocket('ws://127.0.0.1:8081');
socket.binaryType = 'arraybuffer';
// Wait until socket is open
socket.addEventListener('open', function (event) {
// Send binary data
var typedArray = new Uint8Array(4);
const typedArray = new Uint8Array(4);
socket.send(typedArray.buffer);
});
// Receive binary data
socket.addEventListener('message', function (event) {
var arrayBuffer = event.data;
const arrayBuffer = event.data;
// ···
});
```
@ -885,8 +900,8 @@ Fetch API取回的数据就是`ArrayBuffer`对象。
```javascript
fetch(url)
.then(function(request){
return request.arrayBuffer()
.then(function(response){
return response.arrayBuffer()
})
.then(function(arrayBuffer){
// ...
@ -898,12 +913,12 @@ fetch(url)
如果知道一个文件的二进制数据类型,也可以将这个文件读取为`ArrayBuffer`对象。
```javascript
var fileInput = document.getElementById('fileInput');
var file = fileInput.files[0];
var reader = new FileReader();
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function () {
var arrayBuffer = reader.result;
const arrayBuffer = reader.result;
// ···
};
```
@ -911,7 +926,7 @@ reader.onload = function () {
下面以处理 bmp 文件为例。假定`file`变量是一个指向 bmp 文件的文件对象,首先读取文件。
```javascript
var reader = new FileReader();
const reader = new FileReader();
reader.addEventListener("load", processimage, false);
reader.readAsArrayBuffer(file);
```
@ -920,9 +935,9 @@ reader.readAsArrayBuffer(file);
```javascript
function processimage(e) {
var buffer = e.target.result;
var datav = new DataView(buffer);
var bitmap = {};
const buffer = e.target.result;
const datav = new DataView(buffer);
const bitmap = {};
// 具体的处理步骤
}
```
@ -958,7 +973,7 @@ bitmap.infoheader.biClrImportant = datav.getUint32(50, true);
最后处理图像本身的像素信息。
```javascript
var start = bitmap.fileheader.bfOffBits;
const start = bitmap.fileheader.bfOffBits;
bitmap.pixels = new Uint8Array(buffer, start);
```
@ -966,85 +981,247 @@ bitmap.pixels = new Uint8Array(buffer, start);
## SharedArrayBuffer
目前有一种场景需要多个进程共享数据浏览器启动多个WebWorker
JavaScript 是单线程的Web worker 引入了多线程主线程用来与用户互动Worker 线程用来承担计算任务。每个线程的数据都是隔离的,通过`postMessage()`通信。下面是一个例子
```javascript
var w = new Worker('myworker.js');
// 主线程
const w = new Worker('myworker.js');
```
上面代码中,主窗口新建了一个 Worker 进程。该进程与主窗口之间会有一个通信渠道,主窗口通过`w.postMessage`向 Worker 进程发消息,同时通过`message`事件监听 Worker 进程的回应。
上面代码中,主线程新建了一个 Worker 线程。该线程与主线程之间会有一个通信渠道,主线程通过`w.postMessage`向 Worker 线程发消息,同时通过`message`事件监听 Worker 线程的回应。
```javascript
// 主线程
w.postMessage('hi');
w.onmessage = function (ev) {
console.log(ev.data);
}
```
上面代码中,主窗口先发一个消息`hi`,然后在监听到 Worker 进程的回应后,就将其打印出来。
上面代码中,主线程先发一个消息`hi`,然后在监听到 Worker 线程的回应后,就将其打印出来。
Worker 进程也是通过监听`message`事件,来获取主窗口发来的消息,并作出反应。
Worker 线程也是通过监听`message`事件,来获取主线程发来的消息,并作出反应。
```javascript
// Worker 线程
onmessage = function (ev) {
console.log(ev.data);
postMessage('ho');
}
```
主窗口与 Worker 进程之间,可以传送各种数据,不仅仅是字符串,还可以传送二进制数据。很容易想到,如果有大量数据要传送,留出一块内存区域,主窗口与 Worker 进程共享,两方都可以读写,那么就会大大提高效率。
线程之间的数据交换可以是各种格式,不仅仅是字符串,也可以是二进制数据。这种交换采用的是复制机制,即一个进程将需要分享的数据复制一份,通过`postMessage`方法交给另一个进程。如果数据量比较大,这种通信的效率显然比较低。很容易想到,这时可以留出一块内存区域,由主线程与 Worker 线程共享,两方都可以读写,那么就会大大提高效率,协作起来也会比较简单(不像`postMessage`那么麻烦)
现在,有一个[`SharedArrayBuffer`](https://github.com/tc39/ecmascript_sharedmem/blob/master/TUTORIAL.md)提案,允许多个 Worker 进程与主窗口共享内存。这个对象的 API 与`ArrayBuffer`一模一样,唯一的区别是后者无法共享。
ES2017 引入[`SharedArrayBuffer`](https://github.com/tc39/ecmascript_sharedmem/blob/master/TUTORIAL.md),允许 Worker 线程与主线程共享同一块内存。`SharedArrayBuffer`的 API 与`ArrayBuffer`一模一样,唯一的区别是后者无法共享。
```javascript
// 新建 1KB 共享内存
var sab = new SharedArrayBuffer(1024);
// 主线程
// 主窗口发送数据
w.postMessage(sab, [sab])
// 新建 1KB 共享内存
const sharedBuffer = new SharedArrayBuffer(1024);
// 主线程将共享内存的地址发送出去
w.postMessage(sharedBuffer);
// 在共享内存上建立视图,供写入数据
const sharedArray = new Int32Array(sharedBuffer);
```
上面代码中,`postMessage`方法的第一个参数是`SharedArrayBuffer`对象,第二个参数是要写入共享内存的数据。
上面代码中,`postMessage`方法的参数是`SharedArrayBuffer`对象。
Worker 进程从事件的`data`属性上面取到数据。
Worker 线程从事件的`data`属性上面取到数据。
```javascript
var sab;
// Worker 线程
onmessage = function (ev) {
sab = ev.data; // 1KB 的共享内存,就是主窗口共享出来的那块内存
// 主线程共享的数据,就是 1KB 的共享内存
const sharedBuffer = ev.data;
// 在共享内存上建立视图,方便读写
const sharedArray = new Int32Array(sharedBuffer);
// ...
};
```
共享内存也可以在 Worker 进程创建,发给主窗口。
共享内存也可以在 Worker 线程创建,发给主线程
`SharedArrayBuffer``SharedArray`一样,本身是无法读写,必须在上面建立视图,然后通过视图读写。
`SharedArrayBuffer``ArrayBuffer`一样,本身是无法读写,必须在上面建立视图,然后通过视图读写。
```javascript
// 分配 10 万个 32 位整数占据的内存空间
var sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100000);
const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100000);
// 建立 32 位整数视图
var ia = new Int32Array(sab); // ia.length == 100000
const ia = new Int32Array(sab); // ia.length == 100000
// 新建一个质数生成器
var primes = new PrimeGenerator();
const primes = new PrimeGenerator();
// 将 10 万个质数,写入这段内存空间
for ( let i=0 ; i < ia.length ; i++ )
ia[i] = primes.next();
// 向 Worker 程发送这段共享内存
w.postMessage(ia, [ia.buffer]);
// 向 Worker 线程发送这段共享内存
w.postMessage(ia);
```
Worker 收到数据后的处理如下。
Worker 线程收到数据后的处理如下。
```javascript
var ia;
// Worker 线程
let ia;
onmessage = function (ev) {
ia = ev.data;
console.log(ia.length); // 100000
console.log(ia[37]); // 输出 163因为这是第138个质数
console.log(ia[37]); // 输出 163因为这是第38个质数
};
```
## Atomics 对象
多线程共享内存最大的问题就是如何防止两个线程同时修改某个地址或者说当一个线程修改共享内存以后必须有一个机制让其他线程同步。SharedArrayBuffer API 提供`Atomics`对象,保证所有共享内存的操作都是“原子性”的,并且可以在所有线程内同步。
什么叫“原子性操作”呢?现代编程语言中,一条普通的命令被编译器处理以后,会变成多条机器指令。如果是单线程运行,这是没有问题的;多线程环境并且共享内存时,就会出问题,因为这一组机器指令的运行期间,可能会插入其他线程的指令,从而导致运行结果出错。请看下面的例子。
```javascript
// 主线程
ia[42] = 314159; // 原先的值 191
ia[37] = 123456; // 原先的值 163
// Worker 线程
console.log(ia[37]);
console.log(ia[42]);
// 可能的结果
// 123456
// 191
```
上面代码中,主线程的原始顺序是先对 42 号位置赋值,再对 37 号位置赋值。但是,编译器和 CPU 为了优化,可能会改变这两个操作的执行顺序(因为它们之间互不依赖),先对 37 号位置赋值,再对 42 号位置赋值。而执行到一半的时候Worker 线程可能就会来读取数据,导致打印出`123456``191`
下面是另一个例子。
```javascript
// 主线程
const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100000);
const ia = new Int32Array(sab);
for (let i = 0; i < ia.length; i++) {
ia[i] = primes.next(); // 将质数放入 ia
}
// worker 线程
ia[112]++; // 错误
Atomics.add(ia, 112, 1); // 正确
```
上面代码中Worker 线程直接改写共享内存`ia[112]++`是不正确的。因为这行语句会被编译成多条机器指令,这些指令之间无法保证不会插入其他进程的指令。请设想如果两个线程同时`ia[112]++`,很可能它们得到的结果都是不正确的。
`Atomics`对象就是为了解决这个问题而提出,它可以保证一个操作所对应的多条机器指令,一定是作为一个整体运行的,中间不会被打断。也就是说,它所涉及的操作都可以看作是原子性的单操作,这可以避免线程竞争,提高多线程共享内存时的操作安全。所以,`ia[112]++`要改写成`Atomics.add(ia, 112, 1)`
`Atomics`对象提供多种方法。
**1Atomics.store()Atomics.load()**
`store()`方法用来向共享内存写入数据,`load()`方法用来从共享内存读出数据。比起直接的读写操作,它们的好处是保证了读写操作的原子性。
此外它们还用来解决一个问题多个线程使用共享线程的某个位置作为开关flag一旦该位置的值变了就执行特定操作。这时必须保证该位置的赋值操作一定是在它前面的所有可能会改写内存的操作结束后执行而该位置的取值操作一定是在它后面所有可能会读取该位置的操作开始之前执行。`store`方法和`load`方法就能做到这一点,编译器不会为了优化,而打乱机器指令的执行顺序。
```javascript
Atomics.load(array, index)
Atomics.store(array, index, value)
```
`store`方法接受三个参数SharedBuffer 的视图、位置索引和值,返回`sharedArray[index]`的值。`load`方法只接受两个参数SharedBuffer 的视图和位置索引,也是返回`sharedArray[index]`的值。
```javascript
// 主线程 main.js
ia[42] = 314159; // 原先的值 191
Atomics.store(ia, 37, 123456); // 原先的值是 163
// Worker 线程 worker.js
while (Atomics.load(ia, 37) == 163);
console.log(ia[37]); // 123456
console.log(ia[42]); // 314159
```
上面代码中,主线程的`Atomics.store`向 42 号位置的赋值,一定是早于 37 位置的赋值。只要 37 号位置等于 163Worker 线程就不会终止循环,而对 37 号位置和 42 号位置的取值,一定是在`Atomics.load`操作之后。
**2Atomics.wait()Atomics.wake()**
使用`while`循环等待主线程的通知,不是很高效,如果用在主线程,就会造成卡顿,`Atomics`对象提供了`wait()``wake()`两个方法用于等待通知。这两个方法相当于锁内存,即在一个线程进行操作时,让其他线程休眠(建立锁),等到操作结束,再唤醒那些休眠的线程(解除锁)。
```javascript
Atomics.wait(sharedArray, index, value, time)
```
`Atomics.wait`用于当`sharedArray[index]`不等于`value`,就返回`not-equal`,否则就进入休眠,只有使用`Atomics.wake()`或者`time`毫秒以后才能唤醒。被`Atomics.wake()`唤醒时,返回`ok`,超时唤醒时返回`timed-out`
```javascript
Atomics.wake(sharedArray, index, count)
```
`Atomics.wake`用于唤醒`count`数目在`sharedArray[index]`位置休眠的线程,让它继续往下运行。
下面请看一个例子。
```javascript
// 线程一
console.log(ia[37]); // 163
Atomics.store(ia, 37, 123456);
Atomics.wake(ia, 37, 1);
// 线程二
Atomics.wait(ia, 37, 163);
console.log(ia[37]); // 123456
```
上面代码中,共享内存视图`ia`的第 37 号位置,原来的值是`163`。进程二使用`Atomics.wait()`方法,指定只要`ia[37]`等于`163`,就进入休眠状态。进程一使用`Atomics.store()`方法,将`123456`放入`ia[37]`,然后使用`Atomics.wake()`方法将监视`ia[37]`的休眠线程唤醒。
另外,基于`wait``wake`这两个方法的锁内存实现,可以看 Lars T Hansen 的 [js-lock-and-condition](https://github.com/lars-t-hansen/js-lock-and-condition) 这个库。
注意,浏览器的主线程有权“拒绝”休眠,这是为了防止用户失去响应。
**3运算方法**
共享内存上面的某些运算是不能被打断的即不能在运算过程中让其他线程改写内存上面的值。Atomics 对象提供了一些运算方法,防止数据被改写。
```javascript
Atomics.add(sharedArray, index, value)
```
`Atomics.add`用于将`value`加到`sharedArray[index]`,返回`sharedArray[index]`旧的值。
```javascript
Atomics.sub(sharedArray, index, value)
```
`Atomics.sub`用于将`value``sharedArray[index]`减去,返回`sharedArray[index]`旧的值。
```javascript
Atomics.and(sharedArray, index, value)
```
`Atomics.and`用于将`value``sharedArray[index]`进行位运算`and`,放入`sharedArray[index]`,并返回旧的值。
```javascript
Atomics.or(sharedArray, index, value)
```
`Atomics.or`用于将`value``sharedArray[index]`进行位运算`or`,放入`sharedArray[index]`,并返回旧的值。
```javascript
Atomics.xor(sharedArray, index, value)
```
`Atomic.xor`用于将`vaule``sharedArray[index]`进行位运算`xor`,放入`sharedArray[index]`,并返回旧的值。
**4其他方法**
`Atomics`对象还有以下方法。
- `Atomics.compareExchange(sharedArray, index, oldval, newval)`:如果`sharedArray[index]`等于`oldval`,就写入`newval`,返回`oldval`
- `Atomics.exchange(sharedArray, index, value)`:设置`sharedArray[index]`的值,返回旧的值。
- `Atomics.isLockFree(size)`:返回一个布尔值,表示`Atomics`对象是否可以处理某个`size`的内存锁定。如果返回`false`,应用程序就需要自己来实现锁定。
`Atomics.compareExchange`的一个用途是,从 SharedArrayBuffer 读取一个值,然后对该值进行某个操作,操作结束以后,检查一下 SharedArrayBuffer 里面原来那个值是否发生变化(即被其他线程改写过)。如果没有改写过,就将它写回原来的位置,否则读取新的值,再重头进行一次操作。

File diff suppressed because it is too large Load Diff

714
docs/class-extends.md Normal file
View File

@ -0,0 +1,714 @@
# Class 的继承
## 简介
Class 可以通过`extends`关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
```javascript
class Point {
}
class ColorPoint extends Point {
}
```
上面代码定义了一个`ColorPoint`类,该类通过`extends`关键字,继承了`Point`类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个`Point`类。下面,我们在`ColorPoint`内部加上代码。
```javascript
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
```
上面代码中,`constructor`方法和`toString`方法之中,都出现了`super`关键字,它在这里表示父类的构造函数,用来新建父类的`this`对象。
子类必须在`constructor`方法中调用`super`方法,否则新建实例时会报错。这是因为子类没有自己的`this`对象,而是继承父类的`this`对象,然后对其进行加工。如果不调用`super`方法,子类就得不到`this`对象。
```javascript
class Point { /* ... */ }
class ColorPoint extends Point {
constructor() {
}
}
let cp = new ColorPoint(); // ReferenceError
```
上面代码中,`ColorPoint`继承了父类`Point`,但是它的构造函数没有调用`super`方法,导致新建实例时报错。
ES5 的继承,实质是先创造子类的实例对象`this`,然后再将父类的方法添加到`this`上面(`Parent.apply(this)`。ES6 的继承机制完全不同,实质是先创造父类的实例对象`this`(所以必须先调用`super`方法),然后再用子类的构造函数修改`this`
如果子类没有定义`constructor`方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有`constructor`方法。
```javascript
class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
```
另一个需要注意的地方是,在子类的构造函数中,只有调用`super`之后,才可以使用`this`关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有`super`方法才能返回父类实例。
```javascript
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y);
this.color = color; // 正确
}
}
```
上面代码中,子类的`constructor`方法没有调用`super`之前,就使用`this`关键字,结果报错,而放在`super`方法之后就是正确的。
下面是生成子类实例的代码。
```javascript
let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint // true
cp instanceof Point // true
```
上面代码中,实例对象`cp`同时是`ColorPoint``Point`两个类的实例,这与 ES5 的行为完全一致。
最后,父类的静态方法,也会被子类继承。
```javascript
class A {
static hello() {
console.log('hello world');
}
}
class B extends A {
}
B.hello() // hello world
```
上面代码中,`hello()``A`类的静态方法,`B`继承`A`,也继承了`A`的静态方法。
## Object.getPrototypeOf()
`Object.getPrototypeOf`方法可以用来从子类上获取父类。
```javascript
Object.getPrototypeOf(ColorPoint) === Point
// true
```
因此,可以使用这个方法判断,一个类是否继承了另一个类。
## super 关键字
`super`这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
第一种情况,`super`作为函数调用时代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次`super`函数。
```javascript
class A {}
class B extends A {
constructor() {
super();
}
}
```
上面代码中,子类`B`的构造函数之中的`super()`,代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。
注意,`super`虽然代表了父类`A`的构造函数,但是返回的是子类`B`的实例,即`super`内部的`this`指的是`B`,因此`super()`在这里相当于`A.prototype.constructor.call(this)`
```javascript
class A {
constructor() {
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super();
}
}
new A() // A
new B() // B
```
上面代码中,`new.target`指向当前正在执行的函数。可以看到,在`super()`执行时,它指向的是子类`B`的构造函数,而不是父类`A`的构造函数。也就是说,`super()`内部的`this`指向的是`B`
作为函数时,`super()`只能用在子类的构造函数之中,用在其他地方就会报错。
```javascript
class A {}
class B extends A {
m() {
super(); // 报错
}
}
```
上面代码中,`super()`用在`B`类的`m`方法之中,就会造成句法错误。
第二种情况,`super`作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
```javascript
class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}
let b = new B();
```
上面代码中,子类`B`当中的`super.p()`,就是将`super`当作一个对象使用。这时,`super`在普通方法之中,指向`A.prototype`,所以`super.p()`就相当于`A.prototype.p()`
这里需要注意,由于`super`指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过`super`调用的。
```javascript
class A {
constructor() {
this.p = 2;
}
}
class B extends A {
get m() {
return super.p;
}
}
let b = new B();
b.m // undefined
```
上面代码中,`p`是父类`A`实例的属性,`super.p`就引用不到它。
如果属性定义在父类的原型对象上,`super`就可以取到。
```javascript
class A {}
A.prototype.x = 2;
class B extends A {
constructor() {
super();
console.log(super.x) // 2
}
}
let b = new B();
```
上面代码中,属性`x`是定义在`A.prototype`上面的,所以`super.x`可以取到它的值。
ES6 规定,通过`super`调用父类的方法时,方法内部的`this`指向子类。
```javascript
class A {
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
m() {
super.print();
}
}
let b = new B();
b.m() // 2
```
上面代码中,`super.print()`虽然调用的是`A.prototype.print()`,但是`A.prototype.print()`内部的`this`指向子类`B`,导致输出的是`2`,而不是`1`。也就是说,实际上执行的是`super.print.call(this)`
由于`this`指向子类,所以如果通过`super`对某个属性赋值,这时`super`就是`this`,赋值的属性会变成子类实例的属性。
```javascript
class A {
constructor() {
this.x = 1;
}
}
class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.x); // undefined
console.log(this.x); // 3
}
}
let b = new B();
```
上面代码中,`super.x`赋值为`3`,这时等同于对`this.x`赋值为`3`。而当读取`super.x`的时候,读的是`A.prototype.x`,所以返回`undefined`
如果`super`作为对象,用在静态方法之中,这时`super`将指向父类,而不是父类的原型对象。
```javascript
class Parent {
static myMethod(msg) {
console.log('static', msg);
}
myMethod(msg) {
console.log('instance', msg);
}
}
class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg);
}
myMethod(msg) {
super.myMethod(msg);
}
}
Child.myMethod(1); // static 1
var child = new Child();
child.myMethod(2); // instance 2
```
上面代码中,`super`在静态方法之中指向父类,在普通方法之中指向父类的原型对象。
注意,使用`super`的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。
```javascript
class A {}
class B extends A {
constructor() {
super();
console.log(super); // 报错
}
}
```
上面代码中,`console.log(super)`当中的`super`,无法看出是作为函数使用,还是作为对象使用,所以 JavaScript 引擎解析代码的时候就会报错。这时,如果能清晰地表明`super`的数据类型,就不会报错。
```javascript
class A {}
class B extends A {
constructor() {
super();
console.log(super.valueOf() instanceof B); // true
}
}
let b = new B();
```
上面代码中,`super.valueOf()`表明`super`是一个对象,因此就不会报错。同时,由于`super`使得`this`指向`B`,所以`super.valueOf()`返回的是一个`B`的实例。
最后,由于对象总是继承其他对象的,所以可以在任意一个对象中,使用`super`关键字。
```javascript
var obj = {
toString() {
return "MyObject: " + super.toString();
}
};
obj.toString(); // MyObject: [object Object]
```
## 类的 prototype 属性和\_\_proto\_\_属性
大多数浏览器的 ES5 实现之中,每一个对象都有`__proto__`属性,指向对应的构造函数的`prototype`属性。Class 作为构造函数的语法糖,同时有`prototype`属性和`__proto__`属性,因此同时存在两条继承链。
1子类的`__proto__`属性,表示构造函数的继承,总是指向父类。
2子类`prototype`属性的`__proto__`属性,表示方法的继承,总是指向父类的`prototype`属性。
```javascript
class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
```
上面代码中,子类`B``__proto__`属性指向父类`A`,子类`B``prototype`属性的`__proto__`属性指向父类`A``prototype`属性。
这样的结果是因为,类的继承是按照下面的模式实现的。
```javascript
class A {
}
class B {
}
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 的实例继承 A 的静态属性
Object.setPrototypeOf(B, A);
const b = new B();
```
《对象的扩展》一章给出过`Object.setPrototypeOf`方法的实现。
```javascript
Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
```
因此,就得到了上面的结果。
```javascript
Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;
Object.setPrototypeOf(B, A);
// 等同于
B.__proto__ = A;
```
这两条继承链,可以这样理解:作为一个对象,子类(`B`)的原型(`__proto__`属性)是父类(`A`);作为一个构造函数,子类(`B`)的原型对象(`prototype`属性)是父类的原型对象(`prototype`属性)的实例。
```javascript
Object.create(A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;
```
### extends 的继承目标
`extends`关键字后面可以跟多种类型的值。
```javascript
class B extends A {
}
```
上面代码的`A`,只要是一个有`prototype`属性的函数,就能被`B`继承。由于函数都有`prototype`属性(除了`Function.prototype`函数),因此`A`可以是任意函数。
下面,讨论三种特殊情况。
第一种特殊情况,子类继承`Object`类。
```javascript
class A extends Object {
}
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
```
这种情况下,`A`其实就是构造函数`Object`的复制,`A`的实例就是`Object`的实例。
第二种特殊情况,不存在任何继承。
```javascript
class A {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
```
这种情况下,`A`作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承`Function.prototype`。但是,`A`调用后返回一个空对象(即`Object`实例),所以`A.prototype.__proto__`指向构造函数(`Object`)的`prototype`属性。
第三种特殊情况,子类继承`null`
```javascript
class A extends null {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === undefined // true
```
这种情况与第二种情况非常像。`A`也是一个普通函数,所以直接继承`Function.prototype`。但是,`A`调用后返回的对象不继承任何方法,所以它的`__proto__`指向`Function.prototype`,即实质上执行了下面的代码。
```javascript
class C extends null {
constructor() { return Object.create(null); }
}
```
### 实例的 \_\_proto\_\_ 属性
子类实例的`__proto__`属性的`__proto__`属性,指向父类实例的`__proto__`属性。也就是说,子类的原型的原型,是父类的原型。
```javascript
var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');
p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true
```
上面代码中,`ColorPoint`继承了`Point`,导致前者原型的原型是后者的原型。
因此,通过子类实例的`__proto__.__proto__`属性,可以修改父类实例的行为。
```javascript
p2.__proto__.__proto__.printName = function () {
console.log('Ha');
};
p1.printName() // "Ha"
```
上面代码在`ColorPoint`的实例`p2`上向`Point`类添加方法,结果影响到了`Point`的实例`p1`
## 原生构造函数的继承
原生构造函数是指语言内置的构造函数通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。
- Boolean()
- Number()
- String()
- Array()
- Date()
- Function()
- RegExp()
- Error()
- Object()
以前,这些原生构造函数是无法继承的,比如,不能自己定义一个`Array`的子类。
```javascript
function MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
});
```
上面代码定义了一个继承 Array 的`MyArray`类。但是,这个类的行为与`Array`完全不一致。
```javascript
var colors = new MyArray();
colors[0] = "red";
colors.length // 0
colors.length = 0;
colors[0] // "red"
```
之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过`Array.apply()`或者分配给原型对象都不行。原生构造函数会忽略`apply`方法传入的`this`,也就是说,原生构造函数的`this`无法绑定,导致拿不到内部属性。
ES5 是先新建子类的实例对象`this`,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。比如,`Array`构造函数有一个内部属性`[[DefineOwnProperty]]`,用来定义新属性时,更新`length`属性,这个内部属性无法在子类获取,导致子类的`length`属性行为不正常。
下面的例子中,我们想让一个普通对象继承`Error`对象。
```javascript
var e = {};
Object.getOwnPropertyNames(Error.call(e))
// [ 'stack' ]
Object.getOwnPropertyNames(e)
// []
```
上面代码中,我们想通过`Error.call(e)`这种写法,让普通对象`e`具有`Error`对象的实例属性。但是,`Error.call()`完全忽略传入的第一个参数,而是返回一个新对象,`e`本身没有任何变化。这证明了`Error.call(e)`这种写法,无法继承原生构造函数。
ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象`this`,然后再用子类的构造函数修饰`this`,使得父类的所有行为都可以继承。下面是一个继承`Array`的例子。
```javascript
class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined
```
上面代码定义了一个`MyArray`类,继承了`Array`构造函数,因此就可以从`MyArray`生成数组的实例。这意味着ES6 可以自定义原生数据结构(比如`Array``String`等)的子类,这是 ES5 无法做到的。
上面这个例子也说明,`extends`关键字不仅可以用来继承类,还可以用来继承原生的构造函数。因此可以在原生数据结构的基础上,定义自己的数据结构。下面就是定义了一个带版本功能的数组。
```javascript
class VersionedArray extends Array {
constructor() {
super();
this.history = [[]];
}
commit() {
this.history.push(this.slice());
}
revert() {
this.splice(0, this.length, ...this.history[this.history.length - 1]);
}
}
var x = new VersionedArray();
x.push(1);
x.push(2);
x // [1, 2]
x.history // [[]]
x.commit();
x.history // [[], [1, 2]]
x.push(3);
x // [1, 2, 3]
x.history // [[], [1, 2]]
x.revert();
x // [1, 2]
```
上面代码中,`VersionedArray`会通过`commit`方法,将自己的当前状态生成一个版本快照,存入`history`属性。`revert`方法用来将数组重置为最新一次保存的版本。除此之外,`VersionedArray`依然是一个普通数组,所有原生的数组方法都可以在它上面调用。
下面是一个自定义`Error`子类的例子,可以用来定制报错时的行为。
```javascript
class ExtendableError extends Error {
constructor(message) {
super();
this.message = message;
this.stack = (new Error()).stack;
this.name = this.constructor.name;
}
}
class MyError extends ExtendableError {
constructor(m) {
super(m);
}
}
var myerror = new MyError('ll');
myerror.message // "ll"
myerror instanceof Error // true
myerror.name // "MyError"
myerror.stack
// Error
// at MyError.ExtendableError
// ...
```
注意,继承`Object`的子类,有一个[行为差异](http://stackoverflow.com/questions/36203614/super-does-not-pass-arguments-when-instantiating-a-class-extended-from-object)。
```javascript
class NewObj extends Object{
constructor(){
super(...arguments);
}
}
var o = new NewObj({attr: true});
o.attr === true // false
```
上面代码中,`NewObj`继承了`Object`,但是无法通过`super`方法向父类`Object`传参。这是因为 ES6 改变了`Object`构造函数的行为,一旦发现`Object`方法不是通过`new Object()`这种形式调用ES6 规定`Object`构造函数会忽略参数。
## Mixin 模式的实现
Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。它的最简单实现如下。
```javascript
const a = {
a: 'a'
};
const b = {
b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}
```
上面代码中,`c`对象是`a`对象和`b`对象的合成,具有两者的接口。
下面是一个更完备的实现将多个类的接口“混入”mix in另一个类。
```javascript
function mix(...mixins) {
class Mix {}
for (let mixin of mixins) {
copyProperties(Mix, mixin); // 拷贝实例属性
copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
}
return Mix;
}
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== "constructor"
&& key !== "prototype"
&& key !== "name"
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
```
上面代码的`mix`函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可。
```javascript
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}
```

File diff suppressed because it is too large Load Diff

View File

@ -2,22 +2,22 @@
## 类的修饰
修饰器Decorator是一个函数用来修改类的行为。这是ES7的一个[提案](https://github.com/wycats/javascript-decorators)目前Babel转码器已经支持。
修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。
许多面向对象的语言都有修饰器Decorator函数用来修改类的行为。目前有一个[提案](https://github.com/tc39/proposal-decorators)将这项功能,引入了 ECMAScript。
```javascript
@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
@testable
class MyTestableClass {}
console.log(MyTestableClass.isTestable) // true
MyTestableClass.isTestable // true
```
上面代码中,`@testable`就是一个修饰器。它修改了`MyTestableClass`这个类的行为,为它加上了静态属性`isTestable`
上面代码中,`@testable`就是一个修饰器。它修改了`MyTestableClass`这个类的行为,为它加上了静态属性`isTestable``testable`函数的参数`target``MyTestableClass`类本身。
基本上,修饰器的行为就是下面这样。
@ -31,9 +31,7 @@ class A {}
A = decorator(A) || A;
```
也就是说,修饰器本质就是编译时执行的函数。
修饰器函数的第一个参数,就是所要修饰的目标类。
也就是说,修饰器是一个对类进行处理的函数。修饰器函数的第一个参数,就是所要修饰的目标类。
```javascript
function testable(target) {
@ -63,6 +61,8 @@ MyClass.isTestable // false
上面代码中,修饰器`testable`可以接受参数,这就等于可以修改修饰器的行为。
注意,修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。
前面的例子是为类添加一个静态属性,如果想添加实例属性,可以通过目标类的`prototype`对象操作。
```javascript
@ -118,6 +118,23 @@ let obj = new MyClass();
obj.foo() // 'foo'
```
实际开发中React 与 Redux 库结合使用时,常常需要写成下面这样。
```javascript
class MyReactComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
```
有了装饰器,就可以改写上面的代码。
```javascript
@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}
```
相对来说,后一种写法看上去更容易理解。
## 方法的修饰
修饰器不仅可以修饰类,还可以修饰类的属性。
@ -131,7 +148,7 @@ class Person {
上面代码中,修饰器`readonly`用来修饰“类”的`name`方法。
此时,修饰器函数一共可以接受三个参数,第一个参数是所要修饰的目标对象,第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象
修饰器函数`readonly`一共可以接受三个参数。
```javascript
function readonly(target, name, descriptor){
@ -151,7 +168,9 @@ readonly(Person.prototype, 'name', descriptor);
Object.defineProperty(Person.prototype, 'name', descriptor);
```
上面代码说明修饰器readonly会修改属性的描述对象descriptor然后被修改的描述对象再用来定义属性。
修饰器第一个参数是类的原型对象,上例是`Person.prototype`,修饰器的本意是要“修饰”类的实例,但是这个时候实例还没生成,所以只能去修饰原型(这不同于类的修饰,那种情况时`target`参数指的是类本身);第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象。
另外上面代码说明修饰器readonly会修改属性的描述对象descriptor然后被修改的描述对象再用来定义属性。
下面是另一个例子,修改属性描述对象的`enumerable`属性,使得该属性不可遍历。
@ -209,6 +228,26 @@ class Person {
从上面代码中,我们一眼就能看出,`Person`类是可测试的,而`name`方法是只读和不可枚举的。
下面是使用 Decorator 写法的[组件](https://github.com/ionic-team/stencil),看上去一目了然。
```javascript
@Component({
tag: 'my-component',
styleUrl: 'my-component.scss'
})
export class MyComponent {
@Prop() first: string;
@Prop() last: string;
@State() isVisible: boolean = true;
render() {
return (
<p>Hello, my name is {this.first} {this.last}</p>
);
}
}
```
如果同一个方法有多个修饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行。
```javascript
@ -251,13 +290,13 @@ function foo() {
上面的代码,意图是执行后`counter`等于 1但是实际上结果是`counter`等于 0。因为函数提升使得实际执行的代码是下面这样。
```javascript
var counter;
var add;
@add
function foo() {
}
var counter;
var add;
counter = 0;
add = function () {
@ -289,6 +328,25 @@ readOnly = require("some-decorator");
总之,由于存在函数提升,使得修饰器不能用于函数。类是不会提升的,所以就没有这方面的问题。
另一方面,如果一定要修饰函数,可以采用高阶函数的形式直接执行。
```javascript
function doSomething(name) {
console.log('Hello, ' + name);
}
function loggingDecorator(wrapped) {
return function() {
console.log('Starting');
const result = wrapped.apply(this, arguments);
console.log('Finished');
return result;
}
}
const wrapped = loggingDecorator(doSomething);
```
## core-decorators.js
[core-decorators.js](https://github.com/jayphelps/core-decorators.js)是一个第三方模块,提供了几个常见的修饰器,通过它可以更好地理解修饰器。
@ -394,7 +452,7 @@ person.facepalmHarder();
**5@suppressWarnings**
`suppressWarnings`修饰器抑制`decorated`修饰器导致的`console.warn()`调用。但是,异步代码发出的调用除外。
`suppressWarnings`修饰器抑制`deprecated`修饰器导致的`console.warn()`调用。但是,异步代码发出的调用除外。
```javascript
import { suppressWarnings } from 'core-decorators';
@ -420,15 +478,23 @@ person.facepalmWithoutWarning();
我们可以使用修饰器,使得对象的方法被调用时,自动发出一个事件。
```javascript
import postal from "postal/lib/postal.lodash";
const postal = require("postal/lib/postal.lodash");
export default function publish(topic, channel) {
const channelName = channel || '/';
const msgChannel = postal.channel(channelName);
msgChannel.subscribe(topic, v => {
console.log('频道: ', channelName);
console.log('事件: ', topic);
console.log('数据: ', v);
});
return function(target, name, descriptor) {
const fn = descriptor.value;
descriptor.value = function() {
let value = fn.apply(this, arguments);
postal.channel(channel || target.channel || "/").publish(topic, value);
msgChannel.publish(topic, value);
};
};
}
@ -439,29 +505,37 @@ export default function publish(topic, channel) {
它的用法如下。
```javascript
import publish from "path/to/decorators/publish";
// index.js
import publish from './publish';
class FooComponent {
@publish("foo.some.message", "component")
@publish('foo.some.message', 'component')
someMethod() {
return {
my: "data"
};
return { my: 'data' };
}
@publish("foo.some.other")
@publish('foo.some.other')
anotherMethod() {
// ...
}
}
let foo = new FooComponent();
foo.someMethod();
foo.anotherMethod();
```
以后,只要调用`someMethod`或者`anotherMethod`,就会自动发出一个事件。
```javascript
let foo = new FooComponent();
```bash
$ bash-node index.js
频道: component
事件: foo.some.message
数据: { my: 'data' }
foo.someMethod() // 在"component"频道发布"foo.some.message"事件,附带的数据是{ my: "data" }
foo.anotherMethod() // 在"/"频道发布"foo.some.other"事件,不附带数据
频道: /
事件: foo.some.other
数据: undefined
```
## Mixin
@ -485,7 +559,7 @@ obj.foo() // 'foo'
上面代码之中,对象`Foo`有一个`foo`方法,通过`Object.assign`方法,可以将`foo`方法“混入”`MyClass`类,导致`MyClass`的实例`obj`对象都具有`foo`方法。这就是“混入”模式的一个简单实现。
下面,我们部署一个通用脚本`mixins.js`,将mixin写成一个修饰器。
下面,我们部署一个通用脚本`mixins.js`,将 Mixin 写成一个修饰器。
```javascript
export function mixins(...list) {
@ -511,9 +585,9 @@ let obj = new MyClass();
obj.foo() // "foo"
```
通过mixins这个修饰器实现了在MyClass类上面“混入”Foo对象的`foo`方法。
通过`mixins`这个修饰器,实现了在`MyClass`类上面“混入”`Foo`对象的`foo`方法。
不过,上面的方法会改写`MyClass`类的`prototype`对象,如果不喜欢这一点,也可以通过类的继承实现mixin。
不过,上面的方法会改写`MyClass`类的`prototype`对象,如果不喜欢这一点,也可以通过类的继承实现 Mixin。
```javascript
class MyClass extends MyBaseClass {
@ -597,7 +671,7 @@ new C().foo()
Trait 也是一种修饰器,效果与 Mixin 类似,但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等。
下面采用[traits-decorator](https://github.com/CocktailJS/traits-decorator)这个第三方模块作为例子。这个模块提供的traits修饰器不仅可以接受对象还可以接受ES6类作为参数。
下面采用[traits-decorator](https://github.com/CocktailJS/traits-decorator)这个第三方模块作为例子。这个模块提供的`traits`修饰器,不仅可以接受对象,还可以接受 ES6 类作为参数。
```javascript
import { traits } from 'traits-decorator';
@ -618,7 +692,7 @@ obj.foo() // foo
obj.bar() // bar
```
上面代码中通过traits修饰器`MyClass`类上面“混入”了`TFoo`类的`foo`方法和`TBar`对象的`bar`方法。
上面代码中,通过`traits`修饰器,在`MyClass`类上面“混入”了`TFoo`类的`foo`方法和`TBar`对象的`bar`方法。
Trait 不允许“混入”同名方法。
@ -642,9 +716,9 @@ class MyClass { }
// Error: Method named: foo is defined twice.
```
上面代码中TFoo和TBar都有foo方法结果traits修饰器报错。
上面代码中,`TFoo``TBar`都有`foo`方法,结果`traits`修饰器报错。
一种解决方法是排除TBar的foo方法。
一种解决方法是排除`TBar``foo`方法。
```javascript
import { traits, excludes } from 'traits-decorator';
@ -666,9 +740,9 @@ obj.foo() // foo
obj.bar() // bar
```
上面代码使用绑定运算符(::在TBar上排除foo方法混入时就不会报错了。
上面代码使用绑定运算符(::)在`TBar`上排除`foo`方法,混入时就不会报错了。
另一种方法是为TBar的foo方法起一个别名。
另一种方法是为`TBar``foo`方法起一个别名。
```javascript
import { traits, alias } from 'traits-decorator';
@ -691,18 +765,18 @@ obj.aliasFoo() // foo
obj.bar() // bar
```
上面代码为TBar的foo方法起了别名aliasFoo于是MyClass也可以混入TBar的foo方法了。
上面代码为`TBar``foo`方法起了别名`aliasFoo`,于是`MyClass`也可以混入`TBar``foo`方法了。
alias和excludes方法可以结合起来使用。
`alias``excludes`方法,可以结合起来使用。
```javascript
@traits(TExample::excludes('foo','bar')::alias({baz:'exampleBaz'}))
class MyClass {}
```
上面代码排除了TExample的foo方法和bar方法为baz方法起了别名exampleBaz。
上面代码排除`了TExample``foo`方法和`bar`方法,为`baz`方法起了别名`exampleBaz`
as方法则为上面的代码提供了另一种写法。
`as`方法则为上面的代码提供了另一种写法。
```javascript
@traits(TExample::as({excludes:['foo', 'bar'], alias: {baz: 'exampleBaz'}}))

View File

@ -9,15 +9,15 @@ ES6允许按照一定模式从数组和对象中提取值对变量进行
以前,为变量赋值,只能直接指定值。
```javascript
var a = 1;
var b = 2;
var c = 3;
let a = 1;
let b = 2;
let c = 3;
```
ES6 允许写成下面这样。
```javascript
var [a, b, c] = [1, 2, 3];
let [a, b, c] = [1, 2, 3];
```
上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。
@ -50,8 +50,8 @@ z // []
如果解构不成功,变量的值就等于`undefined`
```javascript
var [foo] = [];
var [bar, foo] = [1];
let [foo] = [];
let [bar, foo] = [1];
```
以上两种情况都属于解构不成功,`foo`的值都会等于`undefined`
@ -83,20 +83,12 @@ let [foo] = null;
let [foo] = {};
```
上面的表达式都会报错因为等号右边的值要么转为对象以后不具备Iterator接口前五个表达式要么本身就不具备Iterator接口最后一个表达式
解构赋值不仅适用于var命令也适用于let和const命令。
```javascript
var [v1, v2, ..., vN ] = array;
let [v1, v2, ..., vN ] = array;
const [v1, v2, ..., vN ] = array;
```
上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。
对于 Set 结构,也可以使用数组的解构赋值。
```javascript
let [x, y, z] = new Set(["a", "b", "c"]);
let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"
```
@ -104,39 +96,39 @@ x // "a"
```javascript
function* fibs() {
var a = 0;
var b = 1;
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
var [first, second, third, fourth, fifth, sixth] = fibs();
let [first, second, third, fourth, fifth, sixth] = fibs();
sixth // 5
```
上面代码中,`fibs`是一个Generator函数原生具有Iterator接口。解构赋值会依次从这个接口获取值。
上面代码中,`fibs`是一个 Generator 函数参见《Generator 函数》一章),原生具有 Iterator 接口。解构赋值会依次从这个接口获取值。
### 默认值
解构赋值允许指定默认值。
```javascript
var [foo = true] = [];
let [foo = true] = [];
foo // true
[x, y = 'b'] = ['a']; // x='a', y='b'
[x, y = 'b'] = ['a', undefined]; // x='a', y='b'
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
```
注意ES6内部使用严格相等运算符`===`),判断一个位置是否有值。而且,只有当一个数组成员严格等于`undefined`,默认值才会生效。
```javascript
var [x = 1] = [undefined];
let [x = 1] = [undefined];
x // 1
var [x = 1] = [null];
let [x = 1] = [null];
x // null
```
@ -179,7 +171,7 @@ let [x = y, y = 1] = []; // ReferenceError: y is not defined
解构不仅可以用于数组,还可以用于对象。
```javascript
var { foo, bar } = { foo: "aaa", bar: "bbb" };
let { foo, bar } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
```
@ -187,11 +179,11 @@ bar // "bbb"
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
```javascript
var { bar, foo } = { foo: "aaa", bar: "bbb" };
let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
var { baz } = { foo: "aaa", bar: "bbb" };
let { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined
```
@ -200,7 +192,7 @@ baz // undefined
如果变量名与属性名不一致,必须写成下面这样。
```javascript
var { foo: baz } = { foo: 'aaa', bar: 'bbb' };
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
@ -212,60 +204,54 @@ l // 'world'
这实际上说明,对象的解构赋值是下面形式的简写(参见《对象的扩展》一章)。
```javascript
var { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
```
也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
```javascript
var { foo: baz } = { foo: "aaa", bar: "bbb" };
let { foo: baz } = { foo: "aaa", bar: "bbb" };
baz // "aaa"
foo // error: foo is not defined
```
上面代码中,`foo`是匹配的模式,`baz`才是变量。真正被赋值的是变量`baz`,而不是模式`foo`
注意,采用这种写法时,变量的声明和赋值是一体的。对于`let``const`来说,变量不能重新声明,所以一旦赋值的变量以前声明过,就会报错
与数组一样,解构也可以用于嵌套结构的对象
```javascript
let foo;
let {foo} = {foo: 1}; // SyntaxError: Duplicate declaration "foo"
let baz;
let {bar: baz} = {bar: 1}; // SyntaxError: Duplicate declaration "baz"
```
上面代码中,解构赋值的变量都会重新声明,所以报错了。不过,因为`var`命令允许重新声明,所以这个错误只会在使用`let``const`命令时出现。如果没有第二个`let`命令,上面的代码就不会报错。
```javascript
let foo;
({foo} = {foo: 1}); // 成功
let baz;
({bar: baz} = {bar: 1}); // 成功
```
上面代码中,`let`命令下面一行的圆括号是必须的,否则会报错。因为解析器会将起首的大括号,理解成一个代码块,而不是赋值语句。
和数组一样,解构也可以用于嵌套结构的对象。
```javascript
var obj = {
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
var { p: [x, { y }] } = obj;
let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"
```
注意,这时`p`是模式,不是变量,因此不会被赋值。
注意,这时`p`是模式,不是变量,因此不会被赋值。如果`p`也要作为变量赋值,可以写成下面这样。
```javascript
var node = {
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]
```
下面是另一个例子。
```javascript
const node = {
loc: {
start: {
line: 1,
@ -274,13 +260,13 @@ var node = {
}
};
var { loc: { start: { line }} } = node;
let { loc, loc: { start }, loc: { start: { line }} } = node;
line // 1
loc // error: loc is undefined
start // error: start is undefined
loc // Object {start: Object}
start // Object {line: 1, column: 5}
```
上面代码中,只有`line`是变量,`loc``start`都是模式,不会被赋值
上面代码有三次解构赋值,分别是对`loc``start``line`三个属性的解构赋值。注意,最后一次对`line`属性的解构赋值之中,只有`line`是变量,`loc``start`都是模式,不是变量
下面是嵌套赋值的例子。
@ -329,7 +315,7 @@ x // null
如果解构失败,变量的值等于`undefined`
```javascript
var {foo} = {bar: 'baz'};
let {foo} = {bar: 'baz'};
foo // undefined
```
@ -337,13 +323,13 @@ foo // undefined
```javascript
// 报错
var {foo: {bar}} = {baz: 'baz'};
let {foo: {bar}} = {baz: 'baz'};
```
上面代码中,等号左边对象的`foo`属性,对应一个子对象。该子对象的`bar`属性,解构时会报错。原因很简单,因为`foo`这时等于`undefined`,再取子属性就会报错,请看下面的代码。
```javascript
var _tmp = {baz: 'baz'};
let _tmp = {baz: 'baz'};
_tmp.foo.bar // 报错
```
@ -351,7 +337,7 @@ _tmp.foo.bar // 报错
```javascript
// 错误的写法
var x;
let x;
{x} = {x: 1};
// SyntaxError: syntax error
```
@ -360,12 +346,13 @@ var x;
```javascript
// 正确的写法
let x;
({x} = {x: 1});
```
上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。关于圆括号与解构赋值的关系,参见下文。
解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。
解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。
```javascript
({} = [true, false]);
@ -386,8 +373,8 @@ let { log, sin, cos } = Math;
由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。
```javascript
var arr = [1, 2, 3];
var {0 : first, [arr.length - 1] : last} = arr;
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
```
@ -428,7 +415,7 @@ s === Boolean.prototype.toString // true
上面代码中,数值和布尔值的包装对象都有`toString`属性,因此变量`s`都能取到值。
解构赋值的规则是,只要等号右边的值不是对象,就先将其转为对象。由于`undefined``null`无法转为对象,所以对它们进行解构赋值,都会报错。
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于`undefined``null`无法转为对象,所以对它们进行解构赋值,都会报错。
```javascript
let { prop: x } = undefined; // TypeError
@ -505,32 +492,34 @@ move(); // [0, 0]
以下三种解构赋值不得使用圆括号。
1变量声明语句中,不能带有圆括号。
1变量声明语句
```javascript
// 全部报错
var [(a)] = [1];
let [(a)] = [1];
var {x: (c)} = {};
var ({x: c}) = {};
var {(x: c)} = {};
var {(x): c} = {};
let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};
var { o: ({ p: p }) } = { o: { p: 2 } };
let { o: ({ p: p }) } = { o: { p: 2 } };
```
上面个语句都会报错,因为它们都是变量声明语句,模式不能使用圆括号。
上面 6 个语句都会报错,因为它们都是变量声明语句,模式不能使用圆括号。
2函数参数中,模式不能带有圆括号。
2函数参数
函数参数也属于变量声明,因此不能带有圆括号。
```javascript
// 报错
function f([(z)]) { return z; }
// 报错
function f([z,(x)]) { return x; }
```
3赋值语句中,不能将整个模式,或嵌套模式中的一层,放在圆括号之中。
3赋值语句的模式
```javascript
// 全部报错
@ -545,7 +534,7 @@ function f([(z)]) { return z; }
[({ p: a }), { x: c }] = [{}, {}];
```
上面代码将嵌套模式的一层,放在圆括号之中,导致报错。
上面代码将一部分模式放在圆括号之中,导致报错。
### 可以使用圆括号的情况
@ -557,7 +546,7 @@ function f([(z)]) { return z; }
[(parseInt.prop)] = [3]; // 正确
```
上面三行语句都可以正确执行因为首先它们都是赋值语句而不是声明语句其次它们的圆括号都不属于模式的一部分。第一行语句中模式是取数组的第一个成员跟圆括号无关第二行语句中模式是p而不是d第三行语句与第一行语句的性质一致。
上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是`p`,而不是`d`;第三行语句与第一行语句的性质一致。
## 用途
@ -566,6 +555,9 @@ function f([(z)]) { return z; }
**1交换变量的值**
```javascript
let x = 1;
let y = 2;
[x, y] = [y, x];
```
@ -581,7 +573,7 @@ function f([(z)]) { return z; }
function example() {
return [1, 2, 3];
}
var [a, b, c] = example();
let [a, b, c] = example();
// 返回一个对象
@ -591,7 +583,7 @@ function example() {
bar: 2
};
}
var { foo, bar } = example();
let { foo, bar } = example();
```
**3函数参数的定义**
@ -613,7 +605,7 @@ f({z: 3, y: 2, x: 1});
解构赋值对提取 JSON 对象中的数据,尤其有用。
```javascript
var jsonData = {
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
@ -650,7 +642,7 @@ jQuery.ajax = function (url, {
任何部署了 Iterator 接口的对象,都可以用`for...of`循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。
```javascript
var map = new Map();
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
@ -677,7 +669,7 @@ for (let [,value] of map) {
**7输入模块的指定方法**
加载模块时,往往需要指定输入些方法。解构赋值使得输入语句非常清晰。
加载模块时,往往需要指定输入些方法。解构赋值使得输入语句非常清晰。
```javascript
const { SourceMapConsumer, SourceNode } = require("source-map");

View File

@ -4,7 +4,7 @@
### 基本用法
ES6之前不能直接为函数的参数指定默认值只能采用变通的方法。
ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
```javascript
function log(x, y) {
@ -47,7 +47,7 @@ function Point(x = 0, y = 0) {
this.y = y;
}
var p = new Point();
const p = new Point();
p // { x: 0, y: 0 }
```
@ -64,6 +64,37 @@ function foo(x = 5) {
上面代码中,参数变量`x`是默认声明的,在函数体中,不能用`let``const`再次声明,否则会报错。
使用参数默认值时,函数不能有同名参数。
```javascript
// 不报错
function foo(x, x, y) {
// ...
}
// 报错
function foo(x, x, y = 1) {
// ...
}
// SyntaxError: Duplicate parameter name not allowed in this context
```
另外,一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。
```javascript
let x = 99;
function foo(p = x + 1) {
console.log(p);
}
foo() // 100
x = 100;
foo() // 101
```
上面代码中,参数`p`的默认值是`x + 1`。这时,每次调用函数`foo`,都会重新计算`x + 1`,而不是默认`p`等于 100。
### 与解构赋值默认值结合使用
参数默认值可以与解构赋值的默认值,结合起来使用。
@ -73,15 +104,25 @@ function foo({x, y = 5}) {
console.log(x, y);
}
foo({}) // undefined, 5
foo({x: 1}) // 1, 5
foo({x: 1, y: 2}) // 1, 2
foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined
```
上面代码使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当函数`foo`的参数是一个对象时,变量`x``y`才会通过解构赋值生成。如果函数`foo`调用时参数不是对象,变量`x``y`就不会生成,从而报错。如果参数对象没有`y`属性,`y`的默认值5才会生效
上面代码使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当函数`foo`的参数是一个对象时,变量`x``y`才会通过解构赋值生成。如果函数`foo`调用时没提供参数,变量`x``y`就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况
下面是另一个对象的解构赋值默认值的例子。
```javascript
function foo({x, y = 5} = {}) {
console.log(x, y);
}
foo() // undefined 5
```
上面代码指定,如果没有提供参数,函数`foo`的参数默认为一个空对象。
下面是另一个解构赋值默认值的例子。
```javascript
function fetch(url, { body = '', method = 'GET', headers = {} }) {
@ -95,12 +136,10 @@ fetch('http://example.com')
// 报错
```
上面代码中,如果函数`fetch`的第二个参数是一个对象,就可以为它的三个属性设置默认值。
上面的写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。
上面代码中,如果函数`fetch`的第二个参数是一个对象,就可以为它的三个属性设置默认值。这种写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。
```javascript
function fetch(url, { method = 'GET' } = {}) {
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
console.log(method);
}
@ -110,7 +149,7 @@ fetch('http://example.com')
上面代码中,函数`fetch`没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量`method`才会取到默认值`GET`
请问下面两种写法有什么差别?
作为练习,请问下面两种写法有什么差别?
```javascript
// 写法一
@ -200,7 +239,7 @@ foo(undefined, null)
上面代码中,`length`属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。比如,上面最后一个函数,定义了 3 个参数,其中有一个参数`c`指定了默认值,因此`length`属性等于`3`减去`1`,最后得到`2`
这是因为`length`属性的含义是该函数预期传入的参数个数。某个参数指定默认值以后预期传入的参数个数就不包括这个参数了。同理rest参数也不会计入`length`属性。
这是因为`length`属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,后文的 rest 参数也不会计入`length`属性。
```javascript
(function(...args) {}).length // 0
@ -215,7 +254,7 @@ foo(undefined, null)
### 作用域
个需要注意的地方是,如果参数默认值是一个变量,则该变量所处的作用域,与其他变量的作用域规则是一样的,即先是当前函数的作用域,然后才是全局作用域
旦设置了参数的默认值函数进行声明初始化时参数会形成一个单独的作用域context。等到初始化结束这个作用域就会消失。这种语法行为在不设置参数默认值时是不会出现的
```javascript
var x = 1;
@ -227,9 +266,9 @@ function f(x, y = x) {
f(2) // 2
```
上面代码中,参数`y`的默认值等于`x`。调用时,由于函数作用域内部的变量`x`已经生成,所以`y`等于参数`x`,而不是全局变量`x`。
上面代码中,参数`y`的默认值等于变量`x`。调用函数`f`时,参数形成一个单独的作用域。在这个作用域里面,默认值变量`x`指向第一个参数`x`,而不是全局变量`x`,所以输出是`2`。
如果调用时,函数作用域内部的变量`x`没有生成,结果就会不一样
再看下面的例子
```javascript
let x = 1;
@ -242,7 +281,7 @@ function f(y = x) {
f() // 1
```
上面代码中,函数调用时,`y`的默认值变量`x`尚未在函数内部生成,所以`x`指向全局变量
上面代码中,函数`f`调用时,参数`y = x`形成一个单独的作用域。这个作用域里面,变量`x`本身没有定义,所以指向外层的全局变量`x`。函数调用时,函数体内部的局部变量`x`影响不到默认值变量`x`
如果此时,全局变量`x`不存在,就会报错。
@ -267,22 +306,22 @@ function foo(x = x) {
foo() // ReferenceError: x is not defined
```
上面代码中,函数`foo`的参数`x`的默认值也是`x`。这时,默认值`x`的作用域是函数作用域,而不是全局作用域。由于在函数作用域中,存在变量`x`,但是默认值在`x`赋值之前先执行了所以这时属于暂时性死区参见《let和const命令》一章任何对`x`的操作都会报错
上面代码中,参数`x = x`形成一个单独作用域。实际执行的是`let x = x`由于暂时性死区的原因这行代码会报错”x 未定义“
如果参数的默认值是一个函数,该函数的作用域是其声明时所在的作用域。请看下面的例子。
如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。请看下面的例子。
```javascript
let foo = 'outer';
function bar(func = x => foo) {
function bar(func = () => foo) {
let foo = 'inner';
console.log(func()); // outer
console.log(func());
}
bar();
bar(); // outer
```
上面代码中,函数`bar`的参数`func`的默认值是一个匿名函数,返回值为变量`foo`这个匿名函数声明时,`bar`函数的作用域还没有形成,所以匿名函数里面的`foo`指向外层作用域的`foo`输出`outer`
上面代码中,函数`bar`的参数`func`的默认值是一个匿名函数,返回值为变量`foo`函数参数形成的单独作用域里面,并没有定义变量`foo`,所以`foo`指向外层的全局变量`foo`,因此输出`outer`
如果写成下面这样,就会报错。
@ -295,7 +334,7 @@ function bar(func = () => foo) {
bar() // ReferenceError: foo is not defined
```
上面代码中,匿名函数里面的`foo`指向函数外层,但是函数外层并没有声明`foo`,所以就报错了。
上面代码中,匿名函数里面的`foo`指向函数外层,但是函数外层并没有声明变量`foo`,所以就报错了。
下面是一个更复杂的例子。
@ -308,11 +347,12 @@ function foo(x, y = function() { x = 2; }) {
}
foo() // 3
x // 1
```
上面代码中,函数`foo`的参数`y`的默认值是一个匿名函数。函数`foo`调用时,它的参数`x`的值为`undefined`,所以`y`函数内部的`x`一开始是`undefined`,后来被重新赋值`2`。但是,函数`foo`内部重新声明了一个`x`,值为`3`,这两个`x`是不一样的,互相不产生影响,因此最后输出`3`
上面代码中,函数`foo`的参数形成一个单独作用域。这个作用域里面,首先声明了变量`x`,然后声明了变量`y``y`的默认值是一个匿名函数。这个匿名函数内部的变量`x`,指向同一个作用域的第一个参数`x`。函数`foo`内部又声明了一个内部变量`x`,该变量与第一个参数`x`由于不是同一个作用域,所以不是同一个变量,因此执行`y`后,内部变量`x`和外部全局变量`x`的值都没变
如果将`var x = 3``var`去除,两个`x`就是一样的,最后输出的就是`2`
如果将`var x = 3``var`去除,函数`foo`的内部变量`x`就指向第一个参数`x`,与匿名函数内部的`x`是一致的,所以最后输出的就是`2`,而外层的全局变量`x`依然不受影响
```javascript
var x = 1;
@ -323,6 +363,7 @@ function foo(x, y = function() { x = 2; }) {
}
foo() // 2
x // 1
```
### 应用
@ -344,7 +385,7 @@ foo()
上面代码的`foo`函数,如果调用的时候没有参数,就会调用默认值`throwIfMissing`函数,从而抛出一个错误。
从上面代码还可以看到,参数`mustBeProvided`的默认值等于`throwIfMissing`函数的运行结果(即函数名之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行(即如果参数已经赋值,默认值中的函数就不会运行这与python语言不一样
从上面代码还可以看到,参数`mustBeProvided`的默认值等于`throwIfMissing`函数的运行结果(注意函数名`throwIfMissing`之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行如果参数已经赋值,默认值中的函数就不会运行。
另外,可以将参数默认值设为`undefined`,表明这个参数是可以省略的。
@ -354,7 +395,7 @@ function foo(optional = undefined) { ··· }
## rest 参数
ES6引入rest参数形式为“...变量名”用于获取函数的多余参数这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组该变量将多余的参数放入数组中。
ES6 引入 rest 参数(形式为`...变量名`),用于获取函数的多余参数,这样就不需要使用`arguments`对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
```javascript
function add(...values) {
@ -370,9 +411,9 @@ function add(...values) {
add(2, 5, 3) // 10
```
上面代码的add函数是一个求和函数利用rest参数可以向该函数传入任意数目的参数。
上面代码的`add`函数是一个求和函数,利用 rest 参数,可以向该函数传入任意数目的参数。
下面是一个rest参数代替arguments变量的例子。
下面是一个 rest 参数代替`arguments`变量的例子。
```javascript
// arguments变量的写法
@ -386,7 +427,7 @@ const sortNumbers = (...numbers) => numbers.sort();
上面代码的两种写法比较后可以发现rest 参数的写法更自然也更简洁。
rest参数中的变量代表一个数组所以数组特有的方法都可以用于这个变量。下面是一个利用rest参数改写数组push方法的例子。
`arguments`对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用`Array.prototype.slice.call`先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。下面是一个利用 rest 参数改写数组`push`方法的例子。
```javascript
function push(array, ...items) {
@ -409,7 +450,7 @@ function f(a, ...b, c) {
}
```
函数的length属性不包括rest参数。
函数的`length`属性,不包括 rest 参数。
```javascript
(function(a) {}).length // 1
@ -417,283 +458,6 @@ function f(a, ...b, c) {
(function(a, ...b) {}).length // 1
```
## 扩展运算符
### 含义
扩展运算符spread是三个点`...`。它好比rest参数的逆运算将一个数组转为用逗号分隔的参数序列。
```javascript
console.log(...[1, 2, 3])
// 1 2 3
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]
```
该运算符主要用于函数调用。
```javascript
function push(array, ...items) {
array.push(...items);
}
function add(x, y) {
return x + y;
}
var numbers = [4, 38];
add(...numbers) // 42
```
上面代码中,`array.push(...items)``add(...numbers)`这两行,都是函数的调用,它们的都使用了扩展运算符。该运算符将一个数组,变为参数序列。
扩展运算符与正常的函数参数可以结合使用,非常灵活。
```javascript
function f(v, w, x, y, z) { }
var args = [0, 1];
f(-1, ...args, 2, ...[3]);
```
### 替代数组的apply方法
由于扩展运算符可以展开数组,所以不再需要`apply`方法,将数组转为函数的参数了。
```javascript
// ES5的写法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f.apply(null, args);
// ES6的写法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f(...args);
```
下面是扩展运算符取代`apply`方法的一个实际的例子,应用`Math.max`方法,简化求出一个数组最大元素的写法。
```javascript
// ES5的写法
Math.max.apply(null, [14, 3, 77])
// ES6的写法
Math.max(...[14, 3, 77])
// 等同于
Math.max(14, 3, 77);
```
上面代码表示由于JavaScript不提供求数组最大元素的函数所以只能套用`Math.max`函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用`Math.max`了。
另一个例子是通过`push`函数,将一个数组添加到另一个数组的尾部。
```javascript
// ES5的写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);
// ES6的写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
arr1.push(...arr2);
```
上面代码的ES5写法中`push`方法的参数不能是数组,所以只好通过`apply`方法变通使用`push`方法。有了扩展运算符,就可以直接将数组传入`push`方法。
下面是另外一个例子。
```javascript
// ES5
new (Date.bind.apply(Date, [null, 2015, 1, 1]))
// ES6
new Date(...[2015, 1, 1]);
```
### 扩展运算符的应用
**1合并数组**
扩展运算符提供了数组合并的新写法。
```javascript
// ES5
[1, 2].concat(more)
// ES6
[1, 2, ...more]
var arr1 = ['a', 'b'];
var arr2 = ['c'];
var arr3 = ['d', 'e'];
// ES5的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]
// ES6的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
```
**2与解构赋值结合**
扩展运算符可以与解构赋值结合起来,用于生成数组。
```javascript
// ES5
a = list[0], rest = list.slice(1)
// ES6
[a, ...rest] = list
```
下面是另外一些例子。
```javascript
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest // [2, 3, 4, 5]
const [first, ...rest] = [];
first // undefined
rest // []:
const [first, ...rest] = ["foo"];
first // "foo"
rest // []
```
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
```javascript
const [...butLast, last] = [1, 2, 3, 4, 5];
// 报错
const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 报错
```
**3函数的返回值**
JavaScript的函数只能返回一个值如果需要返回多个值只能返回数组或对象。扩展运算符提供了解决这个问题的一种变通方法。
```javascript
var dateFields = readDateFields(database);
var d = new Date(...dateFields);
```
上面代码从数据库取出一行数据,通过扩展运算符,直接将其传入构造函数`Date`
**4字符串**
扩展运算符还可以将字符串转为真正的数组。
```javascript
[...'hello']
// [ "h", "e", "l", "l", "o" ]
```
上面的写法有一个重要的好处那就是能够正确识别32位的Unicode字符。
```javascript
'x\uD83D\uDE80y'.length // 4
[...'x\uD83D\uDE80y'].length // 3
```
上面代码的第一种写法JavaScript会将32位Unicode字符识别为2个字符采用扩展运算符就没有这个问题。因此正确返回字符串长度的函数可以像下面这样写。
```javascript
function length(str) {
return [...str].length;
}
length('x\uD83D\uDE80y') // 3
```
凡是涉及到操作32位Unicode字符的函数都有这个问题。因此最好都用扩展运算符改写。
```javascript
let str = 'x\uD83D\uDE80y';
str.split('').reverse().join('')
// 'y\uDE80\uD83Dx'
[...str].reverse().join('')
// 'y\uD83D\uDE80x'
```
上面代码中,如果不用扩展运算符,字符串的`reverse`操作就不正确。
**5实现了Iterator接口的对象**
任何Iterator接口的对象都可以用扩展运算符转为真正的数组。
```javascript
var nodeList = document.querySelectorAll('div');
var array = [...nodeList];
```
上面代码中,`querySelectorAll`方法返回的是一个`nodeList`对象。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于`NodeList`对象实现了Iterator接口。
对于那些没有部署Iterator接口的类似数组的对象扩展运算符就无法将其转为真正的数组。
```javascript
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// TypeError: Cannot spread non-iterable object.
let arr = [...arrayLike];
```
上面代码中,`arrayLike`是一个类似数组的对象但是没有部署Iterator接口扩展运算符就会报错。这时可以改为使用`Array.from`方法将`arrayLike`转为真正的数组。
**6Map和Set结构Generator函数**
扩展运算符内部调用的是数据结构的Iterator接口因此只要具有Iterator接口的对象都可以使用扩展运算符比如Map结构。
```javascript
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
let arr = [...map.keys()]; // [1, 2, 3]
```
Generator函数运行后返回一个遍历器对象因此也可以使用扩展运算符。
```javascript
var go = function*(){
yield 1;
yield 2;
yield 3;
};
[...go()] // [1, 2, 3]
```
上面代码中,变量`go`是一个Generator函数执行后返回的是一个遍历器对象对这个遍历器对象执行扩展运算符就会将内部遍历得到的值转为一个数组。
如果对没有`iterator`接口的对象,使用扩展运算符,将会报错。
```javascript
var obj = {a: 1, b: 2};
let arr = [...obj]; // TypeError: Cannot spread non-iterable object
```
## 严格模式
从 ES5 开始,函数内部可以设定为严格模式。
@ -705,7 +469,7 @@ function doSomething(a, b) {
}
```
《ECMAScript 2016标准》做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。
ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。
```javascript
// 报错
@ -735,7 +499,7 @@ const obj = {
};
```
这样规定的原因是,函数内部的严格模式,同时适用于函数体代码和函数参数代码。但是,函数执行的时候,先执行函数参数代码,然后再执行函数体代码。这样就有一个不合理的地方,只有从函数体代码之中,才能知道参数代码是否应该以严格模式执行,但是参数代码却应该先于函数体代码执行。
这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。
```javascript
// 报错
@ -784,16 +548,16 @@ foo.name // "foo"
需要注意的是ES6 对这个属性的行为做出了一些修改。如果将一个匿名函数赋值给一个变量ES5 的`name`属性,会返回空字符串,而 ES6 的`name`属性会返回实际的函数名。
```javascript
var func1 = function () {};
var f = function () {};
// ES5
func1.name // ""
f.name // ""
// ES6
func1.name // "func1"
f.name // "f"
```
上面代码中,变量`func1`等于一个匿名函数ES5和ES6的`name`属性返回的值不一样。
上面代码中,变量`f`等于一个匿名函数ES5 ES6 `name`属性返回的值不一样。
如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的`name`属性都返回这个具名函数原本的名字。
@ -807,13 +571,13 @@ bar.name // "baz"
bar.name // "baz"
```
`Function`构造函数返回的函数实例,`name`属性的值为“anonymous”
`Function`构造函数返回的函数实例,`name`属性的值为`anonymous`
```javascript
(new Function).name // "anonymous"
```
`bind`返回的函数,`name`属性值会加上“bound ”前缀。
`bind`返回的函数,`name`属性值会加上`bound`前缀。
```javascript
function foo() {};
@ -860,10 +624,20 @@ var sum = function(num1, num2) {
var sum = (num1, num2) => { return num1 + num2; }
```
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号。
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错
```javascript
var getTempItem = id => ({ id: id, name: "Temp" });
// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });
```
如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。
```javascript
let fn = () => void doesNotReturn();
```
箭头函数可以与变量解构结合使用。
@ -932,7 +706,7 @@ headAndTail(1, 2, 3, 4, 5)
2不可以当作构造函数也就是说不可以使用`new`命令,否则会抛出一个错误。
3不可以使用`arguments`对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。
3不可以使用`arguments`对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
4不可以使用`yield`命令,因此箭头函数不能用作 Generator 函数。
@ -1137,11 +911,11 @@ var fix = f => (x => f(v => x(x)(v)))
上面两种写法,几乎是一一对应的。由于 λ 演算对于计算机科学非常重要,这使得我们可以用 ES6 作为替代工具,探索计算机科学。
## 绑定 this
## 双冒号运算符
箭头函数可以绑定`this`对象,大大减少了显式绑定`this`对象的写法(`call``apply``bind`)。但是,箭头函数并不适用于所有场合,所以ES7提出了“函数绑定”function bind运算符用来取代`call``apply``bind`调用。虽然该语法还是ES7的一个[提案](https://github.com/zenparsing/es-function-bind)但是Babel转码器已经支持
箭头函数可以绑定`this`对象,大大减少了显式绑定`this`对象的写法(`call``apply``bind`)。但是,箭头函数并不适用于所有场合,所以现在有一个[提案](https://github.com/zenparsing/es-function-bind)提出了“函数绑定”function bind运算符用来取代`call``apply``bind`调用
函数绑定运算符是并排的两个冒号(::双冒号左边是一个对象右边是一个函数。该运算符会自动将左边的对象作为上下文环境即this对象绑定到右边的函数上面。
函数绑定运算符是并排的两个冒号(`::`),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即`this`对象),绑定到右边的函数上面。
```javascript
foo::bar;
@ -1170,7 +944,7 @@ let log = ::console.log;
var log = console.log.bind(console);
```
由于双冒号运算符返回的还是原对象,因此可以采用链式写法。
双冒号运算符的运算结果,还是一个对象,因此可以采用链式写法。
```javascript
// 例一
@ -1201,7 +975,7 @@ function f(x){
}
```
上面代码中函数f的最后一步是调用函数g这就叫尾调用。
上面代码中,函数`f`的最后一步是调用函数`g`,这就叫尾调用。
以下三种情况,都不属于尾调用。
@ -1223,7 +997,7 @@ function f(x){
}
```
上面代码中情况一是调用函数g之后还有赋值操作所以不属于尾调用即使语义完全一样。情况二也属于调用后还有操作即使写在一行内。情况三等同于下面的代码。
上面代码中,情况一是调用函数`g`之后,还有赋值操作,所以不属于尾调用,即使语义完全一样。情况二也属于调用后还有操作,即使写在一行内。情况三等同于下面的代码。
```javascript
function f(x){
@ -1243,13 +1017,13 @@ function f(x) {
}
```
上面代码中函数m和n都属于尾调用因为它们都是函数f的最后一步操作。
上面代码中,函数`m``n`都属于尾调用,因为它们都是函数`f`的最后一步操作。
### 尾调用优化
尾调用之所以与其他调用不同,就在于它的特殊的调用位置。
我们知道函数调用会在内存形成一个“调用记录”又称“调用帧”call frame保存调用位置和内部变量等信息。如果在函数A的内部调用函数B那么在A的调用帧上方还会形成一个B的调用帧。等到B运行结束将结果返回到AB的调用帧才会消失。如果函数B内部还调用函数C那就还有一个C的调用帧以此类推。所有的调用帧就形成一个“调用栈”call stack
我们知道函数调用会在内存形成一个“调用记录”又称“调用帧”call frame保存调用位置和内部变量等信息。如果在函数`A`的内部调用函数`B`,那么在`A`的调用帧上方,还会形成一个`B`的调用帧。等到`B`运行结束,将结果返回到`A``B`的调用帧才会消失。如果函数`B`内部还调用函数`C`,那就还有一个`C`的调用帧以此类推。所有的调用帧就形成一个“调用栈”call stack
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
@ -1271,7 +1045,7 @@ f();
g(3);
```
上面代码中如果函数g不是尾调用函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后函数f就结束了所以执行到最后一步完全可以删除 f(x) 的调用帧,只保留 g(3) 的调用帧。
上面代码中,如果函数`g`不是尾调用,函数`f`就需要保存内部变量`m``n`的值、`g`的调用位置等信息。但由于调用`g`之后,函数`f`就结束了,所以执行到最后一步,完全可以删除`f(x)`的调用帧,只保留`g(3)`的调用帧。
这就叫做“尾调用优化”Tail call optimization即只保留内层函数的调用帧。如果所有函数都是尾调用那么完全可以做到每次执行时调用帧只有一项这将大大节省内存。这就是“尾调用优化”的意义。
@ -1304,7 +1078,7 @@ function factorial(n) {
factorial(5) // 120
```
上面代码是一个阶乘函数计算n的阶乘最多需要保存n个调用记录复杂度 O(n) 。
上面代码是一个阶乘函数,计算`n`的阶乘,最多需要保存`n`个调用记录,复杂度 O(n) 。
如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。
@ -1317,9 +1091,9 @@ function factorial(n, total) {
factorial(5, 1) // 120
```
还有一个比较著名的例子,就是计算fibonacci 数列,也能充分说明尾递归优化的重要性
还有一个比较著名的例子,就是计算 Fibonacci 数列,也能充分说明尾递归优化的重要性
如果是非尾递归的fibonacci 递归方法
非尾递归的 Fibonacci 数列实现如下。
```javascript
function Fibonacci (n) {
@ -1328,13 +1102,12 @@ function Fibonacci (n) {
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10); // 89
// Fibonacci(100)
// Fibonacci(500)
// 堆栈溢出了
Fibonacci(10) // 89
Fibonacci(100) // 堆栈溢出
Fibonacci(500) // 堆栈溢出
```
如果我们使用尾递归优化过的fibonacci 递归算法
尾递归优化过的 Fibonacci 数列实现如下。
```javascript
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
@ -1348,11 +1121,11 @@ Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity
```
由此可见“尾调用优化”对递归操作意义重大所以一些函数式编程语言将其写入了语言规格。ES6也是如此第一次明确规定所有ECMAScript的实现,都必须部署“尾调用优化”。这就是说,ES6中只要使用尾递归,就不会发生栈溢出,相对节省内存。
由此可见“尾调用优化”对递归操作意义重大所以一些函数式编程语言将其写入了语言规格。ES6 是如此,第一次明确规定,所有 ECMAScript 的实现都必须部署“尾调用优化”。这就是说ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存。
### 递归函数的改写
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量 total 那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观第一眼很难看出来为什么计算5的阶乘需要传入两个参数5和1
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量`total`,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来,为什么计算`5`的阶乘,需要传入两个参数`5``1`
两个方法可以解决这个问题。方法一是在尾递归函数之外,再提供一个正常形式的函数。
@ -1369,7 +1142,7 @@ function factorial(n) {
factorial(5) // 120
```
上面代码通过一个正常形式的阶乘函数 factorial ,调用尾递归函数 tailFactorial ,看起来就正常多了。
上面代码通过一个正常形式的阶乘函数`factorial`,调用尾递归函数`tailFactorial`,看起来就正常多了。
函数式编程有一个概念叫做柯里化currying意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化。
@ -1390,7 +1163,7 @@ const factorial = currying(tailFactorial, 1);
factorial(5) // 120
```
上面代码通过柯里化,将尾递归函数 tailFactorial 变为只接受1个参数的 factorial
上面代码通过柯里化,将尾递归函数`tailFactorial`变为只接受一个参数的`factorial`
第二种方法就简单多了,就是采用 ES6 的函数默认值。
@ -1403,7 +1176,7 @@ function factorial(n, total = 1) {
factorial(5) // 120
```
上面代码中,参数 total 有默认值1,所以调用时不用提供这个值。
上面代码中,参数`total`有默认值`1`,所以调用时不用提供这个值。
总结一下,递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言(比如 LuaES6只需要知道循环可以用递归代替而一旦使用递归就最好使用尾递归。
@ -1420,7 +1193,7 @@ ES6的尾调用优化只在严格模式下开启正常模式是无效的。
```javascript
function restricted() {
"use strict";
'use strict';
restricted.caller; // 报错
restricted.arguments; // 报错
}
@ -1522,7 +1295,7 @@ sum(1, 100000)
## 函数参数的尾逗号
ECMAScript 2017将[允许](https://github.com/jeffmo/es-trailing-function-commas)函数的最后一个参数有尾逗号trailing comma
ES2017 [允许](https://github.com/jeffmo/es-trailing-function-commas)函数的最后一个参数有尾逗号trailing comma
此前,函数定义和调用时,都不允许最后一个参数后面出现逗号。
@ -1540,7 +1313,7 @@ clownsEverywhere(
上面代码中,如果在`param2``bar`后面加一个逗号,就会报错。
这样的话,如果以后修改代码,想为函数`clownsEverywhere`添加第三个参数,就势必要在第二个参数后面添加一个逗号。这对版本管理系统来说,就会显示,添加逗号的那一行也发生了变动。这看上去有点冗余,因此新的语法允许定义和调用时,尾部直接有一个逗号。
如果像上面这样,将参数写成多行(即每个参数占据一行),以后修改代码的时候,想为函数`clownsEverywhere`添加第三个参数,或者调整参数的次序,就势必要在原来最后一个参数后面添加一个逗号。这对于版本管理系统来说,就会显示添加逗号的那一行也发生了变动。这看上去有点冗余,因此新的语法允许定义和调用时,尾部直接有一个逗号。
```javascript
function clownsEverywhere(
@ -1553,3 +1326,42 @@ clownsEverywhere(
'bar',
);
```
这样的规定也使得,函数参数与数组和对象的尾逗号规则,保持一致了。
## catch 语句的参数
目前,有一个[提案](https://github.com/tc39/proposal-optional-catch-binding),允许`try...catch`结构中的`catch`语句调用时不带有参数。这个提案跟参数有关,也放在这一章介绍。
传统的写法是`catch`语句必须带有参数,用来接收`try`代码块抛出的错误。
```javascript
try {
// ···
} catch (error) {
// ···
}
```
新的写法允许省略`catch`后面的参数,而不报错。
```javascript
try {
// ···
} catch {
// ···
}
```
新写法只在不需要错误实例的情况下有用,因此不及传统写法的用途广。
```javascript
let jsonData;
try {
jsonData = JSON.parse(str);
} catch {
jsonData = DEFAULT_DATA;
}
```
上面代码中,`JSON.parse`报错只有一种可能:解析失败。因此,可以不需要抛出的错误实例。

790
docs/generator-async.md Normal file
View File

@ -0,0 +1,790 @@
# Generator 函数的异步应用
异步编程对 JavaScript 语言太重要。Javascript 语言的执行环境是“单线程”的,如果没有异步编程,根本没法用,非卡死不可。本章主要介绍 Generator 函数如何完成异步操作。
## 传统方法
ES6 诞生以前,异步编程的方法,大概有下面四种。
- 回调函数
- 事件监听
- 发布/订阅
- Promise 对象
Generator 函数将 JavaScript 异步编程带入了一个全新的阶段。
## 基本概念
### 异步
所谓"异步",简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。
比如,有一个任务是读取文件进行处理,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫做异步。
相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。
### 回调函数
JavaScript 语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。回调函数的英语名字`callback`,直译过来就是"重新调用"。
读取文件进行处理,是这样写的。
```javascript
fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
if (err) throw err;
console.log(data);
});
```
上面代码中,`readFile`函数的第三个参数,就是回调函数,也就是任务的第二段。等到操作系统返回了`/etc/passwd`这个文件以后,回调函数才会执行。
一个有趣的问题是,为什么 Node 约定,回调函数的第一个参数,必须是错误对象`err`(如果没有错误,该参数就是`null`
原因是执行分成两段,第一段执行完以后,任务所在的上下文环境就已经结束了。在这以后抛出的错误,原来的上下文环境已经无法捕捉,只能当作参数,传入第二段。
### Promise
回调函数本身并没有问题,它的问题出现在多个回调函数嵌套。假定读取`A`文件之后,再读取`B`文件,代码如下。
```javascript
fs.readFile(fileA, 'utf-8', function (err, data) {
fs.readFile(fileB, 'utf-8', function (err, data) {
// ...
});
});
```
不难想象,如果依次读取两个以上的文件,就会出现多重嵌套。代码不是纵向发展,而是横向发展,很快就会乱成一团,无法管理。因为多个异步操作形成了强耦合,只要有一个操作需要修改,它的上层回调函数和下层回调函数,可能都要跟着修改。这种情况就称为"回调函数地狱"callback hell
Promise 对象就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。采用 Promise连续读取多个文件写法如下。
```javascript
var readFile = require('fs-readfile-promise');
readFile(fileA)
.then(function (data) {
console.log(data.toString());
})
.then(function () {
return readFile(fileB);
})
.then(function (data) {
console.log(data.toString());
})
.catch(function (err) {
console.log(err);
});
```
上面代码中,我使用了`fs-readfile-promise`模块,它的作用就是返回一个 Promise 版本的`readFile`函数。Promise 提供`then`方法加载回调函数,`catch`方法捕捉执行过程中抛出的错误。
可以看到Promise 的写法只是回调函数的改进,使用`then`方法以后,异步任务的两段执行看得更清楚了,除此以外,并无新意。
Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆`then`,原来的语义变得很不清楚。
那么,有没有更好的写法呢?
## Generator 函数
### 协程
传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做"协程"coroutine意思是多个线程互相协作完成异步任务。
协程有点像函数,又有点像线程。它的运行流程大致如下。
- 第一步,协程`A`开始执行。
- 第二步,协程`A`执行到一半,进入暂停,执行权转移到协程`B`
- 第三步,(一段时间后)协程`B`交还执行权。
- 第四步,协程`A`恢复执行。
上面流程的协程`A`,就是异步任务,因为它分成两段(或多段)执行。
举例来说,读取文件的协程写法如下。
```javascript
function* asyncJob() {
// ...其他代码
var f = yield readFile(fileA);
// ...其他代码
}
```
上面代码的函数`asyncJob`是一个协程,它的奥妙就在其中的`yield`命令。它表示执行到此处,执行权将交给其他协程。也就是说,`yield`命令是异步两个阶段的分界线。
协程遇到`yield`命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除`yield`命令,简直一模一样。
### 协程的 Generator 函数实现
Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用`yield`语句注明。Generator 函数的执行方法如下。
```javascript
function* gen(x) {
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
```
上面代码中,调用 Generator 函数,会返回一个内部指针(即遍历器)`g`。这是 Generator 函数不同于普通函数的另一个地方,即执行它不会返回结果,返回的是指针对象。调用指针`g``next`方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的`yield`语句,上例是执行到`x + 2`为止。
换言之,`next`方法的作用是分阶段执行`Generator`函数。每次调用`next`方法,会返回一个对象,表示当前阶段的信息(`value`属性和`done`属性)。`value`属性是`yield`语句后面表达式的值,表示当前阶段的值;`done`属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。
### Generator 函数的数据交换和错误处理
Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。
`next`返回值的 value 属性,是 Generator 函数向外输出数据;`next`方法还可以接受参数,向 Generator 函数体内输入数据。
```javascript
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }
```
上面代码中,第一`next`方法的`value`属性,返回表达式`x + 2`的值`3`。第二个`next`方法带有参数`2`,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果,被函数体内的变量`y`接收。因此,这一步的`value`属性,返回的就是`2`(变量`y`的值)。
Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。
```javascript
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出错了');
// 出错了
```
上面代码的最后一行Generator 函数体外,使用指针对象的`throw`方法抛出的错误,可以被函数体内的`try...catch`代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。
### 异步任务的封装
下面看看如何使用 Generator 函数,执行一个真实的异步任务。
```javascript
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
```
上面代码中Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。就像前面说过的,这段代码非常像同步操作,除了加上了`yield`命令。
执行这段代码的方法如下。
```javascript
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
```
上面代码中,首先执行 Generator 函数,获取遍历器对象,然后使用`next`方法(第二行),执行异步任务的第一阶段。由于`Fetch`模块返回的是一个 Promise 对象,因此要用`then`方法调用下一个`next`方法。
可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。
## Thunk 函数
Thunk 函数是自动执行 Generator 函数的一种方法。
### 参数的求值策略
Thunk 函数早在上个世纪 60 年代就诞生了。
那时,编程语言刚刚起步,计算机学家还在研究,编译器怎么写比较好。一个争论的焦点是"求值策略",即函数的参数到底应该何时求值。
```javascript
var x = 1;
function f(m) {
return m * 2;
}
f(x + 5)
```
上面代码先定义函数`f`,然后向它传入表达式`x + 5`。请问,这个表达式应该何时求值?
一种意见是"传值调用"call by value即在进入函数体之前就计算`x + 5`的值(等于 6再将这个值传入函数`f`。C 语言就采用这种策略。
```javascript
f(x + 5)
// 传值调用时,等同于
f(6)
```
另一种意见是“传名调用”call by name即直接将表达式`x + 5`传入函数体只在用到它的时候求值。Haskell 语言采用这种策略。
```javascript
f(x + 5)
// 传名调用时,等同于
(x + 5) * 2
```
传值调用和传名调用,哪一种比较好?
回答是各有利弊。传值调用比较简单,但是对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失。
```javascript
function f(a, b){
return b;
}
f(3 * x * x - 2 * x - 1, x);
```
上面代码中,函数`f`的第一个参数是一个复杂的表达式,但是函数体内根本没用到。对这个参数求值,实际上是不必要的。因此,有一些计算机学家倾向于"传名调用",即只在执行时求值。
### Thunk 函数的含义
编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。
```javascript
function f(m) {
return m * 2;
}
f(x + 5);
// 等同于
var thunk = function () {
return x + 5;
};
function f(thunk) {
return thunk() * 2;
}
```
上面代码中,函数 f 的参数`x + 5`被一个函数替换了。凡是用到原参数的地方,对`Thunk`函数求值即可。
这就是 Thunk 函数的定义,它是“传名调用”的一种实现策略,用来替换某个表达式。
### JavaScript 语言的 Thunk 函数
JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
```javascript
// 正常版本的readFile多参数版本
fs.readFile(fileName, callback);
// Thunk版本的readFile单参数版本
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
```
上面代码中,`fs`模块的`readFile`方法是一个多参数函数,两个参数分别为文件名和回调函数。经过转换器处理,它变成了一个单参数函数,只接受回调函数作为参数。这个单参数版本,就叫做 Thunk 函数。
任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。下面是一个简单的 Thunk 函数转换器。
```javascript
// ES5版本
var Thunk = function(fn){
return function (){
var args = Array.prototype.slice.call(arguments);
return function (callback){
args.push(callback);
return fn.apply(this, args);
}
};
};
// ES6版本
const Thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};
```
使用上面的转换器,生成`fs.readFile`的 Thunk 函数。
```javascript
var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);
```
下面是另一个完整的例子。
```javascript
function f(a, cb) {
cb(a);
}
const ft = Thunk(f);
ft(1)(console.log) // 1
```
### Thunkify 模块
生产环境的转换器,建议使用 Thunkify 模块。
首先是安装。
```bash
$ npm install thunkify
```
使用方式如下。
```javascript
var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
// ...
});
```
Thunkify 的源码与上一节那个简单的转换器非常像。
```javascript
function thunkify(fn) {
return function() {
var args = new Array(arguments.length);
var ctx = this;
for (var i = 0; i < args.length; ++i) {
args[i] = arguments[i];
}
return function (done) {
var called;
args.push(function () {
if (called) return;
called = true;
done.apply(null, arguments);
});
try {
fn.apply(ctx, args);
} catch (err) {
done(err);
}
}
}
};
```
它的源码主要多了一个检查机制,变量`called`确保回调函数只运行一次。这样的设计与下文的 Generator 函数相关。请看下面的例子。
```javascript
function f(a, b, callback){
var sum = a + b;
callback(sum);
callback(sum);
}
var ft = thunkify(f);
var print = console.log.bind(console);
ft(1, 2)(print);
// 3
```
上面代码中,由于`thunkify`只允许回调函数执行一次,所以只输出一行结果。
### Generator 函数的流程管理
你可能会问, Thunk 函数有什么用?回答是以前确实没什么用,但是 ES6 有了 Generator 函数Thunk 函数现在可以用于 Generator 函数的自动流程管理。
Generator 函数可以自动执行。
```javascript
function* gen() {
// ...
}
var g = gen();
var res = g.next();
while(!res.done){
console.log(res.value);
res = g.next();
}
```
上面代码中Generator 函数`gen`会自动执行完所有步骤。
但是这不适合异步操作。如果必须保证前一步执行完才能执行后一步上面的自动执行就不可行。这时Thunk 函数就能派上用处。以读取文件为例。下面的 Generator 函数封装了两个异步操作。
```javascript
var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);
var gen = function* (){
var r1 = yield readFileThunk('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFileThunk('/etc/shells');
console.log(r2.toString());
};
```
上面代码中,`yield`命令用于将程序的执行权移出 Generator 函数,那么就需要一种方法,将执行权再交还给 Generator 函数。
这种方法就是 Thunk 函数,因为它可以在回调函数里,将执行权交还给 Generator 函数。为了便于理解,我们先看如何手动执行上面这个 Generator 函数。
```javascript
var g = gen();
var r1 = g.next();
r1.value(function (err, data) {
if (err) throw err;
var r2 = g.next(data);
r2.value(function (err, data) {
if (err) throw err;
g.next(data);
});
});
```
上面代码中,变量`g`是 Generator 函数的内部指针,表示目前执行到哪一步。`next`方法负责将指针移动到下一步,并返回该步的信息(`value`属性和`done`属性)。
仔细查看上面的代码,可以发现 Generator 函数的执行过程,其实是将同一个回调函数,反复传入`next`方法的`value`属性。这使得我们可以用递归来自动完成这个过程。
### Thunk 函数的自动流程管理
Thunk 函数真正的威力,在于可以自动执行 Generator 函数。下面就是一个基于 Thunk 函数的 Generator 执行器。
```javascript
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
function* g() {
// ...
}
run(g);
```
上面代码的`run`函数,就是一个 Generator 函数的自动执行器。内部的`next`函数就是 Thunk 的回调函数。`next`函数先将指针移到 Generator 函数的下一步(`gen.next`方法),然后判断 Generator 函数是否结束(`result.done`属性),如果没结束,就将`next`函数再传入 Thunk 函数(`result.value`属性),否则就直接退出。
有了这个执行器,执行 Generator 函数方便多了。不管内部有多少个异步操作,直接把 Generator 函数传入`run`函数即可。当然,前提是每一个异步操作,都要是 Thunk 函数,也就是说,跟在`yield`命令后面的必须是 Thunk 函数。
```javascript
var g = function* (){
var f1 = yield readFileThunk('fileA');
var f2 = yield readFileThunk('fileB');
// ...
var fn = yield readFileThunk('fileN');
};
run(g);
```
上面代码中,函数`g`封装了`n`个异步的读取文件操作,只要执行`run`函数,这些操作就会自动完成。这样一来,异步操作不仅可以写得像同步操作,而且一行代码就可以执行。
Thunk 函数并不是 Generator 函数自动执行的唯一方案。因为自动执行的关键是,必须有一种机制,自动控制 Generator 函数的流程接收和交还程序的执行权。回调函数可以做到这一点Promise 对象也可以做到这一点。
## co 模块
### 基本用法
[co 模块](https://github.com/tj/co)是著名程序员 TJ Holowaychuk 于 2013 年 6 月发布的一个小工具,用于 Generator 函数的自动执行。
下面是一个 Generator 函数,用于依次读取两个文件。
```javascript
var gen = function* () {
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
```
co 模块可以让你不用编写 Generator 函数的执行器。
```javascript
var co = require('co');
co(gen);
```
上面代码中Generator 函数只要传入`co`函数,就会自动执行。
`co`函数返回一个`Promise`对象,因此可以用`then`方法添加回调函数。
```javascript
co(gen).then(function (){
console.log('Generator 函数执行完成');
});
```
上面代码中,等到 Generator 函数执行结束,就会输出一行提示。
### co 模块的原理
为什么 co 可以自动执行 Generator 函数?
前面说过Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。
两种方法可以做到这一点。
1回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
2Promise 对象。将异步操作包装成 Promise 对象,用`then`方法交回执行权。
co 模块其实就是将两种自动执行器Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是Generator 函数的`yield`命令后面,只能是 Thunk 函数或 Promise 对象。如果数组或对象的成员,全部都是 Promise 对象,也可以使用 co详见后文的例子。
上一节已经介绍了基于 Thunk 函数的自动执行器。下面来看,基于 Promise 对象的自动执行器。这是理解 co 模块必须的。
### 基于 Promise 对象的自动执行
还是沿用上面的例子。首先,把`fs`模块的`readFile`方法包装成一个 Promise 对象。
```javascript
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) return reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
```
然后,手动执行上面的 Generator 函数。
```javascript
var g = gen();
g.next().value.then(function(data){
g.next(data).value.then(function(data){
g.next(data);
});
});
```
手动执行其实就是用`then`方法,层层添加回调函数。理解了这一点,就可以写出一个自动执行器。
```javascript
function run(gen){
var g = gen();
function next(data){
var result = g.next(data);
if (result.done) return result.value;
result.value.then(function(data){
next(data);
});
}
next();
}
run(gen);
```
上面代码中,只要 Generator 函数还没执行到最后一步,`next`函数就调用自身,以此实现自动执行。
### co 模块的源码
co 就是上面那个自动执行器的扩展,它的源码只有几十行,非常简单。
首先co 函数接受 Generator 函数作为参数,返回一个 Promise 对象。
```javascript
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
});
}
```
在返回的 Promise 对象里面co 先检查参数`gen`是否为 Generator 函数。如果是,就执行该函数,得到一个内部指针对象;如果不是就返回,并将 Promise 对象的状态改为`resolved`
```javascript
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
});
}
```
接着co 将 Generator 函数的内部指针对象的`next`方法,包装成`onFulfilled`函数。这主要是为了能够捕捉抛出的错误。
```javascript
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
});
}
```
最后,就是关键的`next`函数,它会反复调用自身。
```javascript
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(
new TypeError(
'You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "'
+ String(ret.value)
+ '"'
)
);
}
```
上面代码中,`next`函数的内部代码,一共只有四行命令。
第一行,检查当前是否为 Generator 函数的最后一步,如果是就返回。
第二行,确保每一步的返回值,是 Promise 对象。
第三行,使用`then`方法,为返回值加上回调函数,然后通过`onFulfilled`函数再次调用`next`函数。
第四行,在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为`rejected`,从而终止执行。
### 处理并发的异步操作
co 支持并发的异步操作,即允许某些操作同时进行,等到它们全部完成,才进行下一步。
这时,要把并发的操作都放在数组或对象里面,跟在`yield`语句后面。
```javascript
// 数组的写法
co(function* () {
var res = yield [
Promise.resolve(1),
Promise.resolve(2)
];
console.log(res);
}).catch(onerror);
// 对象的写法
co(function* () {
var res = yield {
1: Promise.resolve(1),
2: Promise.resolve(2),
};
console.log(res);
}).catch(onerror);
```
下面是另一个例子。
```javascript
co(function* () {
var values = [n1, n2, n3];
yield values.map(somethingAsync);
});
function* somethingAsync(x) {
// do something async
return y
}
```
上面的代码允许并发三个`somethingAsync`异步操作,等到它们全部完成,才会进行下一步。
### 实例:处理 Stream
Node 提供 Stream 模式读写数据特点是一次只处理数据的一部分数据分成一块块依次处理就好像“数据流”一样。这对于处理大规模数据非常有利。Stream 模式使用 EventEmitter API会释放三个事件。
- `data`事件:下一块数据块已经准备好了。
- `end`事件:整个“数据流”处理“完了。
- `error`事件:发生错误。
使用`Promise.race()`函数,可以判断这三个事件之中哪一个最先发生,只有当`data`事件最先发生时,才进入下一个数据块的处理。从而,我们可以通过一个`while`循环,完成所有数据的读取。
```javascript
const co = require('co');
const fs = require('fs');
const stream = fs.createReadStream('./les_miserables.txt');
let valjeanCount = 0;
co(function*() {
while(true) {
const res = yield Promise.race([
new Promise(resolve => stream.once('data', resolve)),
new Promise(resolve => stream.once('end', resolve)),
new Promise((resolve, reject) => stream.once('error', reject))
]);
if (!res) {
break;
}
stream.removeAllListeners('data');
stream.removeAllListeners('end');
stream.removeAllListeners('error');
valjeanCount += (res.toString().match(/valjean/ig) || []).length;
}
console.log('count:', valjeanCount); // count: 1120
});
```
上面代码采用 Stream 模式读取《悲惨世界》的文本文件,对于每个数据块都使用`stream.once`方法,在`data``end``error`三个事件上添加一次性回调函数。变量`res`只有在`data`事件发生时才有值,然后累加每个数据块之中`valjean`这个词出现的次数。

View File

@ -1,16 +1,16 @@
# Generator 函数
# Generator 函数的语法
## 简介
### 基本概念
Generator函数是ES6提供的一种异步编程解决方案语法行为与传统函数完全不同。本章详细介绍Generator函数的语法和API它的异步编程应用请看《异步操作》一章。
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。本章详细介绍 Generator 函数的语法和 API它的异步编程应用请看《Generator 函数的异步应用》一章。
Generator函数有多种理解角度。语法上首先可以把它理解成Generator函数是一个状态机封装了多个内部状态。
Generator 函数有多种理解角度。语法上首先可以把它理解成Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象也就是说Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
形式上Generator函数是一个普通函数但是有两个特征。一是`function`关键字与函数名之间有一个星号;二是,函数体内部使用`yield`语句定义不同的内部状态yield语句在英语里的意思就是“产出”)。
形式上Generator 函数是一个普通函数,但是有两个特征。一是,`function`关键字与函数名之间有一个星号;二是,函数体内部使用`yield`表达式,定义不同的内部状态(`yield`在英语里的意思就是“产出”)。
```javascript
function* helloWorldGenerator() {
@ -22,11 +22,11 @@ function* helloWorldGenerator() {
var hw = helloWorldGenerator();
```
上面代码定义了一个Generator函数`helloWorldGenerator`,它内部有两个`yield`语句“hello”和“world”即该函数有三个状态helloworld和return语句(结束执行)。
上面代码定义了一个 Generator 函数`helloWorldGenerator`,它内部有两个`yield`表达式(`hello``world`即该函数有三个状态helloworld 和 return 语句(结束执行)。
然后Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后该函数并不执行返回的也不是函数运行结果而是一个指向内部状态的指针对象也就是上一章介绍的遍历器对象Iterator Object
下一步必须调用遍历器对象的next方法使得指针移向下一个状态。也就是说每次调用`next`方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个`yield`语句(或`return`语句为止。换言之Generator函数是分段执行的`yield`语句是暂停执行的标记,而`next`方法可以恢复执行。
下一步,必须调用遍历器对象的`next`方法,使得指针移向下一个状态。也就是说,每次调用`next`方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个`yield`表达式(或`return`语句为止。换言之Generator 函数是分段执行的,`yield`表达式是暂停执行的标记,而`next`方法可以恢复执行。
```javascript
hw.next()
@ -44,45 +44,42 @@ hw.next()
上面代码一共调用了四次`next`方法。
第一次调用Generator函数开始执行直到遇到第一个`yield`语句为止。`next`方法返回一个对象,它的`value`属性就是当前`yield`语句的值hello`done`属性的值false,表示遍历还没有结束。
第一次调用Generator 函数开始执行,直到遇到第一个`yield`表达式为止。`next`方法返回一个对象,它的`value`属性就是当前`yield`表达式的值`hello``done`属性的值`false`,表示遍历还没有结束。
第二次调用Generator函数从上次`yield`语句停下的地方,一直执行到下一个`yield`语句。`next`方法返回的对象的`value`属性就是当前`yield`语句的值world`done`属性的值false,表示遍历还没有结束。
第二次调用Generator 函数从上次`yield`表达式停下的地方,一直执行到下一个`yield`表达式。`next`方法返回的对象的`value`属性就是当前`yield`表达式的值`world``done`属性的值`false`,表示遍历还没有结束。
第三次调用Generator函数从上次`yield`语句停下的地方,一直执行到`return`语句如果没有return语句就执行到函数结束`next`方法返回的对象的`value`属性,就是紧跟在`return`语句后面的表达式的值(如果没有`return`语句,则`value`属性的值为undefined`done`属性的值true表示遍历已经结束。
第三次调用Generator 函数从上次`yield`表达式停下的地方,一直执行到`return`语句(如果没有`return`语句,就执行到函数结束)。`next`方法返回的对象的`value`属性,就是紧跟在`return`语句后面的表达式的值(如果没有`return`语句,则`value`属性的值为`undefined``done`属性的值`true`,表示遍历已经结束。
第四次调用此时Generator函数已经运行完毕`next`方法返回对象的`value`属性为undefined`done`属性为true。以后再调用`next`方法,返回的都是这个值。
第四次调用,此时 Generator 函数已经运行完毕,`next`方法返回对象的`value`属性为`undefined``done`属性为`true`。以后再调用`next`方法,返回的都是这个值。
总结一下调用Generator函数返回一个遍历器对象代表Generator函数的内部指针。以后每次调用遍历器对象的`next`方法,就会返回一个有着`value``done`两个属性的对象。`value`属性表示当前的内部状态的值,是`yield`语句后面那个表达式的值;`done`属性是一个布尔值,表示是否遍历结束。
总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的`next`方法,就会返回一个有着`value``done`两个属性的对象。`value`属性表示当前的内部状态的值,是`yield`表达式后面那个表达式的值;`done`属性是一个布尔值,表示是否遍历结束。
ES6 没有规定,`function`关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。
```javascript
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
```
由于 Generator 函数仍然是普通函数,所以一般的写法是上面的第三种,即星号紧跟在`function`关键字后面。本书也采用这种写法。
### yield语句
### yield 表达式
由于Generator函数返回的遍历器对象只有调用`next`方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。`yield`语句就是暂停标志。
由于 Generator 函数返回的遍历器对象,只有调用`next`方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。`yield`表达式就是暂停标志。
遍历器对象的`next`方法的运行逻辑如下。
1遇到`yield`语句,就暂停执行后面的操作,并将紧跟在`yield`后面的那个表达式的值,作为返回的对象的`value`属性值。
1遇到`yield`表达式,就暂停执行后面的操作,并将紧跟在`yield`后面的那个表达式的值,作为返回的对象的`value`属性值。
2下一次调用`next`方法时,再继续往下执行,直到遇到下一个`yield`语句
2下一次调用`next`方法时,再继续往下执行,直到遇到下一个`yield`表达式
3如果没有再遇到新的`yield`语句,就一直运行到函数结束,直到`return`语句为止,并将`return`语句后面的表达式的值,作为返回的对象的`value`属性值。
3如果没有再遇到新的`yield`表达式,就一直运行到函数结束,直到`return`语句为止,并将`return`语句后面的表达式的值,作为返回的对象的`value`属性值。
4如果该函数没有`return`语句,则返回的对象的`value`属性值为`undefined`
需要注意的是,`yield`语句后面的表达式,只有当调用`next`方法、内部指针指向该语句时才会执行因此等于为JavaScript提供了手动的“惰性求值”Lazy Evaluation的语法功能。
需要注意的是,`yield`表达式后面的表达式,只有当调用`next`方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”Lazy Evaluation的语法功能。
```javascript
function* gen() {
@ -90,11 +87,11 @@ function* gen() {
}
```
上面代码中yield后面的表达式`123 + 456`,不会立即求值,只会在`next`方法将指针移到这一句时,才会求值。
上面代码中,`yield`后面的表达式`123 + 456`,不会立即求值,只会在`next`方法将指针移到这一句时,才会求值。
`yield`语句`return`语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到`yield`,函数暂停执行,下一次再从该位置继续向后执行,而`return`语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)`return`语句,但是可以执行多次(或者说多个)`yield`语句。正常函数只能返回一个值,因为只能执行一次`return`Generator函数可以返回一系列的值因为可以有任意多个`yield`。从另一个角度看也可以说Generator生成了一系列的值这也就是它的名称的来历英语中generator这个词是“生成器”的意思
`yield`表达式`return`语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到`yield`,函数暂停执行,下一次再从该位置继续向后执行,而`return`语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)`return`语句,但是可以执行多次(或者说多个)`yield`表达式。正常函数只能返回一个值,因为只能执行一次`return`Generator 函数可以返回一系列的值,因为可以有任意多个`yield`。从另一个角度看,也可以说 Generator 生成了一系列的值这也就是它的名称的来历英语中generator 这个词是“生成器”的意思)。
Generator函数可以不用`yield`语句,这时就变成了一个单纯的暂缓执行函数。
Generator 函数可以不用`yield`表达式,这时就变成了一个单纯的暂缓执行函数。
```javascript
function* f() {
@ -110,7 +107,7 @@ setTimeout(function () {
上面代码中,函数`f`如果是普通函数,在为变量`generator`赋值时就会执行。但是,函数`f`是一个 Generator 函数,就变成只有调用`next`方法时,函数`f`才会执行。
另外需要注意,`yield`语句不能用在普通函数中,否则会报错。
另外需要注意,`yield`表达式只能用在 Generator 函数里面,用在其他地方都会报错。
```javascript
(function (){
@ -119,7 +116,7 @@ setTimeout(function () {
// SyntaxError: Unexpected number
```
上面代码在一个普通函数中使用`yield`语句,结果产生一个句法错误。
上面代码在一个普通函数中使用`yield`表达式,结果产生一个句法错误。
下面是另外一个例子。
@ -133,7 +130,7 @@ var flat = function* (a) {
} else {
yield item;
}
}
});
};
for (var f of flat(arr)){
@ -141,7 +138,7 @@ for (var f of flat(arr)){
}
```
上面代码也会产生句法错误,因为`forEach`方法的参数是一个普通函数,但是在里面使用了`yield`语句(这个函数里面还使用了`yield*`语句,这里可以不用理会,详细说明见后文)。一种修改方法是改用`for`循环。
上面代码也会产生句法错误,因为`forEach`方法的参数是一个普通函数,但是在里面使用了`yield`表达式(这个函数里面还使用了`yield*`表达式,详细介绍见后文)。一种修改方法是改用`for`循环。
```javascript
var arr = [1, [[2, 3], 4], [5, 6]];
@ -164,21 +161,25 @@ for (var f of flat(arr)) {
// 1, 2, 3, 4, 5, 6
```
另外,`yield`语句如果用在一个表达式之中,必须放在圆括号里面。
另外,`yield`表达式如果用在另一个表达式之中,必须放在圆括号里面。
```javascript
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
```
`yield`语句用作函数参数或赋值表达式的右边,可以不加括号。
`yield`表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
```javascript
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
```
### 与 Iterator 接口的关系
@ -217,7 +218,7 @@ g[Symbol.iterator]() === g
## next 方法的参数
`yield`本身没有返回值,或者说总是返回`undefined``next`方法可以带一个参数,该参数就会被当作上一个`yield`语句的返回值。
`yield`表达式本身没有返回值,或者说总是返回`undefined``next`方法可以带一个参数,该参数就会被当作上一个`yield`表达式的返回值。
```javascript
function* f() {
@ -234,7 +235,7 @@ g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
```
上面代码先定义了一个可以无限运行的Generator函数`f`,如果`next`方法没有参数,每次运行到`yield`语句,变量`reset`的值总是`undefined`。当`next`方法带一个参数`true`时,当前的变量`reset`就被重置为这个参数(即`true`),因此`i`会等于-1下一轮循环就会从-1开始递增。
上面代码先定义了一个可以无限运行的 Generator 函数`f`,如果`next`方法没有参数,每次运行到`yield`表达式,变量`reset`的值总是`undefined`。当`next`方法带一个参数`true`时,变量`reset`就被重置为这个参数(即`true`),因此`i`会等于`-1`,下一轮循环就会从`-1`开始递增。
这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行它的上下文状态context是不变的。通过`next`方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
@ -260,9 +261,30 @@ b.next(13) // { value:42, done:true }
上面代码中,第二次运行`next`方法的时候不带参数,导致 y 的值等于`2 * undefined`(即`NaN`),除以 3 以后还是`NaN`,因此返回对象的`value`属性也等于`NaN`。第三次运行`Next`方法的时候不带参数,所以`z`等于`undefined`,返回对象的`value`属性等于`5 + NaN + undefined`,即`NaN`
如果向`next`方法提供参数,返回结果就完全不一样了。上面代码第一次调用`b``next`方法时,返回`x+1`的值6第二次调用`next`方法,将上一次`yield`语句的值设为12因此`y`等于24返回`y / 3`的值8第三次调用`next`方法,将上一次`yield`语句的值设为13因此`z`等于13这时`x`等于5`y`等于24所以`return`语句的值等于42
如果向`next`方法提供参数,返回结果就完全不一样了。上面代码第一次调用`b``next`方法时,返回`x+1`的值`6`;第二次调用`next`方法,将上一次`yield`表达式的值设为`12`,因此`y`等于`24`,返回`y / 3`的值`8`;第三次调用`next`方法,将上一次`yield`表达式的值设为`13`,因此`z`等于`13`,这时`x`等于`5``y`等于`24`,所以`return`语句的值等于`42`
注意,由于`next`方法的参数表示上一个`yield`语句的返回值,所以第一次使用`next`方法时不能带有参数。V8引擎直接忽略第一次使用`next`方法时的参数,只有从第二次使用`next`方法开始,参数才是有效的。从语义上讲,第一个`next`方法用来启动遍历器对象,所以不用带有参数。
注意,由于`next`方法的参数表示上一个`yield`表达式的返回值,所以在第一次使用`next`方法时传递参数是无效的。V8 引擎直接忽略第一次使用`next`方法时的参数,只有从第二次使用`next`方法开始,参数才是有效的。从语义上讲,第一个`next`方法用来启动遍历器对象,所以不用带有参数。
再看一个通过`next`方法的参数,向 Generator 函数内部输入值的例子。
```javascript
function* dataConsumer() {
console.log('Started');
console.log(`1. ${yield}`);
console.log(`2. ${yield}`);
return 'result';
}
let genObj = dataConsumer();
genObj.next();
// Started
genObj.next('a')
// 1. a
genObj.next('b')
// 2. b
```
上面代码是一个很直观的例子,每次通过`next`方法向 Generator 函数输入值,然后打印出来。
如果想要第一次调用`next`方法时,就能够输入值,可以在 Generator 函数外面再包一层。
@ -286,27 +308,6 @@ wrapped().next('hello!')
上面代码中Generator 函数如果不用`wrapper`先包一层,是无法第一次调用`next`方法,就输入参数的。
再看一个通过`next`方法的参数向Generator函数内部输入值的例子。
```javascript
function* dataConsumer() {
console.log('Started');
console.log(`1. ${yield}`);
console.log(`2. ${yield}`);
return 'result';
}
let genObj = dataConsumer();
genObj.next();
// Started
genObj.next('a')
// 1. a
genObj.next('b')
// 2. b
```
上面代码是一个很直观的例子,每次通过`next`方法向Generator函数输入值然后打印出来。
## for...of 循环
`for...of`循环可以自动遍历 Generator 函数时生成的`Iterator`对象,且此时不再需要调用`next`方法。
@ -327,7 +328,7 @@ for (let v of foo()) {
// 1 2 3 4 5
```
上面代码使用`for...of`循环,依次显示5个`yield`语句的值。这里需要注意,一旦`next`方法的返回对象的`done`属性为`true``for...of`循环就会中止,且不包含该返回对象,所以上面代码的`return`语句返回的6不包括在`for...of`循环之中。
上面代码使用`for...of`循环,依次显示 5 个`yield`表达式的值。这里需要注意,一旦`next`方法的返回对象的`done`属性为`true``for...of`循环就会中止,且不包含该返回对象,所以上面代码的`return`语句返回的`6`,不包括在`for...of`循环之中。
下面是一个利用 Generator 函数和`for...of`循环,实现斐波那契数列的例子。
@ -533,7 +534,7 @@ g.throw();
上面代码中,`g.throw`抛出错误以后,没有任何`try...catch`代码块可以捕获这个错误,导致程序报错,中断执行。
`throw`方法被捕获以后,会附带执行下一条`yield`语句。也就是说,会附带执行一次`next`方法。
`throw`方法被捕获以后,会附带执行下一条`yield`表达式。也就是说,会附带执行一次`next`方法。
```javascript
var gen = function* gen(){
@ -576,7 +577,7 @@ try {
上面代码中,`throw`命令抛出的错误不会影响到遍历器的状态,所以两次执行`next`方法,都进行了正确的操作。
这种函数体内捕获错误的机制,大大方便了对错误的处理。多个`yield`语句,可以只用一个`try...catch`代码块来捕获错误。如果使用回调函数的写法想要捕获多个错误就不得不为每个函数内部写一个错误处理语句现在只在Generator函数内部写一次`catch`语句就可以了。
这种函数体内捕获错误的机制,大大方便了对错误的处理。多个`yield`表达式,可以只用一个`try...catch`代码块来捕获错误。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数内部写一个错误处理语句,现在只在 Generator 函数内部写一次`catch`语句就可以了。
Generator 函数体外抛出的错误可以在函数体内捕获反过来Generator 函数体内抛出的错误,也可以被函数体外的`catch`捕获。
@ -695,19 +696,57 @@ function* numbers () {
}
yield 6;
}
var g = numbers()
g.next() // { done: false, value: 1 }
g.next() // { done: false, value: 2 }
g.return(7) // { done: false, value: 4 }
g.next() // { done: false, value: 5 }
g.next() // { done: true, value: 7 }
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
```
上面代码中,调用`return`方法后,就开始执行`finally`代码块,然后等到`finally`代码块执行完,再执行`return`方法。
## yield*语句
## next()、throw()、return() 的共同点
如果在Generater函数内部调用另一个Generator函数默认情况下是没有效果的。
网友 vision57 提出,`next()``throw()``return()`这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换`yield`表达式。
`next()`是将`yield`表达式替换成一个值。
```javascript
const g = function* (x, y) {
let result = yield x + y;
return result;
};
const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}
gen.next(1); // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;
```
上面代码中,第二个`next(1)`方法就相当于将`yield`表达式替换成一个值`1`。如果`next`方法没有参数,就相当于替换成`undefined`
`throw()`是将`yield`表达式替换成一个`throw`语句。
```javascript
gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));
```
`return()`是将`yield`表达式替换成一个`return`语句。
```javascript
gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;
```
## yield\* 表达式
如果在 Generator 函数内部,调用另一个 Generator 函数,默认情况下是没有效果的。
```javascript
function* foo() {
@ -730,7 +769,7 @@ for (let v of bar()){
上面代码中,`foo``bar`都是 Generator 函数,在`bar`里面调用`foo`,是不会有效果的。
这个就需要用到`yield*`语句用来在一个Generator函数里面执行另一个Generator函数。
这个就需要用到`yield*`表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。
```javascript
function* bar() {
@ -797,7 +836,7 @@ gen.next().value // "close"
上面例子中,`outer2`使用了`yield*``outer1`没使用。结果就是,`outer1`返回一个遍历器对象,`outer2`返回该遍历器对象的内部值。
从语法角度看,如果`yield`命令后面跟的是一个遍历器对象,需要在`yield`命令后面加上星号,表明它返回的是一个遍历器对象。这被称为`yield*`语句
从语法角度看,如果`yield`表达式后面跟的是一个遍历器对象,需要在`yield`表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为`yield*`表达式
```javascript
let delegatedIterator = (function* () {
@ -842,7 +881,7 @@ function* concat(iter1, iter2) {
}
```
上面代码说明,`yield*`后面的Generator函数没有`return`语句时),不过是`for...of`的一种简写形式,完全可以用后者替代前者。反之,则需要用`var value = yield* iterator`的形式获取`return`语句的值。
上面代码说明,`yield*`后面的 Generator 函数(没有`return`语句时),不过是`for...of`的一种简写形式,完全可以用后者替代前者。反之,在有`return`语句时,则需要用`var value = yield* iterator`的形式获取`return`语句的值。
如果`yield*`后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。
@ -868,7 +907,7 @@ read.next().value // "hello"
read.next().value // "h"
```
上面代码中,`yield`语句返回整个字符串,`yield*`语句返回单个字符。因为字符串具有Iterator接口所以被`yield*`遍历。
上面代码中,`yield`表达式返回整个字符串,`yield*`语句返回单个字符。因为字符串具有 Iterator 接口,所以被`yield*`遍历。
如果被代理的 Generator 函数有`return`语句,那么就可以向代理它的 Generator 函数返回数据。
@ -1078,7 +1117,7 @@ obj.b // 2
obj.c // 3
```
上面代码中,首先是`F`内部的`this`对象绑定`obj`对象然后调用它返回一个Iterator对象。这个对象执行三次`next`方法(因为`F`内部有两个`yield`语句完成F内部所有代码的运行。这时,所有内部属性都绑定在`obj`对象上了,因此`obj`对象也就成了`F`的实例。
上面代码中,首先是`F`内部的`this`对象绑定`obj`对象,然后调用它,返回一个 Iterator 对象。这个对象执行三次`next`方法(因为`F`内部有两个`yield`表达式),完成 F 内部所有代码的运行。这时,所有内部属性都绑定在`obj`对象上了,因此`obj`对象也就成了`F`的实例。
上面代码中,执行的是遍历器对象`f`,但是生成的对象实例是`obj`,有没有办法将这两个对象统一呢?
@ -1129,7 +1168,7 @@ f.c // 3
### Generator 与状态机
Generator是实现状态机的最佳结构。比如下面的clock函数就是一个状态机。
Generator 是实现状态机的最佳结构。比如,下面的`clock`函数就是一个状态机。
```javascript
var ticking = true;
@ -1142,7 +1181,7 @@ var clock = function() {
}
```
上面代码的clock函数一共有两种状态Tick和Tock每运行一次就改变一次状态。这个函数如果用Generator实现就是下面这样。
上面代码的`clock`函数一共有两种状态(`Tick``Tock`),每运行一次,就改变一次状态。这个函数如果用 Generator 实现,就是下面这样。
```javascript
var clock = function* () {
@ -1171,11 +1210,35 @@ var clock = function*() {
不难看出,协程适合用于多任务运行的环境。在这个意义上,它与普通的线程很相似,都有自己的执行上下文、可以分享全局变量。它们的不同之处在于,同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都处于暂停状态。此外,普通的线程是抢先式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。
由于ECMAScript是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。
由于 JavaScript 是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。
Generator函数是ECMAScript 6对协程的实现但属于不完全实现。Generator函数被称为“半协程”semi-coroutine意思是只有Generator函数的调用者才能将程序的执行权还给Generator函数。如果是完全执行的协程任何函数都可以让暂停的协程继续执行。
Generator 函数是 ES6 对协程的实现但属于不完全实现。Generator 函数被称为“半协程”semi-coroutine意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。
如果将Generator函数当作协程完全可以将多个需要互相协作的任务写成Generator函数它们之间使用yield语句交换控制权。
如果将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用`yield`表示式交换控制权。
### Generator 与上下文
JavaScript 代码运行时会产生一个全局的上下文环境context又称运行环境包含了当前所有的变量和对象。然后执行函数或块级代码的时候又会在当前上下文环境的上层产生一个函数运行的上下文变成当前active的上下文由此形成一个上下文环境的堆栈context stack
这个堆栈是“后进先出”的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行完成它下层的上下文,直至所有代码执行完成,堆栈清空。
Generator 函数不是这样,它执行产生的上下文环境,一旦遇到`yield`命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行`next`命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。
```javascript
function *gen() {
yield 1;
return 2;
}
let g = gen();
console.log(
g.next().value,
g.next().value,
);
```
上面代码中,第一次执行`g.next()`Generator 函数`gen`的上下文会加入堆栈,即开始运行`gen`内部的代码。等遇到`yield 1`时,`gen`上下文退出堆栈,内部状态冻结。第二次执行`g.next()`时,`gen`上下文重新加入堆栈,变成当前的上下文,重新恢复执行。
## 应用
@ -1183,7 +1246,7 @@ Generator可以暂停函数执行返回任意表达式的值。这种特点
### 1异步操作的同步化表达
Generator函数的暂停执行的效果意味着可以把异步操作写在yield语句里面等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield语句下面反正要等到调用next方法时再执行。所以Generator函数的一个重要实际意义就是用来处理异步操作,改写回调函数。
Generator 函数的暂停执行的效果,意味着可以把异步操作写在`yield`表达式里面,等到调用`next`方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在`yield`表达式下面,反正要等到调用`next`方法时再执行。所以Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数。
```javascript
function* loadUI() {
@ -1199,7 +1262,7 @@ loader.next()
loader.next()
```
上面代码表示第一次调用loadUI函数时该函数不会执行仅返回一个遍历器。下一次对该遍历器调用next方法则会显示Loading界面并且异步加载数据。等到数据加载完成再一次使用next方法则会隐藏Loading界面。可以看到这种写法的好处是所有Loading界面的逻辑都被封装在一个函数按部就班非常清晰。
上面代码中,第一次调用`loadUI`函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用`next`方法,则会显示`Loading`界面`showLoadingScreen`,并且异步加载数据`loadUIDataAsynchronously`。等到数据加载完成,再一次使用`next`方法,则会隐藏`Loading`界面。可以看到,这种写法的好处是所有`Loading`界面的逻辑,都被封装在一个函数,按部就班非常清晰。
Ajax 是典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。
@ -1220,7 +1283,7 @@ var it = main();
it.next();
```
上面代码的main函数就是通过Ajax操作获取数据。可以看到除了多了一个yield它几乎与同步操作的写法完全一样。注意makeAjaxCall函数中的next方法必须加上response参数因为yield语句构成的表达式本身是没有值的总是等于undefined
上面代码的`main`函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个`yield`,它几乎与同步操作的写法完全一样。注意,`makeAjaxCall`函数中的`next`方法,必须加上`response`参数,因为`yield`表达式,本身是没有值的,总是等于`undefined`
下面是另一个例子,通过 Generator 函数逐行读取文本文件。
@ -1237,7 +1300,7 @@ function* numbers() {
}
```
上面代码打开文本文件,使用yield语句可以手动逐行读取文件。
上面代码打开文本文件,使用`yield`表达式可以手动逐行读取文件。
### 2控制流管理
@ -1431,5 +1494,4 @@ function doStuff() {
}
```
上面的函数可以用一模一样的for...of循环处理两相一比较就不难看出Generator使得数据或者操作具备了类似数组的接口。
上面的函数,可以用一模一样的`for...of`循环处理!两相一比较,就不难看出 Generator 使得数据或者操作,具备了类似数组的接口。

View File

@ -14,9 +14,9 @@ ECMAScript 6.0以下简称ES6是JavaScript语言的下一代标准
## ES6 与 ECMAScript 2015 的关系
媒体里面经常可以看到”ECMAScript 2015“这个词它与ES6是什么关系呢?
ECMAScript 2015简称 ES2015这个词也是经常可以看到的。它与 ES6 是什么关系呢?
2011ECMAScript 5.1版发布后就开始制定6.0版了。因此”ES6”这个词的原意就是指JavaScript语言的下一个版本。
2011ECMAScript 5.1 版发布后,就开始制定 6.0 版了。因此ES6 这个词的原意,就是指 JavaScript 语言的下一个版本。
但是,因为这个版本引入的语法功能太多,而且制定过程当中,还有很多组织和个人不断提交新功能。事情很快就变得清楚了,不可能在一个版本里面包括所有将要引入的功能。常规的做法是先发布 6.0 版,过一段时间再发 6.1 版,然后是 6.2 版、6.3 版等等。
@ -24,13 +24,15 @@ ECMAScript 6.0以下简称ES6是JavaScript语言的下一代标准
标准委员会最终决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。接下来的时间,就在这个版本的基础上做改动,直到下一年的 6 月份,草案就自然变成了新一年的版本。这样一来,就不需要以前的版本号了,只要用年份标记就可以了。
ES6的第一个版本就这样在2015年6月发布了正式名称就是《ECMAScript 2015标准》简称ES2015。2016年6月小幅修订的《ECMAScript 2016标准》简称ES2016如期发布这个版本可以看作是ES6.1版,因为两者的差异非常小(只新增了数组实例的`includes`方法和指数运算符基本上是同一个标准。根据计划2017年6月发布ES2017标准。
ES6 的第一个版本,就这样在 2015 6 月发布了正式名称就是《ECMAScript 2015 标准》(简称 ES2015。2016 6 小幅修订的《ECMAScript 2016 标准》(简称 ES2016如期发布这个版本可以看作是 ES6.1 版,因为两者的差异非常小(只新增了数组实例的`includes`方法和指数运算符基本上是同一个标准。根据计划2017 6 月发布 ES2017 标准。
因此ES6既是一个历史名词也是一个泛指含义是5.1版以后的JavaScript的下一代标准涵盖了ES2015、ES2016、ES2017等等而ES2015则是正式名称特指该年发布的正式版本的语言标准。本书中提到“ES6”的地方一般是指ES2015标准但有时也是泛指“下一代JavaScript语言”。
因此ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。本书中提到 ES6 的地方,一般是指 ES2015 标准,但有时也是泛指“下一代 JavaScript 语言”。
## 语法提案的批准流程
任何人都可以向TC39标准委员会提案。一种新的语法从提案到变成正式标准需要经历五个阶段。每个阶段的变动都需要由TC39委员会批准。
任何人都可以向标准委员会(又称 TC39 委员会)提案,要求修改语言标准。
一种新的语法从提案到变成正式标准,需要经历五个阶段。每个阶段的变动都需要由 TC39 委员会批准。
- Stage 0 - Strawman展示阶段
- Stage 1 - Proposal征求意见阶段
@ -38,9 +40,9 @@ ES6的第一个版本就这样在2015年6月发布了正式名称就是《
- Stage 3 - Candidate候选人阶段
- Stage 4 - Finished定案阶段
一个提案只要能进入Stage 2就差不多等于肯定会包括在以后的正式标准里面。ECMAScript当前的所有提案可以在TC39的官方网站[Github.com/tc39/ecma262](https://github.com/tc39/ecma262)查看。
一个提案只要能进入 Stage 2就差不多肯定会包括在以后的正式标准里面。ECMAScript 当前的所有提案,可以在 TC39 的官方网站[Github.com/tc39/ecma262](https://github.com/tc39/ecma262)查看。
本书的写作目标之一是跟踪ECMAScript语言的最新进展介绍5.1版本以后所有的新语法。对于那些明确将要列入标准的新语法尤其是那些Babel转码器详见后文已经支持的功能将予以介绍。
本书的写作目标之一,是跟踪 ECMAScript 语言的最新进展,介绍 5.1 版本以后所有的新语法。对于那些明确或很有希望,将要列入标准的新语法,都将予以介绍。
## ECMAScript 的历史
@ -68,60 +70,17 @@ ES6从开始制定到最后发布整整用了15年。
## 部署进度
各大浏览器的最新版本对ES6的支持可以查看[kangax.github.io/es5-compat-table/es6/](http://kangax.github.io/es5-compat-table/es6/)。随着时间的推移,支持度已经越来越高了,ES6的大部分特性都实现了。
各大浏览器的最新版本,对 ES6 的支持可以查看[kangax.github.io/es5-compat-table/es6/](https://kangax.github.io/es5-compat-table/es6/)。随着时间的推移,支持度已经越来越高了,超过 90%的 ES6 语法特性都实现了。
Node.js是JavaScript语言的服务器运行环境对ES6的支持度比浏览器更高。通过Node可以体验更多ES6的特性。建议使用版本管理工具[nvm](https://github.com/creationix/nvm)来安装Node因为可以自由切换版本。不过`nvm`不支持Windows系统如果你使用Windows系统下面的操作可以改用[nvmw](https://github.com/hakobera/nvmw)或[nvm-windows](https://github.com/coreybutler/nvm-windows)代替。
安装nvm需要打开命令行窗口运行下面的命令。
```bash
$ curl -o- https://raw.githubusercontent.com/creationix/nvm/<version number>/install.sh | bash
```
上面命令的`version number`处,需要用版本号替换。本节写作时的版本号是`v0.29.0`。该命令运行后,`nvm`会默认安装在用户主目录的`.nvm`子目录。
然后,激活`nvm`
```bash
$ source ~/.nvm/nvm.sh
```
激活以后安装Node的最新版。
```bash
$ nvm install node
```
安装完成后,切换到该版本。
```bash
$ nvm use node
```
使用下面的命令可以查看Node所有已经实现的ES6特性。
Node 是 JavaScript 的服务器运行环境runtime。它对 ES6 的支持度更高。除了那些默认打开的功能,还有一些语法功能已经实现了,但是默认没有打开。使用下面的命令,可以查看 Node 已经实现的 ES6 特性。
```bash
$ node --v8-options | grep harmony
--harmony_typeof
--harmony_scoping
--harmony_modules
--harmony_symbols
--harmony_proxies
--harmony_collections
--harmony_observation
--harmony_generators
--harmony_iteration
--harmony_numeric_literals
--harmony_strings
--harmony_arrays
--harmony_maths
--harmony
```
上面命令的输出结果,会因为版本的不同而有所不同。
我写了一个[ES-Checker](https://github.com/ruanyf/es-checker)模块用来检查各种运行环境对ES6的支持情况。访问[ruanyf.github.io/es-checker](http://ruanyf.github.io/es-checker)可以看到您的浏览器支持ES6的程度。运行下面的命令可以查看你正在使用的Node环境对ES6的支持程度。
我写了一个工具 [ES-Checker](https://github.com/ruanyf/es-checker),用来检查各种运行环境对 ES6 的支持情况。访问[ruanyf.github.io/es-checker](http://ruanyf.github.io/es-checker),可以看到您的浏览器支持 ES6 的程度。运行下面的命令,可以查看你正在使用的 Node 环境对 ES6 的支持程度。
```bash
$ npm install -g es-checker
@ -147,7 +106,7 @@ input.map(function (item) {
});
```
上面的原始代码用了箭头函数,这个特性还没有得到广泛支持Babel将其转为普通函数就能在现有的JavaScript环境执行了。
上面的原始代码用了箭头函数,Babel 将其转为普通函数,就能在不支持箭头函数的 JavaScript 环境执行了。
### 配置文件`.babelrc`
@ -165,13 +124,13 @@ Babel的配置文件是`.babelrc`存放在项目的根目录下。使用Babel
`presets`字段设定转码规则,官方提供以下的规则集,你可以根据需要安装。
```bash
# ES2015转码规则
$ npm install --save-dev babel-preset-es2015
# 最新转码规则
$ npm install --save-dev babel-preset-latest
# react 转码规则
$ npm install --save-dev babel-preset-react
# ES7不同阶段语法提案的转码规则共有4个阶段选装一个
# 不同阶段语法提案的转码规则共有4个阶段选装一个
$ npm install --save-dev babel-preset-stage-0
$ npm install --save-dev babel-preset-stage-1
$ npm install --save-dev babel-preset-stage-2
@ -183,7 +142,7 @@ $ npm install --save-dev babel-preset-stage-3
```javascript
{
"presets": [
"es2015",
"latest",
"react",
"stage-2"
],
@ -351,7 +310,7 @@ babel.transformFromAst(ast, code, options);
var es6Code = 'let x = n => n + 1';
var es5Code = require('babel-core')
.transform(es6Code, {
presets: ['es2015']
presets: ['latest']
})
.code;
// '"use strict";\n\nvar x = function x(n) {\n return n + 1;\n};'
@ -361,7 +320,7 @@ var es5Code = require('babel-core')
### babel-polyfill
Babel默认只转换新的JavaScript句法syntax而不转换新的API比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象以及一些定义在全局对象上的方法比如`Object.assign`)都不会转码。
Babel 默认只转换新的 JavaScript 句法syntax而不转换新的 API比如`Iterator``Generator``Set``Maps``Proxy``Reflect``Symbol``Promise`等全局对象,以及一些定义在全局对象上的方法(比如`Object.assign`)都不会转码。
举例来说ES6 在`Array`对象上新增了`Array.from`方法。Babel 就不会转码这个方法。如果想让这个方法运行,必须使用`babel-polyfill`,为当前环境提供一个垫片。
@ -383,26 +342,7 @@ Babel默认不转码的API非常多详细清单可以查看`babel-plugin-tran
### 浏览器环境
Babel也可以用于浏览器环境。但是从Babel 6.0开始不再直接提供浏览器版本而是要用构建工具构建出来。如果你没有或不想使用构建工具可以通过安装5.x版本的`babel-core`模块获取。
```bash
$ npm install babel-core@5
```
运行上面的命令以后,就可以在当前目录的`node_modules/babel-core/`子目录里面,找到`babel`的浏览器版本`browser.js`(未精简)和`browser.min.js`(已精简)。
然后,将下面的代码插入网页。
```html
<script src="node_modules/babel-core/browser.js"></script>
<script type="text/babel">
// Your ES6 code
</script>
```
上面代码中,`browser.js`是Babel提供的转换器脚本可以在浏览器运行。用户的ES6脚本放在`script`标签之中,但是要注明`type="text/babel"`
另一种方法是使用[babel-standalone](https://github.com/Daniel15/babel-standalone)模块提供的浏览器版本,将其插入网页。
Babel 也可以用于浏览器环境。但是,从 Babel 6.0 开始,不再直接提供浏览器版本,而是要用构建工具构建出来。如果你没有或不想使用构建工具,可以使用[babel-standalone](https://github.com/Daniel15/babel-standalone)模块提供的浏览器版本,将其插入网页。
```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.4.4/babel.min.js"></script>
@ -411,19 +351,19 @@ $ npm install babel-core@5
</script>
```
注意,网页实时将ES6代码转为ES5对性能会有影响。生产环境需要加载已经转码完成的脚本。
注意,网页实时将 ES6 代码转为 ES5对性能会有影响。生产环境需要加载已经转码完成的脚本。
下面是如何将代码打包成浏览器可以使用的脚本,以`Babel`配合`Browserify`为例。首先,安装`babelify`模块。
```bash
$ npm install --save-dev babelify babel-preset-es2015
$ npm install --save-dev babelify babel-preset-latest
```
然后,再用命令行转换 ES6 脚本。
```bash
$ browserify script.js -o bundle.js \
-t [ babelify --presets [ es2015 ] ]
-t [ babelify --presets [ latest ] ]
```
上面代码将 ES6 脚本`script.js`,转为`bundle.js`,浏览器直接加载后者就可以了。
@ -433,7 +373,7 @@ $ browserify script.js -o bundle.js \
```javascript
{
"browserify": {
"transform": [["babelify", { "presets": ["es2015"] }]]
"transform": [["babelify", { "presets": ["latest"] }]]
}
}
```
@ -527,7 +467,7 @@ Traceur允许将ES6代码直接插入网页。首先必须在网页头部加
</script>
```
正常情况下上面代码会在控制台打印出9。
正常情况下,上面代码会在控制台打印出`9`
如果想对 Traceur 的行为有精确控制,可以采用下面参数配置的写法。
@ -555,7 +495,7 @@ Traceur允许将ES6代码直接插入网页。首先必须在网页头部加
</script>
```
上面代码中首先生成Traceur的全局对象`window.System`,然后`System.import`方法可以用来加载ES6模块。加载的时候,需要传入一个配置对象`metadata`,该对象的`traceurOptions`属性可以配置支持ES6功能。如果设为`experimental: true`就表示除了ES6以外还支持一些实验性的新功能。
上面代码中,首先生成 Traceur 的全局对象`window.System`,然后`System.import`方法可以用来加载 ES6。加载的时候需要传入一个配置对象`metadata`,该对象的`traceurOptions`属性可以配置支持 ES6 功能。如果设为`experimental: true`,就表示除了 ES6 以外,还支持一些实验性的新功能。
### 在线转换
@ -588,7 +528,7 @@ $traceurRuntime.ModuleStore.getAnonymousModule(function() {
### 命令行转换
作为命令行工具使用时Traceur是一个Node的模块首先需要用Npm安装。
作为命令行工具使用时Traceur 是一个 Node 的模块,首先需要用 npm 安装。
```bash
$ npm install -g traceur
@ -596,7 +536,7 @@ $ npm install -g traceur
安装成功后,就可以在命令行下使用 Traceur 了。
Traceur直接运行es6脚本文件,会在标准输出显示运行结果,以前面的`calc.js`为例。
Traceur 直接运行 ES6 脚本文件,会在标准输出显示运行结果,以前面的`calc.js`为例。
```bash
$ traceur calc.js
@ -620,9 +560,9 @@ $ traceur --script calc.es6.js --out calc.es5.js --experimental
命令行下转换生成的文件,就可以直接放到浏览器中运行。
### Node.js环境的用法
### Node 环境的用法
Traceur的Node.js用法如下假定已安装traceur模块)。
Traceur 的 Node 用法如下(假定已安装`traceur`模块)。
```javascript
var traceur = require('traceur');
@ -646,4 +586,3 @@ fs.writeFileSync('out.js', result.js);
// sourceMap 属性对应 map 文件
fs.writeFileSync('out.js.map', result.sourceMap);
```

View File

@ -2,7 +2,7 @@
## Iterator遍历器的概念
JavaScript原有的表示“集合”的数据结构主要是数组Array和对象ObjectES6又添加了Map和Set。这样就有了四种数据集合用户还可以组合使用它们定义自己的数据结构比如数组的成员是MapMap的成员是对象。这样就需要一种统一的接口机制来处理所有不同的数据结构。
JavaScript 原有的表示“集合”的数据结构,主要是数组(`Array`)和对象(`Object`ES6 又添加了`Map``Set`。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是`Map``Map`的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。
遍历器Iterator就是这样一种机制。它是一种接口为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
@ -69,9 +69,9 @@ function makeIterator(array) {
```javascript
var it = idMaker();
it.next().value // '0'
it.next().value // '1'
it.next().value // '2'
it.next().value // 0
it.next().value // 1
it.next().value // 2
// ...
function idMaker() {
@ -87,9 +87,7 @@ function idMaker() {
上面的例子中,遍历器生成函数`idMaker`,返回一个遍历器对象(即指针对象)。但是并没有对应的数据结构,或者说,遍历器对象自己描述了一个数据结构出来。
在ES6中有些数据结构原生具备Iterator接口比如数组即不用任何处理就可以被`for...of`循环遍历,有些就不行(比如对象)。原因在于,这些数据结构原生部署了`Symbol.iterator`属性(详见下文),另外一些数据结构没有。凡是部署了`Symbol.iterator`属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。
如果使用TypeScript的写法遍历器接口Iterable、指针对象Iterator和next方法返回值的规格可以描述如下。
如果使用 TypeScript 的写法遍历器接口Iterable、指针对象Iterator`next`方法返回值的规格可以描述如下。
```javascript
interface Iterable {
@ -106,13 +104,13 @@ interface IterationResult {
}
```
## 数据结构的默认Iterator接口
## 默认 Iterator 接口
Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即`for...of`循环(详见下文)。当使用`for...of`循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。
一种数据结构只要部署了Iterator接口我们就称这种数据结构是”可遍历的“iterable
一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”iterable
ES6规定默认的Iterator接口部署在数据结构的`Symbol.iterator`属性,或者说,一个数据结构只要具有`Symbol.iterator`属性就可以认为是“可遍历的”iterable`Symbol.iterator`属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名`Symbol.iterator`,它是一个表达式,返回`Symbol`对象的`iterator`属性这是一个预定义好的、类型为Symbol的特殊值所以要放在方括号内参见Symbol一章
ES6 规定,默认的 Iterator 接口部署在数据结构的`Symbol.iterator`属性,或者说,一个数据结构只要具有`Symbol.iterator`属性就可以认为是“可遍历的”iterable`Symbol.iterator`属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名`Symbol.iterator`,它是一个表达式,返回`Symbol`对象的`iterator`属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内(参见Symbol一章)。
```javascript
const obj = {
@ -131,7 +129,19 @@ const obj = {
上面代码中,对象`obj`是可遍历的iterable因为具有`Symbol.iterator`属性。执行这个属性,会返回一个遍历器对象。该对象的根本特征就是具有`next`方法。每次调用`next`方法,都会返回一个代表当前成员的信息对象,具有`value``done`两个属性。
在ES6中有三类数据结构原生具备Iterator接口数组、某些类似数组的对象、Set和Map结构。
ES6 的有些数据结构原生具备 Iterator 接口(比如数组),即不用任何处理,就可以被`for...of`循环遍历。原因在于,这些数据结构原生部署了`Symbol.iterator`属性(详见下文),另外一些数据结构没有(比如对象)。凡是部署了`Symbol.iterator`属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。
原生具备 Iterator 接口的数据结构如下。
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
下面的例子是数组的`Symbol.iterator`属性。
```javascript
let arr = ['a', 'b', 'c'];
@ -145,11 +155,11 @@ iter.next() // { value: undefined, done: true }
上面代码中,变量`arr`是一个数组,原生就具有遍历器接口,部署在`arr``Symbol.iterator`属性上面。所以,调用这个属性,就得到遍历器对象。
上面提到原生就部署Iterator接口的数据结构有三类对于这三类数据结构,不用自己写遍历器生成函数,`for...of`循环会自动遍历它们。除此之外其他数据结构主要是对象的Iterator接口都需要自己在`Symbol.iterator`属性上面部署,这样才会被`for...of`循环遍历。
对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,`for...of`循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在`Symbol.iterator`属性上面部署,这样才会被`for...of`循环遍历。
对象Object之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用ES5 没有 Map 结构,而 ES6 原生提供了。
一个对象如果要有可被`for...of`循环调用的Iterator接口,就必须在`Symbol.iterator`的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。
一个对象如果要具备可被`for...of`循环调用的 Iterator 接口,就必须在`Symbol.iterator`的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。
```javascript
class RangeIterator {
@ -165,9 +175,8 @@ class RangeIterator {
if (value < this.stop) {
this.value++;
return {done: false, value: value};
} else {
return {done: true, value: undefined};
}
return {done: true, value: undefined};
}
}
@ -176,7 +185,7 @@ function range(start, stop) {
}
for (var value of range(0, 3)) {
console.log(value);
console.log(value); // 0, 1, 2
}
```
@ -191,9 +200,7 @@ function Obj(value) {
}
Obj.prototype[Symbol.iterator] = function() {
var iterator = {
next: next
};
var iterator = { next: next };
var current = this;
@ -201,14 +208,9 @@ Obj.prototype[Symbol.iterator] = function() {
if (current) {
var value = current.value;
current = current.next;
return {
done: false,
value: value
};
return { done: false, value: value };
} else {
return {
done: true
};
return { done: true };
}
}
return iterator;
@ -222,11 +224,8 @@ one.next = two;
two.next = three;
for (var i of one){
console.log(i);
console.log(i); // 1, 2, 3
}
// 1
// 2
// 3
```
上面代码首先在构造函数的原型链上部署`Symbol.iterator`方法,调用该方法会返回遍历器对象`iterator`,调用该对象的`next`方法,在返回一个值的同时,自动将内部指针移到下一个实例。
@ -255,7 +254,7 @@ let obj = {
};
```
对于类似数组的对象存在数值键名和length属性部署Iterator接口有一个简便方法就是`Symbol.iterator`方法直接引用数组的Iterator接口。
对于类似数组的对象(存在数值键名和`length`属性),部署 Iterator 接口,有一个简便方法,就是`Symbol.iterator`方法直接引用数组的 Iterator 接口。
```javascript
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
@ -265,7 +264,9 @@ NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];
[...document.querySelectorAll('div')] // 可以执行了
```
下面是类似数组的对象调用数组的`Symbol.iterator`方法的例子。
NodeList 对象是类似数组的对象,本来就具有遍历接口,可以直接遍历。上面代码中,我们将它的遍历接口改成数组的`Symbol.iterator`属性,可以看到没有任何影响。
下面是另一个类似数组的对象调用数组的`Symbol.iterator`方法的例子。
```javascript
let iterable = {
@ -305,7 +306,7 @@ obj[Symbol.iterator] = () => 1;
[...obj] // TypeError: [] is not a function
```
上面代码中变量obj的Symbol.iterator方法对应的不是遍历器生成函数因此报错。
上面代码中,变量`obj``Symbol.iterator`方法对应的不是遍历器生成函数,因此报错。
有了遍历器接口,数据结构就可以用`for...of`循环遍历(详见下文),也可以使用`while`循环遍历。
@ -341,7 +342,7 @@ let [first, ...rest] = set;
**2扩展运算符**
扩展运算符(...)也会调用默认的iterator接口。
扩展运算符(...)也会调用默认的 Iterator 接口。
```javascript
// 例一
@ -362,9 +363,9 @@ let arr = ['b', 'c'];
let arr = [...iterable];
```
**3yield* **
**3yield\***
yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
`yield*`后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
```javascript
let generator = function* () {
@ -443,13 +444,13 @@ str // "hi"
`Symbol.iterator`方法的最简单实现,还是使用下一章要介绍的 Generator 函数。
```javascript
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
let myIterable = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
};
}
}
[...myIterable] // [1, 2, 3]
// 或者采用下面的简洁写法
@ -464,8 +465,8 @@ let obj = {
for (let x of obj) {
console.log(x);
}
// hello
// world
// "hello"
// "world"
```
上面代码中,`Symbol.iterator`方法几乎不用部署任何代码,只要用 yield 命令给出每一步的返回值即可。
@ -480,10 +481,7 @@ for (let x of obj) {
function readLinesSync(file) {
return {
next() {
if (file.isAtEndOfFile()) {
file.close();
return { done: true };
}
return { done: false };
},
return() {
file.close();
@ -493,15 +491,30 @@ function readLinesSync(file) {
}
```
上面代码中,函数`readLinesSync`接受一个文件对象作为参数,返回一个遍历器对象,其中除了`next`方法,还部署了`return`方法。下面,我们让文件的遍历提前返回,这样就会触发执行`return`方法。
上面代码中,函数`readLinesSync`接受一个文件对象作为参数,返回一个遍历器对象,其中除了`next`方法,还部署了`return`方法。下面的三种情况,都会触发执行`return`方法。
```javascript
// 情况一
for (let line of readLinesSync(fileName)) {
console.log(line);
break;
}
// 情况二
for (let line of readLinesSync(fileName)) {
console.log(line);
continue;
}
// 情况三
for (let line of readLinesSync(fileName)) {
console.log(line);
throw new Error();
}
```
上面代码中,情况一输出文件的第一行以后,就会执行`return`方法,关闭这个文件;情况二输出所有行以后,执行`return`方法,关闭该文件;情况三会在执行`return`方法关闭文件之后,再抛出错误。
注意,`return`方法必须返回一个对象,这是 Generator 规格决定的。
`throw`方法主要是配合 Generator 函数使用一般的遍历器对象用不到这个方法。请参阅《Generator 函数》一章。
@ -560,7 +573,7 @@ for (let a of arr) {
}
```
上面代码表明,`for...in`循环读取键名,`for...of`循环读取键值。如果要通过`for...of`循环,获取数组的索引,可以借助数组实例的`entries`方法和`keys`方法,参见《数组的扩展》章节
上面代码表明,`for...in`循环读取键名,`for...of`循环读取键值。如果要通过`for...of`循环,获取数组的索引,可以借助数组实例的`entries`方法和`keys`方法(参见《数组的扩展》一章)
`for...of`循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟`for...in`循环也不一样。
@ -625,7 +638,7 @@ for (let [key, value] of map) {
有些数据结构是在现有数据结构的基础上计算生成的。比如ES6 的数组、Set、Map 都部署了以下三个方法,调用后都返回遍历器对象。
- `entries()` 返回一个遍历器对象,用来遍历`[键名, 键值]`组成的数组。对于数组,键名就是索引值;对于Set键名与键值相同。Map结构的iterator接口默认就是调用entries方法。
- `entries()` 返回一个遍历器对象,用来遍历`[键名, 键值]`组成的数组。对于数组,键名就是索引值;对于 Set键名与键值相同。Map 结构的 Iterator 接口,默认就是调用`entries`方法。
- `keys()` 返回一个遍历器对象,用来遍历所有的键名。
- `values()` 返回一个遍历器对象,用来遍历所有的键值。
@ -643,7 +656,7 @@ for (let pair of arr.entries()) {
### 类似数组的对象
类似数组的对象包括好几类。下面是`for...of`循环用于字符串、DOM NodeList对象、arguments对象的例子。
类似数组的对象包括好几类。下面是`for...of`循环用于字符串、DOM NodeList 对象、`arguments`对象的例子。
```javascript
// 字符串
@ -681,7 +694,7 @@ for (let x of 'a\uD83D\uDC0A') {
// '\uD83D\uDC0A'
```
并不是所有类似数组的对象都具有iterator接口一个简便的解决方法就是使用Array.from方法将其转为数组。
并不是所有类似数组的对象都具有 Iterator 接口,一个简便的解决方法,就是使用`Array.from`方法将其转为数组。
```javascript
let arrayLike = { length: 2, 0: 'a', 1: 'b' };
@ -699,10 +712,10 @@ for (let x of Array.from(arrayLike)) {
### 对象
对于普通的对象,`for...of`结构不能直接使用,会报错,必须部署了iterator接口后才能使用。但是,这样情况下,`for...in`循环依然可以用来遍历键名。
对于普通的对象,`for...of`结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。但是,这样情况下,`for...in`循环依然可以用来遍历键名。
```javascript
var es6 = {
let es6 = {
edition: 6,
committee: "TC39",
standard: "ECMA-262"
@ -718,7 +731,7 @@ for (let e in es6) {
for (let e of es6) {
console.log(e);
}
// TypeError: es6 is not iterable
// TypeError: es6[Symbol.iterator] is not a function
```
上面代码表示,对于普通的对象,`for...in`循环可以遍历键名,`for...of`循环会报错。
@ -727,17 +740,10 @@ for (let e of es6) {
```javascript
for (var key of Object.keys(someObject)) {
console.log(key + ": " + someObject[key]);
console.log(key + ': ' + someObject[key]);
}
```
在对象上部署iterator接口的代码参见本章前面部分。一个方便的方法是将数组的`Symbol.iterator`属性,直接赋值给其他对象的`Symbol.iterator`属性。比如,想要让`for...of`环遍历jQuery对象只要加上下面这一行就可以了。
```javascript
jQuery.prototype[Symbol.iterator] =
Array.prototype[Symbol.iterator];
```
另一个方法是使用 Generator 函数将对象重新包装一下。
```javascript
@ -748,7 +754,7 @@ function* entries(obj) {
}
for (let [key, value] of entries(obj)) {
console.log(key, "->", value);
console.log(key, '->', value);
}
// a -> 1
// b -> 2
@ -757,7 +763,7 @@ for (let [key, value] of entries(obj)) {
### 与其他遍历语法的比较
以数组为例JavaScript提供多种遍历语法。最原始的写法就是for循环。
以数组为例JavaScript 提供多种遍历语法。最原始的写法就是`for`循环。
```javascript
for (var index = 0; index < myArray.length; index++) {
@ -765,7 +771,7 @@ for (var index = 0; index < myArray.length; index++) {
}
```
这种写法比较麻烦因此数组提供内置的forEach方法。
这种写法比较麻烦,因此数组提供内置的`forEach`方法。
```javascript
myArray.forEach(function (value) {
@ -773,7 +779,7 @@ myArray.forEach(function (value) {
});
```
这种写法的问题在于,无法中途跳出`forEach`循环break命令或return命令都不能奏效。
这种写法的问题在于,无法中途跳出`forEach`循环,`break`命令或`return`命令都不能奏效。
`for...in`循环可以遍历数组的键名。
@ -783,11 +789,11 @@ for (var index in myArray) {
}
```
for...in循环有几个缺点。
`for...in`循环有几个缺点。
- 数组的键名是数字但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。
- for...in循环不仅遍历数字键名还会遍历手动添加的其他键甚至包括原型链上的键。
- 某些情况下for...in循环会以任意顺序遍历键名。
- 数组的键名是数字,但是`for...in`循环是以字符串作为键名“0”、“1”、“2”等等。
- `for...in`循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
- 某些情况下,`for...in`循环会以任意顺序遍历键名。
总之,`for...in`循环主要是为遍历对象而设计的,不适用于遍历数组。
@ -799,8 +805,8 @@ for (let value of myArray) {
}
```
- 有着同for...in一样的简洁语法但是没有for...in那些缺点。
- 不同于forEach方法它可以与break、continue和return配合使用。
- 有着同`for...in`一样的简洁语法,但是没有`for...in`那些缺点。
- 不同于`forEach`方法,它可以与`break``continue``return`配合使用。
- 提供了遍历所有数据结构的统一操作接口。
下面是一个使用 break 语句,跳出`for...of`循环的例子。
@ -813,4 +819,4 @@ for (var n of fibonacci) {
}
```
上面的例子会输出斐波纳契数列小于等于1000的项。如果当前项大于1000就会使用break语句跳出`for...of`循环。
上面的例子,会输出斐波纳契数列小于等于 1000 的项。如果当前项大于 1000就会使用`break`语句跳出`for...of`循环。

View File

@ -21,7 +21,9 @@ b // 1
`for`循环的计数器,就很合适使用`let`命令。
```javascript
for (let i = 0; i < 10; i++) {}
for (let i = 0; i < 10; i++) {
// ...
}
console.log(i);
// ReferenceError: i is not defined
@ -29,7 +31,7 @@ console.log(i);
上面代码中,计数器`i`只在`for`循环体内有效,在循环体外引用就会报错。
下面的代码如果使用`var`最后输出的是10。
下面的代码如果使用`var`,最后输出的是`10`
```javascript
var a = [];
@ -41,7 +43,7 @@ for (var i = 0; i < 10; i++) {
a[6](); // 10
```
上面代码中,变量`i``var`声明的,在全局范围内都有效。所以每一次循环,新的`i`值都会覆盖旧值,导致最后输出的是最后一轮的`i`的值
上面代码中,变量`i``var`命令声明的,在全局范围内都有效,所以全局只有一个变量`i`。每一次循环,变量`i`的值都会发生改变,而循环内被赋给数组`a`的函数内部的`console.log(i)`,里面的`i`指向的就是全局的`i`。也就是说,所有数组`a`的成员里面的`i`,指向的都是同一个`i`,导致运行时输出的是最后一轮的`i`的值,也就是 10
如果使用`let`,声明的变量仅在块级作用域内有效,最后输出的是 6。
@ -55,17 +57,35 @@ for (let i = 0; i < 10; i++) {
a[6](); // 6
```
上面代码中,变量`i``let`声明的,当前的`i`只在本轮循环有效,所以每一次循环的`i`其实都是一个新的变量所以最后输出的是6。
上面代码中,变量`i``let`声明的,当前的`i`只在本轮循环有效,所以每一次循环的`i`其实都是一个新的变量,所以最后输出的是`6`。你可能会问,如果每一轮循环的变量`i`都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量`i`时,就在上一轮循环的基础上进行计算。
另外,`for`循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
```javascript
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
```
上面代码正确运行,输出了 3 次`abc`。这表明函数内部的变量`i`与循环变量`i`不在同一个作用域,有各自单独的作用域。
### 不存在变量提升
`let`不像`var`那样会发生“变量提升”现象。所以,变量一定要在声明后使用,否则报错。
`var`命令会发生”变量提升“现象,即变量可以在声明之前使用,值为`undefined`。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。
为了纠正这种现象,`let`命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
```javascript
// var 的情况
console.log(foo); // 输出undefined
console.log(bar); // 报错ReferenceError
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
```
@ -88,7 +108,7 @@ if (true) {
ES6 明确规定,如果区块中存在`let``const`命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之在代码块内使用let命令声明变量之前该变量都是不可用的。这在语法上称为“暂时性死区”temporal dead zone简称TDZ
总之,在代码块内,使用`let`命令声明变量之前该变量都是不可用的。这在语法上称为“暂时性死区”temporal dead zone简称 TDZ
```javascript
if (true) {
@ -142,23 +162,36 @@ function bar(x = 2, y = x) {
bar(); // [2, 2]
```
另外,下面的代码也会报错,与`var`的行为不同。
```javascript
// 不报错
var x = x;
// 报错
let x = x;
// ReferenceError: x is not defined
```
上面代码报错,也是因为暂时性死区。使用`let`声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量`x`的声明语句还没有执行完成前,就去取`x`的值导致报错”x 未定义“。
ES6 规定暂时性死区和`let``const`语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。
总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
### 不允许重复声明
let不允许在相同作用域内重复声明同一个变量。
`let`不允许在相同作用域内,重复声明同一个变量。
```javascript
// 报错
function () {
function func() {
let a = 10;
var a = 1;
}
// 报错
function () {
function func() {
let a = 10;
let a = 1;
}
@ -192,14 +225,14 @@ var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = "hello world";
var tmp = 'hello world';
}
}
f(); // undefined
```
上面代码函数f执行后,输出结果为`undefined`原因在于变量提升导致内层的tmp变量覆盖了外层的tmp变量。
上面代码的原意是,`if`代码块的外部使用外层的`tmp`变量,内部使用内层的`tmp`变量。但是,函数`f`执行后,输出结果为`undefined`,原因在于变量提升,导致内层的`tmp`变量覆盖了外层的`tmp`变量。
第二种场景,用来计数的循环变量泄露为全局变量。
@ -213,7 +246,7 @@ for (var i = 0; i < s.length; i++) {
console.log(i); // 5
```
上面代码中变量i只用来控制循环但是循环结束后它并没有消失泄露成了全局变量。
上面代码中,变量`i`只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
### ES6 的块级作用域
@ -229,7 +262,7 @@ function f1() {
}
```
上面的函数有两个代码块,都声明了变量`n`运行后输出5。这表示外层代码块不受内层代码块的影响。如果使用`var`定义变量`n`,最后输出的值就是10。
上面的函数有两个代码块,都声明了变量`n`,运行后输出 5。这表示外层代码块不受内层代码块的影响。如果两次都使用`var`定义变量`n`,最后输出的值才是 10。
ES6 允许块级作用域的任意嵌套。
@ -273,7 +306,7 @@ ES6允许块级作用域的任意嵌套。
### 块级作用域与函数声明
函数能不能在块级作用域之中声明是一个相当令人混淆的问题。
函数能不能在块级作用域之中声明?这是一个相当令人混淆的问题。
ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
@ -287,37 +320,19 @@ if (true) {
try {
function f() {}
} catch(e) {
// ...
}
```
上面代码的两种函数声明根据ES5的规定都是非法的。
上面两种函数声明,根据 ES5 的规定都是非法的。
但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。不过,“严格模式”下还是会报错。
但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。
```javascript
// ES5严格模式
'use strict';
if (true) {
function f() {}
}
// 报错
```
ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。
```javascript
// ES6严格模式
'use strict';
if (true) {
function f() {}
}
// 不报错
```
ES6 规定,块级作用域之中,函数声明语句的行为类似于`let`,在块级作用域之外不可引用。
ES6 引入了块级作用域明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于`let`,在块级作用域之外不可引用。
```javascript
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
@ -331,8 +346,9 @@ function f() { console.log('I am outside!'); }
上面代码在 ES5 中运行会得到“I am inside!”,因为在`if`内声明的函数`f`会被提升到函数头部,实际运行的代码如下。
```javascript
// ES5版本
// ES5 环境
function f() { console.log('I am outside!'); }
(function () {
function f() { console.log('I am inside!'); }
if (false) {
@ -341,17 +357,9 @@ function f() { console.log('I am outside!'); }
}());
```
ES6 的运行结果就完全不一样了会得到“I am outside!”。因为块级作用域内声明的函数类似于`let`,对作用域之外没有影响,实际运行的代码如下。
ES6 就完全不一样了,理论上会得到“I am outside!”。因为块级作用域内声明的函数类似于`let`,对作用域之外没有影响。但是,如果你真的在 ES6 浏览器中运行一下上面的代码,是会报错的,这是为什么呢?
```javascript
// ES6版本
function f() { console.log('I am outside!'); }
(function () {
f();
}());
```
很显然这种行为差异会对老代码产生很大影响。为了减轻因此产生的不兼容问题ES6在[附录B](http://www.ecma-international.org/ecma-262/6.0/index.html#sec-block-level-function-declarations-web-legacy-compatibility-semantics)里面规定,浏览器的实现可以不遵守上面的规定,有自己的[行为方式](http://stackoverflow.com/questions/31419897/what-are-the-precise-semantics-of-block-level-functions-in-es6)。
原来如果改变了块级作用域内声明的函数的处理规则显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题ES6 在[附录 B](http://www.ecma-international.org/ecma-262/6.0/index.html#sec-block-level-function-declarations-web-legacy-compatibility-semantics)里面规定,浏览器的实现可以不遵守上面的规定,有自己的[行为方式](http://stackoverflow.com/questions/31419897/what-are-the-precise-semantics-of-block-level-functions-in-es6)。
- 允许在块级作用域内声明函数。
- 函数声明类似于`var`,即会提升到全局作用域或函数作用域的头部。
@ -359,11 +367,12 @@ function f() { console.log('I am outside!'); }
注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作`let`处理。
前面那段代码,在 Chrome 环境下运行会报错
根据这三条规则,在浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于`var`声明的变量
```javascript
// ES6的浏览器环境
// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
@ -375,10 +384,10 @@ function f() { console.log('I am outside!'); }
// Uncaught TypeError: f is not a function
```
上面的代码报错,因为实际运行的是下面的代码。
上面的代码在符合 ES6 的浏览器中,都会报错,因为实际运行的是下面的代码。
```javascript
// ES6的浏览器环境
// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
var f = undefined;
@ -439,7 +448,7 @@ if (true)
上面代码中,块级作用域将两个语句封装在一起。但是,在块级作用域以外,没有办法得到`t`的值,因为块级作用域不返回值,除非`t`是全局变量。
现在有一个[提案](http://wiki.ecmascript.org/doku.php?id=strawman:do_expressions),使得块级作用域可以变为表达式,也就是说可以返回值,办法就是在块级作用域之前加上`do`,使它变为`do`表达式。
现在有一个[提案](http://wiki.ecmascript.org/doku.php?id=strawman:do_expressions),使得块级作用域可以变为表达式,也就是说可以返回值,办法就是在块级作用域之前加上`do`,使它变为`do`表达式,然后就会返回内部最后执行的表达式的值
```javascript
let x = do {
@ -448,10 +457,12 @@ let x = do {
};
```
上面代码中,变量`x`会得到整个块级作用域的返回值。
上面代码中,变量`x`会得到整个块级作用域的返回值`t * t + 1`
## const 命令
### 基本用法
`const`声明一个只读的常量。一旦声明,常量的值就不能改变。
```javascript
@ -464,7 +475,7 @@ PI = 3;
上面代码表明改变常量的值会报错。
`const`声明的变量不得改变值这意味着const一旦声明变量就必须立即初始化不能留到以后赋值。
`const`声明的变量不得改变值,这意味着,`const`一旦声明变量,就必须立即初始化,不能留到以后赋值。
```javascript
const foo;
@ -505,15 +516,18 @@ const message = "Goodbye!";
const age = 30;
```
对于复合类型的变量,变量名不指向数据,而是指向数据所在的地址。`const`命令只是保证变量名指向的地址不变,并不保证该地址的数据不变,所以将一个对象声明为常量必须非常小心。
### 本质
`const`实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针,`const`只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
```javascript
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
foo.prop
// 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
```
@ -521,7 +535,7 @@ foo = {}; // TypeError: "foo" is read-only
下面是另一个例子。
```js
```javascript
const a = [];
a.push('Hello'); // 可执行
a.length = 0; // 可执行
@ -547,7 +561,7 @@ foo.prop = 123;
```javascript
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, value) => {
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
@ -555,6 +569,8 @@ var constantize = (obj) => {
};
```
### ES6 声明变量的六种方法
ES5 只有两种声明变量的方法:`var`命令和`function`命令。ES6 除了添加`let``const`命令,后面章节还会提到,另外两种声明变量的方法:`import`命令和`class`命令。所以ES6 一共有 6 种声明变量的方法。
## 顶层对象的属性
@ -646,4 +662,3 @@ const global = getGlobal();
```
上面代码将顶层对象放入变量`global`

814
docs/module-loader.md Normal file
View File

@ -0,0 +1,814 @@
# Module 的加载实现
上一章介绍了模块的语法,本章介绍如何在浏览器和 Node 之中加载 ES6 模块,以及实际开发中经常遇到的一些问题(比如循环加载)。
## 浏览器加载
### 传统方法
HTML 网页中,浏览器通过`<script>`标签加载 JavaScript 脚本。
```html
<!-- 页面内嵌的脚本 -->
<script type="application/javascript">
// module code
</script>
<!-- 外部脚本 -->
<script type="application/javascript" src="path/to/myModule.js">
</script>
```
上面代码中,由于浏览器脚本的默认语言是 JavaScript因此`type="application/javascript"`可以省略。
默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到`<script>`标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。
如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。这显然是很不好的体验,所以浏览器允许脚本异步加载,下面就是两种异步加载的语法。
```html
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
```
上面代码中,`<script>`标签打开`defer``async`属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。
`defer``async`的区别是:`defer`要等到整个页面在内存中正常渲染结束DOM 结构完全生成,以及其他脚本执行完成),才会执行;`async`一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,`defer`是“渲染完再执行”,`async`是“下载完就执行”。另外,如果有多个`defer`脚本,会按照它们在页面出现的顺序加载,而多个`async`脚本是不能保证加载顺序的。
### 加载规则
浏览器加载 ES6 模块,也使用`<script>`标签,但是要加入`type="module"`属性。
```html
<script type="module" src="./foo.js"></script>
```
上面代码在网页中插入一个模块`foo.js`,由于`type`属性设为`module`,所以浏览器知道这是一个 ES6 模块。
浏览器对于带有`type="module"``<script>`,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了`<script>`标签的`defer`属性。
```html
<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>
```
如果网页有多个`<script type="module">`,它们会按照在页面出现的顺序依次执行。
`<script>`标签的`async`属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。
```html
<script type="module" src="./foo.js" async></script>
```
一旦使用了`async`属性,`<script type="module">`就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。
ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。
```html
<script type="module">
import utils from "./utils.js";
// other code
</script>
```
对于外部的模块脚本(上例是`foo.js`),有几点需要注意。
- 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
- 模块脚本自动采用严格模式,不管有没有声明`use strict`
- 模块之中,可以使用`import`命令加载其他模块(`.js`后缀不可省略,需要提供绝对 URL 或相对 URL也可以使用`export`命令输出对外接口。
- 模块之中,顶层的`this`关键字返回`undefined`,而不是指向`window`。也就是说,在模块顶层使用`this`关键字,是无意义的。
- 同一个模块如果加载多次,将只执行一次。
下面是一个示例模块。
```javascript
import utils from 'https://example.com/js/utils.js';
const x = 1;
console.log(x === window.x); //false
console.log(this === undefined); // true
delete x; // 句法错误,严格模式禁止删除变量
```
利用顶层的`this`等于`undefined`这个语法点,可以侦测当前代码是否在 ES6 模块之中。
```javascript
const isNotModuleScript = this !== undefined;
```
## ES6 模块与 CommonJS 模块的差异
讨论 Node 加载 ES6 模块之前,必须了解 ES6 模块与 CommonJS 模块完全不同。
它们有两个重大差异。
- CommonJS 模块输出的是一个值的拷贝ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载ES6 模块是编译时输出接口。
第二个差异是因为 CommonJS 加载的是一个对象(即`module.exports`属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
下面重点解释第一个差异。
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件`lib.js`的例子。
```javascript
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
```
上面代码输出内部变量`counter`和改写这个变量的内部方法`incCounter`。然后,在`main.js`里面加载这个模块。
```javascript
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
```
上面代码说明,`lib.js`模块加载以后,它的内部变化就影响不到输出的`mod.counter`了。这是因为`mod.counter`是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
```javascript
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
get counter() {
return counter
},
incCounter: incCounter,
};
```
上面代码中,输出的`counter`属性实际上是一个取值器函数。现在再执行`main.js`,就可以正确读取内部变量`counter`的变动了。
```bash
$ node main.js
3
4
```
ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令`import`就会生成一个只读引用。等到脚本真正执行时再根据这个只读引用到被加载的那个模块里面去取值。换句话说ES6 的`import`有点像 Unix 系统的“符号连接”,原始值变了,`import`加载的值也会跟着变。因此ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
还是举上面的例子。
```javascript
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
```
上面代码说明ES6 模块输入的变量`counter`是活的,完全反应其所在模块`lib.js`内部的变化。
再举一个出现在`export`一节中的例子。
```javascript
// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);
```
上面代码中,`m1.js`的变量`foo`,在刚加载时等于`bar`,过了 500 毫秒,又变为等于`baz`
让我们看看,`m2.js`能否正确读取这个变化。
```bash
$ babel-node m2.js
bar
baz
```
上面代码表明ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。
由于 ES6 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。
```javascript
// lib.js
export let obj = {};
// main.js
import { obj } from './lib';
obj.prop = 123; // OK
obj = {}; // TypeError
```
上面代码中,`main.js``lib.js`输入变量`obj`,可以对`obj`添加属性,但是重新赋值就会报错。因为变量`obj`指向的地址是只读的,不能重新赋值,这就好比`main.js`创造了一个名为`obj``const`变量。
最后,`export`通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。
```javascript
// mod.js
function C() {
this.sum = 0;
this.add = function () {
this.sum += 1;
};
this.show = function () {
console.log(this.sum);
};
}
export let c = new C();
```
上面的脚本`mod.js`,输出的是一个`C`的实例。不同的脚本加载这个模块,得到的都是同一个实例。
```javascript
// x.js
import {c} from './mod';
c.add();
// y.js
import {c} from './mod';
c.show();
// main.js
import './x';
import './y';
```
现在执行`main.js`,输出的是`1`
```bash
$ babel-node main.js
1
```
这就证明了`x.js``y.js`加载的都是`C`的同一个实例。
## Node 加载
### 概述
Node 对 ES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是将两者分开ES6 模块和 CommonJS 采用各自的加载方案。
Node 要求 ES6 模块采用`.mjs`后缀文件名。也就是说,只要脚本文件里面使用`import`或者`export`命令,那么就必须采用`.mjs`后缀名。`require`命令不能加载`.mjs`文件,会报错,只有`import`命令才可以加载`.mjs`文件。反过来,`.mjs`文件里面也不能使用`require`命令,必须使用`import`
目前,这项功能还在试验阶段。安装 Node v8.5.0 或以上版本,要用`--experimental-modules`参数才能打开该功能。
```bash
$ node --experimental-modules my-app.mjs
```
为了与浏览器的`import`加载规则相同Node 的`.mjs`文件支持 URL 路径。
```javascript
import './foo?query=1'; // 加载 ./foo 传入参数 ?query=1
```
上面代码中,脚本路径带有参数`?query=1`Node 会按 URL 规则解读。同一个脚本只要参数不同,就会被加载多次,并且保存成不同的缓存。由于这个原因,只要文件名中含有`:``%``#``?`等特殊字符,最好对这些字符进行转义。
目前Node 的`import`命令只支持加载本地模块(`file:`协议),不支持加载远程模块。
如果模块名不含路径,那么`import`命令会去`node_modules`目录寻找这个模块。
```javascript
import 'baz';
import 'abc/123';
```
如果模块名包含路径,那么`import`命令会按照路径去寻找这个名字的脚本文件。
```javascript
import 'file:///etc/config/app.json';
import './foo';
import './foo?search';
import '../bar';
import '/baz';
```
如果脚本文件省略了后缀名,比如`import './foo'`Node 会依次尝试四个后缀名:`./foo.mjs``./foo.js``./foo.json``./foo.node`。如果这些脚本文件都不存在Node 就会去加载`./foo/package.json``main`字段指定的脚本。如果`./foo/package.json`不存在或者没有`main`字段,那么就会依次加载`./foo/index.mjs``./foo/index.js``./foo/index.json``./foo/index.node`。如果以上四个文件还是都不存在,就会抛出错误。
最后Node 的`import`命令是异步加载,这一点与浏览器的处理方法相同。
### 内部变量
ES6 模块应该是通用的同一个模块不用修改就可以用在浏览器环境和服务器环境。为了达到这个目标Node 规定 ES6 模块之中不能使用 CommonJS 模块的特有的一些内部变量。
首先,就是`this`关键字。ES6 模块之中,顶层的`this`指向`undefined`CommonJS 模块的顶层`this`指向当前模块,这是两者的一个重大差异。
其次,以下这些顶层变量在 ES6 模块之中都是不存在的。
- `arguments`
- `require`
- `module`
- `exports`
- `__filename`
- `__dirname`
如果你一定要使用这些变量,有一个变通方法,就是写一个 CommonJS 模块输出这些变量,然后再用 ES6 模块加载这个 CommonJS 模块。但是这样一来,该 ES6 模块就不能直接用于浏览器环境了,所以不推荐这样做。
```javascript
// expose.js
module.exports = {__dirname};
// use.mjs
import expose from './expose.js';
const {__dirname} = expose;
```
上面代码中,`expose.js`是一个 CommonJS 模块,输出变量`__dirname`,该变量在 ES6 模块之中不存在。ES6 模块加载`expose.js`,就可以得到`__dirname`
### ES6 模块加载 CommonJS 模块
CommonJS 模块的输出都定义在`module.exports`这个属性上面。Node 的`import`命令加载 CommonJS 模块Node 会自动将`module.exports`属性,当作模块的默认输出,即等同于`export default xxx`
下面是一个 CommonJS 模块。
```javascript
// a.js
module.exports = {
foo: 'hello',
bar: 'world'
};
// 等同于
export default {
foo: 'hello',
bar: 'world'
};
```
`import`命令加载上面的模块,`module.exports`会被视为默认输出,即`import`命令实际上输入的是这样一个对象`{ default: module.exports }`
所以,一共有三种写法,可以拿到 CommonJS 模块的`module.exports`
```javascript
// 写法一
import baz from './a';
// baz = {foo: 'hello', bar: 'world'};
// 写法二
import {default as baz} from './a';
// baz = {foo: 'hello', bar: 'world'};
// 写法三
import * as baz from './a';
// baz = {
// get default() {return module.exports;},
// get foo() {return this.default.foo}.bind(baz),
// get bar() {return this.default.bar}.bind(baz)
// }
```
上面代码的第三种写法,可以通过`baz.default`拿到`module.exports``foo`属性和`bar`属性就是可以通过这种方法拿到了`module.exports`
下面是一些例子。
```javascript
// b.js
module.exports = null;
// es.js
import foo from './b';
// foo = null;
import * as bar from './b';
// bar = { default:null };
```
上面代码中,`es.js`采用第二种写法时,要通过`bar.default`这样的写法,才能拿到`module.exports`
```javascript
// c.js
module.exports = function two() {
return 2;
};
// es.js
import foo from './c';
foo(); // 2
import * as bar from './c';
bar.default(); // 2
bar(); // throws, bar is not a function
```
上面代码中,`bar`本身是一个对象,不能当作函数调用,只能通过`bar.default`调用。
CommonJS 模块的输出缓存机制,在 ES6 加载方式下依然有效。
```javascript
// foo.js
module.exports = 123;
setTimeout(_ => module.exports = null);
```
上面代码中,对于加载`foo.js`的脚本,`module.exports`将一直是`123`,而不会变成`null`
由于 ES6 模块是编译时确定输出接口CommonJS 模块是运行时确定输出接口,所以采用`import`命令加载 CommonJS 模块时,不允许采用下面的写法。
```javascript
// 不正确
import { readfile } from 'fs';
```
上面的写法不正确,因为`fs`是 CommonJS 格式,只有在运行时才能确定`readfile`接口,而`import`命令要求编译时就确定这个接口。解决方法就是改为整体输入。
```javascript
// 正确的写法一
import * as express from 'express';
const app = express.default();
// 正确的写法二
import express from 'express';
const app = express();
```
### CommonJS 模块加载 ES6 模块
CommonJS 模块加载 ES6 模块,不能使用`require`命令,而要使用`import()`函数。ES6 模块的所有输出接口,会成为输入对象的属性。
```javascript
// es.mjs
let foo = { bar: 'my-default' };
export default foo;
foo = null;
// cjs.js
const es_namespace = await import('./es');
// es_namespace = {
// get default() {
// ...
// }
// }
console.log(es_namespace.default);
// { bar:'my-default' }
```
上面代码中,`default`接口变成了`es_namespace.default`属性。另外,由于存在缓存机制,`es.mjs``foo`的重新赋值没有在模块外部反映出来。
下面是另一个例子。
```javascript
// es.js
export let foo = { bar:'my-default' };
export { foo as bar };
export function f() {};
export class c {};
// cjs.js
const es_namespace = await import('./es');
// es_namespace = {
// get foo() {return foo;}
// get bar() {return foo;}
// get f() {return f;}
// get c() {return c;}
// }
```
## 循环加载
“循环加载”circular dependency指的是`a`脚本的执行依赖`b`脚本,而`b`脚本的执行又依赖`a`脚本。
```javascript
// a.js
var b = require('b');
// b.js
var a = require('a');
```
通常,“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。
但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目,很容易出现`a`依赖`b``b`依赖`c``c`又依赖`a`这样的情况。这意味着,模块加载机制必须考虑“循环加载”的情况。
对于 JavaScript 语言来说,目前最常见的两种模块格式 CommonJS 和 ES6处理“循环加载”的方法是不一样的返回的结果也不一样。
### CommonJS 模块的加载原理
介绍 ES6 如何处理“循环加载”之前,先介绍目前最流行的 CommonJS 模块格式的加载原理。
CommonJS 的一个模块,就是一个脚本文件。`require`命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。
```javascript
{
id: '...',
exports: { ... },
loaded: true,
...
}
```
上面代码就是 Node 内部加载模块后生成的一个对象。该对象的`id`属性是模块名,`exports`属性是模块输出的各个接口,`loaded`属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。
以后需要用到这个模块的时候,就会到`exports`属性上面取值。即使再次执行`require`命令也不会再次执行该模块而是到缓存之中取值。也就是说CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
### CommonJS 模块的循环加载
CommonJS 模块的重要特性是加载时执行,即脚本代码在`require`的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
让我们来看Node [官方文档](https://nodejs.org/api/modules.html#modules_cycles)里面的例子。脚本文件`a.js`代码如下。
```javascript
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
```
上面代码之中,`a.js`脚本先输出一个`done`变量,然后加载另一个脚本文件`b.js`。注意,此时`a.js`代码就停在这里,等待`b.js`执行完毕,再往下执行。
再看`b.js`的代码。
```javascript
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
```
上面代码之中,`b.js`执行到第二行,就会去加载`a.js`,这时,就发生了“循环加载”。系统会去`a.js`模块对应对象的`exports`属性取值,可是因为`a.js`还没有执行完,从`exports`属性只能取回已经执行的部分,而不是最后的值。
`a.js`已经执行的部分,只有一行。
```javascript
exports.done = false;
```
因此,对于`b.js`来说,它从`a.js`只输入一个变量`done`,值为`false`
然后,`b.js`接着往下执行,等到全部执行完毕,再把执行权交还给`a.js`。于是,`a.js`接着往下执行,直到执行完毕。我们写一个脚本`main.js`,验证这个过程。
```javascript
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
```
执行`main.js`,运行结果如下。
```bash
$ node main.js
在 b.js 之中a.done = false
b.js 执行完毕
在 a.js 之中b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true
```
上面的代码证明了两件事。一是,在`b.js`之中,`a.js`没有执行完毕,只执行了第一行。二是,`main.js`执行到第二行时,不会再次执行`b.js`,而是输出缓存的`b.js`的执行结果,即它的第四行。
```javascript
exports.done = true;
```
总之CommonJS 输入的是被输出值的拷贝,不是引用。
另外,由于 CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。
```javascript
var a = require('a'); // 安全的写法
var foo = require('a').foo; // 危险的写法
exports.good = function (arg) {
return a.foo('good', arg); // 使用的是 a.foo 的最新值
};
exports.bad = function (arg) {
return foo('bad', arg); // 使用的是一个部分加载时的值
};
```
上面代码中,如果发生循环加载,`require('a').foo`的值很可能后面会被改写,改用`require('a')`会更保险一点。
### ES6 模块的循环加载
ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用,如果使用`import`从一个模块加载变量(即`import foo from 'foo'`),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
请看下面这个例子。
```javascript
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';
```
上面代码中,`a.mjs`加载`b.mjs``b.mjs`又加载`a.mjs`,构成循环加载。执行`a.mjs`,结果如下。
```bash
$ node --experimental-modules a.mjs
b.mjs
ReferenceError: foo is not defined
```
上面代码中,执行`a.mjs`以后会报错,`foo`变量未定义,这是为什么?
让我们一行行来看ES6 循环加载是怎么处理的。首先,执行`a.mjs`以后,引擎发现它加载了`b.mjs`,因此会优先执行`b.mjs`,然后再执行`a.js`。接着,执行`b.mjs`的时候,已知它从`a.mjs`输入了`foo`接口,这时不会去执行`a.mjs`,而是认为这个接口已经存在了,继续往下执行。执行到第三行`console.log(foo)`的时候,才发现这个接口根本没定义,因此报错。
解决这个问题的方法,就是让`b.mjs`运行的时候,`foo`已经有定义了。这可以通过将`foo`写成函数来解决。
```javascript
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo' }
export {foo};
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};
```
这时再执行`a.mjs`就可以得到预期结果。
```bash
$ node --experimental-modules a.mjs
b.mjs
foo
a.mjs
bar
```
这是因为函数具有提升作用,在执行`import {bar} from './b'`时,函数`foo`就已经有定义了,所以`b.mjs`加载的时候不会报错。这也意味着,如果把函数`foo`改写成函数表达式,也会报错。
```javascript
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
const foo = () => 'foo';
export {foo};
```
上面代码的第四行,改成了函数表达式,就不具有提升作用,执行就会报错。
我们再来看 ES6 模块加载器[SystemJS](https://github.com/ModuleLoader/es6-module-loader/blob/master/docs/circular-references-bindings.md)给出的一个例子。
```javascript
// even.js
import { odd } from './odd'
export var counter = 0;
export function even(n) {
counter++;
return n === 0 || odd(n - 1);
}
// odd.js
import { even } from './even';
export function odd(n) {
return n !== 0 && even(n - 1);
}
```
上面代码中,`even.js`里面的函数`even`有一个参数`n`,只要不等于 0就会减去 1传入加载的`odd()``odd.js`也会做类似操作。
运行上面这段代码,结果如下。
```javascript
$ babel-node
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
> m.even(20)
true
> m.counter
17
```
上面代码中,参数`n`从 10 变为 0 的过程中,`even()`一共会执行 6 次,所以变量`counter`等于 6。第二次调用`even()`时,参数`n`从 20 变为 0`even()`一共会执行 11 次,加上前面的 6 次,所以变量`counter`等于 17。
这个例子要是改写成 CommonJS就根本无法执行会报错。
```javascript
// even.js
var odd = require('./odd');
var counter = 0;
exports.counter = counter;
exports.even = function (n) {
counter++;
return n == 0 || odd(n - 1);
}
// odd.js
var even = require('./even').even;
module.exports = function (n) {
return n != 0 && even(n - 1);
}
```
上面代码中,`even.js`加载`odd.js`,而`odd.js`又去加载`even.js`,形成“循环加载”。这时,执行引擎就会输出`even.js`已经执行的部分(不存在任何结果),所以在`odd.js`之中,变量`even`等于`null`,等到后面调用`even(n-1)`就会报错。
```bash
$ node
> var m = require('./even');
> m.even(10)
TypeError: even is not a function
```
## ES6 模块的转码
浏览器目前还不支持 ES6 模块,为了现在就能使用,可以将转为 ES5 的写法。除了 Babel 可以用来转码之外,还有以下两个方法,也可以用来转码。
### ES6 module transpiler
[ES6 module transpiler](https://github.com/esnext/es6-module-transpiler)是 square 公司开源的一个转码器,可以将 ES6 模块转为 CommonJS 模块或 AMD 模块的写法,从而在浏览器中使用。
首先,安装这个转码器。
```bash
$ npm install -g es6-module-transpiler
```
然后,使用`compile-modules convert`命令,将 ES6 模块文件转码。
```bash
$ compile-modules convert file1.js file2.js
```
`-o`参数可以指定转码后的文件名。
```bash
$ compile-modules convert -o out.js file1.js
```
### SystemJS
另一种解决方法是使用 [SystemJS](https://github.com/systemjs/systemjs)。它是一个垫片库polyfill可以在浏览器内加载 ES6 模块、AMD 模块和 CommonJS 模块,将其转为 ES5 格式。它在后台调用的是 Google 的 Traceur 转码器。
使用时,先在网页内载入`system.js`文件。
```html
<script src="system.js"></script>
```
然后,使用`System.import`方法加载模块文件。
```html
<script>
System.import('./app.js');
</script>
```
上面代码中的`./app`,指的是当前目录下的 app.js 文件。它可以是 ES6 模块文件,`System.import`会自动将其转码。
需要注意的是,`System.import`使用异步加载,返回一个 Promise 对象,可以针对这个对象编程。下面是一个模块文件。
```javascript
// app/es6-file.js:
export class q {
constructor() {
this.es6 = 'hello';
}
}
```
然后,在网页内加载这个模块文件。
```html
<script>
System.import('app/es6-file').then(function(m) {
console.log(new m.q().es6); // hello
});
</script>
```
上面代码中,`System.import`方法返回的是一个 Promise 对象,所以可以用`then`方法指定回调函数。

View File

@ -1,8 +1,10 @@
# Module
# Module 的语法
## 概述
历史上JavaScript 一直没有模块module体系无法将一个大程序拆分成互相依赖的小文件再用简单的方法拼装起来。其他语言都有这项功能比如 Ruby 的`require`、Python 的`import`,甚至就连 CSS 都有`@import`,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代现有的 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES6 模块的设计思想是尽量的静态化使得编译时就能确定模块的依赖关系以及输入和输出的变量。CommonJS 和 AMD 模块都只能在运行时确定这些东西。比如CommonJS 模块就是对象,输入时必须查找对象属性。
@ -12,7 +14,9 @@ let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat, exists = _fs.exists, readfile = _fs.readfile;
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
```
上面代码的实质是整体加载`fs`模块(即加载`fs`的所有方法),生成一个对象(`_fs`),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
@ -31,9 +35,11 @@ import { stat, exists, readFile } from 'fs';
除了静态加载带来的各种好处ES6 模块还有以下好处。
- 不再需要`UMD`模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
- 将来浏览器的新 API 就能用模块格式提供,不再必做成全局变量或者`navigator`对象的属性。
- 将来浏览器的新 API 就能用模块格式提供,不再必做成全局变量或者`navigator`对象的属性。
- 不再需要对象作为命名空间(比如`Math`对象),未来这些功能可以通过模块提供。
本章介绍 ES6 模块的语法,下一章介绍如何在浏览器和 Node 之中,加载 ES6 模块。
## 严格模式
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上`"use strict";`
@ -58,6 +64,8 @@ ES6 的模块自动采用严格模式,不管你有没有在模块头部加上`
上面这些限制,模块都必须遵守。由于严格模式是 ES5 引入的,不属于 ES6所以请参阅相关 ES5 书籍,本书不再详细介绍了。
其中,尤其需要注意`this`的限制。ES6 模块之中,顶层的`this`指向`undefined`,即不应该在顶层代码使用`this`
## export 命令
模块功能主要由两个命令构成:`export``import``export`命令用于规定模块的对外接口,`import`命令用于输入其他模块提供的功能。
@ -86,7 +94,7 @@ export {firstName, lastName, year};
上面代码在`export`命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在`var`语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。
export命令除了输出变量还可以输出函数或类class
`export`命令除了输出变量还可以输出函数或类class
```javascript
export function multiply(x, y) {
@ -163,7 +171,7 @@ setTimeout(() => foo = 'baz', 500);
上面代码输出变量`foo`,值为`bar`500 毫秒之后变成`baz`
这一点与CommonJS规范完全不同。CommonJS模块输出的是值的缓存不存在动态更新详见下文《ES6模块加载的实质》一节。
这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新,详见下文《Module 的加载实现》一节。
最后,`export`命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的`import`命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
@ -197,7 +205,7 @@ function setName(element) {
import { lastName as surname } from './profile';
```
`import`后面的`from`指定模块文件的位置,可以是相对路径,也可以是绝对路径,`.js`路径可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
`import`后面的`from`指定模块文件的位置,可以是相对路径,也可以是绝对路径,`.js`后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
```javascript
import {myMethod} from 'util';
@ -262,6 +270,14 @@ import { foo, bar } from 'my_module';
上面代码中,虽然`foo``bar`在两个语句中加载,但是它们对应的是同一个`my_module`实例。也就是说,`import`语句是 Singleton 模式。
目前阶段,通过 Babel 转码CommonJS 模块的`require`命令和 ES6 模块的`import`命令,可以写在同一个模块里面,但是最好不要这样做。因为`import`在静态解析阶段执行,所以它是一个模块之中最早执行的。下面的代码可能不会得到预期结果。
```javascript
require('core-js/modules/es6.symbol');
require('core-js/modules/es6.promise');
import React from 'React';
```
## 模块的整体加载
除了指定加载某个输出值,还可以使用整体加载,即用星号(`*`)指定一个对象,所有输出值都加载在这个对象上面。
@ -300,6 +316,16 @@ console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
```
注意,模块整体加载所在的那个对象(上例是`circle`),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。
```javascript
import * as circle from './circle';
// 下面两行都是不允许的
circle.foo = 'hello';
circle.area = function () {};
```
## export default 命令
从前面的例子可以看出,使用`import`命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。
@ -378,9 +404,9 @@ export {add as default};
// export default add;
// app.js
import { default as xxx } from 'modules';
import { default as foo } from 'modules';
// 等同于
// import xxx from 'modules';
// import foo from 'modules';
```
正是因为`export default`命令其实只是输出一个叫做`default`的变量,所以它后面不能跟变量声明语句。
@ -399,16 +425,28 @@ export default var a = 1;
上面代码中,`export default a`的含义是将变量`a`的值赋给变量`default`。所以,最后一种写法会报错。
同样地,因为`export default`本质是将该命令后面的值,赋给`default`变量以后再默认,所以直接将一个值写在`export default`之后。
```javascript
// 正确
export default 42;
// 报错
export 42;
```
上面代码中,后一句报错是因为没有指定对外的接口,而前一句指定外对接口为`default`
有了`export default`命令,输入模块时就非常直观了,以输入 lodash 模块为例。
```javascript
import _ from 'lodash';
```
如果想在一条`import`语句中,同时输入默认方法和其他变量,可以写成下面这样。
如果想在一条`import`语句中,同时输入默认方法和其他接口,可以写成下面这样。
```javascript
import _, { each } from 'lodash';
import _, { each, each as forEach } from 'lodash';
```
对应上面代码的`export`语句如下。
@ -417,20 +455,16 @@ import _, { each } from 'lodash';
export default function (obj) {
// ···
}
export function each(obj, iterator, context) {
// ···
}
export { each as forEach };
```
上面代码的最后一行的意思是,暴露出`forEach`接口,默认指向`each`接口,即`forEach``each`指向同一个方法。
如果要输出默认的值,只需将值跟在`export default`之后即可。
```javascript
export default 42;
```
`export default`也可以用来输出类。
```javascript
@ -451,7 +485,7 @@ export { foo, bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
export { foo, boo};
export { foo, bar };
```
上面代码中,`export``import`语句可以结合在一起,写成一行。
@ -488,14 +522,20 @@ export default es6;
export { default as es6 } from './someModule';
```
另外ES7有一个[提案](https://github.com/leebyron/ecmascript-more-export-from),简化先输入后输出的写法,拿掉输出时的大括号
下面三种`import`语句,没有对应的复合写法
```javascript
// 现行的写法
export {v} from 'mod';
import * as someIdentifier from "someModule";
import someIdentifier from "someModule";
import someIdentifier, { namedIdentifier } from "someModule";
```
// 提案的写法
export v from 'mod';
为了做到形式的对称,现在有[提案](https://github.com/leebyron/ecmascript-export-default-from),提出补上这三种复合写法。
```javascript
export * as someIdentifier from "someModule";
export someIdentifier from "someModule";
export someIdentifier, { namedIdentifier } from "someModule";
```
## 模块的继承
@ -538,477 +578,9 @@ console.log(exp(math.e));
上面代码中的`import exp`表示,将`circleplus`模块的默认方法加载为`exp`方法。
## ES6模块加载的实质
ES6模块加载的机制与CommonJS模块完全不同。CommonJS模块输出的是一个值的拷贝而ES6模块输出的是值的引用。
CommonJS模块输出的是被输出值的拷贝也就是说一旦输出一个值模块内部的变化就影响不到这个值。请看下面这个模块文件`lib.js`的例子。
```javascript
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
```
上面代码输出内部变量`counter`和改写这个变量的内部方法`incCounter`。然后,在`main.js`里面加载这个模块。
```javascript
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
```
上面代码说明,`lib.js`模块加载以后,它的内部变化就影响不到输出的`mod.counter`了。这是因为`mod.counter`是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
```javascript
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
get counter() {
return counter
},
incCounter: incCounter,
};
```
上面代码中,输出的`counter`属性实际上是一个取值器函数。现在再执行`main.js`,就可以正确读取内部变量`counter`的变动了。
```bash
$ node main.js
3
4
```
ES6模块的运行机制与CommonJS不一样它遇到模块加载命令`import`不会去执行模块而是只生成一个动态的只读引用。等到真的需要用到时再到模块里面去取值换句话说ES6的输入有点像Unix系统的“符号连接”原始值变了`import`输入的值也会跟着变。因此ES6模块是动态引用并且不会缓存值模块里面的变量绑定其所在的模块。
还是举上面的例子。
```javascript
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
```
上面代码说明ES6模块输入的变量`counter`是活的,完全反应其所在模块`lib.js`内部的变化。
再举一个出现在`export`一节中的例子。
```javascript
// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);
```
上面代码中,`m1.js`的变量`foo`,在刚加载时等于`bar`过了500毫秒又变为等于`baz`
让我们看看,`m2.js`能否正确读取这个变化。
```bash
$ babel-node m2.js
bar
baz
```
上面代码表明ES6模块不会缓存运行结果而是动态地去被加载的模块取值并且变量总是绑定其所在的模块。
由于ES6输入的模块变量只是一个“符号连接”所以这个变量是只读的对它进行重新赋值会报错。
```javascript
// lib.js
export let obj = {};
// main.js
import { obj } from './lib';
obj.prop = 123; // OK
obj = {}; // TypeError
```
上面代码中,`main.js``lib.js`输入变量`obj`,可以对`obj`添加属性,但是重新赋值就会报错。因为变量`obj`指向的地址是只读的,不能重新赋值,这就好比`main.js`创造了一个名为`obj`的const变量。
最后,`export`通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。
```javascript
// mod.js
function C() {
this.sum = 0;
this.add = function () {
this.sum += 1;
};
this.show = function () {
console.log(this.sum);
};
}
export let c = new C();
```
上面的脚本`mod.js`,输出的是一个`C`的实例。不同的脚本加载这个模块,得到的都是同一个实例。
```javascript
// x.js
import {c} from './mod';
c.add();
// y.js
import {c} from './mod';
c.show();
// main.js
import './x';
import './y';
```
现在执行`main.js`输出的是1。
```bash
$ babel-node main.js
1
```
这就证明了`x.js``y.js`加载的都是`C`的同一个实例。
## 浏览器的模块加载
浏览器使用 ES6 模块的语法如下。
```html
<script type="module" src="foo.js"></script>
```
上面代码在网页中插入一个模块`foo.js`,由于`type`属性设为`module`,所以浏览器知道这是一个 ES6 模块。
浏览器对于带有`type="module"``<script>`,都是异步加载外部脚本,不会造成堵塞浏览器。
对于外部的模块脚本(上例是`foo.js`),有几点需要注意。
- 该脚本自动采用严格模块。
- 该脚本内部的顶层变量,都只在该脚本内部有效,外部不可见。
- 该脚本内部的顶层的`this`关键字,返回`undefined`,而不是指向`window`
## 循环加载
“循环加载”circular dependency指的是`a`脚本的执行依赖`b`脚本,而`b`脚本的执行又依赖`a`脚本。
```javascript
// a.js
var b = require('b');
// b.js
var a = require('a');
```
通常,“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。
但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目,很容易出现`a`依赖`b``b`依赖`c``c`又依赖`a`这样的情况。这意味着,模块加载机制必须考虑“循环加载”的情况。
对于JavaScript语言来说目前最常见的两种模块格式CommonJS和ES6处理“循环加载”的方法是不一样的返回的结果也不一样。
### CommonJS模块的加载原理
介绍ES6如何处理"循环加载"之前先介绍目前最流行的CommonJS模块格式的加载原理。
CommonJS的一个模块就是一个脚本文件。`require`命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。
```javascript
{
id: '...',
exports: { ... },
loaded: true,
...
}
```
上面代码就是Node内部加载模块后生成的一个对象。该对象的`id`属性是模块名,`exports`属性是模块输出的各个接口,`loaded`属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。
以后需要用到这个模块的时候,就会到`exports`属性上面取值。即使再次执行`require`命令也不会再次执行该模块而是到缓存之中取值。也就是说CommonJS模块无论加载多少次都只会在第一次加载时运行一次以后再加载就返回第一次运行的结果除非手动清除系统缓存。
### CommonJS模块的循环加载
CommonJS模块的重要特性是加载时执行即脚本代码在`require`的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
让我们来看Node[官方文档](https://nodejs.org/api/modules.html#modules_cycles)里面的例子。脚本文件`a.js`代码如下。
```javascript
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
```
上面代码之中,`a.js`脚本先输出一个`done`变量,然后加载另一个脚本文件`b.js`。注意,此时`a.js`代码就停在这里,等待`b.js`执行完毕,再往下执行。
再看`b.js`的代码。
```javascript
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
```
上面代码之中,`b.js`执行到第二行,就会去加载`a.js`,这时,就发生了“循环加载”。系统会去`a.js`模块对应对象的`exports`属性取值,可是因为`a.js`还没有执行完,从`exports`属性只能取回已经执行的部分,而不是最后的值。
`a.js`已经执行的部分,只有一行。
```javascript
exports.done = false;
```
因此,对于`b.js`来说,它从`a.js`只输入一个变量`done`,值为`false`
然后,`b.js`接着往下执行,等到全部执行完毕,再把执行权交还给`a.js`。于是,`a.js`接着往下执行,直到执行完毕。我们写一个脚本`main.js`,验证这个过程。
```javascript
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
```
执行`main.js`,运行结果如下。
```bash
$ node main.js
在 b.js 之中a.done = false
b.js 执行完毕
在 a.js 之中b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true
```
上面的代码证明了两件事。一是,在`b.js`之中,`a.js`没有执行完毕,只执行了第一行。二是,`main.js`执行到第二行时,不会再次执行`b.js`,而是输出缓存的`b.js`的执行结果,即它的第四行。
```javascript
exports.done = true;
```
总之CommonJS输入的是被输出值的拷贝不是引用。
另外由于CommonJS模块遇到循环加载时返回的是当前已经执行的部分的值而不是代码全部执行后的值两者可能会有差异。所以输入变量的时候必须非常小心。
```javascript
var a = require('a'); // 安全的写法
var foo = require('a').foo; // 危险的写法
exports.good = function (arg) {
return a.foo('good', arg); // 使用的是 a.foo 的最新值
};
exports.bad = function (arg) {
return foo('bad', arg); // 使用的是一个部分加载时的值
};
```
上面代码中,如果发生循环加载,`require('a').foo`的值很可能后面会被改写,改用`require('a')`会更保险一点。
### ES6模块的循环加载
ES6处理“循环加载”与CommonJS有本质的不同。ES6模块是动态引用如果使用`import`从一个模块加载变量(即`import foo from 'foo'`),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
请看下面这个例子。
```javascript
// a.js如下
import {bar} from './b.js';
console.log('a.js');
console.log(bar);
export let foo = 'foo';
// b.js
import {foo} from './a.js';
console.log('b.js');
console.log(foo);
export let bar = 'bar';
```
上面代码中,`a.js`加载`b.js``b.js`又加载`a.js`,构成循环加载。执行`a.js`,结果如下。
```bash
$ babel-node a.js
b.js
undefined
a.js
bar
```
上面代码中,由于`a.js`的第一行是加载`b.js`,所以先执行的是`b.js`。而`b.js`的第一行又是加载`a.js`,这时由于`a.js`已经开始执行了,所以不会重复执行,而是继续往下执行`b.js`,所以第一行输出的是`b.js`
接着,`b.js`要打印变量`foo`,这时`a.js`还没执行完,取不到`foo`的值,导致打印出来是`undefined``b.js`执行完,开始执行`a.js`,这时就一切正常了。
再看一个稍微复杂的例子(摘自 Dr. Axel Rauschmayer 的[《Exploring ES6》](http://exploringjs.com/es6/ch_modules.html))。
```javascript
// a.js
import {bar} from './b.js';
export function foo() {
console.log('foo');
bar();
console.log('执行完毕');
}
foo();
// b.js
import {foo} from './a.js';
export function bar() {
console.log('bar');
if (Math.random() > 0.5) {
foo();
}
}
```
按照CommonJS规范上面的代码是没法执行的。`a`先加载`b`,然后`b`又加载`a`,这时`a`还没有任何执行结果,所以输出结果为`null`,即对于`b.js`来说,变量`foo`的值等于`null`,后面的`foo()`就会报错。
但是ES6可以执行上面的代码。
```bash
$ babel-node a.js
foo
bar
执行完毕
// 执行结果也有可能是
foo
bar
foo
bar
执行完毕
执行完毕
```
上面代码中,`a.js`之所以能够执行原因就在于ES6加载的变量都是动态引用其所在的模块。只要引用存在代码就能执行。
下面,我们详细分析这段代码的运行过程。
```javascript
// a.js
// 这一行建立一个引用,
// 从`b.js`引用`bar`
import {bar} from './b.js';
export function foo() {
// 执行时第一行输出 foo
console.log('foo');
// 到 b.js 执行 bar
bar();
console.log('执行完毕');
}
foo();
// b.js
// 建立`a.js``foo`引用
import {foo} from './a.js';
export function bar() {
// 执行时,第二行输出 bar
console.log('bar');
// 递归执行 foo一旦随机数
// 小于等于0.5,就停止执行
if (Math.random() > 0.5) {
foo();
}
}
```
我们再来看ES6模块加载器[SystemJS](https://github.com/ModuleLoader/es6-module-loader/blob/master/docs/circular-references-bindings.md)给出的一个例子。
```javascript
// even.js
import { odd } from './odd'
export var counter = 0;
export function even(n) {
counter++;
return n == 0 || odd(n - 1);
}
// odd.js
import { even } from './even';
export function odd(n) {
return n != 0 && even(n - 1);
}
```
上面代码中,`even.js`里面的函数`even`有一个参数`n`只要不等于0就会减去1传入加载的`odd()``odd.js`也会做类似操作。
运行上面这段代码,结果如下。
```javascript
$ babel-node
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
> m.even(20)
true
> m.counter
17
```
上面代码中,参数`n`从10变为0的过程中`even()`一共会执行6次所以变量`counter`等于6。第二次调用`even()`时,参数`n`从20变为0`even()`一共会执行11次加上前面的6次所以变量`counter`等于17。
这个例子要是改写成CommonJS就根本无法执行会报错。
```javascript
// even.js
var odd = require('./odd');
var counter = 0;
exports.counter = counter;
exports.even = function(n) {
counter++;
return n == 0 || odd(n - 1);
}
// odd.js
var even = require('./even').even;
module.exports = function(n) {
return n != 0 && even(n - 1);
}
```
上面代码中,`even.js`加载`odd.js`,而`odd.js`又去加载`even.js`,形成“循环加载”。这时,执行引擎就会输出`even.js`已经执行的部分(不存在任何结果),所以在`odd.js`之中,变量`even`等于`null`,等到后面调用`even(n-1)`就会报错。
```bash
$ node
> var m = require('./even');
> m.even(10)
TypeError: even is not a function
```
## 跨模块常量
本书介绍`const`命令的时候说过,`const`声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),可以采用下面的写法。
本书介绍`const`命令的时候说过,`const`声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法。
```javascript
// constants.js 模块
@ -1053,12 +625,14 @@ export {users} from './users';
```javascript
// script.js
import {db, users} from './constants';
import {db, users} from './index';
```
## import()
上面说过了,`import`语句会被JavaScript引擎静态分析先于模块内的其他模块执行叫做”连接“更合适。所以下面的代码会报错。
### 简介
前面介绍过,`import`命令会被 JavaScript 引擎静态分析,先于模块内的其他模块执行(叫做”连接“更合适)。所以,下面的代码会报错。
```javascript
// 报错
@ -1067,9 +641,9 @@ if (x === 2) {
}
```
上面代码中,引擎处理`import`语句是在执行之前,所以`import`语句放在`if`代码块之中毫无意义,因此会报句法错误,而不是执行时错误。
上面代码中,引擎处理`import`语句是在编译时,这时不会去分析或执行`if`语句,所以`import`语句放在`if`代码块之中毫无意义,因此会报句法错误,而不是执行时错误。也就是说,`import``export`命令只能在模块的顶层,不能在代码块之中(比如,在`if`代码块之中,或在函数之中)。
这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。从长远来看,`import`语句会取代 Node 的`require`方法,但是`require`是运行时加载模块,`import`语句显然无法取代这种动态加载功能。
这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。在语法上,条件加载就不可能实现。如果`import`命令要取代 Node 的`require`方法,这就形成了一个障碍。因为`require`是运行时加载模块,`import`命令无法取代`require`动态加载功能。
```javascript
const path = './' + fileName;
@ -1084,7 +658,7 @@ const myModual = require(path);
import(specifier)
```
上面代码中,`import`函数的参数`specifier`,指定所要加载的模块的位置。`import`语句能够接受什么参数,`import()`函数就能接受什么参数,两者区别主要是后者为动态加载。
上面代码中,`import`函数的参数`specifier`,指定所要加载的模块的位置。`import`命令能够接受什么参数,`import()`函数就能接受什么参数,两者区别主要是后者为动态加载。
`import()`返回一个 Promise 对象。下面是一个例子。
@ -1104,75 +678,109 @@ import(`./section-modules/${someVariable}.js`)
`import()`类似于 Node 的`require`方法,区别主要是前者是异步加载,后者是同步加载。
## ES6模块的转码
### 适用场合
浏览器目前还不支持ES6模块为了现在就能使用可以将转为ES5的写法。除了Babel可以用来转码之外还有以下两个方法也可以用来转码
下面是`import()`的一些适用场合
### ES6 module transpiler
1按需加载。
[ES6 module transpiler](https://github.com/esnext/es6-module-transpiler)是 square 公司开源的一个转码器,可以将 ES6 模块转为 CommonJS 模块或 AMD 模块的写法,从而在浏览器中使用。
首先,安装这个转玛器。
```bash
$ npm install -g es6-module-transpiler
```
然后,使用`compile-modules convert`命令,将 ES6 模块文件转码。
```bash
$ compile-modules convert file1.js file2.js
```
`-o`参数可以指定转码后的文件名。
```bash
$ compile-modules convert -o out.js file1.js
```
### SystemJS
另一种解决方法是使用 [SystemJS](https://github.com/systemjs/systemjs)。它是一个垫片库polyfill可以在浏览器内加载 ES6 模块、AMD 模块和 CommonJS 模块,将其转为 ES5 格式。它在后台调用的是 Google 的 Traceur 转码器。
使用时,先在网页内载入`system.js`文件。
```html
<script src="system.js"></script>
```
然后,使用`System.import`方法加载模块文件。
```html
<script>
System.import('./app.js');
</script>
```
上面代码中的`./app`指的是当前目录下的app.js文件。它可以是ES6模块文件`System.import`会自动将其转码。
需要注意的是,`System.import`使用异步加载,返回一个 Promise 对象,可以针对这个对象编程。下面是一个模块文件。
`import()`可以在需要的时候,再加载某个模块。
```javascript
// app/es6-file.js:
export class q {
constructor() {
this.es6 = 'hello';
}
}
```
然后,在网页内加载这个模块文件。
```html
<script>
System.import('app/es6-file').then(function(m) {
console.log(new m.q().es6); // hello
button.addEventListener('click', event => {
import('./dialogBox.js')
.then(dialogBox => {
dialogBox.open();
})
.catch(error => {
/* Error handling */
})
});
</script>
```
上面代码中,`System.import`方法返回的是一个 Promise 对象,所以可以用`then`方法指定回调函数
上面代码中,`import()`方法放在`click`事件的监听函数之中,只有用户点击了按钮,才会加载这个模块。
2条件加载
`import()`可以放在`if`代码块,根据不同的情况,加载不同的模块。
```javascript
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
```
上面代码中,如果满足条件,就加载模块 A否则加载模块 B。
3动态的模块路径
`import()`允许模块路径动态生成。
```javascript
import(f())
.then(...);
```
上面代码中,根据函数`f`的返回结果,加载不同的模块。
### 注意点
`import()`加载模块成功以后,这个模块会作为一个对象,当作`then`方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。
```javascript
import('./myModule.js')
.then(({export1, export2}) => {
// ...·
});
```
上面代码中,`export1``export2`都是`myModule.js`的输出接口,可以解构获得。
如果模块有`default`输出接口,可以用参数直接获得。
```javascript
import('./myModule.js')
.then(myModule => {
console.log(myModule.default);
});
```
上面的代码也可以使用具名输入的形式。
```javascript
import('./myModule.js')
.then(({default: theDefault}) => {
console.log(theDefault);
});
```
如果想同时加载多个模块,可以采用下面的写法。
```javascript
Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
])
.then(([module1, module2, module3]) => {
···
});
```
`import()`也可以用在 async 函数之中。
```javascript
async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');
const [module1, module2, module3] =
await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
]);
}
main();
```

View File

@ -33,7 +33,7 @@ Number('0o10') // 8
## Number.isFinite(), Number.isNaN()
ES6在Number对象上新提供了`Number.isFinite()``Number.isNaN()`两个方法。
ES6 `Number`对象上,新提供了`Number.isFinite()``Number.isNaN()`两个方法。
`Number.isFinite()`用来检查一个数值是否为有限的finite
@ -94,7 +94,7 @@ ES5通过下面的代码部署`Number.isNaN()`。
})(this);
```
它们与传统的全局方法`isFinite()``isNaN()`的区别在于,传统方法先调用`Number()`将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,非数值一律返回`false`
它们与传统的全局方法`isFinite()``isNaN()`的区别在于,传统方法先调用`Number()`将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,`Number.isFinite()`对于非数值一律返回`false`, `Number.isNaN()`只有对于`NaN`才返回`true`,非`NaN`一律返回`false`
```javascript
isFinite(25) // true
@ -106,11 +106,12 @@ isNaN(NaN) // true
isNaN("NaN") // true
Number.isNaN(NaN) // true
Number.isNaN("NaN") // false
Number.isNaN(1) // false
```
## Number.parseInt(), Number.parseFloat()
ES6将全局方法`parseInt()``parseFloat()`移植到Number对象上面行为完全保持不变。
ES6 将全局方法`parseInt()``parseFloat()`,移植到`Number`对象上面,行为完全保持不变。
```javascript
// ES5的写法
@ -150,8 +151,8 @@ ES5可以通过下面的代码部署`Number.isInteger()`。
Object.defineProperty(Number, 'isInteger', {
value: function isInteger(value) {
return typeof value === 'number' && isFinite(value) &&
value > -9007199254740992 && value < 9007199254740992 &&
return typeof value === 'number' &&
isFinite(value) &&
floor(value) === value;
},
configurable: true,
@ -163,15 +164,21 @@ ES5可以通过下面的代码部署`Number.isInteger()`。
## Number.EPSILON
ES6在Number对象上面新增一个极小的常量`Number.EPSILON`
ES6 在`Number`对象上面,新增一个极小的常量`Number.EPSILON`。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。
对于 64 位浮点数来说,大于 1 的最小浮点数相当于二进制的`1.00..001`,小数点后面有连续 51 个零。这个值减去 1 之后,就等于 2 的-52 次方。
```javascript
Number.EPSILON === Math.pow(2, -52)
// true
Number.EPSILON
// 2.220446049250313e-16
Number.EPSILON.toFixed(20)
// '0.00000000000000022204'
// "0.00000000000000022204"
```
`Number.EPSILON`实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。
引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。
```javascript
@ -185,23 +192,31 @@ Number.EPSILON.toFixed(20)
// '0.00000000000000005551'
```
但是如果这个误差能够小于`Number.EPSILON`,我们就可以认为得到了正确结果
上面代码解释了,为什么比较`0.1 + 0.2``0.3`得到的结果是`false`
```javascript
5.551115123125783e-17 < Number.EPSILON
0.1 + 0.2 === 0.3 // false
```
`Number.EPSILON`可以用来设置“能够接受的误差范围”。比如,误差范围设为 2 的-50 次方(即`Number.EPSILON * Math.pow(2, 2)`),即如果两个浮点数的差小于这个值,我们就认为这两个浮点数相等。
```javascript
5.551115123125783e-17 < Number.EPSILON * Math.pow(2, 2)
// true
```
因此,`Number.EPSILON`的实质是一个可以接受的误差范围。
因此,`Number.EPSILON`的实质是一个可以接受的最小误差范围。
```javascript
function withinErrorMargin (left, right) {
return Math.abs(left - right) < Number.EPSILON;
return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
}
withinErrorMargin(0.1 + 0.2, 0.3)
// true
withinErrorMargin(0.2 + 0.2, 0.3)
// false
0.1 + 0.2 === 0.3 // false
withinErrorMargin(0.1 + 0.2, 0.3) // true
1.1 + 1.3 === 2.4 // false
withinErrorMargin(1.1 + 1.3, 2.4) // true
```
上面的代码为浮点数运算,部署了一个误差检查函数。
@ -330,16 +345,19 @@ Math.trunc(-0.1234) // -0
对于非数值,`Math.trunc`内部使用`Number`方法将其先转为数值。
```javascript
Math.trunc('123.456')
// 123
Math.trunc('123.456') // 123
Math.trunc(true) //1
Math.trunc(false) // 0
Math.trunc(null) // 0
```
对于空值和无法截取整数的值返回NaN。
对于空值和无法截取整数的值,返回`NaN`
```javascript
Math.trunc(NaN); // NaN
Math.trunc('foo'); // NaN
Math.trunc(); // NaN
Math.trunc(undefined) // NaN
```
对于没有部署这个方法的环境,可以用下面的代码模拟。
@ -352,15 +370,15 @@ Math.trunc = Math.trunc || function(x) {
### Math.sign()
`Math.sign`方法用来判断一个数到底是正数、负数、还是零。
`Math.sign`方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。
它会返回五种值。
- 参数为正数,返回+1
- 参数为负数,返回-1
- 参数为0返回0
- 参数为-0返回-0;
- 其他值返回NaN。
- 参数为正数,返回`+1`
- 参数为负数,返回`-1`
- 参数为 0返回`0`
- 参数为-0返回`-0`;
- 其他值,返回`NaN`
```javascript
Math.sign(-5) // -1
@ -368,8 +386,19 @@ Math.sign(5) // +1
Math.sign(0) // +0
Math.sign(-0) // -0
Math.sign(NaN) // NaN
Math.sign('foo'); // NaN
Math.sign(); // NaN
```
如果参数是非数值,会自动转为数值。对于那些无法转为数值的值,会返回`NaN`
```javascript
Math.sign('') // 0
Math.sign(true) // +1
Math.sign(false) // 0
Math.sign(null) // 0
Math.sign('9') // +1
Math.sign('foo') // NaN
Math.sign() // NaN
Math.sign(undefined) // NaN
```
对于没有部署这个方法的环境,可以用下面的代码模拟。
@ -425,7 +454,7 @@ Math.clz32(0b00100000000000000000000000000000) // 2
上面代码中0 的二进制形式全为 0所以有 32 个前导 01 的二进制形式是`0b1`,只占 1 位,所以 32 位之中有 31 个前导 01000 的二进制形式是`0b1111101000`,一共有 10 位,所以 32 位之中有 22 个前导 0。
`clz32`这个函数名就来自”count leading zero bits in 32-bit binary representations of a number“计算32位整数的前导0)的缩写。
`clz32`这个函数名就来自”count leading zero bits in 32-bit binary representation of a number“计算一个数的 32 位二进制形式的前导 0 的个数)的缩写。
左移运算符(`<<`)与`Math.clz32`方法直接相关。
@ -602,9 +631,9 @@ Math.log2 = Math.log2 || function(x) {
};
```
### 三角函数方法
### 双曲函数方法
ES6新增了6个三角函数方法。
ES6 新增了 6 个双曲函数方法。
- `Math.sinh(x)` 返回`x`的双曲正弦hyperbolic sine
- `Math.cosh(x)` 返回`x`的双曲余弦hyperbolic cosine
@ -613,9 +642,41 @@ ES6新增了6个三角函数方法。
- `Math.acosh(x)` 返回`x`的反双曲余弦inverse hyperbolic cosine
- `Math.atanh(x)` 返回`x`的反双曲正切inverse hyperbolic tangent
## Math.signbit()
`Math.sign()`用来判断一个值的正负,但是如果参数是`-0`,它会返回`-0`
```javascript
Math.sign(-0) // -0
```
这导致对于判断符号位的正负,`Math.sign()`不是很有用。JavaScript 内部使用 64 位浮点数(国际标准 IEEE 754表示数值IEEE 754 规定第一位是符号位,`0`表示正数,`1`表示负数。所以会有两种零,`+0`是符号位为`0`时的零值,`-0`是符号位为`1`时的零值。实际编程中,判断一个值是`+0`还是`-0`非常麻烦,因为它们是相等的。
```javascript
+0 === -0 // true
```
目前,有一个[提案](http://jfbastien.github.io/papers/Math.signbit.html),引入了`Math.signbit()`方法判断一个数的符号位是否设置了。
```javascript
Math.signbit(2) //false
Math.signbit(-2) //true
Math.signbit(0) //false
Math.signbit(-0) //true
```
可以看到,该方法正确返回了`-0`的符号位是设置了的。
该方法的算法如下。
- 如果参数是`NaN`,返回`false`
- 如果参数是`-0`,返回`true`
- 如果参数是负值,返回`true`
- 其他情况返回`false`
## 指数运算符
ES7新增了一个指数运算符`**`目前Babel转码器已经支持。
ES2016 新增了一个指数运算符(`**`
```javascript
2 ** 2 // 4
@ -625,11 +686,108 @@ ES7新增了一个指数运算符`**`目前Babel转码器已经支持
指数运算符可以与等号结合,形成一个新的赋值运算符(`**=`)。
```javascript
let a = 2;
let a = 1.5;
a **= 2;
// 等同于 a = a * a;
let b = 3;
let b = 4;
b **= 3;
// 等同于 b = b * b * b;
```
注意,在 V8 引擎中,指数运算符与`Math.pow`的实现不相同,对于特别大的运算结果,两者会有细微的差异。
```javascript
Math.pow(99, 99)
// 3.697296376497263e+197
99 ** 99
// 3.697296376497268e+197
```
上面代码中,两个运算结果的最后一位有效数字是有差异的。
## Integer 数据类型
### 简介
JavaScript 所有数字都保存成 64 位浮点数,这决定了整数的精确程度只能到 53 个二进制位。大于这个范围的整数JavaScript 是无法精确表示的,这使得 JavaScript 不适合进行科学和金融方面的精确计算。
现在有一个[提案](https://github.com/tc39/proposal-bigint),引入了新的数据类型 Integer整数来解决这个问题。整数类型的数据只用来表示整数没有位数的限制任何位数的整数都可以精确表示。
为了与 Number 类型区别Integer 类型的数据必须使用后缀`n`表示。
```javascript
1n + 2n // 3n
```
二进制、八进制、十六进制的表示法,都要加上后缀`n`
```javascript
0b1101n // 二进制
0o777n // 八进制
0xFFn // 十六进制
```
`typeof`运算符对于 Integer 类型的数据返回`integer`
```javascript
typeof 123n
// 'integer'
```
JavaScript 原生提供`Integer`对象,用来生成 Integer 类型的数值。转换规则基本与`Number()`一致。
```javascript
Integer(123) // 123n
Integer('123') // 123n
Integer(false) // 0n
Integer(true) // 1n
```
以下的用法会报错。
```javascript
new Integer() // TypeError
Integer(undefined) //TypeError
Integer(null) // TypeError
Integer('123n') // SyntaxError
Integer('abc') // SyntaxError
```
### 运算
在数学运算方面Integer 类型的`+``-``*``**`这四个二元运算符,与 Number 类型的行为一致。除法运算`/`会舍去小数部分,返回一个整数。
```javascript
9n / 5n
// 1n
```
几乎所有的 Number 运算符都可以用在 Integer但是有两个除外不带符号的右移位运算符`>>>`和一元的求正运算符`+`,使用时会报错。前者是因为`>>>`要求最高位补 0但是 Integer 类型没有最高位,导致这个运算符无意义。后者是因为一元运算符`+`在 asm.js 里面总是返回 Number 类型或者报错。
Integer 类型不能与 Number 类型进行混合运算。
```javascript
1n + 1
// 报错
```
这是因为无论返回的是 Integer 或 Number都会导致丢失信息。比如`(2n**53n + 1n) + 0.5`这个表达式,如果返回 Integer 类型,`0.5`这个小数部分会丢失;如果返回 Number 类型,会超过 53 位精确数字,精度下降。
相等运算符(`==`)会改变数据类型,也是不允许混合使用。
```javascript
0n == 0
// 报错 TypeError
0n == false
// 报错 TypeError
```
精确相等运算符(`===`)不会改变数据类型,因此可以混合使用。
```javascript
0n === 0
// false
```

File diff suppressed because it is too large Load Diff

View File

@ -8,24 +8,26 @@ Promise是异步编程的一种解决方案比传统的解决方案——回
`Promise`对象有以下两个特点。
1对象的状态不受外界影响。`Promise`对象代表一个异步操作,有三种状态:`Pending`(进行中)、`Resolved`已完成又称Fulfilled`Rejected`(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是`Promise`这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
1对象的状态不受外界影响。`Promise`对象代表一个异步操作,有三种状态:`pending`(进行中)、`fulfilled`(已成功)和`rejected`(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是`Promise`这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
2一旦状态改变就不会再变任何时候都可以得到这个结果。`Promise`对象的状态改变,只有两种可能:从`Pending`变为`Resolved`和从`Pending`变为`Rejected`。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对`Promise`对象添加回调函数也会立即得到这个结果。这与事件Event完全不同事件的特点是如果你错过了它再去监听是得不到结果的。
2一旦状态改变就不会再变任何时候都可以得到这个结果。`Promise`对象的状态改变,只有两种可能:从`pending`变为`fulfilled`和从`pending`变为`rejected`。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved已定型。如果改变已经发生了你再对`Promise`对象添加回调函数也会立即得到这个结果。这与事件Event完全不同事件的特点是如果你错过了它再去监听是得不到结果的。
注意,为了行文方便,本章后面的`resolved`统一只指`fulfilled`状态,不包含`rejected`状态。
有了`Promise`对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,`Promise`对象提供统一的接口,使得控制异步操作更加容易。
`Promise`也有一些缺点。首先,无法取消`Promise`,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,`Promise`内部抛出的错误,不会反应到外部。第三,当处于`Pending`状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
`Promise`也有一些缺点。首先,无法取消`Promise`,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,`Promise`内部抛出的错误,不会反应到外部。第三,当处于`pending`状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
如果某些事件不断地反复发生一般来说使用stream模式是比部署`Promise`更好的选择。
如果某些事件不断地反复发生,一般来说,使用 [Stream](https://nodejs.org/api/stream.html) 模式是比部署`Promise`更好的选择。
## 基本用法
ES6规定Promise对象是一个构造函数用来生成Promise实例。
ES6 规定,`Promise`对象是一个构造函数,用来生成`Promise`实例。
下面代码创造了一个Promise实例。
下面代码创造了一个`Promise`实例。
```javascript
var promise = new Promise(function(resolve, reject) {
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
@ -36,11 +38,11 @@ var promise = new Promise(function(resolve, reject) {
});
```
Promise构造函数接受一个函数作为参数该函数的两个参数分别是`resolve``reject`。它们是两个函数由JavaScript引擎提供不用自己部署。
`Promise`构造函数接受一个函数作为参数,该函数的两个参数分别是`resolve``reject`。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
`resolve`函数的作用是将Promise对象的状态从“未完成”变为“成功”即从Pending变为Resolved在异步操作成功时调用并将异步操作的结果作为参数传递出去`reject`函数的作用是将Promise对象的状态从“未完成”变为“失败”即从Pending变为Rejected在异步操作失败时调用并将异步操作报出的错误作为参数传递出去。
`resolve`函数的作用是,将`Promise`对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved在异步操作成功时调用并将异步操作的结果作为参数传递出去`reject`函数的作用是,将`Promise`对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected在异步操作失败时调用并将异步操作报出的错误作为参数传递出去。
Promise实例生成以后可以用`then`方法分别指定`Resolved`状态和`Reject`状态的回调函数。
`Promise`实例生成以后,可以用`then`方法分别指定`resolved`状态和`rejected`状态的回调函数。
```javascript
promise.then(function(value) {
@ -50,9 +52,9 @@ promise.then(function(value) {
});
```
`then`方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为Resolved时调用第二个回调函数是Promise对象的状态变为Reject时调用。其中第二个函数是可选的不一定要提供。这两个函数都接受Promise对象传出的值作为参数。
`then`方法可以接受两个回调函数作为参数。第一个回调函数是`Promise`对象的状态变为`resolved`时调用,第二个回调函数是`Promise`对象的状态变为`rejected`时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受`Promise`对象传出的值作为参数。
下面是一个Promise对象的简单例子。
下面是一个`Promise`对象的简单例子。
```javascript
function timeout(ms) {
@ -66,7 +68,7 @@ timeout(100).then((value) => {
});
```
上面代码中,`timeout`方法返回一个Promise实例表示一段时间以后才会发生的结果。过了指定的时间`ms`参数)以后,Promise实例的状态变为Resolved,就会触发`then`方法绑定的回调函数。
上面代码中,`timeout`方法返回一个`Promise`实例,表示一段时间以后才会发生的结果。过了指定的时间(`ms`参数)以后,`Promise`实例的状态变为`resolved`,就会触发`then`方法绑定的回调函数。
Promise 新建后就会立即执行。
@ -77,24 +79,24 @@ let promise = new Promise(function(resolve, reject) {
});
promise.then(function() {
console.log('Resolved.');
console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// Resolved
// resolved
```
上面代码中Promise新建后立即执行所以首先输出的是“Promise”。然后,`then`方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以“Resolved”最后输出。
上面代码中Promise 新建后立即执行,所以首先输出的是`Promise`。然后,`then`方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以`resolved`最后输出。
下面是异步加载图片的例子。
```javascript
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
var image = new Image();
const image = new Image();
image.onload = function() {
resolve(image);
@ -109,21 +111,14 @@ function loadImageAsync(url) {
}
```
上面代码中使用Promise包装了一个图片加载的异步操作。如果加载成功就调用`resolve`方法,否则就调用`reject`方法。
上面代码中,使用`Promise`包装了一个图片加载的异步操作。如果加载成功,就调用`resolve`方法,否则就调用`reject`方法。
下面是一个用Promise对象实现的Ajax操作的例子。
下面是一个用`Promise`对象实现的 Ajax 操作的例子。
```javascript
var getJSON = function(url) {
var promise = new Promise(function(resolve, reject){
var client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
function handler() {
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
@ -133,6 +128,13 @@ var getJSON = function(url) {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return promise;
@ -145,16 +147,16 @@ getJSON("/posts.json").then(function(json) {
});
```
上面代码中,`getJSON`是对XMLHttpRequest对象的封装用于发出一个针对JSON数据的HTTP请求并且返回一个Promise对象。需要注意的是`getJSON`内部,`resolve`函数和`reject`函数调用时,都带有参数。
上面代码中,`getJSON`是对 XMLHttpRequest 对象的封装,用于发出一个针对 JSON 数据的 HTTP 请求,并且返回一个`Promise`对象。需要注意的是,在`getJSON`内部,`resolve`函数和`reject`函数调用时,都带有参数。
如果调用`resolve`函数和`reject`函数时带有参数,那么它们的参数会被传递给回调函数。`reject`函数的参数通常是Error对象的实例表示抛出的错误`resolve`函数的参数除了正常的值以外还可能是另一个Promise实例,表示异步操作的结果有可能是一个值,也有可能是另一个异步操作,比如像下面这样。
如果调用`resolve`函数和`reject`函数时带有参数,那么它们的参数会被传递给回调函数。`reject`函数的参数通常是`Error`对象的实例,表示抛出的错误;`resolve`函数的参数除了正常的值以外,还可能是另一个 Promise 实例,比如像下面这样。
```javascript
var p1 = new Promise(function (resolve, reject) {
const p1 = new Promise(function (resolve, reject) {
// ...
});
var p2 = new Promise(function (resolve, reject) {
const p2 = new Promise(function (resolve, reject) {
// ...
resolve(p1);
})
@ -162,14 +164,14 @@ var p2 = new Promise(function (resolve, reject) {
上面代码中,`p1``p2`都是 Promise 的实例,但是`p2``resolve`方法将`p1`作为参数,即一个异步操作的结果是返回另一个异步操作。
注意,这时`p1`的状态就会传递给`p2`,也就是说,`p1`的状态决定了`p2`的状态。如果`p1`的状态是`Pending`,那么`p2`的回调函数就会等待`p1`的状态改变;如果`p1`的状态已经是`Resolved`或者`Rejected`,那么`p2`的回调函数将会立刻执行。
注意,这时`p1`的状态就会传递给`p2`,也就是说,`p1`的状态决定了`p2`的状态。如果`p1`的状态是`pending`,那么`p2`的回调函数就会等待`p1`的状态改变;如果`p1`的状态已经是`resolved`或者`rejected`,那么`p2`的回调函数将会立刻执行。
```javascript
var p1 = new Promise(function (resolve, reject) {
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
var p2 = new Promise(function (resolve, reject) {
const p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
@ -179,13 +181,38 @@ p2
// Error: fail
```
上面代码中,`p1`是一个Promise3秒之后变为`rejected``p2`的状态在1秒之后改变`resolve`方法返回的是`p1`。此时,由于`p2`返回的是另一个Promise所以后面的`then`语句都变成针对后者(`p1`。又过了2秒`p1`变为`rejected`,导致触发`catch`方法指定的回调函数。
上面代码中,`p1`是一个 Promise3 秒之后变为`rejected``p2`的状态在 1 秒之后改变,`resolve`方法返回的是`p1`。由于`p2`返回的是另一个 Promise导致`p2`自己的状态无效了,由`p1`的状态决定`p2`的状态。所以,后面的`then`语句都变成针对后者(`p1`)。又过了 2 秒,`p1`变为`rejected`,导致触发`catch`方法指定的回调函数。
注意,调用`resolve``reject`并不会终结 Promise 的参数函数的执行。
```javascript
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
```
上面代码中,调用`resolve(1)`以后,后面的`console.log(2)`还是会执行,并且会首先打印出来。这是因为立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。
一般来说,调用`resolve``reject`以后Promise 的使命就完成了,后继操作应该放到`then`方法里面,而不应该直接写在`resolve``reject`的后面。所以,最好在它们前面加上`return`语句,这样就不会有意外。
```javascript
new Promise((resolve, reject) => {
return resolve(1);
// 后面的语句不会执行
console.log(2);
})
```
## Promise.prototype.then()
Promise实例具有`then`方法,也就是说,`then`方法是定义在原型对象Promise.prototype上的。它的作用是为Promise实例添加状态改变时的回调函数。前面说过`then`方法的第一个参数是Resolved状态的回调函数第二个参数可选是Rejected状态的回调函数。
Promise 实例具有`then`方法,也就是说,`then`方法是定义在原型对象`Promise.prototype`上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,`then`方法的第一个参数是`resolved`状态的回调函数,第二个参数(可选)是`rejected`状态的回调函数。
`then`方法返回的是一个新的Promise实例注意不是原来那个Promise实例。因此可以采用链式写法`then`方法后面再调用另一个`then`方法。
`then`方法返回的是一个新的`Promise`实例(注意,不是原来那个`Promise`实例)。因此可以采用链式写法,即`then`方法后面再调用另一个`then`方法。
```javascript
getJSON("/posts.json").then(function(json) {
@ -197,19 +224,19 @@ getJSON("/posts.json").then(function(json) {
上面的代码使用`then`方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。
采用链式的`then`可以指定一组按照次序调用的回调函数。这时前一个回调函数有可能返回的还是一个Promise对象即有异步操作这时后一个回调函数就会等待该Promise对象的状态发生变化才会被调用。
采用链式的`then`,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个`Promise`对象(即有异步操作),这时后一个回调函数,就会等待该`Promise`对象的状态发生变化,才会被调用。
```javascript
getJSON("/post/1.json").then(function(post) {
return getJSON(post.commentURL);
}).then(function funcA(comments) {
console.log("Resolved: ", comments);
console.log("resolved: ", comments);
}, function funcB(err){
console.log("Rejected: ", err);
console.log("rejected: ", err);
});
```
上面代码中,第一个`then`方法指定的回调函数返回的是另一个Promise对象。这时第二个`then`方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为Resolved就调用`funcA`如果状态变为Rejected,就调用`funcB`
上面代码中,第一个`then`方法指定的回调函数,返回的是另一个`Promise`对象。这时,第二个`then`方法指定的回调函数,就会等待这个新的`Promise`对象状态发生变化。如果变为`resolved`,就调用`funcA`,如果状态变为`rejected`,就调用`funcB`
如果采用箭头函数,上面的代码可以写得更简洁。
@ -217,8 +244,8 @@ getJSON("/post/1.json").then(function(post) {
getJSON("/post/1.json").then(
post => getJSON(post.commentURL)
).then(
comments => console.log("Resolved: ", comments),
err => console.log("Rejected: ", err)
comments => console.log("resolved: ", comments),
err => console.log("rejected: ", err)
);
```
@ -227,7 +254,7 @@ getJSON("/post/1.json").then(
`Promise.prototype.catch`方法是`.then(null, rejection)`的别名,用于指定发生错误时的回调函数。
```javascript
getJSON("/posts.json").then(function(posts) {
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
@ -235,21 +262,21 @@ getJSON("/posts.json").then(function(posts) {
});
```
上面代码中,`getJSON`方法返回一个Promise对象如果该对象状态变为`Resolved`,则会调用`then`方法指定的回调函数;如果异步操作抛出错误,状态就会变为`Rejected`,就会调用`catch`方法指定的回调函数,处理这个错误。另外,`then`方法指定的回调函数,如果运行中抛出错误,也会被`catch`方法捕获。
上面代码中,`getJSON`方法返回一个 Promise 对象,如果该对象状态变为`resolved`,则会调用`then`方法指定的回调函数;如果异步操作抛出错误,状态就会变为`rejected`,就会调用`catch`方法指定的回调函数,处理这个错误。另外,`then`方法指定的回调函数,如果运行中抛出错误,也会被`catch`方法捕获。
```javascript
p.then((val) => console.log("fulfilled:", val))
.catch((err) => console.log("rejected:", err));
p.then((val) => console.log('fulfilled:', val))
.catch((err) => console.log('rejected', err));
// 等同于
p.then((val) => console.log("fulfilled:", val))
p.then((val) => console.log('fulfilled:', val))
.then(null, (err) => console.log("rejected:", err));
```
下面是一个例子。
```javascript
var promise = new Promise(function(resolve, reject) {
const promise = new Promise(function(resolve, reject) {
throw new Error('test');
});
promise.catch(function(error) {
@ -262,7 +289,7 @@ promise.catch(function(error) {
```javascript
// 写法一
var promise = new Promise(function(resolve, reject) {
const promise = new Promise(function(resolve, reject) {
try {
throw new Error('test');
} catch(e) {
@ -274,7 +301,7 @@ promise.catch(function(error) {
});
// 写法二
var promise = new Promise(function(resolve, reject) {
const promise = new Promise(function(resolve, reject) {
reject(new Error('test'));
});
promise.catch(function(error) {
@ -284,10 +311,10 @@ promise.catch(function(error) {
比较上面两种写法,可以发现`reject`方法的作用,等同于抛出错误。
如果Promise状态已经变成`Resolved`,再抛出错误是无效的。
如果 Promise 状态已经变成`resolved`,再抛出错误是无效的。
```javascript
var promise = new Promise(function(resolve, reject) {
const promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
@ -297,12 +324,12 @@ promise
// ok
```
上面代码中Promise在`resolve`语句后面,再抛出错误,不会被捕获,等于没有抛出。
上面代码中Promise `resolve`语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个`catch`语句捕获。
```javascript
getJSON("/post/1.json").then(function(post) {
getJSON('/post/1.json').then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// some code
@ -339,7 +366,7 @@ promise
跟传统的`try/catch`代码块不同的是,如果没有使用`catch`方法指定错误处理的回调函数Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。
```javascript
var someAsyncThing = function() {
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错因为x没有声明
resolve(x + 2);
@ -349,13 +376,31 @@ var someAsyncThing = function() {
someAsyncThing().then(function() {
console.log('everything is great');
});
setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123
```
上面代码中,`someAsyncThing`函数产生的Promise对象会报错但是由于没有指定`catch`方法这个错误不会被捕获也不会传递到外层代码导致运行后没有任何输出。注意Chrome浏览器不遵守这条规定它会抛出错误“ReferenceError: x is not defined”。
上面代码中,`someAsyncThing`函数产生的 Promise 对象,内部有语法错误。浏览器运行到这一行,会打印出错误提示`ReferenceError: x is not defined`但是不会退出进程、终止脚本执行2 秒之后还是会输出`123`。这就是说Promise 内部的错误不会影响到 Promise 外部的代码通俗的说法就是“Promise 会吃掉错误”。
这个脚本放在服务器执行,退出码就是`0`即表示执行成功。不过Node 有一个`unhandledRejection`事件,专门监听未捕获的`reject`错误,上面的脚本会触发这个事件的监听函数,可以在监听函数里面抛出错误。
```javascript
var promise = new Promise(function(resolve, reject) {
resolve("ok");
process.on('unhandledRejection', function (err, p) {
throw err;
});
```
上面代码中,`unhandledRejection`事件的监听函数有两个参数,第一个是错误对象,第二个是报错的 Promise 实例,它可以用来了解发生错误的环境信息。
注意Node 有计划在未来废除`unhandledRejection`事件。如果 Promise 内部有未捕获的错误,会直接终止进程,并且进程的退出码不为 0。
再看下面的例子。
```javascript
const promise = new Promise(function (resolve, reject) {
resolve('ok');
setTimeout(function () { throw new Error('test') }, 0)
});
promise.then(function (value) { console.log(value) });
@ -363,22 +408,12 @@ promise.then(function(value) { console.log(value) });
// Uncaught Error: test
```
上面代码中Promise指定在下一轮“事件循环”再抛出错误,结果由于没有指定使用`try...catch`语句就冒泡到最外层成了未捕获的错误。因为此时Promise的函数体已经运行结束了所以这个错误是在Promise函数体外抛出的
上面代码中Promise 指定在下一轮“事件循环”再抛出错误。到了那个时候Promise 的运行已经结束了,所以这个错误是在 Promise 函数体外抛出的,会冒泡到最外层,成了未捕获的错误
Node.js有一个`unhandledRejection`事件,专门监听未捕获的`reject`错误
一般总是建议Promise 对象后面要跟`catch`方法,这样可以处理 Promise 内部发生的错误。`catch`方法返回的还是一个 Promise 对象,因此后面还可以接着调用`then`方法
```javascript
process.on('unhandledRejection', function (err, p) {
console.error(err.stack)
});
```
上面代码中,`unhandledRejection`事件的监听函数有两个参数第一个是错误对象第二个是报错的Promise实例它可以用来了解发生错误的环境信息。。
需要注意的是,`catch`方法返回的还是一个Promise对象因此后面还可以接着调用`then`方法。
```javascript
var someAsyncThing = function() {
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错因为x没有声明
resolve(x + 2);
@ -414,7 +449,7 @@ Promise.resolve()
`catch`方法之中,还能再抛出错误。
```javascript
var someAsyncThing = function() {
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错因为x没有声明
resolve(x + 2);
@ -456,10 +491,10 @@ someAsyncThing().then(function() {
`Promise.all`方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
```javascript
var p = Promise.all([p1, p2, p3]);
const p = Promise.all([p1, p2, p3]);
```
上面代码中,`Promise.all`方法接受一个数组作为参数,`p1``p2``p3`都是Promise对象的实例,如果不是,就会先调用下面讲到的`Promise.resolve`方法将参数转为Promise实例再进一步处理。`Promise.all`方法的参数可以不是数组但必须具有Iterator接口且返回的每个成员都是Promise实例。
上面代码中,`Promise.all`方法接受一个数组作为参数,`p1``p2``p3`都是 Promise 实例,如果不是,就会先调用下面讲到的`Promise.resolve`方法,将参数转为 Promise 实例,再进一步处理。(`Promise.all`方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。)
`p`的状态由`p1``p2``p3`决定,分成两种情况。
@ -471,8 +506,8 @@ var p = Promise.all([p1, p2, p3]);
```javascript
// 生成一个Promise对象的数组
var promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON("/post/" + id + ".json");
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON('/post/' + id + ".json");
});
Promise.all(promises).then(function (posts) {
@ -489,7 +524,7 @@ Promise.all(promises).then(function (posts) {
```javascript
const databasePromise = connectDatabase();
const booksPromise = databaseProimse
const booksPromise = databasePromise
.then(findAllBooks);
const userPromise = databasePromise
@ -504,12 +539,54 @@ Promise.all([
上面代码中,`booksPromise``userPromise`是两个异步操作,只有等到它们的结果都返回了,才会触发`pickTopRecommentations`这个回调函数。
注意,如果作为参数的 Promise 实例,自己定义了`catch`方法,那么它一旦被`rejected`,并不会触发`Promise.all()``catch`方法。
```javascript
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result)
.catch(e => e);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]
```
上面代码中,`p1``resolved``p2`首先会`rejected`,但是`p2`有自己的`catch`方法,该方法返回的是一个新的 Promise 实例,`p2`指向的实际上是这个实例。该实例执行完`catch`方法后,也会变成`resolved`,导致`Promise.all()`方法参数里面的两个实例都会`resolved`,因此会调用`then`方法指定的回调函数,而不会调用`catch`方法指定的回调函数。
如果`p2`没有自己的`catch`方法,就会调用`Promise.all()``catch`方法。
```javascript
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 报错了
```
## Promise.race()
`Promise.race`方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
```javascript
var p = Promise.race([p1, p2, p3]);
const p = Promise.race([p1, p2, p3]);
```
上面代码中,只要`p1``p2``p3`之中有一个实例率先改变状态,`p`的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给`p`的回调函数。
@ -519,14 +596,14 @@ var p = Promise.race([p1, p2, p3]);
下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为`reject`,否则变为`resolve`
```javascript
var p = Promise.race([
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
])
p.then(response => console.log(response))
p.catch(error => console.log(error))
]);
p.then(response => console.log(response));
p.catch(error => console.log(error));
```
上面代码中,如果 5 秒之内`fetch`方法无法返回结果,变量`p`的状态就会变为`rejected`,从而触发`catch`方法指定的回调函数。
@ -536,7 +613,7 @@ p.catch(error => console.log(error))
有时需要将现有对象转为 Promise 对象,`Promise.resolve`方法就起到这个作用。
```javascript
var jsPromise = Promise.resolve($.ajax('/whatever.json'));
const jsPromise = Promise.resolve($.ajax('/whatever.json'));
```
上面代码将 jQuery 生成的`deferred`对象,转为一个新的 Promise 对象。
@ -586,10 +663,10 @@ p1.then(function(value) {
**3参数不是具有`then`方法的对象,或根本就不是对象**
如果参数是一个原始值,或者是一个不具有`then`方法的对象,则`Promise.resolve`方法返回一个新的Promise对象状态为`Resolved`。
如果参数是一个原始值,或者是一个不具有`then`方法的对象,则`Promise.resolve`方法返回一个新的 Promise 对象,状态为`resolved`。
```javascript
var p = Promise.resolve('Hello');
const p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
@ -597,16 +674,16 @@ p.then(function (s){
// Hello
```
上面代码生成一个新的Promise对象的实例`p`。由于字符串`Hello`不属于异步操作(判断方法是它不是具有then方法的对象返回Promise实例的状态从一生成就是`Resolved`,所以回调函数会立即执行。`Promise.resolve`方法的参数,会同时传给回调函数。
上面代码生成一个新的 Promise 对象的实例`p`。由于字符串`Hello`不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是`resolved`,所以回调函数会立即执行。`Promise.resolve`方法的参数,会同时传给回调函数。
**4不带有任何参数**
`Promise.resolve`方法允许调用时不带参数,直接返回一个`Resolved`状态的Promise对象。
`Promise.resolve`方法允许调用时不带参数,直接返回一个`resolved`状态的 Promise 对象。
所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用`Promise.resolve`方法。
```javascript
var p = Promise.resolve();
const p = Promise.resolve();
p.then(function () {
// ...
@ -633,16 +710,16 @@ console.log('one');
// three
```
上面代码中,`setTimeout(fn, 0)`在下一轮“事件循环”开始时执行,`Promise.resolve()`在本轮“事件循环”结束时执行,`console.log(one)`则是立即执行,因此最先输出。
上面代码中,`setTimeout(fn, 0)`在下一轮“事件循环”开始时执行,`Promise.resolve()`在本轮“事件循环”结束时执行,`console.log('one')`则是立即执行,因此最先输出。
## Promise.reject()
`Promise.reject(reason)`方法也会返回一个新的Promise实例该实例的状态为`rejected`它的参数用法与`Promise.resolve`方法完全一致。
`Promise.reject(reason)`方法也会返回一个新的 Promise 实例,该实例的状态为`rejected`
```javascript
var p = Promise.reject('出错了');
const p = Promise.reject('出错了');
// 等同于
var p = new Promise((resolve, reject) => reject('出错了'))
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
console.log(s)
@ -652,6 +729,24 @@ p.then(null, function (s){
上面代码生成一个 Promise 对象的实例`p`,状态为`rejected`,回调函数会立即执行。
注意,`Promise.reject()`方法的参数,会原封不动地作为`reject`的理由,变成后续方法的参数。这一点与`Promise.resolve`方法不一致。
```javascript
const thenable = {
then(resolve, reject) {
reject('出错了');
}
};
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true
```
上面代码中,`Promise.reject`方法的参数是一个`thenable`对象,执行以后,后面`catch`方法的参数不是`reject`抛出的“出错了”这个字符串,而是`thenable`对象。
## 两个有用的附加方法
ES6 的 Promise API 提供的方法不是很多,有些有用的方法可以自己部署。下面介绍如何部署两个不在 ES6 之中、但很有用的方法。
@ -680,7 +775,7 @@ Promise.prototype.done = function (onFulfilled, onRejected) {
};
```
从上面代码可见,`done`方法的使用,可以像`then`方法那样用,提供`Fulfilled`和`Rejected`状态的回调函数,也可以不提供任何参数。但不管怎样,`done`都会捕捉到任何可能出现的错误,并向全局抛出。
从上面代码可见,`done`方法的使用,可以像`then`方法那样用,提供`fulfilled`和`rejected`状态的回调函数,也可以不提供任何参数。但不管怎样,`done`都会捕捉到任何可能出现的错误,并向全局抛出。
### finally()
@ -719,7 +814,7 @@ Promise.prototype.finally = function (callback) {
```javascript
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
var image = new Image();
const image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
@ -738,9 +833,9 @@ function getFoo () {
});
}
var g = function* () {
const g = function* () {
try {
var foo = yield getFoo();
const foo = yield getFoo();
console.log(foo);
} catch (e) {
console.log(e);
@ -748,7 +843,7 @@ var g = function* () {
};
function run (generator) {
var it = generator();
const it = generator();
function go(result) {
if (result.done) return result.value;
@ -776,7 +871,7 @@ run(g);
Promise.resolve().then(f)
```
上面的写法有一个缺点,就是如果`f`是同步函数,那么它会在下一轮事件循环执行。
上面的写法有一个缺点,就是如果`f`是同步函数,那么它会在本轮事件循环的末尾执行。
```javascript
const f = () => console.log('now');
@ -798,7 +893,7 @@ console.log('next');
// next
```
上面代码中,第行是一个立即执行的匿名函数,会立即执行里面的`async`函数,因此如果`f`是同步的,就会得到同步的结果;如果`f`是异步的,就可以用`then`指定下一步,就像下面的写法。
上面代码中,第行是一个立即执行的匿名函数,会立即执行里面的`async`函数,因此如果`f`是同步的,就会得到同步的结果;如果`f`是异步的,就可以用`then`指定下一步,就像下面的写法。
```javascript
(async () => f())()
@ -881,4 +976,3 @@ Promise.try(database.users.get({id: userId}))
```
事实上,`Promise.try`就是模拟`try`代码块,就像`promise.catch`模拟的是`catch`代码块。

View File

@ -1,6 +1,6 @@
# Proxy 和 Reflect
# Proxy
## Proxy 概述
## 概述
Proxy 用于修改某些操作的默认行为等同于在语言层面做出修改所以属于一种“元编程”meta programming即对编程语言进行编程。
@ -68,7 +68,7 @@ proxy.a = 'b';
target.a // "b"
```
上面代码中,`handler`是一个空对象,没有任何拦截效果,访问`handler`就等同于访问`target`
上面代码中,`handler`是一个空对象,没有任何拦截效果,访问`proxy`就等同于访问`target`
一个技巧是将 Proxy 对象,设置到`object.proxy`属性,从而可以在`object`对象上调用。
@ -118,68 +118,26 @@ var fproxy = new Proxy(function(x, y) {
fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo // "Hello, foo"
fproxy.foo === "Hello, foo" // true
```
下面是 Proxy 支持的拦截操作一览。
对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。
**1get(target, propKey, receiver)**
下面是 Proxy 支持的拦截操作一览,一共 13 种。
拦截对象属性的读取,比如`proxy.foo``proxy['foo']`
最后一个参数`receiver`是一个对象,可选,参见下面`Reflect.get`的部分。
**2set(target, propKey, value, receiver)**
拦截对象属性的设置,比如`proxy.foo = v``proxy['foo'] = v`,返回一个布尔值。
**3has(target, propKey)**
拦截`propKey in proxy`的操作,以及对象的`hasOwnProperty`方法,返回一个布尔值。
**4deleteProperty(target, propKey)**
拦截`delete proxy[propKey]`的操作,返回一个布尔值。
**5ownKeys(target)**
拦截`Object.getOwnPropertyNames(proxy)``Object.getOwnPropertySymbols(proxy)``Object.keys(proxy)`,返回一个数组。该方法返回对象所有自身的属性,而`Object.keys()`仅返回对象可遍历的属性。
**6getOwnPropertyDescriptor(target, propKey)**
拦截`Object.getOwnPropertyDescriptor(proxy, propKey)`,返回属性的描述对象。
**7defineProperty(target, propKey, propDesc)**
拦截`Object.defineProperty(proxy, propKey, propDesc``Object.defineProperties(proxy, propDescs)`,返回一个布尔值。
**8preventExtensions(target)**
拦截`Object.preventExtensions(proxy)`,返回一个布尔值。
**9getPrototypeOf(target)**
拦截`Object.getPrototypeOf(proxy)`,返回一个对象。
**10isExtensible(target)**
拦截`Object.isExtensible(proxy)`,返回一个布尔值。
**11setPrototypeOf(target, proto)**
拦截`Object.setPrototypeOf(proxy, proto)`,返回一个布尔值。
如果目标对象是函数,那么还有两种额外操作可以拦截。
**12apply(target, object, args)**
拦截 Proxy 实例作为函数调用的操作,比如`proxy(...args)``proxy.call(object, ...args)``proxy.apply(...)`
**13construct(target, args)**
拦截 Proxy 实例作为构造函数调用的操作,比如`new proxy(...args)`
- **get(target, propKey, receiver)**:拦截对象属性的读取,比如`proxy.foo``proxy['foo']`
- **set(target, propKey, value, receiver)**:拦截对象属性的设置,比如`proxy.foo = v``proxy['foo'] = v`,返回一个布尔值。
- **has(target, propKey)**:拦截`propKey in proxy`的操作,返回一个布尔值。
- **deleteProperty(target, propKey)**:拦截`delete proxy[propKey]`的操作,返回一个布尔值。
- **ownKeys(target)**:拦截`Object.getOwnPropertyNames(proxy)``Object.getOwnPropertySymbols(proxy)``Object.keys(proxy)`,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而`Object.keys()`的返回结果仅包括目标对象自身的可遍历属性。
- **getOwnPropertyDescriptor(target, propKey)**:拦截`Object.getOwnPropertyDescriptor(proxy, propKey)`,返回属性的描述对象。
- **defineProperty(target, propKey, propDesc)**:拦截`Object.defineProperty(proxy, propKey, propDesc``Object.defineProperties(proxy, propDescs)`,返回一个布尔值。
- **preventExtensions(target)**:拦截`Object.preventExtensions(proxy)`,返回一个布尔值。
- **getPrototypeOf(target)**:拦截`Object.getPrototypeOf(proxy)`,返回一个对象。
- **isExtensible(target)**:拦截`Object.isExtensible(proxy)`,返回一个布尔值。
- **setPrototypeOf(target, proto)**:拦截`Object.setPrototypeOf(proxy, proto)`,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
- **apply(target, object, args)**:拦截 Proxy 实例作为函数调用的操作,比如`proxy(...args)``proxy.call(object, ...args)``proxy.apply(...)`
- **construct(target, args)**:拦截 Proxy 实例作为构造函数调用的操作,比如`new proxy(...args)`
## Proxy 实例的方法
@ -187,7 +145,9 @@ fproxy.foo // "Hello, foo"
### get()
`get`方法用于拦截某个属性的读取操作。上文已经有一个例子,下面是另一个拦截读取操作的例子。
`get`方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(即`this`关键字指向的那个对象),其中最后一个参数可选。
`get`方法的用法,上文已经有一个例子,下面是另一个拦截读取操作的例子。
```javascript
var person = {
@ -221,7 +181,7 @@ let proto = new Proxy({}, {
});
let obj = Object.create(proto);
obj.xxx // "GET xxx"
obj.foo // "GET foo"
```
上面代码中,拦截操作定义在`Prototype`对象上面,所以如果读取`obj`对象继承的属性时,拦截会生效。
@ -317,9 +277,45 @@ const el = dom.div({},
document.body.appendChild(el);
```
下面是一个`get`方法的第三个参数的例子。
```javascript
const proxy = new Proxy({}, {
get: function(target, property, receiver) {
return receiver;
}
});
proxy.getReceiver === proxy // true
```
上面代码中,`get`方法的第三个参数`receiver`,总是为当前的 Proxy 实例。
如果一个属性不可配置configurable和不可写writable则该属性不能被代理通过 Proxy 对象访问该属性会报错。
```javascript
const target = Object.defineProperties({}, {
foo: {
value: 123,
writable: false,
configurable: false
},
});
const handler = {
get(target, propKey) {
return 'abc';
}
};
const proxy = new Proxy(target, handler);
proxy.foo
// TypeError: Invariant check failed
```
### set()
`set`方法用来拦截某个属性的赋值操作。
`set`方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选
假定`Person`对象有一个`age`属性,该属性应该是一个不大于 200 的整数,那么可以使用`Proxy`保证`age`的属性值符合要求。
@ -335,7 +331,7 @@ let validator = {
}
}
// 对于age以外的属性,直接保存
// 对于满足条件的 age 属性以及其他属性,直接保存
obj[prop] = value;
}
};
@ -349,12 +345,12 @@ person.age = 'young' // 报错
person.age = 300 // 报错
```
上面代码中,由于设置了存值函数`set`,任何不符合要求的`age`属性赋值,都会抛出一个错误。利用`set`方法还可以数据绑定即每当对象发生变化时会自动更新DOM。
上面代码中,由于设置了存值函数`set`,任何不符合要求的`age`属性赋值,都会抛出一个错误,这是数据验证的一种实现方法。利用`set`方法,还可以数据绑定,即每当对象发生变化时,会自动更新 DOM。
有时,我们会在对象上面设置内部属性,属性名的第一个字符使用下划线开头,表示这些属性不应该被外部使用。结合`get``set`方法,就可以做到防止这些内部属性被外部读写。
```javascript
var handler = {
const handler = {
get (target, key) {
invariant(key, 'get');
return target[key];
@ -370,8 +366,8 @@ function invariant (key, action) {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
var target = {};
var proxy = new Proxy(target, handler);
const target = {};
const proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
@ -380,9 +376,28 @@ proxy._prop = 'c'
上面代码中,只要读写的属性名的第一个字符是下划线,一律抛错,从而达到禁止读写内部属性的目的。
下面是`set`方法第四个参数的例子。
```javascript
const handler = {
set: function(obj, prop, value, receiver) {
obj[prop] = receiver;
}
};
const proxy = new Proxy({}, handler);
proxy.foo = 'bar';
proxy.foo === proxy // true
```
上面代码中,`set`方法的第四个参数`receiver`,总是返回`this`关键字所指向的那个对象,即`proxy`实例本身。
注意,如果目标对象自身的某个属性,不可写也不可配置,那么`set`不得改变这个属性的值,只能返回同样的值,否则报错。
### apply()
`apply`方法拦截函数的调用、call和apply操作。
`apply`方法拦截函数的调用、`call``apply`操作。
`apply`方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(`this`)和目标对象的参数数组。
```javascript
var handler = {
@ -392,8 +407,6 @@ var handler = {
};
```
`apply`方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(`this`)和目标对象的参数数组。
下面是一个例子。
```javascript
@ -464,6 +477,7 @@ var proxy = new Proxy(target, handler);
```javascript
var obj = { a: 10 };
Object.preventExtensions(obj);
var p = new Proxy(obj, {
has: function(target, prop) {
return false;
@ -473,7 +487,7 @@ var p = new Proxy(obj, {
'a' in p // TypeError is thrown
```
上面代码中,`obj`对象禁止扩展,结果使用`has`拦截就会报错。
上面代码中,`obj`对象禁止扩展,结果使用`has`拦截就会报错。也就是说,如果某个属性不可配置(或者目标对象不可扩展),则`has`方法就不得“隐藏”(即返回`false`)目标对象的该属性。
值得注意的是,`has`方法拦截的是`HasProperty`操作,而不是`HasOwnProperty`操作,即`has`方法不判断一个属性是对象自身的属性,还是继承的属性。
@ -516,7 +530,7 @@ for (let b in oproxy2) {
// 99
```
上面代码中,`has`拦截只对`in`循环生效,对`for...in`循环不生效,导致不符合要求的属性没有被排除在`for...in`循环之外。
上面代码中,`has`拦截只对`in`运算符生效,对`for...in`循环不生效,导致不符合要求的属性没有被排除在`for...in`循环之外。
### construct()
@ -545,7 +559,7 @@ var p = new Proxy(function() {}, {
}
});
new p(1).value
(new p(1)).value
// "called: 1"
// 10
```
@ -587,6 +601,8 @@ delete proxy._prop
上面代码中,`deleteProperty`方法拦截了`delete`操作符,删除第一个字符为下划线的属性会报错。
注意目标对象自身的不可配置configurable的属性不能被`deleteProperty`方法删除,否则报错。
### defineProperty()
`defineProperty`方法拦截了`Object.defineProperty`操作。
@ -605,9 +621,11 @@ proxy.foo = 'bar'
上面代码中,`defineProperty`方法返回`false`,导致添加新属性会抛出错误。
注意如果目标对象不可扩展extensible`defineProperty`不能增加目标对象上不存在的属性否则会报错。另外如果目标对象的某个属性不可写writable或不可配置configurable`defineProperty`方法不得改变这两个设置。
### getOwnPropertyDescriptor()
`getOwnPropertyDescriptor`方法拦截`Object.getOwnPropertyDescriptor`,返回一个属性描述对象或者`undefined`
`getOwnPropertyDescriptor`方法拦截`Object.getOwnPropertyDescriptor()`,返回一个属性描述对象或者`undefined`
```javascript
var handler = {
@ -632,13 +650,13 @@ Object.getOwnPropertyDescriptor(proxy, 'baz')
### getPrototypeOf()
`getPrototypeOf`方法主要用来拦截`Object.getPrototypeOf()`运算符,以及其他一些操作。
`getPrototypeOf`方法主要用来拦截获取对象原型。具体来说,拦截下面这些操作。
- `Object.prototype.__proto__`
- `Object.prototype.isPrototypeOf()`
- `Object.getPrototypeOf()`
- `Reflect.getPrototypeOf()`
- `instanceof`运算符
- `instanceof`
下面是一个例子。
@ -654,6 +672,8 @@ Object.getPrototypeOf(p) === proto // true
上面代码中,`getPrototypeOf`方法拦截`Object.getPrototypeOf()`,返回`proto`对象。
注意,`getPrototypeOf`方法的返回值必须是对象或者`null`否则报错。另外如果目标对象不可扩展extensible `getPrototypeOf`方法必须返回目标对象的原型对象。
### isExtensible()
`isExtensible`方法拦截`Object.isExtensible`操作。
@ -673,7 +693,9 @@ Object.isExtensible(p)
上面代码设置了`isExtensible`方法,在调用`Object.isExtensible`时会输出`called`
这个方法有一个强限制,如果不能满足下面的条件,就会抛出错误。
注意,该方法只能返回布尔值,否则返回值会被自动转为布尔值。
这个方法有一个强限制,它的返回值必须与目标对象的`isExtensible`属性保持一致,否则就会抛出错误。
```javascript
Object.isExtensible(proxy) === Object.isExtensible(target)
@ -693,24 +715,34 @@ Object.isExtensible(p) // 报错
### ownKeys()
`ownKeys`方法用来拦截`Object.keys()`操作。
`ownKeys`方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作。
- `Object.getOwnPropertyNames()`
- `Object.getOwnPropertySymbols()`
- `Object.keys()`
下面是拦截`Object.keys()`的例子。
```javascript
let target = {};
let target = {
a: 1,
b: 2,
c: 3
};
let handler = {
ownKeys(target) {
return ['hello', 'world'];
return ['a'];
}
};
let proxy = new Proxy(target, handler);
Object.keys(proxy)
// [ 'hello', 'world' ]
// [ 'a' ]
```
上面代码拦截了对于`target`对象的`Object.keys()`操作,返回预先设定的数组
上面代码拦截了对于`target`对象的`Object.keys()`操作,只返回`a``b``c`三个属性之中的`a`属性
下面的例子是拦截第一个字符为下划线的属性名。
@ -734,11 +766,119 @@ for (let key of Object.keys(proxy)) {
// "baz"
```
注意,使用`Object.keys`方法时,有三类属性会被`ownKeys`方法自动过滤,不会返回。
- 目标对象上不存在的属性
- 属性名为 Symbol 值
- 不可遍历(`enumerable`)的属性
```javascript
let target = {
a: 1,
b: 2,
c: 3,
[Symbol.for('secret')]: '4',
};
Object.defineProperty(target, 'key', {
enumerable: false,
configurable: true,
writable: true,
value: 'static'
});
let handler = {
ownKeys(target) {
return ['a', 'd', Symbol.for('secret'), 'key'];
}
};
let proxy = new Proxy(target, handler);
Object.keys(proxy)
// ['a']
```
上面代码中,`ownKeys`方法之中,显式返回不存在的属性(`d`、Symbol 值(`Symbol.for('secret')`)、不可遍历的属性(`key`),结果都被自动过滤掉。
`ownKeys`方法还可以拦截`Object.getOwnPropertyNames()`
```javascript
var p = new Proxy({}, {
ownKeys: function(target) {
return ['a', 'b', 'c'];
}
});
Object.getOwnPropertyNames(p)
// [ 'a', 'b', 'c' ]
```
`ownKeys`方法返回的数组成员,只能是字符串或 Symbol 值。如果有其他类型的值,或者返回的根本不是数组,就会报错。
```javascript
var obj = {};
var p = new Proxy(obj, {
ownKeys: function(target) {
return [123, true, undefined, null, {}, []];
}
});
Object.getOwnPropertyNames(p)
// Uncaught TypeError: 123 is not a valid property name
```
上面代码中,`ownKeys`方法虽然返回一个数组,但是每一个数组成员都不是字符串或 Symbol 值,因此就报错了。
如果目标对象自身包含不可配置的属性,则该属性必须被`ownKeys`方法返回,否则报错。
```javascript
var obj = {};
Object.defineProperty(obj, 'a', {
configurable: false,
enumerable: true,
value: 10 }
);
var p = new Proxy(obj, {
ownKeys: function(target) {
return ['b'];
}
});
Object.getOwnPropertyNames(p)
// Uncaught TypeError: 'ownKeys' on proxy: trap result did not include 'a'
```
上面代码中,`obj`对象的`a`属性是不可配置的,这时`ownKeys`方法返回的数组之中,必须包含`a`,否则会报错。
另外如果目标对象是不可扩展的non-extensition这时`ownKeys`方法返回的数组之中,必须包含原对象的所有属性,且不能包含多余的属性,否则报错。
```javascript
var obj = {
a: 1
};
Object.preventExtensions(obj);
var p = new Proxy(obj, {
ownKeys: function(target) {
return ['a', 'b'];
}
});
Object.getOwnPropertyNames(p)
// Uncaught TypeError: 'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensible
```
上面代码中,`obj`对象是不可扩展的,这时`ownKeys`方法返回的数组之中,包含了`obj`对象的多余属性`b`,所以导致了报错。
### preventExtensions()
`preventExtensions`方法拦截`Object.preventExtensions()`。该方法必须返回一个布尔值。
`preventExtensions`方法拦截`Object.preventExtensions()`。该方法必须返回一个布尔值,否则会被自动转为布尔值
这个方法有一个限制,只有当`Object.isExtensible(proxy)``false`(即不可扩展)时,`proxy.preventExtensions`才能返回`true`,否则会报错。
这个方法有一个限制,只有目标对象不可扩展时(即`Object.isExtensible(proxy)``false``proxy.preventExtensions`才能返回`true`,否则会报错。
```javascript
var p = new Proxy({}, {
@ -757,7 +897,7 @@ Object.preventExtensions(p) // 报错
```javascript
var p = new Proxy({}, {
preventExtensions: function(target) {
console.log("called");
console.log('called');
Object.preventExtensions(target);
return true;
}
@ -783,15 +923,17 @@ var handler = {
var proto = {};
var target = function () {};
var proxy = new Proxy(target, handler);
proxy.setPrototypeOf(proxy, proto);
Object.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden
```
上面代码中,只要修改`target`的原型对象,就会报错。
注意该方法只能返回布尔值否则会被自动转为布尔值。另外如果目标对象不可扩展extensible`setPrototypeOf`方法不得改变目标对象的原型。
## Proxy.revocable()
Proxy.revocable方法返回一个可取消的Proxy实例。
`Proxy.revocable`方法返回一个可取消的 Proxy 实例。
```javascript
let target = {};
@ -808,6 +950,8 @@ proxy.foo // TypeError: Revoked
`Proxy.revocable`方法返回一个对象,该对象的`proxy`属性是`Proxy`实例,`revoke`属性是一个函数,可以取消`Proxy`实例。上面代码中,当执行`revoke`函数之后,再访问`Proxy`实例,就会抛出一个错误。
`Proxy.revocable`的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。
## this 问题
虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的`this`关键字会指向 Proxy 代理。
@ -879,200 +1023,29 @@ const proxy = new Proxy(target, handler);
proxy.getDate() // 1
```
## Reflect概述
## 实例Web 服务的客户端
`Reflect`对象与`Proxy`对象一样也是ES6为了操作对象而提供的新API。`Reflect`对象的设计目的有这样几个。
1`Object`对象的一些明显属于语言内部的方法(比如`Object.defineProperty`),放到`Reflect`对象上。现阶段,某些方法同时在`Object``Reflect`对象上部署,未来的新方法将只部署在`Reflect`对象上。
2 修改某些Object方法的返回结果让其变得更合理。比如`Object.defineProperty(obj, name, desc)`在无法定义属性时,会抛出一个错误,而`Reflect.defineProperty(obj, name, desc)`则会返回`false`
Proxy 对象可以拦截目标对象的任意属性,这使得它很合适用来写 Web 服务的客户端。
```javascript
// 老写法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
const service = createWebService('http://example.com/data');
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
```
3`Object`操作都变成函数行为。某些`Object`操作是命令式,比如`name in obj``delete obj[name]`,而`Reflect.has(obj, name)``Reflect.deleteProperty(obj, name)`让它们变成了函数行为。
```javascript
// 老写法
'assign' in Object // true
// 新写法
Reflect.has(Object, 'assign') // true
```
4`Reflect`对象的方法与`Proxy`对象的方法一一对应,只要是`Proxy`对象的方法,就能在`Reflect`对象上找到对应的方法。这就让`Proxy`对象可以方便地调用对应的`Reflect`方法,完成默认行为,作为修改行为的基础。也就是说,不管`Proxy`怎么修改默认行为,你总可以在`Reflect`上获取默认行为。
```javascript
Proxy(target, {
set: function(target, name, value, receiver) {
var success = Reflect.set(target,name, value, receiver);
if (success) {
log('property ' + name + ' on ' + target + ' set to ' + value);
}
return success;
}
service.employees().then(json => {
const employees = JSON.parse(json);
// ···
});
```
上面代码中,`Proxy`方法拦截`target`对象的属性赋值行为。它采用`Reflect.set`方法将值赋值给对象的属性,然后再部署额外的功能。
下面是另一个例子。
上面代码新建了一个 Web 服务的接口这个接口返回各种数据。Proxy 可以拦截这个对象的任意属性,所以不用为每一种数据写一个适配方法,只要写一个 Proxy 拦截就可以了。
```javascript
var loggedObj = new Proxy(obj, {
get(target, name) {
console.log('get', target, name);
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log('delete' + name);
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log('has' + name);
return Reflect.has(target, name);
function createWebService(baseUrl) {
return new Proxy({}, {
get(target, propKey, receiver) {
return () => httpGet(baseUrl+'/' + propKey);
}
});
```
上面代码中,每一个`Proxy`对象的拦截操作(`get``delete``has`内部都调用对应的Reflect方法保证原生行为能够正常执行。添加的工作就是将每一个操作输出一行日志。
有了`Reflect`对象以后,很多操作会更易读。
```javascript
// 老写法
Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1
// 新写法
Reflect.apply(Math.floor, undefined, [1.75]) // 1
```
## Reflect对象的方法
`Reflect`对象的方法清单如下共13个。
- Reflect.apply(target,thisArg,args)
- Reflect.construct(target,args)
- Reflect.get(target,name,receiver)
- Reflect.set(target,name,value,receiver)
- Reflect.defineProperty(target,name,desc)
- Reflect.deleteProperty(target,name)
- Reflect.has(target,name)
- Reflect.ownKeys(target)
- Reflect.isExtensible(target)
- Reflect.preventExtensions(target)
- Reflect.getOwnPropertyDescriptor(target, name)
- Reflect.getPrototypeOf(target)
- Reflect.setPrototypeOf(target, prototype)
上面这些方法的作用,大部分与`Object`对象的同名方法的作用都是相同的,而且它与`Proxy`对象的方法是一一对应的。下面是对其中几个方法的解释。
**1Reflect.get(target, name, receiver)**
查找并返回`target`对象的`name`属性,如果没有该属性,则返回`undefined`
如果`name`属性部署了读取函数,则读取函数的`this`绑定`receiver`
```javascript
var obj = {
get foo() { return this.bar(); },
bar: function() { ... }
};
// 下面语句会让 this.bar()
// 变成调用 wrapper.bar()
Reflect.get(obj, "foo", wrapper);
```
**2Reflect.set(target, name, value, receiver)**
设置`target`对象的`name`属性等于`value`。如果`name`属性设置了赋值函数,则赋值函数的`this`绑定`receiver`
**3Reflect.has(obj, name)**
等同于`name in obj`
**4Reflect.deleteProperty(obj, name)**
等同于`delete obj[name]`
**5Reflect.construct(target, args)**
等同于`new target(...args)`,这提供了一种不使用`new`,来调用构造函数的方法。
**6Reflect.getPrototypeOf(obj)**
读取对象的`__proto__`属性,对应`Object.getPrototypeOf(obj)`
**7Reflect.setPrototypeOf(obj, newProto)**
设置对象的`__proto__`属性,对应`Object.setPrototypeOf(obj, newProto)`
**8Reflect.apply(fun,thisArg,args)**
等同于`Function.prototype.apply.call(fun,thisArg,args)`。一般来说如果要绑定一个函数的this对象可以这样写`fn.apply(obj, args)`,但是如果函数定义了自己的`apply`方法,就只能写成`Function.prototype.apply.call(fn, obj, args)`采用Reflect对象可以简化这种操作。
另外,需要注意的是,`Reflect.set()``Reflect.defineProperty()``Reflect.freeze()``Reflect.seal()``Reflect.preventExtensions()`返回一个布尔值表示操作是否成功。它们对应的Object方法失败时都会抛出错误。
```javascript
// 失败时抛出错误
Object.defineProperty(obj, name, desc);
// 失败时返回false
Reflect.defineProperty(obj, name, desc);
```
上面代码中,`Reflect.defineProperty`方法的作用与`Object.defineProperty`是一样的,都是为对象定义一个属性。但是,`Reflect.defineProperty`方法失败时,不会抛出错误,只会返回`false`
## 实例:使用 Proxy 实现观察者模式
观察者模式Observer mode指的是函数自动观察数据对象一旦对象有变化函数就会自动执行。
```javascript
const person = observable({
name: '张三',
age: 20
});
function print() {
console.log(`${person.name}, ${person.age}`)
}
observe(print);
person.name = '李四';
// 输出
// 李四, 20
```
上面代码中,数据对象`person`是观察目标,函数`print`是观察者。一旦数据对象发生变化,`print`就会自动执行。
下面,使用 Proxy 写一个观察者模式的最简单实现,即实现`observable``observe`这两个函数。思路是`observable`函数返回一个原始对象的 Proxy 代理,拦截赋值操作,触发充当观察者的各个函数。
```javascript
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
queuedObservers.forEach(observer => observer());
return result;
}
```
上面代码中,先定义了一个`Set`集合,所有观察者函数都放进这个集合。然后,`observable`函数返回原始对象的代理,拦截赋值操作。拦截函数`set`之中,会自动执行所有观察者。
同理Proxy 也可以用来实现数据库的 ORM 层。

View File

@ -12,6 +12,7 @@
- [ECMAScript Daily](https://ecmascript-daily.github.io/): TC39 委员会的动态
- [The TC39 Process](https://tc39.github.io/process-document/): 提案进入正式规格的流程
- [TC39: A Process Sketch, Stages 0 and 1](https://thefeedbackloop.xyz/tc39-a-process-sketch-stages-0-and-1/): Stage 0 和 Stage 1 的含义
- [TC39 Process Sketch, Stage 2](https://thefeedbackloop.xyz/tc39-process-sketch-stage-2/): Stage 2 的含义
## 综合介绍
@ -64,10 +65,12 @@
- Mathias Bynens, [Unicode-aware regular expressions in ES6](https://mathiasbynens.be/notes/es6-unicode-regex): 详细介绍正则表达式的 u 修饰符
- Axel Rauschmayer, [New regular expression features in ECMAScript 6](http://www.2ality.com/2015/07/regexp-es6.html)ES6 正则特性的详细介绍
- Yang Guo, [RegExp lookbehind assertions](http://v8project.blogspot.jp/2016/02/regexp-lookbehind-assertions.html):介绍后行断言
- Axel Rauschmayer, [ES proposal: RegExp named capture groups](http://2ality.com/2017/05/regexp-named-capture-groups.html): 具名组匹配的介绍
## 数值
- Nicolas Bevacqua, [ES6 Number Improvements in Depth](http://ponyfoo.com/articles/es6-number-improvements-in-depth)
- Axel Rauschmayer, [ES proposal: arbitrary precision integers](http://2ality.com/2017/03/es-integer.html)
## 数组
@ -87,6 +90,7 @@
- Derick Bailey, [Do ES6 Arrow Functions Really Solve “this” In JavaScript?](http://derickbailey.com/2015/09/28/do-es6-arrow-functions-really-solve-this-in-javascript/):使用箭头函数处理 this 指向,必须非常小心
- Mark McDonnell, [Understanding recursion in functional JavaScript programming](http://www.integralist.co.uk/posts/js-recursion.html): 如何自己实现尾递归优化
- Nicholas C. Zakas, [The ECMAScript 2016 change you probably don't know](https://www.nczonline.net/blog/2016/10/the-ecmascript-2016-change-you-probably-dont-know/): 使用参数默认值时,不能在函数内部显式开启严格模式
- Axel Rauschmayer, [ES proposal: optional catch binding](http://2ality.com/2017/08/optional-catch-binding.html)
## 对象
@ -129,6 +133,7 @@
- Nicolas Bevacqua, [More ES6 Proxy Traps in Depth](http://ponyfoo.com/articles/more-es6-proxy-traps-in-depth)
- Axel Rauschmayer, [Pitfall: not all objects can be wrapped transparently by proxies](http://www.2ality.com/2016/11/proxying-builtins.html)
- Bertalan Miklos, [Writing a JavaScript Framework - Data Binding with ES6 Proxies](https://blog.risingstack.com/writing-a-javascript-framework-data-binding-es6-proxy/): 使用 Proxy 实现观察者模式
- Keith Cirkel, [Metaprogramming in ES6: Part 2 - Reflect](https://www.keithcirkel.co.uk/metaprogramming-in-es6-part-2-reflect/): Reflect API 的详细介绍
## Promise 对象
@ -179,6 +184,7 @@
- Daniel Brain, [Understand promises before you start using async/await](https://medium.com/@bluepnume/learn-about-promises-before-you-start-using-async-await-eb148164a9c8): 讨论 async/await 与 Promise 的关系
- Jake Archibald, [Async functions - making promises friendly](https://developers.google.com/web/fundamentals/getting-started/primers/async-functions)
- Axel Rauschmayer, [ES proposal: asynchronous iteration](http://www.2ality.com/2016/10/asynchronous-iteration.html): 异步遍历器的详细介绍
- Dima Grossman, [How to write async await without try-catch blocks in Javascript](http://blog.grossman.io/how-to-write-async-await-without-try-catch-blocks-in-javascript/): 除了 try/catch 以外的 async 函数内部捕捉错误的方法
## Class
@ -206,6 +212,10 @@
- Jason Orendorff, [ES6 In Depth: Modules](https://hacks.mozilla.org/2015/08/es6-in-depth-modules/): ES6 模块设计思想的介绍
- Ben Newman, [The Importance of import and export](http://benjamn.github.io/empirenode-2015/#/): ES6 模块的设计思想
- ESDiscuss, [Why is "export default var a = 1;" invalid syntax?](https://esdiscuss.org/topic/why-is-export-default-var-a-1-invalid-syntax)
- Bradley Meck, [ES6 Module Interoperability](https://github.com/nodejs/node-eps/blob/master/002-es6-modules.md): 介绍 Node 如何处理 ES6 语法加载 CommonJS 模块
- Axel Rauschmayer, [Making transpiled ES modules more spec-compliant](http://www.2ality.com/2017/01/babel-esm-spec-mode.html): ES6 模块编译成 CommonJS 模块的详细介绍
- Axel Rauschmayer, [ES proposal: import() dynamically importing ES modules](http://www.2ality.com/2017/01/import-operator.html): import() 的用法
- Node EPS, [ES Module Interoperability](https://github.com/nodejs/node-eps/blob/master/002-es-modules.md): Node 对 ES6 模块的处理规格
## 二进制数组
@ -214,6 +224,9 @@
- Ian Elliot, [Reading A BMP File In JavaScript](http://www.i-programmer.info/projects/36-web/6234-reading-a-bmp-file-in-javascript.html)
- Renato Mangini, [How to convert ArrayBuffer to and from String](http://updates.html5rocks.com/2012/06/How-to-convert-ArrayBuffer-to-and-from-String)
- Axel Rauschmayer, [Typed Arrays in ECMAScript 6](http://www.2ality.com/2015/09/typed-arrays.html)
- Axel Rauschmayer, [ES proposal: Shared memory and atomics](http://2ality.com/2017/01/shared-array-buffer.html)
- Lin Clark, [Avoiding race conditions in SharedArrayBuffers with Atomics](https://hacks.mozilla.org/2017/06/avoiding-race-conditions-in-sharedarraybuffers-with-atomics/): Atomics 对象使用场景的解释
- Lars T Hansen, [Shared memory - a brief tutorial](https://github.com/tc39/ecmascript_sharedmem/blob/master/TUTORIAL.md)
## SIMD
@ -236,4 +249,3 @@
- SystemJS, [SystemJS](https://github.com/systemjs/systemjs): 在浏览器中加载 AMD、CJS、ES6 模块的一个垫片库
- Modernizr, [HTML5 Cross Browser Polyfills](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills#ecmascript-6-harmony): ES6 垫片库清单
- Facebook, [regenerator](https://github.com/facebook/regenerator): 将 Generator 函数转为 ES5 的转码器

519
docs/reflect.md Normal file
View File

@ -0,0 +1,519 @@
# Reflect
## 概述
`Reflect`对象与`Proxy`对象一样,也是 ES6 为了操作对象而提供的新 API。`Reflect`对象的设计目的有这样几个。
1`Object`对象的一些明显属于语言内部的方法(比如`Object.defineProperty`),放到`Reflect`对象上。现阶段,某些方法同时在`Object``Reflect`对象上部署,未来的新方法将只部署在`Reflect`对象上。也就是说,从`Reflect`对象上可以拿到语言内部的方法。
2 修改某些`Object`方法的返回结果,让其变得更合理。比如,`Object.defineProperty(obj, name, desc)`在无法定义属性时,会抛出一个错误,而`Reflect.defineProperty(obj, name, desc)`则会返回`false`
```javascript
// 老写法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
```
3`Object`操作都变成函数行为。某些`Object`操作是命令式,比如`name in obj``delete obj[name]`,而`Reflect.has(obj, name)``Reflect.deleteProperty(obj, name)`让它们变成了函数行为。
```javascript
// 老写法
'assign' in Object // true
// 新写法
Reflect.has(Object, 'assign') // true
```
4`Reflect`对象的方法与`Proxy`对象的方法一一对应,只要是`Proxy`对象的方法,就能在`Reflect`对象上找到对应的方法。这就让`Proxy`对象可以方便地调用对应的`Reflect`方法,完成默认行为,作为修改行为的基础。也就是说,不管`Proxy`怎么修改默认行为,你总可以在`Reflect`上获取默认行为。
```javascript
Proxy(target, {
set: function(target, name, value, receiver) {
var success = Reflect.set(target,name, value, receiver);
if (success) {
log('property ' + name + ' on ' + target + ' set to ' + value);
}
return success;
}
});
```
上面代码中,`Proxy`方法拦截`target`对象的属性赋值行为。它采用`Reflect.set`方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能。
下面是另一个例子。
```javascript
var loggedObj = new Proxy(obj, {
get(target, name) {
console.log('get', target, name);
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log('delete' + name);
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log('has' + name);
return Reflect.has(target, name);
}
});
```
上面代码中,每一个`Proxy`对象的拦截操作(`get``delete``has`),内部都调用对应的`Reflect`方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。
有了`Reflect`对象以后,很多操作会更易读。
```javascript
// 老写法
Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1
// 新写法
Reflect.apply(Math.floor, undefined, [1.75]) // 1
```
## 静态方法
`Reflect`对象一共有 13 个静态方法。
- Reflect.apply(target, thisArg, args)
- Reflect.construct(target, args)
- Reflect.get(target, name, receiver)
- Reflect.set(target, name, value, receiver)
- Reflect.defineProperty(target, name, desc)
- Reflect.deleteProperty(target, name)
- Reflect.has(target, name)
- Reflect.ownKeys(target)
- Reflect.isExtensible(target)
- Reflect.preventExtensions(target)
- Reflect.getOwnPropertyDescriptor(target, name)
- Reflect.getPrototypeOf(target)
- Reflect.setPrototypeOf(target, prototype)
上面这些方法的作用,大部分与`Object`对象的同名方法的作用都是相同的,而且它与`Proxy`对象的方法是一一对应的。下面是对它们的解释。
### Reflect.get(target, name, receiver)
`Reflect.get`方法查找并返回`target`对象的`name`属性,如果没有该属性,则返回`undefined`
```javascript
var myObject = {
foo: 1,
bar: 2,
get baz() {
return this.foo + this.bar;
},
}
Reflect.get(myObject, 'foo') // 1
Reflect.get(myObject, 'bar') // 2
Reflect.get(myObject, 'baz') // 3
```
如果`name`属性部署了读取函数getter则读取函数的`this`绑定`receiver`
```javascript
var myObject = {
foo: 1,
bar: 2,
get baz() {
return this.foo + this.bar;
},
};
var myReceiverObject = {
foo: 4,
bar: 4,
};
Reflect.get(myObject, 'baz', myReceiverObject) // 8
```
如果第一个参数不是对象,`Reflect.get`方法会报错。
```javascript
Reflect.get(1, 'foo') // 报错
Reflect.get(false, 'foo') // 报错
```
### Reflect.set(target, name, value, receiver)
`Reflect.set`方法设置`target`对象的`name`属性等于`value`
```javascript
var myObject = {
foo: 1,
set bar(value) {
return this.foo = value;
},
}
myObject.foo // 1
Reflect.set(myObject, 'foo', 2);
myObject.foo // 2
Reflect.set(myObject, 'bar', 3)
myObject.foo // 3
```
如果`name`属性设置了赋值函数,则赋值函数的`this`绑定`receiver`
```javascript
var myObject = {
foo: 4,
set bar(value) {
return this.foo = value;
},
};
var myReceiverObject = {
foo: 0,
};
Reflect.set(myObject, 'bar', 1, myReceiverObject);
myObject.foo // 4
myReceiverObject.foo // 1
```
注意,如果 Proxy 对象和 Reflect 对象联合使用,前者拦截赋值操作,后者完成赋值的默认行为,而且传入了`receiver`,那么`Reflect.set`会触发`Proxy.defineProperty`拦截。
```javascript
let p = {
a: 'a'
};
let handler = {
set(target, key, value, receiver) {
console.log('set');
Reflect.set(target, key, value, receiver)
},
defineProperty(target, key, attribute) {
console.log('defineProperty');
Reflect.defineProperty(target, key, attribute);
}
};
let obj = new Proxy(p, handler);
obj.a = 'A';
// set
// defineProperty
```
上面代码中,`Proxy.set`拦截里面使用了`Reflect.set`,而且传入了`receiver`,导致触发`Proxy.defineProperty`拦截。这是因为`Proxy.set``receiver`参数总是指向当前的 Proxy 实例(即上例的`obj`),而`Reflect.set`一旦传入`receiver`,就会将属性赋值到`receiver`上面(即`obj`),导致触发`defineProperty`拦截。如果`Reflect.set`没有传入`receiver`,那么就不会触发`defineProperty`拦截。
```javascript
let p = {
a: 'a'
};
let handler = {
set(target, key, value, receiver) {
console.log('set');
Reflect.set(target, key, value)
},
defineProperty(target, key, attribute) {
console.log('defineProperty');
Reflect.defineProperty(target, key, attribute);
}
};
let obj = new Proxy(p, handler);
obj.a = 'A';
// set
```
如果第一个参数不是对象,`Reflect.set`会报错。
```javascript
Reflect.set(1, 'foo', {}) // 报错
Reflect.set(false, 'foo', {}) // 报错
```
### Reflect.has(obj, name)
`Reflect.has`方法对应`name in obj`里面的`in`运算符。
```javascript
var myObject = {
foo: 1,
};
// 旧写法
'foo' in myObject // true
// 新写法
Reflect.has(myObject, 'foo') // true
```
如果第一个参数不是对象,`Reflect.has``in`运算符都会报错。
### Reflect.deleteProperty(obj, name)
`Reflect.deleteProperty`方法等同于`delete obj[name]`,用于删除对象的属性。
```javascript
const myObj = { foo: 'bar' };
// 旧写法
delete myObj.foo;
// 新写法
Reflect.deleteProperty(myObj, 'foo');
```
该方法返回一个布尔值。如果删除成功,或者被删除的属性不存在,返回`true`;删除失败,被删除的属性依然存在,返回`false`
### Reflect.construct(target, args)
`Reflect.construct`方法等同于`new target(...args)`,这提供了一种不使用`new`,来调用构造函数的方法。
```javascript
function Greeting(name) {
this.name = name;
}
// new 的写法
const instance = new Greeting('张三');
// Reflect.construct 的写法
const instance = Reflect.construct(Greeting, ['张三']);
```
### Reflect.getPrototypeOf(obj)
`Reflect.getPrototypeOf`方法用于读取对象的`__proto__`属性,对应`Object.getPrototypeOf(obj)`
```javascript
const myObj = new FancyThing();
// 旧写法
Object.getPrototypeOf(myObj) === FancyThing.prototype;
// 新写法
Reflect.getPrototypeOf(myObj) === FancyThing.prototype;
```
`Reflect.getPrototypeOf``Object.getPrototypeOf`的一个区别是,如果参数不是对象,`Object.getPrototypeOf`会将这个参数转为对象,然后再运行,而`Reflect.getPrototypeOf`会报错。
```javascript
Object.getPrototypeOf(1) // Number {[[PrimitiveValue]]: 0}
Reflect.getPrototypeOf(1) // 报错
```
### Reflect.setPrototypeOf(obj, newProto)
`Reflect.setPrototypeOf`方法用于设置对象的`__proto__`属性,返回第一个参数对象,对应`Object.setPrototypeOf(obj, newProto)`
```javascript
const myObj = new FancyThing();
// 旧写法
Object.setPrototypeOf(myObj, OtherThing.prototype);
// 新写法
Reflect.setPrototypeOf(myObj, OtherThing.prototype);
```
如果第一个参数不是对象,`Object.setPrototypeOf`会返回第一个参数本身,而`Reflect.setPrototypeOf`会报错。
```javascript
Object.setPrototypeOf(1, {})
// 1
Reflect.setPrototypeOf(1, {})
// TypeError: Reflect.setPrototypeOf called on non-object
```
如果第一个参数是`undefined``null``Object.setPrototypeOf``Reflect.setPrototypeOf`都会报错。
```javascript
Object.setPrototypeOf(null, {})
// TypeError: Object.setPrototypeOf called on null or undefined
Reflect.setPrototypeOf(null, {})
// TypeError: Reflect.setPrototypeOf called on non-object
```
### Reflect.apply(func, thisArg, args)
`Reflect.apply`方法等同于`Function.prototype.apply.call(func, thisArg, args)`,用于绑定`this`对象后执行给定函数。
一般来说,如果要绑定一个函数的`this`对象,可以这样写`fn.apply(obj, args)`,但是如果函数定义了自己的`apply`方法,就只能写成`Function.prototype.apply.call(fn, obj, args)`,采用`Reflect`对象可以简化这种操作。
```javascript
const ages = [11, 33, 12, 54, 18, 96];
// 旧写法
const youngest = Math.min.apply(Math, ages);
const oldest = Math.max.apply(Math, ages);
const type = Object.prototype.toString.call(youngest);
// 新写法
const youngest = Reflect.apply(Math.min, Math, ages);
const oldest = Reflect.apply(Math.max, Math, ages);
const type = Reflect.apply(Object.prototype.toString, youngest, []);
```
### Reflect.defineProperty(target, propertyKey, attributes)
`Reflect.defineProperty`方法基本等同于`Object.defineProperty`,用来为对象定义属性。未来,后者会被逐渐废除,请从现在开始就使用`Reflect.defineProperty`代替它。
```javascript
function MyDate() {
/*…*/
}
// 旧写法
Object.defineProperty(MyDate, 'now', {
value: () => Date.now()
});
// 新写法
Reflect.defineProperty(MyDate, 'now', {
value: () => Date.now()
});
```
如果`Reflect.defineProperty`的第一个参数不是对象,就会抛出错误,比如`Reflect.defineProperty(1, 'foo')`
### Reflect.getOwnPropertyDescriptor(target, propertyKey)
`Reflect.getOwnPropertyDescriptor`基本等同于`Object.getOwnPropertyDescriptor`,用于得到指定属性的描述对象,将来会替代掉后者。
```javascript
var myObject = {};
Object.defineProperty(myObject, 'hidden', {
value: true,
enumerable: false,
});
// 旧写法
var theDescriptor = Object.getOwnPropertyDescriptor(myObject, 'hidden');
// 新写法
var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden');
```
`Reflect.getOwnPropertyDescriptor``Object.getOwnPropertyDescriptor`的一个区别是,如果第一个参数不是对象,`Object.getOwnPropertyDescriptor(1, 'foo')`不报错,返回`undefined`,而`Reflect.getOwnPropertyDescriptor(1, 'foo')`会抛出错误,表示参数非法。
### Reflect.isExtensible (target)
`Reflect.isExtensible`方法对应`Object.isExtensible`,返回一个布尔值,表示当前对象是否可扩展。
```javascript
const myObject = {};
// 旧写法
Object.isExtensible(myObject) // true
// 新写法
Reflect.isExtensible(myObject) // true
```
如果参数不是对象,`Object.isExtensible`会返回`false`,因为非对象本来就是不可扩展的,而`Reflect.isExtensible`会报错。
```javascript
Object.isExtensible(1) // false
Reflect.isExtensible(1) // 报错
```
### Reflect.preventExtensions(target)
`Reflect.preventExtensions`对应`Object.preventExtensions`方法,用于让一个对象变为不可扩展。它返回一个布尔值,表示是否操作成功。
```javascript
var myObject = {};
// 旧写法
Object.preventExtensions(myObject) // Object {}
// 新写法
Reflect.preventExtensions(myObject) // true
```
如果参数不是对象,`Object.preventExtensions`在 ES5 环境报错,在 ES6 环境返回传入的参数,而`Reflect.preventExtensions`会报错。
```javascript
// ES5 环境
Object.preventExtensions(1) // 报错
// ES6 环境
Object.preventExtensions(1) // 1
// 新写法
Reflect.preventExtensions(1) // 报错
```
### Reflect.ownKeys (target)
`Reflect.ownKeys`方法用于返回对象的所有属性,基本等同于`Object.getOwnPropertyNames``Object.getOwnPropertySymbols`之和。
```javascript
var myObject = {
foo: 1,
bar: 2,
[Symbol.for('baz')]: 3,
[Symbol.for('bing')]: 4,
};
// 旧写法
Object.getOwnPropertyNames(myObject)
// ['foo', 'bar']
Object.getOwnPropertySymbols(myObject)
//[Symbol(baz), Symbol(bing)]
// 新写法
Reflect.ownKeys(myObject)
// ['foo', 'bar', Symbol(baz), Symbol(bing)]
```
## 实例:使用 Proxy 实现观察者模式
观察者模式Observer mode指的是函数自动观察数据对象一旦对象有变化函数就会自动执行。
```javascript
const person = observable({
name: '张三',
age: 20
});
function print() {
console.log(`${person.name}, ${person.age}`)
}
observe(print);
person.name = '李四';
// 输出
// 李四, 20
```
上面代码中,数据对象`person`是观察目标,函数`print`是观察者。一旦数据对象发生变化,`print`就会自动执行。
下面,使用 Proxy 写一个观察者模式的最简单实现,即实现`observable``observe`这两个函数。思路是`observable`函数返回一个原始对象的 Proxy 代理,拦截赋值操作,触发充当观察者的各个函数。
```javascript
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
queuedObservers.forEach(observer => observer());
return result;
}
```
上面代码中,先定义了一个`Set`集合,所有观察者函数都放进这个集合。然后,`observable`函数返回原始对象的代理,拦截赋值操作。拦截函数`set`之中,会自动执行所有观察者。

View File

@ -2,7 +2,7 @@
## RegExp 构造函数
在ES5中RegExp构造函数的参数有两种情况。
ES5 中,`RegExp`构造函数的参数有两种情况。
第一种情况是参数是字符串这时第二个参数表示正则表达式的修饰符flag
@ -20,14 +20,14 @@ var regex = new RegExp(/xyz/i);
var regex = /xyz/i;
```
但是ES5不允许此时使用第二个参数添加修饰符,否则会报错。
但是ES5 不允许此时使用第二个参数添加修饰符,否则会报错。
```javascript
var regex = new RegExp(/xyz/, 'i');
// Uncaught TypeError: Cannot supply flags when constructing one RegExp from another
```
ES6改变了这种行为。如果RegExp构造函数第一个参数是一个正则对象那么可以使用第二个参数指定修饰符。而且返回的正则表达式会忽略原有的正则表达式的修饰符只使用新指定的修饰符。
ES6 改变了这种行为。如果`RegExp`构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。
```javascript
new RegExp(/abc/ig, 'i').flags
@ -40,7 +40,7 @@ new RegExp(/abc/ig, 'i').flags
字符串对象共有 4 个方法,可以使用正则表达式:`match()``replace()``search()``split()`
ES6将这4个方法在语言内部全部调用RegExp的实例方法从而做到所有与正则相关的方法全都定义在RegExp对象上。
ES6 将这 4 个方法,在语言内部全部调用`RegExp`的实例方法,从而做到所有与正则相关的方法,全都定义在`RegExp`对象上。
- `String.prototype.match` 调用 `RegExp.prototype[Symbol.match]`
- `String.prototype.replace` 调用 `RegExp.prototype[Symbol.replace]`
@ -52,10 +52,8 @@ ES6将这4个方法在语言内部全部调用RegExp的实例方法从而
ES6 对正则表达式添加了`u`修饰符含义为“Unicode 模式”,用来正确处理大于`\uFFFF`的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码。
```javascript
/^\uD83D/u.test('\uD83D\uDC2A')
// false
/^\uD83D/.test('\uD83D\uDC2A')
// true
/^\uD83D/u.test('\uD83D\uDC2A') // false
/^\uD83D/.test('\uD83D\uDC2A') // true
```
上面代码中,`\uD83D\uDC2A`是一个四个字节的 UTF-16 编码代表一个字符。但是ES5 不支持四个字节的 UTF-16 编码,会将其识别为两个字符,导致第二行代码结果为`true`。加了`u`修饰符以后ES6 就会识别其为一个字符,所以第一行代码结果为`false`
@ -77,7 +75,7 @@ var s = '𠮷';
**2Unicode 字符表示法**
ES6新增了使用大括号表示Unicode字符这种表示法在正则表达式中必须加上`u`修饰符,才能识别。
ES6 新增了使用大括号表示 Unicode 字符,这种表示法在正则表达式中必须加上`u`修饰符,才能识别当中的大括号,否则会被解读为量词
```javascript
/\u{61}/.test('a') // false
@ -98,14 +96,6 @@ ES6新增了使用大括号表示Unicode字符这种表示法在正则表达
/𠮷{2}/u.test('𠮷𠮷') // true
```
另外,只有在使用`u`修饰符的情况下Unicode表达式当中的大括号才会被正确解读否则会被解读为量词。
```javascript
/^\u{3}$/.test('uuu') // true
```
上面代码中,由于正则表达式没有`u`修饰符,所以大括号被解读为量词。加上`u`修饰符就会被解读为Unicode表达式。
**4预定义模式**
`u`修饰符也影响到预定义模式,能否正确识别码点大于`0xFFFF`的 Unicode 字符。
@ -140,7 +130,7 @@ codePointLength(s) // 2
/[a-z]/iu.test('\u212A') // true
```
上面代码中,不加`u`修饰符就无法识别非规范的K字符。
上面代码中,不加`u`修饰符,就无法识别非规范的`K`字符。
## y 修饰符
@ -217,7 +207,7 @@ match.index // 3
REGEX.lastIndex // 4
```
进一步说`y`修饰符号隐含了头部匹配的标志`^`
实际上`y`修饰符号隐含了头部匹配的标志`^`
```javascript
/b/y.exec('aba')
@ -226,28 +216,6 @@ REGEX.lastIndex // 4
上面代码由于不能保证头部匹配,所以返回`null``y`修饰符的设计本意,就是让头部匹配的标志`^`在全局匹配中都有效。
`split`方法中使用`y`修饰符,原字符串必须以分隔符开头。这也意味着,只要匹配成功,数组的第一个成员肯定是空字符串。
```javascript
// 没有找到匹配
'x##'.split(/#/y)
// [ 'x##' ]
// 找到两个匹配
'##x'.split(/#/y)
// [ '', '', 'x' ]
```
后续的分隔符只有紧跟前面的分隔符,才会被识别。
```javascript
'#x#'.split(/#/y)
// [ '', 'x#' ]
'##'.split(/#/y)
// [ '', '', '' ]
```
下面是字符串对象的`replace`方法的例子。
```javascript
@ -255,7 +223,7 @@ const REGEX = /a/gy;
'aaxa'.replace(REGEX, '-') // '--xa'
```
上面代码中,最后一个`a`因为不是出现下一次匹配的头部,所以不会被替换。
上面代码中,最后一个`a`因为不是出现下一次匹配的头部,所以不会被替换。
单单一个`y`修饰符对`match`方法,只能返回第一个匹配,必须与`g`修饰符联用,才能返回所有匹配。
@ -321,51 +289,6 @@ ES6为正则表达式新增了`flags`属性,会返回正则表达式的修饰
// 'gi'
```
## RegExp.escape()
字符串必须转义,才能作为正则模式。
```javascript
function escapeRegExp(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
}
let str = '/path/to/resource.html?search=query';
escapeRegExp(str)
// "\/path\/to\/resource\.html\?search=query"
```
上面代码中,`str`是一个正常字符串,必须使用反斜杠对其中的特殊字符转义,才能用来作为一个正则匹配的模式。
已经有[提议](https://esdiscuss.org/topic/regexp-escape)将这个需求标准化作为RegExp对象的静态方法[RegExp.escape()](https://github.com/benjamingr/RexExp.escape)放入ES7。2015年7月31日TC39认为这个方法有安全风险又不愿这个方法变得过于复杂没有同意将其列入ES7但这不失为一个真实的需求。
```javascript
RegExp.escape('The Quick Brown Fox');
// "The Quick Brown Fox"
RegExp.escape('Buy it. use it. break it. fix it.');
// "Buy it\. use it\. break it\. fix it\."
RegExp.escape('(*.*)');
// "\(\*\.\*\)"
```
字符串转义以后可以使用RegExp构造函数生成正则模式。
```javascript
var str = 'hello. how are you?';
var regex = new RegExp(RegExp.escape(str), 'g');
assert.equal(String(regex), '/hello\. how are you\?/g');
```
目前,该方法可以用上文的`escapeRegExp`函数或者垫片模块[regexp.escape](https://github.com/ljharb/regexp.escape)实现。
```javascript
var escape = require('regexp.escape');
escape('hi. how are you?');
// "hi\\. how are you\\?"
```
## s 修饰符dotAll 模式
正则表达式中,点(`.`是一个特殊字符代表任意的单个字符但是行终止符line terminator character除外。
@ -413,9 +336,7 @@ re.flags // 's'
## 后行断言
JavaScript语言的正则表达式只支持先行断言lookahead和先行否定断言negative lookahead不支持后行断言lookbehind和后行否定断言negative lookbehind
目前,有一个[提案](https://github.com/goyakin/es-regexp-lookbehind)在ES7加入后行断言。V8引擎4.9版已经支持Chrome浏览器49版打开”experimental JavaScript features“开关地址栏键入`about:flags`),就可以使用这项功能。
JavaScript 语言的正则表达式只支持先行断言lookahead和先行否定断言negative lookahead不支持后行断言lookbehind和后行否定断言negative lookbehind。目前有一个[提案](https://github.com/goyakin/es-regexp-lookbehind)引入后行断言V8 引擎 4.9 版已经支持。
”先行断言“指的是,`x`只有在`y`前面才匹配,必须写成`/x(?=y)/`。比如,只匹配百分号之前的数字,要写成`/\d+(?=%)/`。”先行否定断言“指的是,`x`只有不在`y`前面才匹配,必须写成`/x(?!y)/`。比如,只匹配不在百分号之前的数字,要写成`/\d+(?!%)/`
@ -424,18 +345,28 @@ JavaScript语言的正则表达式只支持先行断言lookahead和先
/\d+(?!%)/.exec('thats all 44 of them') // ["44"]
```
上面两个字符串,如果互换正则表达式,就会匹配失败。另外,还可以看到,”先行断言“括号之中的部分(`(?=%)`),是不计入返回结果的。
上面两个字符串,如果互换正则表达式,就不会得到相同结果。另外,还可以看到,”先行断言“括号之中的部分(`(?=%)`),是不计入返回结果的。
"后行断言"正好与"先行断言"相反,`x`只有在`y`后面才匹配,必须写成`/(?<=y)x/`。比如,只匹配美元符号之后的数字,要写成`/(?<=\$)\d+/`。”后行否定断言“则与”先行否定断言“相反,`x`只有不在`y`后面才匹配,必须写成`/(?<!y)x/`。比如,只匹配不在美元符号后面的数字,要写成`/(?<!\$)\d+/`
“后行断言”正好与“先行断言”相反,`x`只有在`y`后面才匹配,必须写成`/(?<=y)x/`。比如,只匹配美元符号之后的数字,要写成`/(?<=\$)\d+/`。”后行否定断言“则与”先行否定断言“相反,`x`只有不在`y`后面才匹配,必须写成`/(?<!y)x/`。比如,只匹配不在美元符号后面的数字,要写成`/(?<!\$)\d+/`
```javascript
/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill') // ["100"]
/(?<!\$)\d+/.exec('its is worth about €90') // ["90"]
```
上面的例子中,"后行断言"的括号之中的部分(`(?<=\$)`),也是不计入返回结果。
上面的例子中,“后行断言”的括号之中的部分(`(?<=\$)`),也是不计入返回结果。
"后行断言"的实现,需要先匹配`/(?<=y)x/``x`,然后再回到左边,匹配`y`的部分。这种"先右后左"的执行顺序,与所有其他正则操作相反,导致了一些不符合预期的行为。
下面的例子是使用后行断言进行字符串替换。
```javascript
const RE_DOLLAR_PREFIX = /(?<=\$)foo/g;
'$foo %foo foo'.replace(RE_DOLLAR_PREFIX, 'bar');
// '$bar %foo foo'
```
上面代码中,只有在美元符号后面的`foo`才会被替换。
“后行断言”的实现,需要先匹配`/(?<=y)x/``x`,然后再回到左边,匹配`y`的部分。这种“先右后左”的执行顺序,与所有其他正则操作相反,导致了一些不符合预期的行为。
首先,”后行断言“的组匹配,与正常情况下结果是不一样的。
@ -453,7 +384,7 @@ JavaScript语言的正则表达式只支持先行断言lookahead和先
/(?<=\1d(o))r/.exec('hodor') // ["r", "o"]
```
上面代码中,如果后行断言的反斜杠引用(`\1`)放在括号的后面,就不会得到匹配结果,必须放在前面才可以。
上面代码中,如果后行断言的反斜杠引用(`\1`)放在括号的后面,就不会得到匹配结果,必须放在前面才可以。因为后行断言是先从左到右扫描,发现匹配以后再回过头,从右到左完成反斜杠引用。
## Unicode 属性类
@ -461,7 +392,7 @@ JavaScript语言的正则表达式只支持先行断言lookahead和先
```javascript
const regexGreekSymbol = /\p{Script=Greek}/u;
regexGreekSymbol.test('π') // u
regexGreekSymbol.test('π') // true
```
上面代码中,`\p{Script=Greek}`指定匹配一个希腊文字母,所以匹配`π`成功。
@ -508,10 +439,123 @@ regex.test('ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ') // true
[\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]
// 匹配各种文字的所有非字母的字符,等同于 Unicode 版的 \W
[\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]
[^\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]
// 匹配所有的箭头字符
const regexArrows = /^\p{Block=Arrows}+$/u;
regexArrows.test('←↑→↓↔↕↖↗↘↙⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇧⇩') // true
```
## 具名组匹配
### 简介
正则表达式使用圆括号进行组匹配。
```javascript
const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;
```
上面代码中,正则表达式里面有三组圆括号。使用`exec`方法,就可以将这三组匹配结果提取出来。
```javascript
const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj[1]; // 1999
const month = matchObj[2]; // 12
const day = matchObj[3]; // 31
```
组匹配的一个问题是,每一组的匹配含义不容易看出来,而且只能用数字序号引用,要是组的顺序变了,引用的时候就必须修改序号。
现在有一个“具名组匹配”Named Capture Groups的[提案](https://github.com/tc39/proposal-regexp-named-groups),允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用。
```javascript
const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj.groups.year; // 1999
const month = matchObj.groups.month; // 12
const day = matchObj.groups.day; // 31
```
上面代码中,“具名组匹配”在圆括号内部,模式的头部添加“问号 + 尖括号 + 组名”(`?<year>`),然后就可以在`exec`方法返回结果的`groups`属性上引用该组名。同时,数字序号(`matchObj[1]`)依然有效。
具名组匹配等于为每一组匹配加上了 ID便于描述匹配的目的。如果组的顺序变了也不用改变匹配后的处理代码。
如果具名组没有匹配,那么对应的`groups`对象属性会是`undefined`
```javascript
const RE_OPT_A = /^(?<as>a+)?$/;
const matchObj = RE_OPT_A.exec('');
matchObj.groups.as // undefined
'as' in matchObj.groups // true
```
上面代码中,具名组`as`没有找到匹配,那么`matchObj.groups.as`属性值就是`undefined`,并且`as`这个键名在`groups`是始终存在的。
### 解构赋值和替换
有了具名组匹配以后,可以使用解构赋值直接从匹配结果上为变量赋值。
```javascript
let {groups: {one, two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar');
one // foo
two // bar
```
字符串替换时,使用`$<组名>`引用具名组。
```javascript
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
'2015-01-02'.replace(re, '$<day>/$<month>/$<year>')
// '02/01/2015'
```
上面代码中,`replace`方法的第二个参数是一个字符串,而不是正则表达式。
`replace`方法的第二个参数也可以是函数,该函数的参数序列如下。
```javascript
'2015-01-02'.replace(re, (
matched, // 整个匹配结果 2015-01-02
capture1, // 第一个组匹配 2015
capture2, // 第二个组匹配 01
capture3, // 第三个组匹配 02
position, // 匹配开始的位置 0
S, // 原字符串 2015-01-02
groups // 具名组构成的一个对象 {year, month, day}
) => {
let {day, month, year} = args[args.length - 1];
return `${day}/${month}/${year}`;
});
```
具名组匹配在原来的基础上,新增了最后一个函数参数:具名组构成的一个对象。函数内部可以直接对这个对象进行解构赋值。
### 引用
如果要在正则表达式内部引用某个“具名组匹配”,可以使用`\k<组名>`的写法。
```javascript
const RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/;
RE_TWICE.test('abc!abc') // true
RE_TWICE.test('abc!ab') // false
```
数字引用(`\1`)依然有效。
```javascript
const RE_TWICE = /^(?<word>[a-z]+)!\1$/;
RE_TWICE.test('abc!abc') // true
RE_TWICE.test('abc!ab') // false
```
这两种引用语法还可以同时使用。
```javascript
const RE_TWICE = /^(?<word>[a-z]+)!\k<word>!\1$/;
RE_TWICE.test('abc!abc!abc') // true
RE_TWICE.test('abc!abc!ab') // false
```

View File

@ -9,9 +9,9 @@ ES6提供了新的数据结构Set。它类似于数组但是成员的值都
Set 本身是一个构造函数,用来生成 Set 数据结构。
```javascript
var s = new Set();
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].map(x => s.add(x));
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
@ -21,16 +21,16 @@ for (let i of s) {
上面代码通过`add`方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。
Set函数可以接受一个数组(或类似数组的对象)作为参数,用来初始化。
Set 函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
```javascript
// 例一
var set = new Set([1, 2, 3, 4, 4]);
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
// 例二
var items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5
// 例三
@ -38,7 +38,7 @@ function divs () {
return [...document.querySelectorAll('div')];
}
var set = new Set(divs());
const set = new Set(divs());
set.size // 56
// 类似于
@ -116,7 +116,7 @@ s.has(2) // false
```javascript
// 对象的写法
var properties = {
const properties = {
'width': 1,
'height': 1
};
@ -126,7 +126,7 @@ if (properties[someName]) {
}
// Set的写法
var properties = new Set();
const properties = new Set();
properties.add('width');
properties.add('height');
@ -139,8 +139,8 @@ if (properties.has(someName)) {
`Array.from`方法可以将 Set 结构转为数组。
```javascript
var items = new Set([1, 2, 3, 4, 5]);
var array = Array.from(items);
const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);
```
这就提供了去除数组重复成员的另一种方法。
@ -166,7 +166,7 @@ Set结构的实例有四个遍历方法可以用于遍历成员。
**1`keys()``values()``entries()`**
`key`方法、`value`方法、`entries`方法返回的都是遍历器对象详见《Iterator对象》一章。由于Set结构没有键名只有键值或者说键名和键值是同一个值所以`key`方法和`value`方法的行为完全一致。
`keys`方法、`values`方法、`entries`方法返回的都是遍历器对象详见《Iterator 对象》一章)。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以`keys`方法和`values`方法的行为完全一致。
```javascript
let set = new Set(['red', 'green', 'blue']);
@ -217,17 +217,19 @@ for (let x of set) {
**2`forEach()`**
Set结构的实例的`forEach`方法,用于对每个成员执行某种操作,没有返回值。
Set 结构的实例与数组一样,也拥有`forEach`方法,用于对每个成员执行某种操作,没有返回值。
```javascript
let set = new Set([1, 2, 3]);
set.forEach((value, key) => console.log(value * 2) )
// 2
// 4
// 6
set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9
```
上面代码说明,`forEach`方法的参数就是一个处理函数。该函数的参数依次为键值、键名、集合本身(上例省略了该参数)。另外,`forEach`方法还可以有第二个参数表示绑定的this对象。
上面代码说明,`forEach`方法的参数就是一个处理函数。该函数的参数与数组的`forEach`一致依次为键值、键名、集合本身上例省略了该参数。这里需要注意Set 结构的键名就是键值(两者是同一个值),因此第一个参数与第二个参数的值永远都是一样的。
另外,`forEach`方法还可以有第二个参数,表示绑定处理函数内部的`this`对象。
**3遍历的应用**
@ -247,7 +249,7 @@ let unique = [...new Set(arr)];
// [3, 5, 2]
```
而且,数组的`map``filter`方法也可以用于Set了。
而且,数组的`map``filter`方法也可以间接用于 Set 了。
```javascript
let set = new Set([1, 2, 3]);
@ -296,14 +298,14 @@ set = new Set(Array.from(set, val => val * 2));
## WeakSet
### 含义
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
首先WeakSet 的成员只能是对象,而不能是其他类型的值。
其次WeakSet中的对象都是弱引用即垃圾回收机制不考虑WeakSet对该对象的引用也就是说如果其他对象都不再引用该对象那么垃圾回收机制会自动回收该对象所占用的内存不考虑该对象还存在于WeakSet之中。这个特点意味着无法引用WeakSet的成员因此WeakSet是不可遍历的。
```javascript
var ws = new WeakSet();
const ws = new WeakSet();
ws.add(1)
// TypeError: Invalid value used in weak set
ws.add(Symbol())
@ -312,17 +314,28 @@ ws.add(Symbol())
上面代码试图向 WeakSet 添加一个数值和`Symbol`值,结果报错,因为 WeakSet 只能放置对象。
其次WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
这是因为垃圾回收机制依赖引用计数,如果一个值的引用次数不为`0`垃圾回收机制就不会释放这块内存。结束使用该值之后有时会忘记取消引用导致内存无法释放进而可能会引发内存泄漏。WeakSet 里面的引用都不计入垃圾回收机制所以就不存在这个问题。因此WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。
由于上面这个特点WeakSet 的成员是不适合引用的,因为它会随时消失。另外,由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。
这些特点同样适用于本章后面要介绍的 WeakMap 结构。
### 语法
WeakSet 是一个构造函数,可以使用`new`命令,创建 WeakSet 数据结构。
```javascript
var ws = new WeakSet();
const ws = new WeakSet();
```
作为构造函数WeakSet可以接受一个数组或类似数组的对象作为参数。实际上任何具有iterable接口的对象都可以作为WeakSet的参数。该数组的所有成员都会自动成为WeakSet实例对象的成员。
作为构造函数WeakSet 可以接受一个数组或类似数组的对象作为参数。(实际上,任何具有 Iterable 接口的对象,都可以作为 WeakSet 的参数。)该数组的所有成员,都会自动成为 WeakSet 实例对象的成员。
```javascript
var a = [[1,2], [3,4]];
var ws = new WeakSet(a);
const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}
```
上面代码中,`a`是一个数组,它有两个成员,也都是数组。将`a`作为 WeakSet 构造函数的参数,`a`的成员会自动成为 WeakSet 的成员。
@ -330,8 +343,8 @@ var ws = new WeakSet(a);
注意,是`a`数组的成员成为 WeakSet 的成员,而不是`a`数组本身。这意味着,数组的成员只能是对象。
```javascript
var b = [3, 4];
var ws = new WeakSet(b);
const b = [3, 4];
const ws = new WeakSet(b);
// Uncaught TypeError: Invalid value used in weak set(…)
```
@ -346,9 +359,9 @@ WeakSet结构有以下三个方法。
下面是一个例子。
```javascript
var ws = new WeakSet();
var obj = {};
var foo = {};
const ws = new WeakSet();
const obj = {};
const foo = {};
ws.add(window);
ws.add(obj);
@ -394,13 +407,13 @@ class Foo {
## Map
### Map结构的目的和基本用法
### 含义和基本用法
JavaScript 的对象Object本质上是键值对的集合Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
```javascript
var data = {};
var element = document.getElementById('myDiv');
const data = {};
const element = document.getElementById('myDiv');
data[element] = 'metadata';
data['[object HTMLDivElement]'] // "metadata"
@ -411,8 +424,8 @@ data['[object HTMLDivElement]'] // "metadata"
为了解决这个问题ES6 提供了 Map 数据结构。它类似于对象也是键值对的集合但是“键”的范围不限于字符串各种类型的值包括对象都可以当作键。也就是说Object 结构提供了“字符串—值”的对应Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构Map 比 Object 更合适。
```javascript
var m = new Map();
var o = {p: 'Hello World'};
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
@ -422,12 +435,12 @@ m.delete(o) // true
m.has(o) // false
```
上面代码使用`set`方法,将对象`o`当作`m`的一个键,然后又使用`get`方法读取这个键,接着使用`delete`方法删除了这个键。
上面代码使用 Map 结构的`set`方法,将对象`o`当作`m`的一个键,然后又使用`get`方法读取这个键,接着使用`delete`方法删除了这个键。
作为构造函数Map也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
上面的例子展示了如何向 Map 添加成员。作为构造函数Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
```javascript
var map = new Map([
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
@ -441,33 +454,42 @@ map.get('title') // "Author"
上面代码在新建 Map 实例时,就指定了两个键`name``title`
Map构造函数接受数组作为参数实际上执行的是下面的算法。
`Map`构造函数接受数组作为参数,实际上执行的是下面的算法。
```javascript
var items = [
const items = [
['name', '张三'],
['title', 'Author']
];
var map = new Map();
items.forEach(([key, value]) => map.set(key, value));
const map = new Map();
items.forEach(
([key, value]) => map.set(key, value)
);
```
下面的例子中,字符串`true`和布尔值`true`是两个不同的键。
事实上,不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构详见《Iterator》一章都可以当作`Map`构造函数的参数。这就是说,`Set``Map`都可以用来生成新的 Map
```javascript
var m = new Map([
[true, 'foo'],
['true', 'bar']
const set = new Set([
['foo', 1],
['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1
m.get(true) // 'foo'
m.get('true') // 'bar'
const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3
```
上面代码中,我们分别使用 Set 对象和 Map 对象,当作`Map`构造函数的参数,结果都生成了新的 Map 对象。
如果对同一个键多次赋值,后面的值将覆盖前面的值。
```javascript
let map = new Map();
const map = new Map();
map
.set(1, 'aaa')
@ -488,7 +510,7 @@ new Map().get('asfddfsasadf')
注意只有对同一个对象的引用Map 结构才将其视为同一个键。这一点要非常小心。
```javascript
var map = new Map();
const map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
@ -499,10 +521,10 @@ map.get(['a']) // undefined
同理,同样的值的两个实例,在 Map 结构中被视为两个键。
```javascript
var map = new Map();
const map = new Map();
var k1 = ['a'];
var k2 = ['a'];
const k1 = ['a'];
const k2 = ['a'];
map
.set(k1, 111)
@ -516,16 +538,24 @@ map.get(k2) // 222
由上可知Map 的键实际上是跟内存地址绑定的只要内存地址不一样就视为两个键。这就解决了同名属性碰撞clash的问题我们扩展别人的库的时候如果使用对象作为键名就不用担心自己的属性与原作者的属性同名。
如果Map的键是一个简单类型的值数字、字符串、布尔值则只要两个值严格相等Map将其视为一个键,包括`0``-0`。另外,虽然`NaN`不严格相等于自身但Map将其视为同一个键。
如果 Map 的键是一个简单类型的值数字、字符串、布尔值则只要两个值严格相等Map 将其视为一个键,比如`0``-0`就是一个键,布尔值`true`和字符串`true`则是两个不同的键。另外,`undefined``null`也是两个不同的键。虽然`NaN`不严格相等于自身,但 Map 将其视为同一个键。
```javascript
let map = new Map();
map.set(NaN, 123);
map.get(NaN) // 123
map.set(-0, 123);
map.get(+0) // 123
map.set(true, 1);
map.set('true', 2);
map.get(true) // 1
map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3
map.set(NaN, 123);
map.get(NaN) // 123
```
### 实例的属性和操作方法
@ -537,7 +567,7 @@ Map结构的实例有以下属性和操作方法。
`size`属性返回 Map 结构的成员总数。
```javascript
let map = new Map();
const map = new Map();
map.set('foo', true);
map.set('bar', false);
@ -546,17 +576,17 @@ map.size // 2
**2set(key, value)**
`set`方法设置`key`对应的键值然后返回整个Map结构。如果`key`已经有值,则键值会被更新,否则就新生成该键。
`set`方法设置键名`key`对应的键值`value`,然后返回整个 Map 结构。如果`key`已经有值,则键值会被更新,否则就新生成该键。
```javascript
var m = new Map();
const m = new Map();
m.set("edition", 6) // 键是字符串
m.set(262, "standard") // 键是数值
m.set(undefined, "nah") // 键是undefined
m.set('edition', 6) // 键是字符串
m.set(262, 'standard') // 键是数值
m.set(undefined, 'nah') // 键是 undefined
```
`set`方法返回的是Map本身,因此可以采用链式写法。
`set`方法返回的是当前的`Map`对象,因此可以采用链式写法。
```javascript
let map = new Map()
@ -570,43 +600,44 @@ let map = new Map()
`get`方法读取`key`对应的键值,如果找不到`key`,返回`undefined`
```javascript
var m = new Map();
const m = new Map();
var hello = function() {console.log("hello");}
m.set(hello, "Hello ES6!") // 键是函数
const hello = function() {console.log('hello');};
m.set(hello, 'Hello ES6!') // 键是函数
m.get(hello) // Hello ES6!
```
**4has(key)**
`has`方法返回一个布尔值,表示某个键是否在Map数据结构中。
`has`方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
```javascript
var m = new Map();
const m = new Map();
m.set("edition", 6);
m.set(262, "standard");
m.set(undefined, "nah");
m.set('edition', 6);
m.set(262, 'standard');
m.set(undefined, 'nah');
m.has("edition") // true
m.has("years") // false
m.has('edition') // true
m.has('years') // false
m.has(262) // true
m.has(undefined) // true
```
**5delete(key)**
`delete`方法删除某个键返回true。如果删除失败返回false。
`delete`方法删除某个键,返回`true`。如果删除失败,返回`false`
```javascript
var m = new Map();
m.set(undefined, "nah");
const m = new Map();
m.set(undefined, 'nah');
m.has(undefined) // true
m.delete(undefined)
m.has(undefined) // false
```
**6clear()**
`clear`方法清除所有成员,没有返回值。
@ -623,7 +654,7 @@ map.size // 0
### 遍历方法
Map原生提供三个遍历器生成函数和一个遍历方法。
Map 结构原生提供三个遍历器生成函数和一个遍历方法。
- `keys()`:返回键名的遍历器。
- `values()`:返回键值的遍历器。
@ -632,10 +663,8 @@ Map原生提供三个遍历器生成函数和一个遍历方法。
需要特别注意的是Map 的遍历顺序就是插入顺序。
下面是使用实例。
```javascript
let map = new Map([
const map = new Map([
['F', 'no'],
['T', 'yes'],
]);
@ -662,11 +691,15 @@ for (let item of map.entries()) {
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
```
上面代码最后的那个例子,表示 Map 结构的默认遍历器接口(`Symbol.iterator`属性),就是`entries`方法。
@ -676,10 +709,10 @@ map[Symbol.iterator] === map.entries
// true
```
Map结构转为数组结构比较快速的方法是结合使用扩展运算符(`...`)。
Map 结构转为数组结构,比较快速的方法是使用扩展运算符(`...`)。
```javascript
let map = new Map([
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
@ -701,17 +734,17 @@ let map = new Map([
结合数组的`map`方法、`filter`方法,可以实现 Map 的遍历和过滤Map 本身没有`map``filter`方法)。
```javascript
let map0 = new Map()
const map0 = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c');
let map1 = new Map(
const map1 = new Map(
[...map0].filter(([k, v]) => k < 3)
);
// 产生 Map 结构 {1 => 'a', 2 => 'b'}
let map2 = new Map(
const map2 = new Map(
[...map0].map(([k, v]) => [k * 2, '_' + v])
);
// 产生 Map 结构 {2 => '_a', 4 => '_b', 6 => '_c'}
@ -728,7 +761,7 @@ map.forEach(function(value, key, map) {
`forEach`方法还可以接受第二个参数,用来绑定`this`
```javascript
var reporter = {
const reporter = {
report: function(key, value) {
console.log("Key: %s, Value: %s", key, value);
}
@ -745,21 +778,29 @@ map.forEach(function(value, key, map) {
**1Map 转为数组**
前面已经提过Map转为数组最方便的方法就是使用扩展运算符...)。
前面已经提过Map 转为数组最方便的方法,就是使用扩展运算符(`...`)。
```javascript
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
const myMap = new Map()
.set(true, 7)
.set({foo: 3}, ['abc']);
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
```
**2数组 转为 Map**
将数组转入Map构造函数就可以转为Map。
将数组传入 Map 构造函数,就可以转为 Map。
```javascript
new Map([[true, 7], [{foo: 3}, ['abc']]])
// Map {true => 7, Object {foo: 3} => ['abc']}
new Map([
[true, 7],
[{foo: 3}, ['abc']]
])
// Map {
// true => 7,
// Object {foo: 3} => ['abc']
// }
```
**3Map 转为对象**
@ -775,7 +816,9 @@ function strMapToObj(strMap) {
return obj;
}
let myMap = new Map().set('yes', true).set('no', false);
const myMap = new Map()
.set('yes', true)
.set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }
```
@ -792,7 +835,7 @@ function objToStrMap(obj) {
}
objToStrMap({yes: true, no: false})
// [ [ 'yes', true ], [ 'no', false ] ]
// Map {"yes" => true, "no" => false}
```
**5Map 转为 JSON**
@ -847,48 +890,188 @@ jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
## WeakMap
`WeakMap`结构与`Map`结构基本类似,唯一的区别是它只接受对象作为键名(`null`除外),不接受其他类型的值作为键名,而且键名所指向的对象,不计入垃圾回收机制。
### 含义
`WeakMap`结构与`Map`结构类似,也是用于生成键值对的集合。
```javascript
var map = new WeakMap()
// WeakMap 可以使用 set 方法添加成员
const wm1 = new WeakMap();
const key = {foo: 1};
wm1.set(key, 2);
wm1.get(key) // 2
// WeakMap 也可以接受一个数组,
// 作为构造函数的参数
const k1 = [1, 2, 3];
const k2 = [4, 5, 6];
const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);
wm2.get(k2) // "bar"
```
`WeakMap``Map`的区别有两点。
首先,`WeakMap`只接受对象作为键名(`null`除外),不接受其他类型的值作为键名。
```javascript
const map = new WeakMap();
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
map.set(null, 2)
// TypeError: Invalid value used as weak map key
```
上面代码中,如果将`1``Symbol`作为WeakMap的键名都会报错。
上面代码中,如果将数值`1``Symbol`作为 WeakMap 的键名,都会报错。
`WeakMap`的设计目的在于,键名是对象的弱引用(垃圾回收机制不将该引用考虑在内),所以其所对应的对象可能会被自动回收。当对象被回收后,`WeakMap`自动移除对应的键值对。典型应用是一个对应DOM元素的`WeakMap`结构当某个DOM元素被清除其所对应的`WeakMap`记录就会自动被移除。基本上,`WeakMap`的专用场合就是,它的键所对应的对象,可能会在将来消失。`WeakMap`结构有助于防止内存泄漏。
其次,`WeakMap`的键名所指向的对象,不计入垃圾回收机制
下面是`WeakMap`结构的一个例子,可以看到用法上与`Map`几乎一样。
`WeakMap`的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。请看下面的例子
```javascript
var wm = new WeakMap();
var element = document.querySelector(".element");
wm.set(element, "Original");
wm.get(element) // "Original"
element.parentNode.removeChild(element);
element = null;
wm.get(element) // undefined
const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
[e1, 'foo 元素'],
[e2, 'bar 元素'],
];
```
上面代码中,变量`wm`是一个`WeakMap`实例,我们将一个`DOM`节点`element`作为键名,然后销毁这个节点,`element`对应的键就自动消失了,再引用这个键名就返回`undefined`
上面代码中,`e1``e2`是两个对象,我们通过`arr`数组对这两个对象添加一些文字说明。这就形成了`arr``e1``e2`的引用。
WeakMap与Map在API上的区别主要是两个一是没有遍历操作即没有`key()``values()``entries()`方法),也没有`size`属性;二是无法清空,即不支持`clear`方法。这与`WeakMap`的键不被计入引用、被垃圾回收机制忽略有关。因此,`WeakMap`只有四个方法可用:`get()``set()``has()``delete()`
一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放`e1``e2`占用的内存
```javascript
var wm = new WeakMap();
wm.size
// undefined
wm.forEach
// undefined
// 不需要 e1 和 e2 的时候
// 必须手动删除引用
arr [0] = null;
arr [1] = null;
```
上面这样的写法显然很不方便。一旦忘了写,就会造成内存泄露。
WeakMap 就是为了解决这个问题而诞生的它的键名所引用的对象都是弱引用即垃圾回收机制不将该引用考虑在内。因此只要所引用的对象的其他引用都被清除垃圾回收机制就会释放该对象所占用的内存。也就是说一旦不再需要WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。一个典型应用场景是在网页的 DOM 元素上添加数据,就可以使用`WeakMap`结构。当该 DOM 元素被清除,其所对应的`WeakMap`记录就会自动被移除。
```javascript
const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"
```
上面代码中,先新建一个 Weakmap 实例。然后,将一个 DOM 节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在 WeakMap 里面。这时WeakMap 里面对`element`的引用就是弱引用,不会被计入垃圾回收机制。
也就是说,上面的 DOM 节点对象的引用计数是`1`,而不是`2`。这时一旦消除对该节点的引用它占用的内存就会被垃圾回收机制释放。Weakmap 保存的这个键值对,也会自动消失。
总之,`WeakMap`的专用场合就是,它的键所对应的对象,可能会在将来消失。`WeakMap`结构有助于防止内存泄漏。
注意WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。
```javascript
const wm = new WeakMap();
let key = {};
let obj = {foo: 1};
wm.set(key, obj);
obj = null;
wm.get(key)
// Object {foo: 1}
```
上面代码中,键值`obj`是正常引用。所以,即使在 WeakMap 外部消除了`obj`的引用WeakMap 内部的引用依然存在。
### WeakMap 的语法
WeakMap 与 Map 在 API 上的区别主要是两个,一是没有遍历操作(即没有`key()``values()``entries()`方法),也没有`size`属性。因为没有办法列出所有键名,某个键名是否存在完全不可预测,跟垃圾回收机制是否运行相关。这一刻可以取到键名,下一刻垃圾回收机制突然运行了,这个键名就没了,为了防止出现不确定性,就统一规定不能取到键名。二是无法清空,即不支持`clear`方法。因此,`WeakMap`只有四个方法可用:`get()``set()``has()``delete()`
```javascript
const wm = new WeakMap();
// size、forEach、clear 方法都不存在
wm.size // undefined
wm.forEach // undefined
wm.clear // undefined
```
### WeakMap 的示例
WeakMap 的例子很难演示,因为无法观察它里面的引用会自动消失。此时,其他引用都解除了,已经没有引用指向 WeakMap 的键名了,导致无法证实那个键名是不是存在。
贺师俊老师[提示](https://github.com/ruanyf/es6tutorial/issues/362#issuecomment-292109104),如果引用所指向的值占用特别多的内存,就可以通过 Node 的`process.memoryUsage`方法看出来。根据这个思路,网友[vtxf](https://github.com/ruanyf/es6tutorial/issues/362#issuecomment-292451925)补充了下面的例子。
首先,打开 Node 命令行。
```bash
$ node --expose-gc
```
上面代码中,`--expose-gc`参数表示允许手动执行垃圾回收机制。
然后,执行下面的代码。
```javascript
// 手动执行一次垃圾回收,保证获取的内存使用状态准确
> global.gc();
undefined
// 查看内存占用的初始状态heapUsed 为 4M 左右
> process.memoryUsage();
{ rss: 21106688,
heapTotal: 7376896,
heapUsed: 4153936,
external: 9059 }
> let wm = new WeakMap();
undefined
// 新建一个变量 key指向一个 5*1024*1024 的数组
> let key = new Array(5 * 1024 * 1024);
undefined
// 设置 WeakMap 实例的键名,也指向 key 数组
// 这时key 数组实际被引用了两次,
// 变量 key 引用一次WeakMap 的键名引用了第二次
// 但是WeakMap 是弱引用对于引擎来说引用计数还是1
> wm.set(key, 1);
WeakMap {}
> global.gc();
undefined
// 这时内存占用 heapUsed 增加到 45M 了
> process.memoryUsage();
{ rss: 67538944,
heapTotal: 7376896,
heapUsed: 45782816,
external: 8945 }
// 清除变量 key 对数组的引用,
// 但没有手动清除 WeakMap 实例的键名对数组的引用
> key = null;
null
// 再次执行垃圾回收
> global.gc();
undefined
// 内存占用 heapUsed 变回 4M 左右,
// 可以看到 WeakMap 的键名引用没有阻止 gc 对内存的回收
> process.memoryUsage();
{ rss: 20639744,
heapTotal: 8425472,
heapUsed: 3979792,
external: 8956 }
```
上面代码中只要外部的引用消失WeakMap 内部的引用,就会自动被垃圾回收清除。由此可见,有了 WeakMap 的帮助,解决内存泄漏就会简单很多。
### WeakMap 的用途
前文说过WeakMap 应用的典型场合就是 DOM 节点作为键名。下面是一个例子。
```javascript
@ -908,8 +1091,8 @@ myElement.addEventListener('click', function() {
WeakMap 的另一个用处是部署私有属性。
```javascript
let _counter = new WeakMap();
let _action = new WeakMap();
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
@ -927,11 +1110,11 @@ class Countdown {
}
}
let c = new Countdown(2, () => console.log('DONE'));
const c = new Countdown(2, () => console.log('DONE'));
c.dec()
c.dec()
// DONE
```
上面代码中Countdown类的两个内部属性`_counter``_action`,是实例的弱引用,所以如果删除实例,它们也就随之消失,不会造成内存泄漏。
上面代码中,`Countdown`类的两个内部属性`_counter``_action`,是实例的弱引用,所以如果删除实例,它们也就随之消失,不会造成内存泄漏。

View File

@ -43,7 +43,7 @@ v + w = 〈v1, …, vn〉+ 〈w1, …, wn〉
上面代码中,`v``w`是两个多元矢量。它们的加运算,在 SIMD 下是一个指令、而不是 n 个指令完成的,这就大大提高了效率。这对于 3D 动画、图像处理、信号处理、数值处理、加密等运算是非常重要的。比如Canvas 的`getImageData()`会将图像文件读成一个二进制数组SIMD 就很适合对于这种数组的处理。
得来说SIMD是数据并行处理parallelism的一种手段可以加速一些运算密集型操作的速度。
的来说SIMD 是数据并行处理parallelism的一种手段可以加速一些运算密集型操作的速度。将来与 WebAssembly 结合以后,可以让 JavaScript 达到二进制代码的运行速度。
## 数据类型
@ -167,7 +167,7 @@ SIMD.Int16x8.subSaturate(c, d)
// Int16x8[-32768, 0, 0, 0, 0, 0, 0, 0, 0]
```
上面代码中,`Uint16`的最小值是`0``subSaturate`的最小值是`-32678`。一旦运算发生溢出,就返回最小值。
上面代码中,`Uint16`的最小值是`0``Int16`的最小值是`-32678`。一旦运算发生溢出,就返回最小值。
### SIMD.%type%.mul()SIMD.%type%.div()SIMD.%type%.sqrt()
@ -713,4 +713,3 @@ function average(list) {
```
上面代码先是每隔四位,将所有的值读入一个 SIMD然后立刻累加。然后得到累加值四个通道的总和再除以`n`就可以了。

View File

@ -40,19 +40,19 @@ ECMAScript 6规格的26章之中第1章到第3章是对文件本身的介绍
> 1. ReturnIfAbrupt(x).
> 1. ReturnIfAbrupt(y).
> 1. If `Type(x)` is the same as `Type(y)`, then
> 1. If `Type(x)` is the same as `Type(y)`, then\
> Return the result of performing Strict Equality Comparison `x === y`.
> 1. If `x` is `null` and `y` is `undefined`, return `true`.
> 1. If `x` is `undefined` and `y` is `null`, return `true`.
> 1. If `Type(x)` is Number and `Type(y)` is String,
> 1. If `Type(x)` is Number and `Type(y)` is String,\
> return the result of the comparison `x == ToNumber(y)`.
> 1. If `Type(x)` is String and `Type(y)` is Number,
> 1. If `Type(x)` is String and `Type(y)` is Number,\
> return the result of the comparison `ToNumber(x) == y`.
> 1. If `Type(x)` is Boolean, return the result of the comparison `ToNumber(x) == y`.
> 1. If `Type(y)` is Boolean, return the result of the comparison `x == ToNumber(y)`.
> 1. If `Type(x)` is either String, Number, or Symbol and `Type(y)` is Object, then
> 1. If `Type(x)` is either String, Number, or Symbol and `Type(y)` is Object, then\
> return the result of the comparison `x == ToPrimitive(y)`.
> 1. If `Type(x)` is Object and `Type(y)` is either String, Number, or Symbol, then
> 1. If `Type(x)` is Object and `Type(y)` is either String, Number, or Symbol, then\
> return the result of the comparison `ToPrimitive(x) == y`.
> 1. Return `false`.
@ -145,17 +145,17 @@ a2.map(n => 1) // [, , ,]
> 1. Let `A` be `ArraySpeciesCreate(O, len)`.
> 1. `ReturnIfAbrupt(A)`.
> 1. Let `k` be 0.
> 1. Repeat, while `k` < `len`
> a. Let `Pk` be `ToString(k)`.
> b. Let `kPresent` be `HasProperty(O, Pk)`.
> c. `ReturnIfAbrupt(kPresent)`.
> d. If `kPresent` is `true`, then
> d-1. Let `kValue` be `Get(O, Pk)`.
> d-2. `ReturnIfAbrupt(kValue)`.
> d-3. Let `mappedValue` be `Call(callbackfn, T, «kValue, k, O»)`.
> d-4. `ReturnIfAbrupt(mappedValue)`.
> d-5. Let `status` be `CreateDataPropertyOrThrow (A, Pk, mappedValue)`.
> d-6. `ReturnIfAbrupt(status)`.
> 1. Repeat, while `k` < `len`\
> a. Let `Pk` be `ToString(k)`.\
> b. Let `kPresent` be `HasProperty(O, Pk)`.\
> c. `ReturnIfAbrupt(kPresent)`.\
> d. If `kPresent` is `true`, then\
> d-1. Let `kValue` be `Get(O, Pk)`.\
> d-2. `ReturnIfAbrupt(kValue)`.\
> d-3. Let `mappedValue` be `Call(callbackfn, T, «kValue, k, O»)`.\
> d-4. `ReturnIfAbrupt(mappedValue)`.\
> d-5. Let `status` be `CreateDataPropertyOrThrow (A, Pk, mappedValue)`.\
> d-6. `ReturnIfAbrupt(status)`.\
> e. Increase `k` by 1.
> 1. Return `A`.
@ -170,17 +170,17 @@ a2.map(n => 1) // [, , ,]
> 1. 生成一个新的数组`A`,跟当前数组的`length`属性保持一致
> 1. 如果报错就返回
> 1. 设定`k`等于 0
> 1. 只要`k`小于当前数组的`length`属性,就重复下面步骤
> a. 设定`Pk`等于`ToString(k)`,即将`K`转为字符串
> b. 设定`kPresent`等于`HasProperty(O, Pk)`,即求当前数组有没有指定属性
> c. 如果报错就返回
> d. 如果`kPresent`等于`true`,则进行下面步骤
> d-1. 设定`kValue`等于`Get(O, Pk)`,取出当前数组的指定属性
> d-2. 如果报错就返回
> d-3. 设定`mappedValue`等于`Call(callbackfn, T, «kValue, k, O»)`,即执行回调函数
> d-4. 如果报错就返回
> d-5. 设定`status`等于`CreateDataPropertyOrThrow (A, Pk, mappedValue)`,即将回调函数的值放入`A`数组的指定位置
> d-6. 如果报错就返回
> 1. 只要`k`小于当前数组的`length`属性,就重复下面步骤\
> a. 设定`Pk`等于`ToString(k)`,即将`K`转为字符串\
> b. 设定`kPresent`等于`HasProperty(O, Pk)`,即求当前数组有没有指定属性\
> c. 如果报错就返回\
> d. 如果`kPresent`等于`true`,则进行下面步骤\
> d-1. 设定`kValue`等于`Get(O, Pk)`,取出当前数组的指定属性\
> d-2. 如果报错就返回\
> d-3. 设定`mappedValue`等于`Call(callbackfn, T, «kValue, k, O»)`,即执行回调函数\
> d-4. 如果报错就返回\
> d-5. 设定`status`等于`CreateDataPropertyOrThrow (A, Pk, mappedValue)`,即将回调函数的值放入`A`数组的指定位置\
> d-6. 如果报错就返回\
> e. `k`增加 1
> 1. 返回`A`

View File

@ -4,14 +4,14 @@ ES6加强了对Unicode的支持并且扩展了字符串对象。
## 字符的 Unicode 表示法
JavaScript允许采用`\uxxxx`形式表示一个字符,其中“xxxx”表示字符的码点。
JavaScript 允许采用`\uxxxx`形式表示一个字符,其中`xxxx`表示字符的 Unicode 码点。
```javascript
"\u0061"
// "a"
```
但是,这种表示法只限于`\u0000`——`\uFFFF`之间的字符。超出这个范围的字符,必须用两个双字节的形式表
但是,这种表示法只限于码点在`\u0000`~`\uFFFF`之间的字符。超出这个范围的字符,必须用两个双字节的形式表
```javascript
"\uD842\uDFB7"
@ -65,12 +65,12 @@ s.charCodeAt(0) // 55362
s.charCodeAt(1) // 57271
```
上面代码中,汉字“𠮷”(注意,这个字不是”吉祥“的”吉“)的码点是`0x20BB7`UTF-16编码为`0xD842 0xDFB7`(十进制为`55362 57271`),需要`4`个字节储存。对于这种`4`个字节的字符JavaScript不能正确处理字符串长度会误判为`2`,而且`charAt`方法无法读取整个字符,`charCodeAt`方法只能分别返回前两个字节和后两个字节的值。
上面代码中,汉字“𠮷”(注意,这个字不是“吉祥”的“吉”)的码点是`0x20BB7`UTF-16 编码为`0xD842 0xDFB7`(十进制为`55362 57271`),需要`4`个字节储存。对于这种`4`个字节的字符JavaScript 不能正确处理,字符串长度会误判为`2`,而且`charAt`方法无法读取整个字符,`charCodeAt`方法只能分别返回前两个字节和后两个字节的值。
ES6 提供了`codePointAt`方法,能够正确处理 4 个字节储存的字符,返回一个字符的码点。
```javascript
var s = '𠮷a';
let s = '𠮷a';
s.codePointAt(0) // 134071
s.codePointAt(1) // 57271
@ -85,7 +85,7 @@ s.codePointAt(2) // 97
`codePointAt`方法返回的是码点的十进制值,如果想要十六进制的值,可以使用`toString`方法转换一下。
```javascript
var s = '𠮷a';
let s = '𠮷a';
s.codePointAt(0).toString(16) // "20bb7"
s.codePointAt(2).toString(16) // "61"
@ -94,7 +94,7 @@ s.codePointAt(2).toString(16) // "61"
你可能注意到了,`codePointAt`方法的参数,仍然是不正确的。比如,上面代码中,字符`a`在字符串`s`的正确位置序号应该是 1但是必须向`codePointAt`方法传入 2。解决这个问题的一个办法是使用`for...of`循环,因为它会正确识别 32 位的 UTF-16 字符。
```javascript
var s = '𠮷a';
let s = '𠮷a';
for (let ch of s) {
console.log(ch.codePointAt(0).toString(16));
}
@ -124,7 +124,7 @@ String.fromCharCode(0x20BB7)
上面代码中,`String.fromCharCode`不能识别大于`0xFFFF`的码点,所以`0x20BB7`就发生了溢出,最高位`2`被舍弃了,最后返回码点`U+0BB7`对应的字符,而不是码点`U+20BB7`对应的字符。
ES6提供了`String.fromCodePoint`方法,可以识别`0xFFFF`的字符,弥补了`String.fromCharCode`方法的不足。在作用上,正好与`codePointAt`方法相反。
ES6 提供了`String.fromCodePoint`方法,可以识别大于`0xFFFF`的字符,弥补了`String.fromCharCode`方法的不足。在作用上,正好与`codePointAt`方法相反。
```javascript
String.fromCodePoint(0x20BB7)
@ -153,7 +153,7 @@ for (let codePoint of 'foo') {
除了遍历字符串,这个遍历器最大的优点是可以识别大于`0xFFFF`的码点,传统的`for`循环无法识别这样的码点。
```javascript
var text = String.fromCodePoint(0x20BB7);
let text = String.fromCodePoint(0x20BB7);
for (let i = 0; i < text.length; i++) {
console.log(text[i]);
@ -232,11 +232,11 @@ ES6提供字符串实例的`normalize()`方法,用来将字符的不同表示
传统上JavaScript 只有`indexOf`方法可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。
- **includes()**:返回布尔值,表示是否找到了参数字符串。
- **startsWith()**:返回布尔值,表示参数字符串是否在字符串的头部。
- **endsWith()**:返回布尔值,表示参数字符串是否在字符串的尾部。
- **startsWith()**:返回布尔值,表示参数字符串是否在字符串的头部。
- **endsWith()**:返回布尔值,表示参数字符串是否在字符串的尾部。
```javascript
var s = 'Hello world!';
let s = 'Hello world!';
s.startsWith('Hello') // true
s.endsWith('!') // true
@ -246,7 +246,7 @@ s.includes('o') // true
这三个方法都支持第二个参数,表示开始搜索的位置。
```javascript
var s = 'Hello world!';
let s = 'Hello world!';
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
@ -301,7 +301,7 @@ s.includes('Hello', 6) // false
## padStart()padEnd()
ES7推出了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。`padStart`用于头部补全,`padEnd`用于尾部补全。
ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。`padStart()`用于头部补全,`padEnd()`用于尾部补全。
```javascript
'x'.padStart(5, 'ab') // 'ababx'
@ -327,7 +327,7 @@ ES7推出了字符串补全长度的功能。如果某个字符串不够指定
// '0123456abc'
```
如果省略第二个参数,则会用空格补全长度。
如果省略第二个参数,默认使用空格补全长度。
```javascript
'x'.padStart(4) // ' x'
@ -386,14 +386,14 @@ console.log(`string text line 1
string text line 2`);
// 字符串中嵌入变量
var name = "Bob", time = "today";
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
```
上面代码中的模板字符串,都是用反引号表示。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。
```javascript
var greeting = `\`Yo\` World!`;
let greeting = `\`Yo\` World!`;
```
如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。
@ -409,7 +409,6 @@ $('#list').html(`
上面代码中,所有模板字符串的空格和换行,都是被保留的,比如`<ul>`标签前面会有一个换行。如果你不想要这个换行,可以使用`trim`方法消除它。
```javascript
$('#list').html(`
<ul>
@ -439,8 +438,8 @@ function authorize(user, action) {
大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。
```javascript
var x = 1;
var y = 2;
let x = 1;
let y = 2;
`${x} + ${y} = ${x + y}`
// "1 + 2 = 3"
@ -448,9 +447,9 @@ var y = 2;
`${x} + ${y * 2} = ${x + y * 2}`
// "1 + 4 = 5"
var obj = {x: 1, y: 2};
let obj = {x: 1, y: 2};
`${obj.x + obj.y}`
// 3
// "3"
```
模板字符串之中还能调用函数。
@ -470,7 +469,7 @@ function fn() {
```javascript
// 变量place没有声明
var msg = `Hello, ${place}`;
let msg = `Hello, ${place}`;
// 报错
```
@ -533,9 +532,9 @@ func('Jack') // "Hello Jack!"
下面,我们来看一个通过模板字符串,生成正式模板的实例。
```javascript
var template = `
let template = `
<ul>
<% for(var i=0; i < data.supplies.length; i++) { %>
<% for(let i=0; i < data.supplies.length; i++) { %>
<li><%= data.supplies[i] %></li>
<% } %>
</ul>
@ -550,7 +549,7 @@ var template = `
```javascript
echo('<ul>');
for(var i=0; i < data.supplies.length; i++) {
for(let i=0; i < data.supplies.length; i++) {
echo('<li>');
echo(data.supplies[i]);
echo('</li>');
@ -561,8 +560,8 @@ echo('</ul>');
这个转换使用正则表达式就行了。
```javascript
var evalExpr = /<%=(.+?)%>/g;
var expr = /<%([\s\S]+?)%>/g;
let evalExpr = /<%=(.+?)%>/g;
let expr = /<%([\s\S]+?)%>/g;
template = template
.replace(evalExpr, '`); \n echo( $1 ); \n echo(`')
@ -574,9 +573,9 @@ template = 'echo(`' + template + '`);';
然后,将`template`封装在一个函数里面返回,就可以了。
```javascript
var script =
let script =
`(function parse(data){
var output = "";
let output = "";
function echo(html){
output += html;
@ -594,8 +593,8 @@ return script;
```javascript
function compile(template){
var evalExpr = /<%=(.+?)%>/g;
var expr = /<%([\s\S]+?)%>/g;
const evalExpr = /<%=(.+?)%>/g;
const expr = /<%([\s\S]+?)%>/g;
template = template
.replace(evalExpr, '`); \n echo( $1 ); \n echo(`')
@ -603,9 +602,9 @@ function compile(template){
template = 'echo(`' + template + '`);';
var script =
let script =
`(function parse(data){
var output = "";
let output = "";
function echo(html){
output += html;
@ -623,7 +622,7 @@ function compile(template){
`compile`函数的用法如下。
```javascript
var parse = eval(compile(template));
let parse = eval(compile(template));
div.innerHTML = parse({ supplies: [ "broom", "mop", "cleaner" ] });
// <ul>
// <li>broom</li>
@ -647,8 +646,8 @@ alert(123)
但是,如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。
```javascript
var a = 5;
var b = 10;
let a = 5;
let b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
@ -690,8 +689,8 @@ tag(['Hello ', ' world ', ''], 15, 50)
我们可以按照需要编写`tag`函数的代码。下面是`tag`函数的一种写法,以及运行结果。
```javascript
var a = 5;
var b = 10;
let a = 5;
let b = 10;
function tag(s, v1, v2) {
console.log(s[0]);
@ -715,12 +714,12 @@ tag`Hello ${ a + b } world ${ a * b}`;
下面是一个更复杂的例子。
```javascript
var total = 30;
var msg = passthru`The total is ${total} (${total*1.05} with tax)`;
let total = 30;
let msg = passthru`The total is ${total} (${total*1.05} with tax)`;
function passthru(literals) {
var result = '';
var i = 0;
let result = '';
let i = 0;
while (i < literals.length) {
result += literals[i++];
@ -741,8 +740,9 @@ msg // "The total is 30 (31.5 with tax)"
```javascript
function passthru(literals, ...values) {
var output = "";
for (var index = 0; index < values.length; index++) {
let output = "";
let index;
for (index = 0; index < values.length; index++) {
output += literals[index] + values[index];
}
@ -754,13 +754,13 @@ function passthru(literals, ...values) {
“标签模板”的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容。
```javascript
var message =
let message =
SaferHTML`<p>${sender} has sent you a message.</p>`;
function SaferHTML(templateData) {
var s = templateData[0];
for (var i = 1; i < arguments.length; i++) {
var arg = String(arguments[i]);
let s = templateData[0];
for (let i = 1; i < arguments.length; i++) {
let arg = String(arguments[i]);
// Escape special characters in the substitution.
s += arg.replace(/&/g, "&amp;")
@ -777,14 +777,13 @@ function SaferHTML(templateData) {
上面代码中,`sender`变量往往是用户提供的,经过`SaferHTML`函数处理,里面的特殊字符都会被转义。
```javascript
var sender = '<script>alert("abc")</script>'; // 恶意代码
var message = SaferHTML`<p>${sender} has sent you a message.</p>`;
let sender = '<script>alert("abc")</script>'; // 恶意代码
let message = SaferHTML`<p>${sender} has sent you a message.</p>`;
message
// <p>&lt;script&gt;alert("abc")&lt;/script&gt; has sent you a message.</p>
```
标签模板的另一个应用,就是多语言转换(国际化处理)。
```javascript
@ -797,7 +796,7 @@ i18n`Welcome to ${siteName}, you are visitor number ${visitorNumber}!`
```javascript
// 下面的hashTemplate函数
// 是一个自定义的模板处理函数
var libraryHtml = hashTemplate`
let libraryHtml = hashTemplate`
<ul>
#for book in ${myBooks}
<li><i>#{book.title}</i> by #{book.author}</li>
@ -851,7 +850,8 @@ tag`First line\nSecond line`
function tag(strings) {
console.log(strings.raw[0]);
// "First line\\nSecond line"
// strings.raw[0] 为 "First line\\nSecond line"
// 打印输出 "First line\nSecond line"
}
```
@ -882,8 +882,9 @@ String.raw`Hi\\n`
```javascript
String.raw = function (strings, ...values) {
var output = "";
for (var index = 0; index < values.length; index++) {
let output = "";
let index;
for (index = 0; index < values.length; index++) {
output += strings.raw[index] + values[index];
}
@ -906,9 +907,9 @@ String.raw({ raw: ['t','e','s','t'] }, 0, 1, 2);
## 模板字符串的限制
前面提到标签模板里面,可以内嵌其他语言。但是,模板字符串默认会将字符串转义,因此导致无法嵌入其他语言。
前面提到标签模板里面,可以内嵌其他语言。但是,模板字符串默认会将字符串转义,导致无法嵌入其他语言。
举例来说,在标签模板里面可以嵌入Latex语言。
举例来说,标签模板里面可以嵌入 LaTEX 语言。
```javascript
function latex(strings) {
@ -924,9 +925,9 @@ Breve over the h goes \u{h}ere // 报错
`
```
上面代码中,变量`document`内嵌的模板字符串,对于Latex语言来说完全是合法的但是JavaScript引擎会报错。原因就在于字符串的转义。
上面代码中,变量`document`内嵌的模板字符串,对于 LaTEX 语言来说完全是合法的,但是 JavaScript 引擎会报错。原因就在于字符串的转义。
模板字符串会将`\u00FF``\u{42}`当作Unicode字符进行转义所以`\unicode`解析时报错;而`\x56`会被当作十六进制字符串转义,所以`\xerxes`会报错。
模板字符串会将`\u00FF``\u{42}`当作 Unicode 字符进行转义,所以`\unicode`解析时报错;而`\x56`会被当作十六进制字符串转义,所以`\xerxes`会报错。也就是说,`\u``\x`在 LaTEX 里面有特殊含义,但是 JavaScript 将它们转义了。
为了解决这个问题,现在有一个[提案](https://tc39.github.io/proposal-template-literal-revision/),放松对标签模板里面的字符串转义的限制。如果遇到不合法的字符串转义,就返回`undefined`,而不是报错,并且从`raw`属性上面可以得到原始字符串。

View File

@ -2,7 +2,7 @@
本章探讨如何将 ES6 的新语法,运用到编码实践之中,与传统的 JavaScript 语法结合在一起,写出合理的、易于阅读和维护的代码。
多家公司和组织已经公开了它们的风格规范,具体可参阅[jscs.info](http://jscs.info/)下面的内容主要参考了[Airbnb](https://github.com/airbnb/javascript)的JavaScript风格规范。
多家公司和组织已经公开了它们的风格规范,下面的内容主要参考了 [Airbnb](https://github.com/airbnb/javascript) 公司 JavaScript 风格规范。
## 块级作用域
@ -43,7 +43,7 @@ if(true) {
`let``const`之间,建议优先使用`const`,尤其是在全局环境,不应该设置变量,只应设置常量。
`const`优于`let`有几个原因。一个是`const`可以提醒阅读程序的人,这个变量不应该改变;另一个是`const`比较符合函数式编程思想,运算不改变值,只是新建值,而且这样也有利于将来的分布式运算;最后一个原因是 JavaScript 编译器会对`const`进行优化,所以多使用`const`,有利于提程序的运行效率,也就是说`let``const`的本质区别,其实是编译器内部的处理不同。
`const`优于`let`有几个原因。一个是`const`可以提醒阅读程序的人,这个变量不应该改变;另一个是`const`比较符合函数式编程思想,运算不改变值,只是新建值,而且这样也有利于将来的分布式运算;最后一个原因是 JavaScript 编译器会对`const`进行优化,所以多使用`const`,有利于提程序的运行效率,也就是说`let``const`的本质区别,其实是编译器内部的处理不同。
```javascript
// bad
@ -62,7 +62,7 @@ const [a, b, c] = [1, 2, 3];
所有的函数都应该设置为常量。
长远来看JavaScript可能会有多线程的实现比如Intel的River Trail那一类的项目这时`let`表示的变量,只应出现在单线程运行的代码中,不能是多线程共享的,这样有利于保证线程安全。
长远来看JavaScript 可能会有多线程的实现(比如 Intel 公司 River Trail 那一类的项目),这时`let`表示的变量,只应出现在单线程运行的代码中,不能是多线程共享的,这样有利于保证线程安全。
## 字符串
@ -427,22 +427,22 @@ module.exports = Breadcrumbs;
// ES6的写法
import React from 'react';
const Breadcrumbs = React.createClass({
class Breadcrumbs extends React.Component {
render() {
return <nav />;
}
});
};
export default Breadcrumbs
export default Breadcrumbs;
```
如果模块只有一个输出值,就使用`export default`,如果模块有多个输出值,就不使用`export default`不要`export default`与普通的`export`同时使用。
如果模块只有一个输出值,就使用`export default`,如果模块有多个输出值,就不使用`export default``export default`与普通的`export`不要同时使用。
不要在模块输入中使用通配符。因为这样可以确保你的模块之中有一个默认输出export default
```javascript
// bad
import * as myObject './importModule';
import * as myObject from './importModule';
// good
import myObject from './importModule';
@ -478,10 +478,11 @@ ESLint是一个语法规则和代码风格的检查工具可以用来保证
$ npm i -g eslint
```
然后安装Airbnb语法规则。
然后,安装 Airbnb 语法规则,以及 import、a11y、react 插件
```bash
$ npm i -g eslint-config-airbnb
$ npm i -g eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react
```
最后,在项目的根目录下新建一个`.eslintrc`文件,配置 ESLint。
@ -507,16 +508,18 @@ function greet() {
greet();
```
使用ESLint检查这个文件。
使用 ESLint 检查这个文件,就会报出错误
```bash
$ eslint index.js
index.js
1:1 error Unexpected var, use let or const instead no-var
1:5 error unusued is defined but never used no-unused-vars
4:5 error Expected indentation of 2 characters but found 4 indent
4:5 error Unexpected var, use let or const instead no-var
5:5 error Expected indentation of 2 characters but found 4 indent
3 problems (3 errors, 0 warnings)
5 problems (5 errors, 0 warnings)
```
上面代码说明,原文件有三个错误一个是定义了变量却没有使用另外两个是行首缩进为4个空格而不是规定的2个空格。
上面代码说明,原文件有五个错误,其中两个是不应该使用`var`命令,而要使用`let``const`;一个是定义了变量,却没有使用;另外两个是行首缩进为 4 个空格,而不是规定的 2 个空格。

View File

@ -2,9 +2,9 @@
## 概述
ES5的对象属性名都是字符串这容易造成属性名的冲突。比如你使用了一个他人提供的对象但又想为这个对象添加新的方法mixin模式新方法的名字就有可能与现有方法产生冲突。如果有一种机制保证每个属性的名字都是独一无二的就好了这样就从根本上防止属性名的冲突。这就是ES6引入Symbol的原因。
ES5 的对象属性名都是字符串这容易造成属性名的冲突。比如你使用了一个他人提供的对象但又想为这个对象添加新的方法mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入`Symbol`的原因。
ES6引入了一种新的原始数据类型Symbol表示独一无二的值。它是JavaScript语言的第七种数据类型前六种是Undefined、Null、布尔值Boolean、字符串String、数值Number、对象Object
ES6 引入了一种新的原始数据类型`Symbol`,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:`undefined``null`、布尔值Boolean、字符串String、数值Number、对象Object
Symbol 值通过`Symbol`函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
@ -22,8 +22,8 @@ typeof s
`Symbol`函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
```javascript
var s1 = Symbol('foo');
var s2 = Symbol('bar');
let s1 = Symbol('foo');
let s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
@ -50,14 +50,14 @@ sym // Symbol(abc)
```javascript
// 没有参数的情况
var s1 = Symbol();
var s2 = Symbol();
let s1 = Symbol();
let s2 = Symbol();
s1 === s2 // false
// 有参数的情况
var s1 = Symbol('foo');
var s2 = Symbol('foo');
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // false
```
@ -67,7 +67,7 @@ s1 === s2 // false
Symbol 值不能与其他类型的值进行运算,会报错。
```javascript
var sym = Symbol('My symbol');
let sym = Symbol('My symbol');
"your symbol is " + sym
// TypeError: can't convert symbol to string
@ -78,7 +78,7 @@ var sym = Symbol('My symbol');
但是Symbol 值可以显式转为字符串。
```javascript
var sym = Symbol('My symbol');
let sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
@ -87,7 +87,7 @@ sym.toString() // 'Symbol(My symbol)'
另外Symbol 值也可以转为布尔值,但是不能转为数值。
```javascript
var sym = Symbol();
let sym = Symbol();
Boolean(sym) // true
!sym // false
@ -104,19 +104,19 @@ sym + 2 // TypeError
由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。
```javascript
var mySymbol = Symbol();
let mySymbol = Symbol();
// 第一种写法
var a = {};
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
var a = {
let a = {
[mySymbol]: 'Hello!'
};
// 第三种写法
var a = {};
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
@ -128,8 +128,8 @@ a[mySymbol] // "Hello!"
注意Symbol 值作为对象属性名时,不能用点运算符。
```javascript
var mySymbol = Symbol();
var a = {};
const mySymbol = Symbol();
const a = {};
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
@ -196,11 +196,11 @@ function getComplement(color) {
## 实例:消除魔术字符串
魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,由含义清晰的变量代替。
魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,由含义清晰的变量代替。
```javascript
function getArea(shape, options) {
var area = 0;
let area = 0;
switch (shape) {
case 'Triangle': // 魔术字符串
@ -215,17 +215,17 @@ function getArea(shape, options) {
getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串
```
上面代码中,字符串“Triangle”就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。
上面代码中,字符串`Triangle`就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。
常用的消除魔术字符串的方法,就是把它写成一个变量。
```javascript
var shapeType = {
const shapeType = {
triangle: 'Triangle'
};
function getArea(shape, options) {
var area = 0;
let area = 0;
switch (shape) {
case shapeType.triangle:
area = .5 * options.width * options.height;
@ -237,7 +237,7 @@ function getArea(shape, options) {
getArea(shapeType.triangle, { width: 100, height: 100 });
```
上面代码中,我们把“Triangle”写成`shapeType`对象的`triangle`属性,这样就消除了强耦合。
上面代码中,我们把`Triangle`写成`shapeType`对象的`triangle`属性,这样就消除了强耦合。
如果仔细分析,可以发现`shapeType.triangle`等于哪个值并不重要,只要确保不会跟其他`shapeType`属性的值冲突即可。因此,这里就很适合改用 Symbol 值。
@ -256,14 +256,14 @@ Symbol 作为属性名,该属性不会出现在`for...in`、`for...of`循环
`Object.getOwnPropertySymbols`方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
```javascript
var obj = {};
var a = Symbol('a');
var b = Symbol('b');
const obj = {};
let a = Symbol('a');
let b = Symbol('b');
obj[a] = 'Hello';
obj[b] = 'World';
var objectSymbols = Object.getOwnPropertySymbols(obj);
const objectSymbols = Object.getOwnPropertySymbols(obj);
objectSymbols
// [Symbol(a), Symbol(b)]
@ -272,15 +272,15 @@ objectSymbols
下面是另一个例子,`Object.getOwnPropertySymbols`方法与`for...in`循环、`Object.getOwnPropertyNames`方法进行对比的例子。
```javascript
var obj = {};
const obj = {};
var foo = Symbol("foo");
let foo = Symbol("foo");
Object.defineProperty(obj, foo, {
value: "foobar",
});
for (var i in obj) {
for (let i in obj) {
console.log(i); // 无输出
}
@ -303,13 +303,13 @@ let obj = {
};
Reflect.ownKeys(obj)
// [Symbol(my_key), 'enum', 'nonEnum']
// ["enum", "nonEnum", Symbol(my_key)]
```
由于以 Symbol 值作为名称的属性,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。
```javascript
var size = Symbol('size');
let size = Symbol('size');
class Collection {
constructor() {
@ -326,7 +326,7 @@ class Collection {
}
}
var x = new Collection();
let x = new Collection();
Collection.sizeOf(x) // 0
x.add('foo');
@ -344,8 +344,8 @@ Object.getOwnPropertySymbols(x) // [Symbol(size)]
有时,我们希望重新使用同一个 Symbol 值,`Symbol.for`方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。
```javascript
var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
s1 === s2 // true
```
@ -367,10 +367,10 @@ Symbol("bar") === Symbol("bar")
`Symbol.keyFor`方法返回一个已登记的 Symbol 类型值的`key`
```javascript
var s1 = Symbol.for("foo");
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
var s2 = Symbol("foo");
let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
```
@ -413,7 +413,7 @@ module.exports = global._foo;
然后,加载上面的`mod.js`
```javascript
var a = require('./mod.js');
const a = require('./mod.js');
console.log(a.foo);
```
@ -422,7 +422,7 @@ console.log(a.foo);
但是,这里有一个问题,全局变量`global._foo`是可写的,任何文件都可以修改。
```javascript
var a = require('./mod.js');
const a = require('./mod.js');
global._foo = 123;
```
@ -448,7 +448,7 @@ module.exports = global[FOO_KEY];
上面代码中,可以保证`global[FOO_KEY]`不会被无意间覆盖,但还是可以被改写。
```javascript
var a = require('./mod.js');
const a = require('./mod.js');
global[Symbol.for('foo')] = 123;
```
@ -492,6 +492,13 @@ class Even {
}
}
// 等同于
const Even = {
[Symbol.hasInstance](obj) {
return Number(obj) % 2 === 0;
}
};
1 instanceof Even // false
2 instanceof Even // true
12345 instanceof Even // false
@ -499,7 +506,7 @@ class Even {
### Symbol.isConcatSpreadable
对象的`Symbol.isConcatSpreadable`属性等于一个布尔值,表示该对象使`Array.prototype.concat()`时,是否可以展开。
对象的`Symbol.isConcatSpreadable`属性等于一个布尔值,表示该对象用`Array.prototype.concat()`时,是否可以展开。
```javascript
let arr1 = ['c', 'd'];
@ -511,9 +518,9 @@ arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']
```
上面代码说明,数组的默认行为是可以展开`Symbol.isConcatSpreadable`属性等于`true``undefined`,都有这个效果。
上面代码说明,数组的默认行为是可以展开`Symbol.isConcatSpreadable`默认等于`undefined`。该属性等于`true`时,也有展开的效果。
类似数组的对象也可以展开,但它的`Symbol.isConcatSpreadable`属性默认为`false`,必须手动打开。
类似数组的对象正好相反,默认不展开。它的`Symbol.isConcatSpreadable`属性设为`true`,才可以展开。
```javascript
let obj = {length: 2, 0: 'c', 1: 'd'};
@ -523,7 +530,7 @@ obj[Symbol.isConcatSpreadable] = true;
['a', 'b'].concat(obj, 'e') // ['a', 'b', 'c', 'd', 'e']
```
对于一个类来说,`Symbol.isConcatSpreadable`属性必须写成实例的属性
`Symbol.isConcatSpreadable`属性也可以定义在类里面
```javascript
class A1 extends Array {
@ -535,7 +542,9 @@ class A1 extends Array {
class A2 extends Array {
constructor(args) {
super(args);
this[Symbol.isConcatSpreadable] = false;
}
get [Symbol.isConcatSpreadable] () {
return false;
}
}
let a1 = new A1();
@ -550,11 +559,31 @@ a2[1] = 6;
上面代码中,类`A1`是可展开的,类`A2`是不可展开的,所以使用`concat`时有不一样的结果。
注意,`Symbol.isConcatSpreadable`的位置差异,`A1`是定义在实例上,`A2`是定义在类本身,效果相同。
### Symbol.species
对象的`Symbol.species`属性,指向一个方法。该对象作为构造函数创造实例时,会调用这个方法。即如果`this.constructor[Symbol.species]`存在,就会使用这个属性作为构造函数,来创造新的实例对象
对象的`Symbol.species`属性,指向一个构造函数。创建造衍生对象时,会使用该属性
`Symbol.species`属性默认的读取器如下。
```javascript
class MyArray extends Array {
}
const a = new MyArray();
a.map(x => x) instanceof MyArray // true
```
上面代码中,子类`MyArray`继承了父类`Array``a.map(x => x)`会创建一个`MyArray`的衍生对象,该衍生对象还是`MyArray`的实例。
现在,`MyArray`设置`Symbol.species`属性。
```javascript
class MyArray extends Array {
static get [Symbol.species]() { return Array; }
}
```
上面代码中,由于定义了`Symbol.species`属性,创建衍生对象时就会使用这个属性返回的的函数,作为构造函数。这个例子也说明,定义`Symbol.species`属性要采用`get`读取器。默认的`Symbol.species`属性等同于下面的写法。
```javascript
static get [Symbol.species]() {
@ -562,6 +591,40 @@ static get [Symbol.species]() {
}
```
现在,再来看前面的例子。
```javascript
class MyArray extends Array {
static get [Symbol.species]() { return Array; }
}
const a = new MyArray();
a.map(x => x) instanceof MyArray // false
a.map(x => x) instanceof Array // true
```
上面代码中,`a.map(x => x)`创建的衍生对象,就不是`MyArray`的实例,而直接就是`Array`的实例。
再看一个例子。
```javascript
class T1 extends Promise {
}
class T2 extends Promise {
static get [Symbol.species]() {
return Promise;
}
}
new T1(r => r()).then(v => v) instanceof T1 // true
new T2(r => r()).then(v => v) instanceof T2 // false
```
上面代码中,`T2`定义了`Symbol.species`属性,`T1`没有。结果就导致了创建衍生对象时(`then`方法),`T1`调用的是自身的构造方法,而`T2`调用的是`Promise`的构造方法。
总之,`Symbol.species`的作用在于,实例对象在运行过程中,需要再次调用自身的构造函数时,会调用该属性指定的构造函数。它主要的用途是,有些类库是在基类的基础上修改的,那么子类使用继承的方法时,作者可能希望返回基类的实例,而不是子类的实例。
### Symbol.match
对象的`Symbol.match`属性,指向一个函数。当执行`str.match(myObject)`时,如果该属性存在,会调用它,返回该方法的返回值。
@ -631,12 +694,43 @@ String.prototype.split(separator, limit)
separator[Symbol.split](this, limit)
```
下面是一个例子。
```javascript
class MySplitter {
constructor(value) {
this.value = value;
}
[Symbol.split](string) {
let index = string.indexOf(this.value);
if (index === -1) {
return string;
}
return [
string.substr(0, index),
string.substr(index + this.value.length)
];
}
}
'foobar'.split(new MySplitter('foo'))
// ['', 'bar']
'foobar'.split(new MySplitter('bar'))
// ['foo', '']
'foobar'.split(new MySplitter('baz'))
// 'foobar'
```
上面方法使用`Symbol.split`方法,重新定义了字符串对象的`split`方法的行为,
### Symbol.iterator
对象的`Symbol.iterator`属性,指向该对象的默认遍历器方法。
```javascript
var myIterable = {};
const myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
@ -717,7 +811,7 @@ class Collection {
return 'xxx';
}
}
var x = new Collection();
let x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"
```
@ -752,15 +846,16 @@ Array.prototype[Symbol.unscopables]
// entries: true,
// fill: true,
// find: true,
// findIndex: true,
//   findIndex: true,
// includes: true,
// keys: true
// }
Object.keys(Array.prototype[Symbol.unscopables])
// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'keys']
// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'includes', 'keys']
```
上面代码说明,数组有6个属性会被with命令排除。
上面代码说明,数组有 7 个属性,会被`with`命令排除。
```javascript
// 没有 unscopables 时
@ -789,3 +884,4 @@ with (MyClass.prototype) {
}
```
上面代码通过指定`Symbol.unscopables`属性,使得`with`语法块不会在当前作用域寻找`foo`属性,即`foo`将指向外层作用域的变量。

BIN
images/cover-3rd.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="app/bower_components/normalize-css/normalize.css">
<link rel="stylesheet" href="css/app.css">
@ -23,7 +23,7 @@
<div id="back_to_top">back to top</div>
<div id="edit">edit</div>
<div id="loading">Loading ...</div>
<div id="error">Opps! ... File not found!</div>
<div id="error">Oops! ... File not found!</div>
<div id="flip"><div id="pageup">上一章</div><div id="pagedown">下一章</div></div>
<div class="progress-indicator-2"></div>

View File

@ -139,7 +139,7 @@ function init_back_to_top_button() {
function goTop(e) {
if(e) e.preventDefault();
$('html body').animate({
$('html, body').animate({
scrollTop: 0
}, 200);
history.pushState(null, null, '#' + location.hash.split('#')[1]);

View File

@ -12,23 +12,26 @@
1. [字符串的扩展](#docs/string)
1. [正则的扩展](#docs/regex)
1. [数值的扩展](#docs/number)
1. [数组的扩展](#docs/array)
1. [函数的扩展](#docs/function)
1. [数组的扩展](#docs/array)
1. [对象的扩展](#docs/object)
1. [Symbol](#docs/symbol)
1. [Set 和 Map 数据结构](#docs/set-map)
1. [Proxy和Reflect](#docs/proxy)
1. [Iterator和for...of循环](#docs/iterator)
1. [Generator函数](#docs/generator)
1. [Proxy](#docs/proxy)
1. [Reflect](#docs/reflect)
1. [Promise 对象](#docs/promise)
1. [异步操作和Async函数](#docs/async)
1. [Class](#docs/class)
1. [Iterator 和 for...of 循环](#docs/iterator)
1. [Generator 函数的语法](#docs/generator)
1. [Generator 函数的异步应用](#docs/generator-async)
1. [async 函数](#docs/async)
1. [Class 的基本语法](#docs/class)
1. [Class 的继承](#docs/class-extends)
1. [Decorator](#docs/decorator)
1. [Module](#docs/module)
1. [Module 的语法](#docs/module)
1. [Module 的加载实现](#docs/module-loader)
1. [编程风格](#docs/style)
1. [读懂规格](#docs/spec)
1. [二进制数组](#docs/arraybuffer)
1. [SIMD](#docs/simd)
1. [ArrayBuffer](#docs/arraybuffer)
1. [参考链接](#docs/reference)
## 其他