mirror of
https://github.com/ruanyf/es6tutorial.git
synced 2025-05-25 03:02:21 +00:00
Merge branch 'gh-pages' into gh-pages
This commit is contained in:
commit
e3f7e972e5
23
.gitignore
vendored
23
.gitignore
vendored
@ -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/
|
||||||
|
|
||||||
|
|
||||||
|
17
README.md
17
README.md
@ -2,20 +2,21 @@
|
|||||||
|
|
||||||
《ECMAScript 6 入门》是一本开源的 JavaScript 语言教程,全面介绍 ECMAScript 6 新引入的语法特性。
|
《ECMAScript 6 入门》是一本开源的 JavaScript 语言教程,全面介绍 ECMAScript 6 新引入的语法特性。
|
||||||
|
|
||||||
[](images/cover-2nd.jpg)
|
[](images/cover-3rd.jpg)
|
||||||
|
|
||||||
本书覆盖 ES6/ES7 与 ES5 的所有不同之处,对涉及的语法知识给予详细介绍,并给出大量简洁易懂的示例代码。
|
本书覆盖 ES6 与上一个版本 ES5 的所有不同之处,对涉及的语法知识给予详细介绍,并给出大量简洁易懂的示例代码。
|
||||||
|
|
||||||
本书为中级难度,适合已经掌握 ES5 的读者,用来了解这门语言的最新发展;也可当作参考手册,查寻新增的语法点。
|
本书为中级难度,适合已经掌握 ES5 的读者,用来了解这门语言的最新发展;也可当作参考手册,查寻新增的语法点。
|
||||||
|
|
||||||
全书已由电子工业出版社出版,目前是第二版,书名为《ES6 标准入门》。纸版是基于网站内容排版印刷的。
|
全书已由电子工业出版社出版,2017年9月推出了第三版,书名为《ES6 标准入门》。纸版是基于网站内容排版印刷的。
|
||||||
|
|
||||||
感谢张春雨编辑支持我将全书开源的做法。如果您认可这本书,建议购买纸版。这样可以使出版社不因出版开源书籍而亏钱,进而鼓励更多的作者开源自己的书籍。
|
感谢张春雨编辑支持我将全书开源的做法。如果您认可这本书,建议购买纸版。这样可以使出版社不因出版开源书籍而亏钱,进而鼓励更多的作者开源自己的书籍。下面是第三版的购买地址。
|
||||||
|
|
||||||
- [京东](http://item.jd.com/11849235.html)
|
- [淘宝](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)
|
||||||
- [当当](http://product.dangdang.com/23840431.html)
|
- [京东](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://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/)
|
- [当当](http://product.dangdang.com/25156888.html)
|
||||||
- [China-pub](http://product.china-pub.com/4904712)
|
- [亚马逊](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)
|
||||||
|
|
||||||
### 版权许可
|
### 版权许可
|
||||||
|
|
||||||
|
@ -120,6 +120,7 @@ input[type=search] {
|
|||||||
height: 18px;
|
height: 18px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border: none;
|
border: none;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.searchButton {
|
input.searchButton {
|
||||||
|
354
docs/array.md
354
docs/array.md
@ -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`转为真正的数组。
|
||||||
|
|
||||||
|
**(6)Map 和 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.from`方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
|
`Array.from`方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
|
||||||
@ -26,8 +346,8 @@ let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
|
|||||||
```javascript
|
```javascript
|
||||||
// NodeList对象
|
// NodeList对象
|
||||||
let ps = document.querySelectorAll('p');
|
let ps = document.querySelectorAll('p');
|
||||||
Array.from(ps).forEach(function (p) {
|
Array.from(ps).filter(p => {
|
||||||
console.log(p);
|
return p.textContent.length > 100;
|
||||||
});
|
});
|
||||||
|
|
||||||
// arguments对象
|
// arguments对象
|
||||||
@ -37,7 +357,7 @@ function foo() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,`querySelectorAll`方法返回的是一个类似数组的对象,只有将这个对象转为真正的数组,才能使用`forEach`方法。
|
上面代码中,`querySelectorAll`方法返回的是一个类似数组的对象,可以将这个对象转为真正的数组,再使用`forEach`方法。
|
||||||
|
|
||||||
只要是部署了 Iterator 接口的数据结构,`Array.from`都能将其转为数组。
|
只要是部署了 Iterator 接口的数据结构,`Array.from`都能将其转为数组。
|
||||||
|
|
||||||
@ -63,14 +383,14 @@ Array.from([1, 2, 3])
|
|||||||
```javascript
|
```javascript
|
||||||
// arguments对象
|
// arguments对象
|
||||||
function foo() {
|
function foo() {
|
||||||
var args = [...arguments];
|
const args = [...arguments];
|
||||||
}
|
}
|
||||||
|
|
||||||
// NodeList对象
|
// NodeList对象
|
||||||
[...document.querySelectorAll('div')]
|
[...document.querySelectorAll('div')]
|
||||||
```
|
```
|
||||||
|
|
||||||
扩展运算符背后调用的是遍历器接口(`Symbol.iterator`),如果一个对象没有部署这个接口,就无法转换。`Array.from`方法则是还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有`length`属性。因此,任何有`length`属性的对象,都可以通过`Array.from`方法转为数组,而此时扩展运算符就无法转换。
|
扩展运算符背后调用的是遍历器接口(`Symbol.iterator`),如果一个对象没有部署这个接口,就无法转换。`Array.from`方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有`length`属性。因此,任何有`length`属性的对象,都可以通过`Array.from`方法转为数组,而此时扩展运算符就无法转换。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
Array.from({ length: 3 });
|
Array.from({ length: 3 });
|
||||||
@ -224,7 +544,7 @@ Array.prototype.copyWithin(target, start = 0, end = this.length)
|
|||||||
// {0: 1, 3: 1, length: 5}
|
// {0: 1, 3: 1, length: 5}
|
||||||
|
|
||||||
// 将2号位到数组结束,复制到0号位
|
// 将2号位到数组结束,复制到0号位
|
||||||
var i32a = new Int32Array([1, 2, 3, 4, 5]);
|
let i32a = new Int32Array([1, 2, 3, 4, 5]);
|
||||||
i32a.copyWithin(0, 2);
|
i32a.copyWithin(0, 2);
|
||||||
// Int32Array [3, 4, 5, 4, 5]
|
// Int32Array [3, 4, 5, 4, 5]
|
||||||
|
|
||||||
@ -263,7 +583,7 @@ i32a.copyWithin(0, 2);
|
|||||||
|
|
||||||
这两个方法都可以接受第二个参数,用来绑定回调函数的`this`对象。
|
这两个方法都可以接受第二个参数,用来绑定回调函数的`this`对象。
|
||||||
|
|
||||||
另外,这两个方法都可以发现`NaN`,弥补了数组的`IndexOf`方法的不足。
|
另外,这两个方法都可以发现`NaN`,弥补了数组的`indexOf`方法的不足。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
[NaN].indexOf(NaN)
|
[NaN].indexOf(NaN)
|
||||||
@ -334,15 +654,15 @@ console.log(entries.next().value); // [2, 'c']
|
|||||||
|
|
||||||
## 数组实例的 includes()
|
## 数组实例的 includes()
|
||||||
|
|
||||||
`Array.prototype.includes`方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的`includes`方法类似。该方法属于ES7,但Babel转码器已经支持。
|
`Array.prototype.includes`方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的`includes`方法类似。ES2016 引入了该方法。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
[1, 2, 3].includes(2); // true
|
[1, 2, 3].includes(2) // true
|
||||||
[1, 2, 3].includes(4); // false
|
[1, 2, 3].includes(4) // false
|
||||||
[1, 2, NaN].includes(NaN); // true
|
[1, 2, NaN].includes(NaN) // true
|
||||||
```
|
```
|
||||||
|
|
||||||
该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。
|
该方法的第二个参数表示搜索的起始位置,默认为`0`。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为`-4`,但数组长度为`3`),则会重置为从`0`开始。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
[1, 2, 3].includes(3, 3); // false
|
[1, 2, 3].includes(3, 3); // false
|
||||||
@ -357,7 +677,7 @@ if (arr.indexOf(el) !== -1) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`indexOf`方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相当运算符(===)进行判断,这会导致对`NaN`的误判。
|
`indexOf`方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于`-1`,表达起来不够直观。二是,它内部使用严格相等运算符(`===`)进行判断,这会导致对`NaN`的误判。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
[NaN].indexOf(NaN)
|
[NaN].indexOf(NaN)
|
||||||
@ -379,7 +699,7 @@ const contains = (() =>
|
|||||||
? (arr, value) => arr.includes(value)
|
? (arr, value) => arr.includes(value)
|
||||||
: (arr, value) => arr.some(el => el === value)
|
: (arr, value) => arr.some(el => el === value)
|
||||||
)();
|
)();
|
||||||
contains(["foo", "bar"], "baz"); // => false
|
contains(['foo', 'bar'], 'baz'); // => false
|
||||||
```
|
```
|
||||||
|
|
||||||
另外,Map 和 Set 数据结构有一个`has`方法,需要注意与`includes`区分。
|
另外,Map 和 Set 数据结构有一个`has`方法,需要注意与`includes`区分。
|
||||||
@ -408,7 +728,7 @@ Array(3) // [, , ,]
|
|||||||
|
|
||||||
ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。
|
ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。
|
||||||
|
|
||||||
- `forEach()`, `filter()`, `every()` 和`some()`都会跳过空位。
|
- `forEach()`, `filter()`, `reduce()`, `every()` 和`some()`都会跳过空位。
|
||||||
- `map()`会跳过空位,但会保留这个值
|
- `map()`会跳过空位,但会保留这个值
|
||||||
- `join()`和`toString()`会将空位视为`undefined`,而`undefined`和`null`会被处理成空字符串。
|
- `join()`和`toString()`会将空位视为`undefined`,而`undefined`和`null`会被处理成空字符串。
|
||||||
|
|
||||||
@ -422,6 +742,9 @@ ES5对空位的处理,已经很不一致了,大多数情况下会忽略空
|
|||||||
// every方法
|
// every方法
|
||||||
[,'a'].every(x => x==='a') // true
|
[,'a'].every(x => x==='a') // true
|
||||||
|
|
||||||
|
// reduce方法
|
||||||
|
[1,,2].reduce((x,y) => return x+y) // 3
|
||||||
|
|
||||||
// some方法
|
// some方法
|
||||||
[,'a'].some(x => x !== 'a') // false
|
[,'a'].some(x => x !== 'a') // false
|
||||||
|
|
||||||
@ -496,4 +819,3 @@ for (let i of arr) {
|
|||||||
```
|
```
|
||||||
|
|
||||||
由于空位的处理规则非常不统一,所以建议避免出现空位。
|
由于空位的处理规则非常不统一,所以建议避免出现空位。
|
||||||
|
|
||||||
|
@ -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 位整数,以二进制形式原封不动地送入显卡,脚本的性能就会大幅提升。
|
这个接口的原始设计目的,与 WebGL 项目有关。所谓 WebGL,就是指浏览器与显卡之间的通信接口,为了满足 JavaScript 与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式。文本格式传递一个 32 位整数,两端的 JavaScript 脚本与显卡都要进行格式转化,将非常耗时。这时要是存在一种机制,可以像 C 语言那样,直接操作字节,将 4 个字节的 32 位整数,以二进制形式原封不动地送入显卡,脚本的性能就会大幅提升。
|
||||||
|
|
||||||
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
**(1)`ArrayBuffer`对象**:代表内存之中的一段二进制数据,可以通过“视图”进行操作。“视图”部署了数组接口,这意味着,可以用数组的方法操作内存。
|
**(1)`ArrayBuffer`对象**:代表内存之中的一段二进制数据,可以通过“视图”进行操作。“视图”部署了数组接口,这意味着,可以用数组的方法操作内存。
|
||||||
|
|
||||||
**(2)TypedArray视图**:共包括9种类型的视图,比如`Uint8Array`(无符号8位整数)数组视图, `Int16Array`(16位整数)数组视图, `Float32Array`(32位浮点数)数组视图等等。
|
**(2)`TypedArray`视图**:共包括 9 种类型的视图,比如`Uint8Array`(无符号 8 位整数)数组视图, `Int16Array`(16 位整数)数组视图, `Float32Array`(32 位浮点数)数组视图等等。
|
||||||
|
|
||||||
**(3)`DataView`视图**:可以自定义复合格式的视图,比如第一个字节是 Uint8(无符号 8 位整数)、第二、三个字节是 Int16(16 位整数)、第四个字节开始是 Float32(32 位浮点数)等等,此外还可以自定义字节序。
|
**(3)`DataView`视图**:可以自定义复合格式的视图,比如第一个字节是 Uint8(无符号 8 位整数)、第二、三个字节是 Int16(16 位整数)、第四个字节开始是 Float32(32 位浮点数)等等,此外还可以自定义字节序。
|
||||||
|
|
||||||
@ -18,17 +18,17 @@
|
|||||||
|
|
||||||
TypedArray 视图支持的数据类型一共有 9 种(`DataView`视图支持除`Uint8C`以外的其他 8 种)。
|
TypedArray 视图支持的数据类型一共有 9 种(`DataView`视图支持除`Uint8C`以外的其他 8 种)。
|
||||||
|
|
||||||
数据类型 | 字节长度 | 含义 | 对应的C语言类型
|
| 数据类型 | 字节长度 | 含义 | 对应的 C 语言类型 |
|
||||||
--------|--------|----|---------------
|
| -------- | -------- | -------------------------------- | ----------------- |
|
||||||
Int8|1|8位带符号整数|signed char
|
| Int8 | 1 | 8 位带符号整数 | signed char |
|
||||||
Uint8|1|8位不带符号整数|unsigned char
|
| Uint8 | 1 | 8 位不带符号整数 | unsigned char |
|
||||||
Uint8C|1|8位不带符号整数(自动过滤溢出)|unsigned char
|
| Uint8C | 1 | 8 位不带符号整数(自动过滤溢出) | unsigned char |
|
||||||
Int16|2|16位带符号整数|short
|
| Int16 | 2 | 16 位带符号整数 | short |
|
||||||
Uint16|2|16位不带符号整数|unsigned short
|
| Uint16 | 2 | 16 位不带符号整数 | unsigned short |
|
||||||
Int32|4|32位带符号整数|int
|
| Int32 | 4 | 32 位带符号整数 | int |
|
||||||
Uint32|4|32位不带符号的整数|unsigned int
|
| Uint32 | 4 | 32 位不带符号的整数 | unsigned int |
|
||||||
Float32|4|32位浮点数|float
|
| Float32 | 4 | 32 位浮点数 | float |
|
||||||
Float64|8|64位浮点数|double
|
| Float64 | 8 | 64 位浮点数 | double |
|
||||||
|
|
||||||
注意,二进制数组并不是真正的数组,而是类似数组的对象。
|
注意,二进制数组并不是真正的数组,而是类似数组的对象。
|
||||||
|
|
||||||
@ -44,12 +44,12 @@ Float64|8|64位浮点数|double
|
|||||||
|
|
||||||
### 概述
|
### 概述
|
||||||
|
|
||||||
`ArrayBuffer`对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(TypedArray视图和`DataView`视图)来读写,视图的作用是以指定格式解读二进制数据。
|
`ArrayBuffer`对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(`TypedArray`视图和`DataView`视图)来读写,视图的作用是以指定格式解读二进制数据。
|
||||||
|
|
||||||
`ArrayBuffer`也是一个构造函数,可以分配一段可以存放数据的连续内存区域。
|
`ArrayBuffer`也是一个构造函数,可以分配一段可以存放数据的连续内存区域。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var buf = new ArrayBuffer(32);
|
const buf = new ArrayBuffer(32);
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码生成了一段 32 字节的内存区域,每个字节的值默认都是 0。可以看到,`ArrayBuffer`构造函数的参数是所需要的内存大小(单位字节)。
|
上面代码生成了一段 32 字节的内存区域,每个字节的值默认都是 0。可以看到,`ArrayBuffer`构造函数的参数是所需要的内存大小(单位字节)。
|
||||||
@ -57,21 +57,21 @@ var buf = new ArrayBuffer(32);
|
|||||||
为了读写这段内容,需要为它指定视图。`DataView`视图的创建,需要提供`ArrayBuffer`对象实例作为参数。
|
为了读写这段内容,需要为它指定视图。`DataView`视图的创建,需要提供`ArrayBuffer`对象实例作为参数。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var buf = new ArrayBuffer(32);
|
const buf = new ArrayBuffer(32);
|
||||||
var dataView = new DataView(buf);
|
const dataView = new DataView(buf);
|
||||||
dataView.getUint8(0) // 0
|
dataView.getUint8(0) // 0
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码对一段32字节的内存,建立`DataView`视图,然后以不带符号的8位整数格式,读取第一个元素,结果得到0,因为原始内存的`ArrayBuffer`对象,默认所有位都是0。
|
上面代码对一段 32 字节的内存,建立`DataView`视图,然后以不带符号的 8 位整数格式,从头读取 8 位二进制数据,结果得到 0,因为原始内存的`ArrayBuffer`对象,默认所有位都是 0。
|
||||||
|
|
||||||
另一种 TypedArray 视图,与`DataView`视图的一个区别是,它不是一个构造函数,而是一组构造函数,代表不同的数据格式。
|
另一种 TypedArray 视图,与`DataView`视图的一个区别是,它不是一个构造函数,而是一组构造函数,代表不同的数据格式。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var buffer = new ArrayBuffer(12);
|
const buffer = new ArrayBuffer(12);
|
||||||
|
|
||||||
var x1 = new Int32Array(buffer);
|
const x1 = new Int32Array(buffer);
|
||||||
x1[0] = 1;
|
x1[0] = 1;
|
||||||
var x2 = new Uint8Array(buffer);
|
const x2 = new Uint8Array(buffer);
|
||||||
x2[0] = 2;
|
x2[0] = 2;
|
||||||
|
|
||||||
x1[0] // 2
|
x1[0] // 2
|
||||||
@ -82,7 +82,7 @@ x1[0] // 2
|
|||||||
TypedArray 视图的构造函数,除了接受`ArrayBuffer`实例作为参数,还可以接受普通数组作为参数,直接分配内存生成底层的`ArrayBuffer`实例,并同时完成对这段内存的赋值。
|
TypedArray 视图的构造函数,除了接受`ArrayBuffer`实例作为参数,还可以接受普通数组作为参数,直接分配内存生成底层的`ArrayBuffer`实例,并同时完成对这段内存的赋值。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var typedArray = new Uint8Array([0,1,2]);
|
const typedArray = new Uint8Array([0,1,2]);
|
||||||
typedArray.length // 3
|
typedArray.length // 3
|
||||||
|
|
||||||
typedArray[0] = 5;
|
typedArray[0] = 5;
|
||||||
@ -96,7 +96,7 @@ typedArray // [5, 1, 2]
|
|||||||
`ArrayBuffer`实例的`byteLength`属性,返回所分配的内存区域的字节长度。
|
`ArrayBuffer`实例的`byteLength`属性,返回所分配的内存区域的字节长度。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var buffer = new ArrayBuffer(32);
|
const buffer = new ArrayBuffer(32);
|
||||||
buffer.byteLength
|
buffer.byteLength
|
||||||
// 32
|
// 32
|
||||||
```
|
```
|
||||||
@ -116,8 +116,8 @@ if (buffer.byteLength === n) {
|
|||||||
`ArrayBuffer`实例有一个`slice`方法,允许将内存区域的一部分,拷贝生成一个新的`ArrayBuffer`对象。
|
`ArrayBuffer`实例有一个`slice`方法,允许将内存区域的一部分,拷贝生成一个新的`ArrayBuffer`对象。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var buffer = new ArrayBuffer(8);
|
const buffer = new ArrayBuffer(8);
|
||||||
var newBuffer = buffer.slice(0, 3);
|
const newBuffer = buffer.slice(0, 3);
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码拷贝`buffer`对象的前 3 个字节(从 0 开始,到第 3 个字节前面结束),生成一个新的`ArrayBuffer`对象。`slice`方法其实包含两步,第一步是先分配一段新内存,第二步是将原来那个`ArrayBuffer`对象拷贝过去。
|
上面代码拷贝`buffer`对象的前 3 个字节(从 0 开始,到第 3 个字节前面结束),生成一个新的`ArrayBuffer`对象。`slice`方法其实包含两步,第一步是先分配一段新内存,第二步是将原来那个`ArrayBuffer`对象拷贝过去。
|
||||||
@ -131,10 +131,10 @@ var newBuffer = buffer.slice(0, 3);
|
|||||||
`ArrayBuffer`有一个静态方法`isView`,返回一个布尔值,表示参数是否为`ArrayBuffer`的视图实例。这个方法大致相当于判断参数,是否为 TypedArray 实例或`DataView`实例。
|
`ArrayBuffer`有一个静态方法`isView`,返回一个布尔值,表示参数是否为`ArrayBuffer`的视图实例。这个方法大致相当于判断参数,是否为 TypedArray 实例或`DataView`实例。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var buffer = new ArrayBuffer(8);
|
const buffer = new ArrayBuffer(8);
|
||||||
ArrayBuffer.isView(buffer) // false
|
ArrayBuffer.isView(buffer) // false
|
||||||
|
|
||||||
var v = new Int32Array(buffer);
|
const v = new Int32Array(buffer);
|
||||||
ArrayBuffer.isView(v) // true
|
ArrayBuffer.isView(v) // true
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -175,16 +175,16 @@ TypedArray数组提供9种构造函数,用来生成相应类型的数组实例
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 创建一个8字节的ArrayBuffer
|
// 创建一个8字节的ArrayBuffer
|
||||||
var b = new ArrayBuffer(8);
|
const b = new ArrayBuffer(8);
|
||||||
|
|
||||||
// 创建一个指向b的Int32视图,开始于字节0,直到缓冲区的末尾
|
// 创建一个指向b的Int32视图,开始于字节0,直到缓冲区的末尾
|
||||||
var v1 = new Int32Array(b);
|
const v1 = new Int32Array(b);
|
||||||
|
|
||||||
// 创建一个指向b的Uint8视图,开始于字节2,直到缓冲区的末尾
|
// 创建一个指向b的Uint8视图,开始于字节2,直到缓冲区的末尾
|
||||||
var v2 = new Uint8Array(b, 2);
|
const v2 = new Uint8Array(b, 2);
|
||||||
|
|
||||||
// 创建一个指向b的Int16视图,开始于字节2,长度为2
|
// 创建一个指向b的Int16视图,开始于字节2,长度为2
|
||||||
var v3 = new Int16Array(b, 2, 2);
|
const v3 = new Int16Array(b, 2, 2);
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码在一段长度为 8 个字节的内存(`b`)之上,生成了三个视图:`v1`、`v2`和`v3`。
|
上面代码在一段长度为 8 个字节的内存(`b`)之上,生成了三个视图:`v1`、`v2`和`v3`。
|
||||||
@ -200,8 +200,8 @@ var v3 = new Int16Array(b, 2, 2);
|
|||||||
注意,`byteOffset`必须与所要建立的数据类型一致,否则会报错。
|
注意,`byteOffset`必须与所要建立的数据类型一致,否则会报错。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var buffer = new ArrayBuffer(8);
|
const buffer = new ArrayBuffer(8);
|
||||||
var i16 = new Int16Array(buffer, 1);
|
const i16 = new Int16Array(buffer, 1);
|
||||||
// Uncaught RangeError: start offset of Int16Array should be a multiple of 2
|
// Uncaught RangeError: start offset of Int16Array should be a multiple of 2
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -214,7 +214,7 @@ var i16 = new Int16Array(buffer, 1);
|
|||||||
视图还可以不通过`ArrayBuffer`对象,直接分配内存而生成。
|
视图还可以不通过`ArrayBuffer`对象,直接分配内存而生成。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var f64a = new Float64Array(8);
|
const f64a = new Float64Array(8);
|
||||||
f64a[0] = 10;
|
f64a[0] = 10;
|
||||||
f64a[1] = 20;
|
f64a[1] = 20;
|
||||||
f64a[2] = f64a[0] + f64a[1];
|
f64a[2] = f64a[0] + f64a[1];
|
||||||
@ -227,7 +227,7 @@ f64a[2] = f64a[0] + f64a[1];
|
|||||||
TypedArray 数组的构造函数,可以接受另一个 TypedArray 实例作为参数。
|
TypedArray 数组的构造函数,可以接受另一个 TypedArray 实例作为参数。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var typedArray = new Int8Array(new Uint8Array(4));
|
const typedArray = new Int8Array(new Uint8Array(4));
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,`Int8Array`构造函数接受一个`Uint8Array`实例作为参数。
|
上面代码中,`Int8Array`构造函数接受一个`Uint8Array`实例作为参数。
|
||||||
@ -235,8 +235,8 @@ var typedArray = new Int8Array(new Uint8Array(4));
|
|||||||
注意,此时生成的新数组,只是复制了参数数组的值,对应的底层内存是不一样的。新数组会开辟一段新的内存储存数据,不会在原数组的内存之上建立视图。
|
注意,此时生成的新数组,只是复制了参数数组的值,对应的底层内存是不一样的。新数组会开辟一段新的内存储存数据,不会在原数组的内存之上建立视图。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var x = new Int8Array([1, 1]);
|
const x = new Int8Array([1, 1]);
|
||||||
var y = new Int8Array(x);
|
const y = new Int8Array(x);
|
||||||
x[0] // 1
|
x[0] // 1
|
||||||
y[0] // 1
|
y[0] // 1
|
||||||
|
|
||||||
@ -249,8 +249,8 @@ y[0] // 1
|
|||||||
如果想基于同一段内存,构造不同的视图,可以采用下面的写法。
|
如果想基于同一段内存,构造不同的视图,可以采用下面的写法。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var x = new Int8Array([1, 1]);
|
const x = new Int8Array([1, 1]);
|
||||||
var y = new Int8Array(x.buffer);
|
const y = new Int8Array(x.buffer);
|
||||||
x[0] // 1
|
x[0] // 1
|
||||||
y[0] // 1
|
y[0] // 1
|
||||||
|
|
||||||
@ -263,7 +263,7 @@ y[0] // 2
|
|||||||
构造函数的参数也可以是一个普通数组,然后直接生成 TypedArray 实例。
|
构造函数的参数也可以是一个普通数组,然后直接生成 TypedArray 实例。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var typedArray = new Uint8Array([1, 2, 3, 4]);
|
const typedArray = new Uint8Array([1, 2, 3, 4]);
|
||||||
```
|
```
|
||||||
|
|
||||||
注意,这时 TypedArray 视图会重新开辟内存,不会在原数组的内存上建立视图。
|
注意,这时 TypedArray 视图会重新开辟内存,不会在原数组的内存上建立视图。
|
||||||
@ -273,7 +273,11 @@ var typedArray = new Uint8Array([1, 2, 3, 4]);
|
|||||||
TypedArray 数组也可以转换回普通数组。
|
TypedArray 数组也可以转换回普通数组。
|
||||||
|
|
||||||
```javascript
|
```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
|
```javascript
|
||||||
var buffer = new ArrayBuffer(16);
|
const buffer = new ArrayBuffer(16);
|
||||||
var int32View = new Int32Array(buffer);
|
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;
|
int32View[i] = i * 2;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -356,9 +360,9 @@ for (var i = 0; i < int32View.length; i++) {
|
|||||||
如果在这段数据上接着建立一个 16 位整数的视图,则可以读出完全不一样的结果。
|
如果在这段数据上接着建立一个 16 位整数的视图,则可以读出完全不一样的结果。
|
||||||
|
|
||||||
```javascript
|
```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]);
|
console.log("Entry " + i + ": " + int16View[i]);
|
||||||
}
|
}
|
||||||
// Entry 0: 0
|
// Entry 0: 0
|
||||||
@ -381,14 +385,14 @@ for (var i = 0; i < int16View.length; i++) {
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 假定某段buffer包含如下字节 [0x02, 0x01, 0x03, 0x07]
|
// 假定某段buffer包含如下字节 [0x02, 0x01, 0x03, 0x07]
|
||||||
var buffer = new ArrayBuffer(4);
|
const buffer = new ArrayBuffer(4);
|
||||||
var v1 = new Uint8Array(buffer);
|
const v1 = new Uint8Array(buffer);
|
||||||
v1[0] = 2;
|
v1[0] = 2;
|
||||||
v1[1] = 1;
|
v1[1] = 1;
|
||||||
v1[2] = 3;
|
v1[2] = 3;
|
||||||
v1[3] = 7;
|
v1[3] = 7;
|
||||||
|
|
||||||
var uInt16View = new Uint16Array(buffer);
|
const uInt16View = new Uint16Array(buffer);
|
||||||
|
|
||||||
// 计算机采用小端字节序
|
// 计算机采用小端字节序
|
||||||
// 所以头两个字节等于258
|
// 所以头两个字节等于258
|
||||||
@ -448,14 +452,16 @@ Float64Array.BYTES_PER_ELEMENT // 8
|
|||||||
```javascript
|
```javascript
|
||||||
// ArrayBuffer 转为字符串,参数为 ArrayBuffer 对象
|
// ArrayBuffer 转为字符串,参数为 ArrayBuffer 对象
|
||||||
function ab2str(buf) {
|
function ab2str(buf) {
|
||||||
|
// 注意,如果是大型二进制数组,为了避免溢出,
|
||||||
|
// 必须一个一个字符地转
|
||||||
return String.fromCharCode.apply(null, new Uint16Array(buf));
|
return String.fromCharCode.apply(null, new Uint16Array(buf));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 字符串转为 ArrayBuffer 对象,参数为字符串
|
// 字符串转为 ArrayBuffer 对象,参数为字符串
|
||||||
function str2ab(str) {
|
function str2ab(str) {
|
||||||
var buf = new ArrayBuffer(str.length * 2); // 每个字符占用2个字节
|
const buf = new ArrayBuffer(str.length * 2); // 每个字符占用2个字节
|
||||||
var bufView = new Uint16Array(buf);
|
const bufView = new Uint16Array(buf);
|
||||||
for (var i = 0, strLen = str.length; i < strLen; i++) {
|
for (let i = 0, strLen = str.length; i < strLen; i++) {
|
||||||
bufView[i] = str.charCodeAt(i);
|
bufView[i] = str.charCodeAt(i);
|
||||||
}
|
}
|
||||||
return buf;
|
return buf;
|
||||||
@ -469,7 +475,7 @@ function str2ab(str) {
|
|||||||
TypedArray 数组的溢出处理规则,简单来说,就是抛弃溢出的位,然后按照视图类型进行解释。
|
TypedArray 数组的溢出处理规则,简单来说,就是抛弃溢出的位,然后按照视图类型进行解释。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var uint8 = new Uint8Array(1);
|
const uint8 = new Uint8Array(1);
|
||||||
|
|
||||||
uint8[0] = 256;
|
uint8[0] = 256;
|
||||||
uint8[0] // 0
|
uint8[0] // 0
|
||||||
@ -487,10 +493,19 @@ uint8[0] // 255
|
|||||||
- 正向溢出(overflow):当输入值大于当前数据类型的最大值,结果等于当前数据类型的最小值加上余值,再减去 1。
|
- 正向溢出(overflow):当输入值大于当前数据类型的最大值,结果等于当前数据类型的最小值加上余值,再减去 1。
|
||||||
- 负向溢出(underflow):当输入值小于当前数据类型的最小值,结果等于当前数据类型的最大值减去余值,再加上 1。
|
- 负向溢出(underflow):当输入值小于当前数据类型的最小值,结果等于当前数据类型的最大值减去余值,再加上 1。
|
||||||
|
|
||||||
|
上面的“余值”就是模运算的结果,即 JavaScript 里面的`%`运算符的结果。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
12 % 4 // 0
|
||||||
|
12 % 5 // 2
|
||||||
|
```
|
||||||
|
|
||||||
|
上面代码中,12 除以 4 是没有余值的,而除以 5 会得到余值 2。
|
||||||
|
|
||||||
请看下面的例子。
|
请看下面的例子。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var int8 = new Int8Array(1);
|
const int8 = new Int8Array(1);
|
||||||
|
|
||||||
int8[0] = 128;
|
int8[0] = 128;
|
||||||
int8[0] // -128
|
int8[0] // -128
|
||||||
@ -499,12 +514,12 @@ int8[0] = -129;
|
|||||||
int8[0] // 127
|
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。
|
`Uint8ClampedArray`视图的溢出规则,与上面的规则不同。它规定,凡是发生正向溢出,该值一律等于当前数据类型的最大值,即 255;如果发生负向溢出,该值一律等于当前数据类型的最小值,即 0。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var uint8c = new Uint8ClampedArray(1);
|
const uint8c = new Uint8ClampedArray(1);
|
||||||
|
|
||||||
uint8c[0] = 256;
|
uint8c[0] = 256;
|
||||||
uint8c[0] // 255
|
uint8c[0] // 255
|
||||||
@ -520,8 +535,8 @@ uint8c[0] // 0
|
|||||||
TypedArray 实例的`buffer`属性,返回整段内存区域对应的`ArrayBuffer`对象。该属性为只读属性。
|
TypedArray 实例的`buffer`属性,返回整段内存区域对应的`ArrayBuffer`对象。该属性为只读属性。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var a = new Float32Array(64);
|
const a = new Float32Array(64);
|
||||||
var b = new Uint8Array(a.buffer);
|
const b = new Uint8Array(a.buffer);
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码的`a`视图对象和`b`视图对象,对应同一个`ArrayBuffer`对象,即同一段内存。
|
上面代码的`a`视图对象和`b`视图对象,对应同一个`ArrayBuffer`对象,即同一段内存。
|
||||||
@ -531,11 +546,11 @@ var b = new Uint8Array(a.buffer);
|
|||||||
`byteLength`属性返回 TypedArray 数组占据的内存长度,单位为字节。`byteOffset`属性返回 TypedArray 数组从底层`ArrayBuffer`对象的哪个字节开始。这两个属性都是只读属性。
|
`byteLength`属性返回 TypedArray 数组占据的内存长度,单位为字节。`byteOffset`属性返回 TypedArray 数组从底层`ArrayBuffer`对象的哪个字节开始。这两个属性都是只读属性。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var b = new ArrayBuffer(8);
|
const b = new ArrayBuffer(8);
|
||||||
|
|
||||||
var v1 = new Int32Array(b);
|
const v1 = new Int32Array(b);
|
||||||
var v2 = new Uint8Array(b, 2);
|
const v2 = new Uint8Array(b, 2);
|
||||||
var v3 = new Int16Array(b, 2, 2);
|
const v3 = new Int16Array(b, 2, 2);
|
||||||
|
|
||||||
v1.byteLength // 8
|
v1.byteLength // 8
|
||||||
v2.byteLength // 6
|
v2.byteLength // 6
|
||||||
@ -551,7 +566,7 @@ v3.byteOffset // 2
|
|||||||
`length`属性表示 TypedArray 数组含有多少个成员。注意将`byteLength`属性和`length`属性区分,前者是字节长度,后者是成员长度。
|
`length`属性表示 TypedArray 数组含有多少个成员。注意将`byteLength`属性和`length`属性区分,前者是字节长度,后者是成员长度。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var a = new Int16Array(8);
|
const a = new Int16Array(8);
|
||||||
|
|
||||||
a.length // 8
|
a.length // 8
|
||||||
a.byteLength // 16
|
a.byteLength // 16
|
||||||
@ -562,8 +577,8 @@ a.byteLength // 16
|
|||||||
TypedArray 数组的`set`方法用于复制数组(普通数组或 TypedArray 数组),也就是将一段内容完全复制到另一段内存。
|
TypedArray 数组的`set`方法用于复制数组(普通数组或 TypedArray 数组),也就是将一段内容完全复制到另一段内存。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var a = new Uint8Array(8);
|
const a = new Uint8Array(8);
|
||||||
var b = new Uint8Array(8);
|
const b = new Uint8Array(8);
|
||||||
|
|
||||||
b.set(a);
|
b.set(a);
|
||||||
```
|
```
|
||||||
@ -573,8 +588,8 @@ b.set(a);
|
|||||||
`set`方法还可以接受第二个参数,表示从`b`对象的哪一个成员开始复制`a`对象。
|
`set`方法还可以接受第二个参数,表示从`b`对象的哪一个成员开始复制`a`对象。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var a = new Uint16Array(8);
|
const a = new Uint16Array(8);
|
||||||
var b = new Uint16Array(10);
|
const b = new Uint16Array(10);
|
||||||
|
|
||||||
b.set(a, 2)
|
b.set(a, 2)
|
||||||
```
|
```
|
||||||
@ -586,8 +601,8 @@ b.set(a, 2)
|
|||||||
`subarray`方法是对于 TypedArray 数组的一部分,再建立一个新的视图。
|
`subarray`方法是对于 TypedArray 数组的一部分,再建立一个新的视图。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var a = new Uint16Array(8);
|
const a = new Uint16Array(8);
|
||||||
var b = a.subarray(2,3);
|
const b = a.subarray(2,3);
|
||||||
|
|
||||||
a.byteLength // 16
|
a.byteLength // 16
|
||||||
b.byteLength // 2
|
b.byteLength // 2
|
||||||
@ -646,7 +661,7 @@ Uint16Array.from([0, 1, 2])
|
|||||||
这个方法还可以将一种 TypedArray 实例,转为另一种。
|
这个方法还可以将一种 TypedArray 实例,转为另一种。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2));
|
const ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2));
|
||||||
ui16 instanceof Uint16Array // true
|
ui16 instanceof Uint16Array // true
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -667,11 +682,11 @@ Int16Array.from(Int8Array.of(127, 126, 125), x => 2 * x)
|
|||||||
由于视图的构造函数可以指定起始位置和长度,所以在同一段内存之中,可以依次存放不同类型的数据,这叫做“复合视图”。
|
由于视图的构造函数可以指定起始位置和长度,所以在同一段内存之中,可以依次存放不同类型的数据,这叫做“复合视图”。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var buffer = new ArrayBuffer(24);
|
const buffer = new ArrayBuffer(24);
|
||||||
|
|
||||||
var idView = new Uint32Array(buffer, 0, 1);
|
const idView = new Uint32Array(buffer, 0, 1);
|
||||||
var usernameView = new Uint8Array(buffer, 4, 16);
|
const usernameView = new Uint8Array(buffer, 4, 16);
|
||||||
var amountDueView = new Float32Array(buffer, 20, 1);
|
const amountDueView = new Float32Array(buffer, 20, 1);
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码将一个 24 字节长度的`ArrayBuffer`对象,分成三个部分:
|
上面代码将一个 24 字节长度的`ArrayBuffer`对象,分成三个部分:
|
||||||
@ -705,8 +720,8 @@ DataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]);
|
|||||||
下面是一个例子。
|
下面是一个例子。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var buffer = new ArrayBuffer(24);
|
const buffer = new ArrayBuffer(24);
|
||||||
var dv = new DataView(buffer);
|
const dv = new DataView(buffer);
|
||||||
```
|
```
|
||||||
|
|
||||||
`DataView`实例有以下属性,含义与 TypedArray 实例的同名方法相同。
|
`DataView`实例有以下属性,含义与 TypedArray 实例的同名方法相同。
|
||||||
@ -729,17 +744,17 @@ var dv = new DataView(buffer);
|
|||||||
这一系列`get`方法的参数都是一个字节序号(不能是负数,否则会报错),表示从哪个字节开始读取。
|
这一系列`get`方法的参数都是一个字节序号(不能是负数,否则会报错),表示从哪个字节开始读取。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var buffer = new ArrayBuffer(24);
|
const buffer = new ArrayBuffer(24);
|
||||||
var dv = new DataView(buffer);
|
const dv = new DataView(buffer);
|
||||||
|
|
||||||
// 从第1个字节读取一个8位无符号整数
|
// 从第1个字节读取一个8位无符号整数
|
||||||
var v1 = dv.getUint8(0);
|
const v1 = dv.getUint8(0);
|
||||||
|
|
||||||
// 从第2个字节读取一个16位无符号整数
|
// 从第2个字节读取一个16位无符号整数
|
||||||
var v2 = dv.getUint16(1);
|
const v2 = dv.getUint16(1);
|
||||||
|
|
||||||
// 从第4个字节读取一个16位无符号整数
|
// 从第4个字节读取一个16位无符号整数
|
||||||
var v3 = dv.getUint16(3);
|
const v3 = dv.getUint16(3);
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码读取了`ArrayBuffer`对象的前 5 个字节,其中有一个 8 位整数和两个十六位整数。
|
上面代码读取了`ArrayBuffer`对象的前 5 个字节,其中有一个 8 位整数和两个十六位整数。
|
||||||
@ -748,13 +763,13 @@ var v3 = dv.getUint16(3);
|
|||||||
|
|
||||||
```javascript
|
```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 个方法写入内存。
|
DataView 视图提供 8 个方法写入内存。
|
||||||
@ -784,8 +799,8 @@ dv.setFloat32(8, 2.5, true);
|
|||||||
如果不确定正在使用的计算机的字节序,可以采用下面的判断方式。
|
如果不确定正在使用的计算机的字节序,可以采用下面的判断方式。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var littleEndian = (function() {
|
const littleEndian = (function() {
|
||||||
var buffer = new ArrayBuffer(2);
|
const buffer = new ArrayBuffer(2);
|
||||||
new DataView(buffer).setInt16(0, 256, true);
|
new DataView(buffer).setInt16(0, 256, true);
|
||||||
return new Int16Array(buffer)[0] === 256;
|
return new Int16Array(buffer)[0] === 256;
|
||||||
})();
|
})();
|
||||||
@ -802,7 +817,7 @@ var littleEndian = (function() {
|
|||||||
传统上,服务器通过 AJAX 操作只能返回文本数据,即`responseType`属性默认为`text`。`XMLHttpRequest`第二版`XHR2`允许服务器返回二进制数据,这时分成两种情况。如果明确知道返回的二进制数据类型,可以把返回类型(`responseType`)设为`arraybuffer`;如果不知道,就设为`blob`。
|
传统上,服务器通过 AJAX 操作只能返回文本数据,即`responseType`属性默认为`text`。`XMLHttpRequest`第二版`XHR2`允许服务器返回二进制数据,这时分成两种情况。如果明确知道返回的二进制数据类型,可以把返回类型(`responseType`)设为`arraybuffer`;如果不知道,就设为`blob`。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var xhr = new XMLHttpRequest();
|
let xhr = new XMLHttpRequest();
|
||||||
xhr.open('GET', someUrl);
|
xhr.open('GET', someUrl);
|
||||||
xhr.responseType = 'arraybuffer';
|
xhr.responseType = 'arraybuffer';
|
||||||
|
|
||||||
@ -819,9 +834,9 @@ xhr.send();
|
|||||||
```javascript
|
```javascript
|
||||||
xhr.onreadystatechange = function () {
|
xhr.onreadystatechange = function () {
|
||||||
if (req.readyState === 4 ) {
|
if (req.readyState === 4 ) {
|
||||||
var arrayResponse = xhr.response;
|
const arrayResponse = xhr.response;
|
||||||
var dataView = new DataView(arrayResponse);
|
const dataView = new DataView(arrayResponse);
|
||||||
var ints = new Uint32Array(dataView.byteLength / 4);
|
const ints = new Uint32Array(dataView.byteLength / 4);
|
||||||
|
|
||||||
xhrDiv.style.backgroundColor = "#00FF00";
|
xhrDiv.style.backgroundColor = "#00FF00";
|
||||||
xhrDiv.innerText = "Array is " + ints.length + "uints long";
|
xhrDiv.innerText = "Array is " + ints.length + "uints long";
|
||||||
@ -834,11 +849,11 @@ xhr.onreadystatechange = function () {
|
|||||||
网页`Canvas`元素输出的二进制像素数据,就是 TypedArray 数组。
|
网页`Canvas`元素输出的二进制像素数据,就是 TypedArray 数组。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var canvas = document.getElementById('myCanvas');
|
const canvas = document.getElementById('myCanvas');
|
||||||
var ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
var uint8ClampedArray = imageData.data;
|
const uint8ClampedArray = imageData.data;
|
||||||
```
|
```
|
||||||
|
|
||||||
需要注意的是,上面代码的`uint8ClampedArray`虽然是一个 TypedArray 数组,但是它的视图类型是一种针对`Canvas`元素的专有类型`Uint8ClampedArray`。这个视图类型的特点,就是专门针对颜色,把每个字节解读为无符号的 8 位整数,即只能取值 0 ~ 255,而且发生运算的时候自动过滤高位溢出。这为图像处理带来了巨大的方便。
|
需要注意的是,上面代码的`uint8ClampedArray`虽然是一个 TypedArray 数组,但是它的视图类型是一种针对`Canvas`元素的专有类型`Uint8ClampedArray`。这个视图类型的特点,就是专门针对颜色,把每个字节解读为无符号的 8 位整数,即只能取值 0 ~ 255,而且发生运算的时候自动过滤高位溢出。这为图像处理带来了巨大的方便。
|
||||||
@ -862,19 +877,19 @@ pixels[i] *= gamma;
|
|||||||
`WebSocket`可以通过`ArrayBuffer`,发送或接收二进制数据。
|
`WebSocket`可以通过`ArrayBuffer`,发送或接收二进制数据。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var socket = new WebSocket('ws://127.0.0.1:8081');
|
let socket = new WebSocket('ws://127.0.0.1:8081');
|
||||||
socket.binaryType = 'arraybuffer';
|
socket.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
// Wait until socket is open
|
// Wait until socket is open
|
||||||
socket.addEventListener('open', function (event) {
|
socket.addEventListener('open', function (event) {
|
||||||
// Send binary data
|
// Send binary data
|
||||||
var typedArray = new Uint8Array(4);
|
const typedArray = new Uint8Array(4);
|
||||||
socket.send(typedArray.buffer);
|
socket.send(typedArray.buffer);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Receive binary data
|
// Receive binary data
|
||||||
socket.addEventListener('message', function (event) {
|
socket.addEventListener('message', function (event) {
|
||||||
var arrayBuffer = event.data;
|
const arrayBuffer = event.data;
|
||||||
// ···
|
// ···
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
@ -885,8 +900,8 @@ Fetch API取回的数据,就是`ArrayBuffer`对象。
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then(function(request){
|
.then(function(response){
|
||||||
return request.arrayBuffer()
|
return response.arrayBuffer()
|
||||||
})
|
})
|
||||||
.then(function(arrayBuffer){
|
.then(function(arrayBuffer){
|
||||||
// ...
|
// ...
|
||||||
@ -898,12 +913,12 @@ fetch(url)
|
|||||||
如果知道一个文件的二进制数据类型,也可以将这个文件读取为`ArrayBuffer`对象。
|
如果知道一个文件的二进制数据类型,也可以将这个文件读取为`ArrayBuffer`对象。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var fileInput = document.getElementById('fileInput');
|
const fileInput = document.getElementById('fileInput');
|
||||||
var file = fileInput.files[0];
|
const file = fileInput.files[0];
|
||||||
var reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.readAsArrayBuffer(file);
|
reader.readAsArrayBuffer(file);
|
||||||
reader.onload = function () {
|
reader.onload = function () {
|
||||||
var arrayBuffer = reader.result;
|
const arrayBuffer = reader.result;
|
||||||
// ···
|
// ···
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
@ -911,7 +926,7 @@ reader.onload = function () {
|
|||||||
下面以处理 bmp 文件为例。假定`file`变量是一个指向 bmp 文件的文件对象,首先读取文件。
|
下面以处理 bmp 文件为例。假定`file`变量是一个指向 bmp 文件的文件对象,首先读取文件。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.addEventListener("load", processimage, false);
|
reader.addEventListener("load", processimage, false);
|
||||||
reader.readAsArrayBuffer(file);
|
reader.readAsArrayBuffer(file);
|
||||||
```
|
```
|
||||||
@ -920,9 +935,9 @@ reader.readAsArrayBuffer(file);
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function processimage(e) {
|
function processimage(e) {
|
||||||
var buffer = e.target.result;
|
const buffer = e.target.result;
|
||||||
var datav = new DataView(buffer);
|
const datav = new DataView(buffer);
|
||||||
var bitmap = {};
|
const bitmap = {};
|
||||||
// 具体的处理步骤
|
// 具体的处理步骤
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -958,7 +973,7 @@ bitmap.infoheader.biClrImportant = datav.getUint32(50, true);
|
|||||||
最后处理图像本身的像素信息。
|
最后处理图像本身的像素信息。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var start = bitmap.fileheader.bfOffBits;
|
const start = bitmap.fileheader.bfOffBits;
|
||||||
bitmap.pixels = new Uint8Array(buffer, start);
|
bitmap.pixels = new Uint8Array(buffer, start);
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -966,85 +981,247 @@ bitmap.pixels = new Uint8Array(buffer, start);
|
|||||||
|
|
||||||
## SharedArrayBuffer
|
## SharedArrayBuffer
|
||||||
|
|
||||||
目前有一种场景,需要多个进程共享数据:浏览器启动多个WebWorker。
|
JavaScript 是单线程的,Web worker 引入了多线程:主线程用来与用户互动,Worker 线程用来承担计算任务。每个线程的数据都是隔离的,通过`postMessage()`通信。下面是一个例子。
|
||||||
|
|
||||||
```javascript
|
```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
|
```javascript
|
||||||
|
// 主线程
|
||||||
w.postMessage('hi');
|
w.postMessage('hi');
|
||||||
w.onmessage = function (ev) {
|
w.onmessage = function (ev) {
|
||||||
console.log(ev.data);
|
console.log(ev.data);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,主窗口先发一个消息`hi`,然后在监听到 Worker 进程的回应后,就将其打印出来。
|
上面代码中,主线程先发一个消息`hi`,然后在监听到 Worker 线程的回应后,就将其打印出来。
|
||||||
|
|
||||||
Worker 进程也是通过监听`message`事件,来获取主窗口发来的消息,并作出反应。
|
Worker 线程也是通过监听`message`事件,来获取主线程发来的消息,并作出反应。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
// Worker 线程
|
||||||
onmessage = function (ev) {
|
onmessage = function (ev) {
|
||||||
console.log(ev.data);
|
console.log(ev.data);
|
||||||
postMessage('ho');
|
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
|
```javascript
|
||||||
// 新建 1KB 共享内存
|
// 主线程
|
||||||
var sab = new SharedArrayBuffer(1024);
|
|
||||||
|
|
||||||
// 主窗口发送数据
|
// 新建 1KB 共享内存
|
||||||
w.postMessage(sab, [sab])
|
const sharedBuffer = new SharedArrayBuffer(1024);
|
||||||
|
|
||||||
|
// 主线程将共享内存的地址发送出去
|
||||||
|
w.postMessage(sharedBuffer);
|
||||||
|
|
||||||
|
// 在共享内存上建立视图,供写入数据
|
||||||
|
const sharedArray = new Int32Array(sharedBuffer);
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,`postMessage`方法的第一个参数是`SharedArrayBuffer`对象,第二个参数是要写入共享内存的数据。
|
上面代码中,`postMessage`方法的参数是`SharedArrayBuffer`对象。
|
||||||
|
|
||||||
Worker 进程从事件的`data`属性上面取到数据。
|
Worker 线程从事件的`data`属性上面取到数据。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var sab;
|
// Worker 线程
|
||||||
onmessage = function (ev) {
|
onmessage = function (ev) {
|
||||||
sab = ev.data; // 1KB 的共享内存,就是主窗口共享出来的那块内存
|
// 主线程共享的数据,就是 1KB 的共享内存
|
||||||
|
const sharedBuffer = ev.data;
|
||||||
|
|
||||||
|
// 在共享内存上建立视图,方便读写
|
||||||
|
const sharedArray = new Int32Array(sharedBuffer);
|
||||||
|
|
||||||
|
// ...
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
共享内存也可以在 Worker 进程创建,发给主窗口。
|
共享内存也可以在 Worker 线程创建,发给主线程。
|
||||||
|
|
||||||
`SharedArrayBuffer`与`SharedArray`一样,本身是无法读写,必须在上面建立视图,然后通过视图读写。
|
`SharedArrayBuffer`与`ArrayBuffer`一样,本身是无法读写的,必须在上面建立视图,然后通过视图读写。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 分配 10 万个 32 位整数占据的内存空间
|
// 分配 10 万个 32 位整数占据的内存空间
|
||||||
var sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100000);
|
const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100000);
|
||||||
|
|
||||||
// 建立 32 位整数视图
|
// 建立 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 万个质数,写入这段内存空间
|
// 将 10 万个质数,写入这段内存空间
|
||||||
for ( let i=0 ; i < ia.length ; i++ )
|
for ( let i=0 ; i < ia.length ; i++ )
|
||||||
ia[i] = primes.next();
|
ia[i] = primes.next();
|
||||||
|
|
||||||
// 向 Worker 进程发送这段共享内存
|
// 向 Worker 线程发送这段共享内存
|
||||||
w.postMessage(ia, [ia.buffer]);
|
w.postMessage(ia);
|
||||||
```
|
```
|
||||||
|
|
||||||
Worker 收到数据后的处理如下。
|
Worker 线程收到数据后的处理如下。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var ia;
|
// Worker 线程
|
||||||
|
let ia;
|
||||||
onmessage = function (ev) {
|
onmessage = function (ev) {
|
||||||
ia = ev.data;
|
ia = ev.data;
|
||||||
console.log(ia.length); // 100000
|
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`对象提供多种方法。
|
||||||
|
|
||||||
|
**(1)Atomics.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 号位置等于 163,Worker 线程就不会终止循环,而对 37 号位置和 42 号位置的取值,一定是在`Atomics.load`操作之后。
|
||||||
|
|
||||||
|
**(2)Atomics.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 里面原来那个值是否发生变化(即被其他线程改写过)。如果没有改写过,就将它写回原来的位置,否则读取新的值,再重头进行一次操作。
|
||||||
|
1277
docs/async.md
1277
docs/async.md
File diff suppressed because it is too large
Load Diff
714
docs/class-extends.md
Normal file
714
docs/class-extends.md
Normal 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) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
844
docs/class.md
844
docs/class.md
File diff suppressed because it is too large
Load Diff
@ -2,22 +2,22 @@
|
|||||||
|
|
||||||
## 类的修饰
|
## 类的修饰
|
||||||
|
|
||||||
修饰器(Decorator)是一个函数,用来修改类的行为。这是ES7的一个[提案](https://github.com/wycats/javascript-decorators),目前Babel转码器已经支持。
|
许多面向对象的语言都有修饰器(Decorator)函数,用来修改类的行为。目前,有一个[提案](https://github.com/tc39/proposal-decorators)将这项功能,引入了 ECMAScript。
|
||||||
|
|
||||||
修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
@testable
|
||||||
|
class MyTestableClass {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
function testable(target) {
|
function testable(target) {
|
||||||
target.isTestable = true;
|
target.isTestable = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@testable
|
MyTestableClass.isTestable // true
|
||||||
class MyTestableClass {}
|
|
||||||
|
|
||||||
console.log(MyTestableClass.isTestable) // true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,`@testable`就是一个修饰器。它修改了`MyTestableClass`这个类的行为,为它加上了静态属性`isTestable`。
|
上面代码中,`@testable`就是一个修饰器。它修改了`MyTestableClass`这个类的行为,为它加上了静态属性`isTestable`。`testable`函数的参数`target`是`MyTestableClass`类本身。
|
||||||
|
|
||||||
基本上,修饰器的行为就是下面这样。
|
基本上,修饰器的行为就是下面这样。
|
||||||
|
|
||||||
@ -31,9 +31,7 @@ class A {}
|
|||||||
A = decorator(A) || A;
|
A = decorator(A) || A;
|
||||||
```
|
```
|
||||||
|
|
||||||
也就是说,修饰器本质就是编译时执行的函数。
|
也就是说,修饰器是一个对类进行处理的函数。修饰器函数的第一个参数,就是所要修饰的目标类。
|
||||||
|
|
||||||
修饰器函数的第一个参数,就是所要修饰的目标类。
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function testable(target) {
|
function testable(target) {
|
||||||
@ -63,6 +61,8 @@ MyClass.isTestable // false
|
|||||||
|
|
||||||
上面代码中,修饰器`testable`可以接受参数,这就等于可以修改修饰器的行为。
|
上面代码中,修饰器`testable`可以接受参数,这就等于可以修改修饰器的行为。
|
||||||
|
|
||||||
|
注意,修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。
|
||||||
|
|
||||||
前面的例子是为类添加一个静态属性,如果想添加实例属性,可以通过目标类的`prototype`对象操作。
|
前面的例子是为类添加一个静态属性,如果想添加实例属性,可以通过目标类的`prototype`对象操作。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
@ -118,6 +118,23 @@ let obj = new MyClass();
|
|||||||
obj.foo() // 'foo'
|
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`用来修饰“类”的`name`方法。
|
||||||
|
|
||||||
此时,修饰器函数一共可以接受三个参数,第一个参数是所要修饰的目标对象,第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象。
|
修饰器函数`readonly`一共可以接受三个参数。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function readonly(target, name, descriptor){
|
function readonly(target, name, descriptor){
|
||||||
@ -151,7 +168,9 @@ readonly(Person.prototype, 'name', descriptor);
|
|||||||
Object.defineProperty(Person.prototype, 'name', descriptor);
|
Object.defineProperty(Person.prototype, 'name', descriptor);
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码说明,修饰器(readonly)会修改属性的描述对象(descriptor),然后被修改的描述对象再用来定义属性。
|
修饰器第一个参数是类的原型对象,上例是`Person.prototype`,修饰器的本意是要“修饰”类的实例,但是这个时候实例还没生成,所以只能去修饰原型(这不同于类的修饰,那种情况时`target`参数指的是类本身);第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象。
|
||||||
|
|
||||||
|
另外,上面代码说明,修饰器(readonly)会修改属性的描述对象(descriptor),然后被修改的描述对象再用来定义属性。
|
||||||
|
|
||||||
下面是另一个例子,修改属性描述对象的`enumerable`属性,使得该属性不可遍历。
|
下面是另一个例子,修改属性描述对象的`enumerable`属性,使得该属性不可遍历。
|
||||||
|
|
||||||
@ -209,6 +228,26 @@ class Person {
|
|||||||
|
|
||||||
从上面代码中,我们一眼就能看出,`Person`类是可测试的,而`name`方法是只读和不可枚举的。
|
从上面代码中,我们一眼就能看出,`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
|
```javascript
|
||||||
@ -251,13 +290,13 @@ function foo() {
|
|||||||
上面的代码,意图是执行后`counter`等于 1,但是实际上结果是`counter`等于 0。因为函数提升,使得实际执行的代码是下面这样。
|
上面的代码,意图是执行后`counter`等于 1,但是实际上结果是`counter`等于 0。因为函数提升,使得实际执行的代码是下面这样。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var counter;
|
|
||||||
var add;
|
|
||||||
|
|
||||||
@add
|
@add
|
||||||
function foo() {
|
function foo() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var counter;
|
||||||
|
var add;
|
||||||
|
|
||||||
counter = 0;
|
counter = 0;
|
||||||
|
|
||||||
add = function () {
|
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
|
||||||
|
|
||||||
[core-decorators.js](https://github.com/jayphelps/core-decorators.js)是一个第三方模块,提供了几个常见的修饰器,通过它可以更好地理解修饰器。
|
[core-decorators.js](https://github.com/jayphelps/core-decorators.js)是一个第三方模块,提供了几个常见的修饰器,通过它可以更好地理解修饰器。
|
||||||
@ -394,7 +452,7 @@ person.facepalmHarder();
|
|||||||
|
|
||||||
**(5)@suppressWarnings**
|
**(5)@suppressWarnings**
|
||||||
|
|
||||||
`suppressWarnings`修饰器抑制`decorated`修饰器导致的`console.warn()`调用。但是,异步代码发出的调用除外。
|
`suppressWarnings`修饰器抑制`deprecated`修饰器导致的`console.warn()`调用。但是,异步代码发出的调用除外。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { suppressWarnings } from 'core-decorators';
|
import { suppressWarnings } from 'core-decorators';
|
||||||
@ -420,15 +478,23 @@ person.facepalmWithoutWarning();
|
|||||||
我们可以使用修饰器,使得对象的方法被调用时,自动发出一个事件。
|
我们可以使用修饰器,使得对象的方法被调用时,自动发出一个事件。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import postal from "postal/lib/postal.lodash";
|
const postal = require("postal/lib/postal.lodash");
|
||||||
|
|
||||||
export default function publish(topic, channel) {
|
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) {
|
return function(target, name, descriptor) {
|
||||||
const fn = descriptor.value;
|
const fn = descriptor.value;
|
||||||
|
|
||||||
descriptor.value = function() {
|
descriptor.value = function() {
|
||||||
let value = fn.apply(this, arguments);
|
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
|
```javascript
|
||||||
import publish from "path/to/decorators/publish";
|
// index.js
|
||||||
|
import publish from './publish';
|
||||||
|
|
||||||
class FooComponent {
|
class FooComponent {
|
||||||
@publish("foo.some.message", "component")
|
@publish('foo.some.message', 'component')
|
||||||
someMethod() {
|
someMethod() {
|
||||||
return {
|
return { my: 'data' };
|
||||||
my: "data"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
@publish("foo.some.other")
|
@publish('foo.some.other')
|
||||||
anotherMethod() {
|
anotherMethod() {
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let foo = new FooComponent();
|
||||||
|
|
||||||
|
foo.someMethod();
|
||||||
|
foo.anotherMethod();
|
||||||
```
|
```
|
||||||
|
|
||||||
以后,只要调用`someMethod`或者`anotherMethod`,就会自动发出一个事件。
|
以后,只要调用`someMethod`或者`anotherMethod`,就会自动发出一个事件。
|
||||||
|
|
||||||
```javascript
|
```bash
|
||||||
let foo = new FooComponent();
|
$ 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
|
## Mixin
|
||||||
@ -485,7 +559,7 @@ obj.foo() // 'foo'
|
|||||||
|
|
||||||
上面代码之中,对象`Foo`有一个`foo`方法,通过`Object.assign`方法,可以将`foo`方法“混入”`MyClass`类,导致`MyClass`的实例`obj`对象都具有`foo`方法。这就是“混入”模式的一个简单实现。
|
上面代码之中,对象`Foo`有一个`foo`方法,通过`Object.assign`方法,可以将`foo`方法“混入”`MyClass`类,导致`MyClass`的实例`obj`对象都具有`foo`方法。这就是“混入”模式的一个简单实现。
|
||||||
|
|
||||||
下面,我们部署一个通用脚本`mixins.js`,将mixin写成一个修饰器。
|
下面,我们部署一个通用脚本`mixins.js`,将 Mixin 写成一个修饰器。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
export function mixins(...list) {
|
export function mixins(...list) {
|
||||||
@ -511,9 +585,9 @@ let obj = new MyClass();
|
|||||||
obj.foo() // "foo"
|
obj.foo() // "foo"
|
||||||
```
|
```
|
||||||
|
|
||||||
通过mixins这个修饰器,实现了在MyClass类上面“混入”Foo对象的`foo`方法。
|
通过`mixins`这个修饰器,实现了在`MyClass`类上面“混入”`Foo`对象的`foo`方法。
|
||||||
|
|
||||||
不过,上面的方法会改写`MyClass`类的`prototype`对象,如果不喜欢这一点,也可以通过类的继承实现mixin。
|
不过,上面的方法会改写`MyClass`类的`prototype`对象,如果不喜欢这一点,也可以通过类的继承实现 Mixin。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
class MyClass extends MyBaseClass {
|
class MyClass extends MyBaseClass {
|
||||||
@ -597,7 +671,7 @@ new C().foo()
|
|||||||
|
|
||||||
Trait 也是一种修饰器,效果与 Mixin 类似,但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等。
|
Trait 也是一种修饰器,效果与 Mixin 类似,但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等。
|
||||||
|
|
||||||
下面采用[traits-decorator](https://github.com/CocktailJS/traits-decorator)这个第三方模块作为例子。这个模块提供的traits修饰器,不仅可以接受对象,还可以接受ES6类作为参数。
|
下面采用[traits-decorator](https://github.com/CocktailJS/traits-decorator)这个第三方模块作为例子。这个模块提供的`traits`修饰器,不仅可以接受对象,还可以接受 ES6 类作为参数。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { traits } from 'traits-decorator';
|
import { traits } from 'traits-decorator';
|
||||||
@ -618,7 +692,7 @@ obj.foo() // foo
|
|||||||
obj.bar() // bar
|
obj.bar() // bar
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,通过traits修饰器,在`MyClass`类上面“混入”了`TFoo`类的`foo`方法和`TBar`对象的`bar`方法。
|
上面代码中,通过`traits`修饰器,在`MyClass`类上面“混入”了`TFoo`类的`foo`方法和`TBar`对象的`bar`方法。
|
||||||
|
|
||||||
Trait 不允许“混入”同名方法。
|
Trait 不允许“混入”同名方法。
|
||||||
|
|
||||||
@ -642,9 +716,9 @@ class MyClass { }
|
|||||||
// Error: Method named: foo is defined twice.
|
// Error: Method named: foo is defined twice.
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,TFoo和TBar都有foo方法,结果traits修饰器报错。
|
上面代码中,`TFoo`和`TBar`都有`foo`方法,结果`traits`修饰器报错。
|
||||||
|
|
||||||
一种解决方法是排除TBar的foo方法。
|
一种解决方法是排除`TBar`的`foo`方法。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { traits, excludes } from 'traits-decorator';
|
import { traits, excludes } from 'traits-decorator';
|
||||||
@ -666,9 +740,9 @@ obj.foo() // foo
|
|||||||
obj.bar() // bar
|
obj.bar() // bar
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码使用绑定运算符(::)在TBar上排除foo方法,混入时就不会报错了。
|
上面代码使用绑定运算符(::)在`TBar`上排除`foo`方法,混入时就不会报错了。
|
||||||
|
|
||||||
另一种方法是为TBar的foo方法起一个别名。
|
另一种方法是为`TBar`的`foo`方法起一个别名。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { traits, alias } from 'traits-decorator';
|
import { traits, alias } from 'traits-decorator';
|
||||||
@ -691,18 +765,18 @@ obj.aliasFoo() // foo
|
|||||||
obj.bar() // bar
|
obj.bar() // bar
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码为TBar的foo方法起了别名aliasFoo,于是MyClass也可以混入TBar的foo方法了。
|
上面代码为`TBar`的`foo`方法起了别名`aliasFoo`,于是`MyClass`也可以混入`TBar`的`foo`方法了。
|
||||||
|
|
||||||
alias和excludes方法,可以结合起来使用。
|
`alias`和`excludes`方法,可以结合起来使用。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
@traits(TExample::excludes('foo','bar')::alias({baz:'exampleBaz'}))
|
@traits(TExample::excludes('foo','bar')::alias({baz:'exampleBaz'}))
|
||||||
class MyClass {}
|
class MyClass {}
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码排除了TExample的foo方法和bar方法,为baz方法起了别名exampleBaz。
|
上面代码排除`了TExample`的`foo`方法和`bar`方法,为`baz`方法起了别名`exampleBaz`。
|
||||||
|
|
||||||
as方法则为上面的代码提供了另一种写法。
|
`as`方法则为上面的代码提供了另一种写法。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
@traits(TExample::as({excludes:['foo', 'bar'], alias: {baz: 'exampleBaz'}}))
|
@traits(TExample::as({excludes:['foo', 'bar'], alias: {baz: 'exampleBaz'}}))
|
||||||
|
@ -9,15 +9,15 @@ ES6允许按照一定模式,从数组和对象中提取值,对变量进行
|
|||||||
以前,为变量赋值,只能直接指定值。
|
以前,为变量赋值,只能直接指定值。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var a = 1;
|
let a = 1;
|
||||||
var b = 2;
|
let b = 2;
|
||||||
var c = 3;
|
let c = 3;
|
||||||
```
|
```
|
||||||
|
|
||||||
ES6 允许写成下面这样。
|
ES6 允许写成下面这样。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var [a, b, c] = [1, 2, 3];
|
let [a, b, c] = [1, 2, 3];
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。
|
上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。
|
||||||
@ -50,8 +50,8 @@ z // []
|
|||||||
如果解构不成功,变量的值就等于`undefined`。
|
如果解构不成功,变量的值就等于`undefined`。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var [foo] = [];
|
let [foo] = [];
|
||||||
var [bar, foo] = [1];
|
let [bar, foo] = [1];
|
||||||
```
|
```
|
||||||
|
|
||||||
以上两种情况都属于解构不成功,`foo`的值都会等于`undefined`。
|
以上两种情况都属于解构不成功,`foo`的值都会等于`undefined`。
|
||||||
@ -83,20 +83,12 @@ let [foo] = null;
|
|||||||
let [foo] = {};
|
let [foo] = {};
|
||||||
```
|
```
|
||||||
|
|
||||||
上面的表达式都会报错,因为等号右边的值,要么转为对象以后不具备Iterator接口(前五个表达式),要么本身就不具备Iterator接口(最后一个表达式)。
|
上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。
|
||||||
|
|
||||||
解构赋值不仅适用于var命令,也适用于let和const命令。
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
var [v1, v2, ..., vN ] = array;
|
|
||||||
let [v1, v2, ..., vN ] = array;
|
|
||||||
const [v1, v2, ..., vN ] = array;
|
|
||||||
```
|
|
||||||
|
|
||||||
对于 Set 结构,也可以使用数组的解构赋值。
|
对于 Set 结构,也可以使用数组的解构赋值。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let [x, y, z] = new Set(["a", "b", "c"]);
|
let [x, y, z] = new Set(['a', 'b', 'c']);
|
||||||
x // "a"
|
x // "a"
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -104,39 +96,39 @@ x // "a"
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function* fibs() {
|
function* fibs() {
|
||||||
var a = 0;
|
let a = 0;
|
||||||
var b = 1;
|
let b = 1;
|
||||||
while (true) {
|
while (true) {
|
||||||
yield a;
|
yield a;
|
||||||
[a, b] = [b, a + b];
|
[a, b] = [b, a + b];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var [first, second, third, fourth, fifth, sixth] = fibs();
|
let [first, second, third, fourth, fifth, sixth] = fibs();
|
||||||
sixth // 5
|
sixth // 5
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,`fibs`是一个Generator函数,原生具有Iterator接口。解构赋值会依次从这个接口获取值。
|
上面代码中,`fibs`是一个 Generator 函数(参见《Generator 函数》一章),原生具有 Iterator 接口。解构赋值会依次从这个接口获取值。
|
||||||
|
|
||||||
### 默认值
|
### 默认值
|
||||||
|
|
||||||
解构赋值允许指定默认值。
|
解构赋值允许指定默认值。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var [foo = true] = [];
|
let [foo = true] = [];
|
||||||
foo // true
|
foo // true
|
||||||
|
|
||||||
[x, y = 'b'] = ['a']; // x='a', y='b'
|
let [x, y = 'b'] = ['a']; // x='a', y='b'
|
||||||
[x, y = 'b'] = ['a', undefined]; // x='a', y='b'
|
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
|
||||||
```
|
```
|
||||||
|
|
||||||
注意,ES6内部使用严格相等运算符(`===`),判断一个位置是否有值。而且,只有当一个数组成员严格等于`undefined`,默认值才会生效。
|
注意,ES6内部使用严格相等运算符(`===`),判断一个位置是否有值。而且,只有当一个数组成员严格等于`undefined`,默认值才会生效。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var [x = 1] = [undefined];
|
let [x = 1] = [undefined];
|
||||||
x // 1
|
x // 1
|
||||||
|
|
||||||
var [x = 1] = [null];
|
let [x = 1] = [null];
|
||||||
x // null
|
x // null
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -179,7 +171,7 @@ let [x = y, y = 1] = []; // ReferenceError: y is not defined
|
|||||||
解构不仅可以用于数组,还可以用于对象。
|
解构不仅可以用于数组,还可以用于对象。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var { foo, bar } = { foo: "aaa", bar: "bbb" };
|
let { foo, bar } = { foo: "aaa", bar: "bbb" };
|
||||||
foo // "aaa"
|
foo // "aaa"
|
||||||
bar // "bbb"
|
bar // "bbb"
|
||||||
```
|
```
|
||||||
@ -187,11 +179,11 @@ bar // "bbb"
|
|||||||
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
|
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var { bar, foo } = { foo: "aaa", bar: "bbb" };
|
let { bar, foo } = { foo: "aaa", bar: "bbb" };
|
||||||
foo // "aaa"
|
foo // "aaa"
|
||||||
bar // "bbb"
|
bar // "bbb"
|
||||||
|
|
||||||
var { baz } = { foo: "aaa", bar: "bbb" };
|
let { baz } = { foo: "aaa", bar: "bbb" };
|
||||||
baz // undefined
|
baz // undefined
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -200,7 +192,7 @@ baz // undefined
|
|||||||
如果变量名与属性名不一致,必须写成下面这样。
|
如果变量名与属性名不一致,必须写成下面这样。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var { foo: baz } = { foo: 'aaa', bar: 'bbb' };
|
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
|
||||||
baz // "aaa"
|
baz // "aaa"
|
||||||
|
|
||||||
let obj = { first: 'hello', last: 'world' };
|
let obj = { first: 'hello', last: 'world' };
|
||||||
@ -212,60 +204,54 @@ l // 'world'
|
|||||||
这实际上说明,对象的解构赋值是下面形式的简写(参见《对象的扩展》一章)。
|
这实际上说明,对象的解构赋值是下面形式的简写(参见《对象的扩展》一章)。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
|
let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
|
||||||
```
|
```
|
||||||
|
|
||||||
也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
|
也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var { foo: baz } = { foo: "aaa", bar: "bbb" };
|
let { foo: baz } = { foo: "aaa", bar: "bbb" };
|
||||||
baz // "aaa"
|
baz // "aaa"
|
||||||
foo // error: foo is not defined
|
foo // error: foo is not defined
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,`foo`是匹配的模式,`baz`才是变量。真正被赋值的是变量`baz`,而不是模式`foo`。
|
上面代码中,`foo`是匹配的模式,`baz`才是变量。真正被赋值的是变量`baz`,而不是模式`foo`。
|
||||||
|
|
||||||
注意,采用这种写法时,变量的声明和赋值是一体的。对于`let`和`const`来说,变量不能重新声明,所以一旦赋值的变量以前声明过,就会报错。
|
与数组一样,解构也可以用于嵌套结构的对象。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let foo;
|
let obj = {
|
||||||
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 = {
|
|
||||||
p: [
|
p: [
|
||||||
'Hello',
|
'Hello',
|
||||||
{ y: 'World' }
|
{ y: 'World' }
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
var { p: [x, { y }] } = obj;
|
let { p: [x, { y }] } = obj;
|
||||||
x // "Hello"
|
x // "Hello"
|
||||||
y // "World"
|
y // "World"
|
||||||
```
|
```
|
||||||
|
|
||||||
注意,这时`p`是模式,不是变量,因此不会被赋值。
|
注意,这时`p`是模式,不是变量,因此不会被赋值。如果`p`也要作为变量赋值,可以写成下面这样。
|
||||||
|
|
||||||
```javascript
|
```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: {
|
loc: {
|
||||||
start: {
|
start: {
|
||||||
line: 1,
|
line: 1,
|
||||||
@ -274,13 +260,13 @@ var node = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var { loc: { start: { line }} } = node;
|
let { loc, loc: { start }, loc: { start: { line }} } = node;
|
||||||
line // 1
|
line // 1
|
||||||
loc // error: loc is undefined
|
loc // Object {start: Object}
|
||||||
start // error: start is undefined
|
start // Object {line: 1, column: 5}
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,只有`line`是变量,`loc`和`start`都是模式,不会被赋值。
|
上面代码有三次解构赋值,分别是对`loc`、`start`、`line`三个属性的解构赋值。注意,最后一次对`line`属性的解构赋值之中,只有`line`是变量,`loc`和`start`都是模式,不是变量。
|
||||||
|
|
||||||
下面是嵌套赋值的例子。
|
下面是嵌套赋值的例子。
|
||||||
|
|
||||||
@ -329,7 +315,7 @@ x // null
|
|||||||
如果解构失败,变量的值等于`undefined`。
|
如果解构失败,变量的值等于`undefined`。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var {foo} = {bar: 'baz'};
|
let {foo} = {bar: 'baz'};
|
||||||
foo // undefined
|
foo // undefined
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -337,13 +323,13 @@ foo // undefined
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 报错
|
// 报错
|
||||||
var {foo: {bar}} = {baz: 'baz'};
|
let {foo: {bar}} = {baz: 'baz'};
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,等号左边对象的`foo`属性,对应一个子对象。该子对象的`bar`属性,解构时会报错。原因很简单,因为`foo`这时等于`undefined`,再取子属性就会报错,请看下面的代码。
|
上面代码中,等号左边对象的`foo`属性,对应一个子对象。该子对象的`bar`属性,解构时会报错。原因很简单,因为`foo`这时等于`undefined`,再取子属性就会报错,请看下面的代码。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var _tmp = {baz: 'baz'};
|
let _tmp = {baz: 'baz'};
|
||||||
_tmp.foo.bar // 报错
|
_tmp.foo.bar // 报错
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -351,7 +337,7 @@ _tmp.foo.bar // 报错
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 错误的写法
|
// 错误的写法
|
||||||
var x;
|
let x;
|
||||||
{x} = {x: 1};
|
{x} = {x: 1};
|
||||||
// SyntaxError: syntax error
|
// SyntaxError: syntax error
|
||||||
```
|
```
|
||||||
@ -360,12 +346,13 @@ var x;
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 正确的写法
|
// 正确的写法
|
||||||
|
let x;
|
||||||
({x} = {x: 1});
|
({x} = {x: 1});
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。关于圆括号与解构赋值的关系,参见下文。
|
上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。关于圆括号与解构赋值的关系,参见下文。
|
||||||
|
|
||||||
解构赋值允许,等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。
|
解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
({} = [true, false]);
|
({} = [true, false]);
|
||||||
@ -386,8 +373,8 @@ let { log, sin, cos } = Math;
|
|||||||
由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。
|
由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var arr = [1, 2, 3];
|
let arr = [1, 2, 3];
|
||||||
var {0 : first, [arr.length - 1] : last} = arr;
|
let {0 : first, [arr.length - 1] : last} = arr;
|
||||||
first // 1
|
first // 1
|
||||||
last // 3
|
last // 3
|
||||||
```
|
```
|
||||||
@ -428,7 +415,7 @@ s === Boolean.prototype.toString // true
|
|||||||
|
|
||||||
上面代码中,数值和布尔值的包装对象都有`toString`属性,因此变量`s`都能取到值。
|
上面代码中,数值和布尔值的包装对象都有`toString`属性,因此变量`s`都能取到值。
|
||||||
|
|
||||||
解构赋值的规则是,只要等号右边的值不是对象,就先将其转为对象。由于`undefined`和`null`无法转为对象,所以对它们进行解构赋值,都会报错。
|
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于`undefined`和`null`无法转为对象,所以对它们进行解构赋值,都会报错。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let { prop: x } = undefined; // TypeError
|
let { prop: x } = undefined; // TypeError
|
||||||
@ -505,32 +492,34 @@ move(); // [0, 0]
|
|||||||
|
|
||||||
以下三种解构赋值不得使用圆括号。
|
以下三种解构赋值不得使用圆括号。
|
||||||
|
|
||||||
(1)变量声明语句中,不能带有圆括号。
|
(1)变量声明语句
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 全部报错
|
// 全部报错
|
||||||
var [(a)] = [1];
|
let [(a)] = [1];
|
||||||
|
|
||||||
var {x: (c)} = {};
|
let {x: (c)} = {};
|
||||||
var ({x: c}) = {};
|
let ({x: c}) = {};
|
||||||
var {(x: c)} = {};
|
let {(x: c)} = {};
|
||||||
var {(x): c} = {};
|
let {(x): c} = {};
|
||||||
|
|
||||||
var { o: ({ p: p }) } = { o: { p: 2 } };
|
let { o: ({ p: p }) } = { o: { p: 2 } };
|
||||||
```
|
```
|
||||||
|
|
||||||
上面三个语句都会报错,因为它们都是变量声明语句,模式不能使用圆括号。
|
上面 6 个语句都会报错,因为它们都是变量声明语句,模式不能使用圆括号。
|
||||||
|
|
||||||
(2)函数参数中,模式不能带有圆括号。
|
(2)函数参数
|
||||||
|
|
||||||
函数参数也属于变量声明,因此不能带有圆括号。
|
函数参数也属于变量声明,因此不能带有圆括号。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 报错
|
// 报错
|
||||||
function f([(z)]) { return z; }
|
function f([(z)]) { return z; }
|
||||||
|
// 报错
|
||||||
|
function f([z,(x)]) { return x; }
|
||||||
```
|
```
|
||||||
|
|
||||||
(3)赋值语句中,不能将整个模式,或嵌套模式中的一层,放在圆括号之中。
|
(3)赋值语句的模式
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 全部报错
|
// 全部报错
|
||||||
@ -545,7 +534,7 @@ function f([(z)]) { return z; }
|
|||||||
[({ p: a }), { x: c }] = [{}, {}];
|
[({ p: a }), { x: c }] = [{}, {}];
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码将嵌套模式的一层,放在圆括号之中,导致报错。
|
上面代码将一部分模式放在圆括号之中,导致报错。
|
||||||
|
|
||||||
### 可以使用圆括号的情况
|
### 可以使用圆括号的情况
|
||||||
|
|
||||||
@ -557,7 +546,7 @@ function f([(z)]) { return z; }
|
|||||||
[(parseInt.prop)] = [3]; // 正确
|
[(parseInt.prop)] = [3]; // 正确
|
||||||
```
|
```
|
||||||
|
|
||||||
上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是p,而不是d;第三行语句与第一行语句的性质一致。
|
上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是`p`,而不是`d`;第三行语句与第一行语句的性质一致。
|
||||||
|
|
||||||
## 用途
|
## 用途
|
||||||
|
|
||||||
@ -566,6 +555,9 @@ function f([(z)]) { return z; }
|
|||||||
**(1)交换变量的值**
|
**(1)交换变量的值**
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
let x = 1;
|
||||||
|
let y = 2;
|
||||||
|
|
||||||
[x, y] = [y, x];
|
[x, y] = [y, x];
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -581,7 +573,7 @@ function f([(z)]) { return z; }
|
|||||||
function example() {
|
function example() {
|
||||||
return [1, 2, 3];
|
return [1, 2, 3];
|
||||||
}
|
}
|
||||||
var [a, b, c] = example();
|
let [a, b, c] = example();
|
||||||
|
|
||||||
// 返回一个对象
|
// 返回一个对象
|
||||||
|
|
||||||
@ -591,7 +583,7 @@ function example() {
|
|||||||
bar: 2
|
bar: 2
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
var { foo, bar } = example();
|
let { foo, bar } = example();
|
||||||
```
|
```
|
||||||
|
|
||||||
**(3)函数参数的定义**
|
**(3)函数参数的定义**
|
||||||
@ -613,7 +605,7 @@ f({z: 3, y: 2, x: 1});
|
|||||||
解构赋值对提取 JSON 对象中的数据,尤其有用。
|
解构赋值对提取 JSON 对象中的数据,尤其有用。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var jsonData = {
|
let jsonData = {
|
||||||
id: 42,
|
id: 42,
|
||||||
status: "OK",
|
status: "OK",
|
||||||
data: [867, 5309]
|
data: [867, 5309]
|
||||||
@ -650,7 +642,7 @@ jQuery.ajax = function (url, {
|
|||||||
任何部署了 Iterator 接口的对象,都可以用`for...of`循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。
|
任何部署了 Iterator 接口的对象,都可以用`for...of`循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var map = new Map();
|
const map = new Map();
|
||||||
map.set('first', 'hello');
|
map.set('first', 'hello');
|
||||||
map.set('second', 'world');
|
map.set('second', 'world');
|
||||||
|
|
||||||
@ -677,7 +669,7 @@ for (let [,value] of map) {
|
|||||||
|
|
||||||
**(7)输入模块的指定方法**
|
**(7)输入模块的指定方法**
|
||||||
|
|
||||||
加载模块时,往往需要指定输入那些方法。解构赋值使得输入语句非常清晰。
|
加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const { SourceMapConsumer, SourceNode } = require("source-map");
|
const { SourceMapConsumer, SourceNode } = require("source-map");
|
||||||
|
502
docs/function.md
502
docs/function.md
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
### 基本用法
|
### 基本用法
|
||||||
|
|
||||||
在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
|
ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function log(x, y) {
|
function log(x, y) {
|
||||||
@ -47,7 +47,7 @@ function Point(x = 0, y = 0) {
|
|||||||
this.y = y;
|
this.y = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
var p = new Point();
|
const p = new Point();
|
||||||
p // { x: 0, y: 0 }
|
p // { x: 0, y: 0 }
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -64,6 +64,37 @@ function foo(x = 5) {
|
|||||||
|
|
||||||
上面代码中,参数变量`x`是默认声明的,在函数体中,不能用`let`或`const`再次声明,否则会报错。
|
上面代码中,参数变量`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);
|
console.log(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
foo({}) // undefined, 5
|
foo({}) // undefined 5
|
||||||
foo({x: 1}) // 1, 5
|
foo({x: 1}) // 1 5
|
||||||
foo({x: 1, y: 2}) // 1, 2
|
foo({x: 1, y: 2}) // 1 2
|
||||||
foo() // TypeError: Cannot read property 'x' of undefined
|
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
|
```javascript
|
||||||
function fetch(url, { body = '', method = 'GET', headers = {} }) {
|
function fetch(url, { body = '', method = 'GET', headers = {} }) {
|
||||||
@ -95,12 +136,10 @@ fetch('http://example.com')
|
|||||||
// 报错
|
// 报错
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,如果函数`fetch`的第二个参数是一个对象,就可以为它的三个属性设置默认值。
|
上面代码中,如果函数`fetch`的第二个参数是一个对象,就可以为它的三个属性设置默认值。这种写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。
|
||||||
|
|
||||||
上面的写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function fetch(url, { method = 'GET' } = {}) {
|
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
|
||||||
console.log(method);
|
console.log(method);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,7 +149,7 @@ fetch('http://example.com')
|
|||||||
|
|
||||||
上面代码中,函数`fetch`没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量`method`才会取到默认值`GET`。
|
上面代码中,函数`fetch`没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量`method`才会取到默认值`GET`。
|
||||||
|
|
||||||
再请问下面两种写法有什么差别?
|
作为练习,请问下面两种写法有什么差别?
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 写法一
|
// 写法一
|
||||||
@ -200,7 +239,7 @@ foo(undefined, null)
|
|||||||
|
|
||||||
上面代码中,`length`属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。比如,上面最后一个函数,定义了 3 个参数,其中有一个参数`c`指定了默认值,因此`length`属性等于`3`减去`1`,最后得到`2`。
|
上面代码中,`length`属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。比如,上面最后一个函数,定义了 3 个参数,其中有一个参数`c`指定了默认值,因此`length`属性等于`3`减去`1`,最后得到`2`。
|
||||||
|
|
||||||
这是因为`length`属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,rest参数也不会计入`length`属性。
|
这是因为`length`属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,后文的 rest 参数也不会计入`length`属性。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
(function(...args) {}).length // 0
|
(function(...args) {}).length // 0
|
||||||
@ -215,7 +254,7 @@ foo(undefined, null)
|
|||||||
|
|
||||||
### 作用域
|
### 作用域
|
||||||
|
|
||||||
一个需要注意的地方是,如果参数默认值是一个变量,则该变量所处的作用域,与其他变量的作用域规则是一样的,即先是当前函数的作用域,然后才是全局作用域。
|
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var x = 1;
|
var x = 1;
|
||||||
@ -227,9 +266,9 @@ function f(x, y = x) {
|
|||||||
f(2) // 2
|
f(2) // 2
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,参数`y`的默认值等于`x`。调用时,由于函数作用域内部的变量`x`已经生成,所以`y`等于参数`x`,而不是全局变量`x`。
|
上面代码中,参数`y`的默认值等于变量`x`。调用函数`f`时,参数形成一个单独的作用域。在这个作用域里面,默认值变量`x`指向第一个参数`x`,而不是全局变量`x`,所以输出是`2`。
|
||||||
|
|
||||||
如果调用时,函数作用域内部的变量`x`没有生成,结果就会不一样。
|
再看下面的例子。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let x = 1;
|
let x = 1;
|
||||||
@ -242,7 +281,7 @@ function f(y = x) {
|
|||||||
f() // 1
|
f() // 1
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,函数调用时,`y`的默认值变量`x`尚未在函数内部生成,所以`x`指向全局变量。
|
上面代码中,函数`f`调用时,参数`y = x`形成一个单独的作用域。这个作用域里面,变量`x`本身没有定义,所以指向外层的全局变量`x`。函数调用时,函数体内部的局部变量`x`影响不到默认值变量`x`。
|
||||||
|
|
||||||
如果此时,全局变量`x`不存在,就会报错。
|
如果此时,全局变量`x`不存在,就会报错。
|
||||||
|
|
||||||
@ -267,22 +306,22 @@ function foo(x = x) {
|
|||||||
foo() // ReferenceError: x is not defined
|
foo() // ReferenceError: x is not defined
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,函数`foo`的参数`x`的默认值也是`x`。这时,默认值`x`的作用域是函数作用域,而不是全局作用域。由于在函数作用域中,存在变量`x`,但是默认值在`x`赋值之前先执行了,所以这时属于暂时性死区(参见《let和const命令》一章),任何对`x`的操作都会报错。
|
上面代码中,参数`x = x`形成一个单独作用域。实际执行的是`let x = x`,由于暂时性死区的原因,这行代码会报错”x 未定义“。
|
||||||
|
|
||||||
如果参数的默认值是一个函数,该函数的作用域是其声明时所在的作用域。请看下面的例子。
|
如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。请看下面的例子。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let foo = 'outer';
|
let foo = 'outer';
|
||||||
|
|
||||||
function bar(func = x => foo) {
|
function bar(func = () => foo) {
|
||||||
let foo = 'inner';
|
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
|
bar() // ReferenceError: foo is not defined
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,匿名函数里面的`foo`指向函数外层,但是函数外层并没有声明`foo`,所以就报错了。
|
上面代码中,匿名函数里面的`foo`指向函数外层,但是函数外层并没有声明变量`foo`,所以就报错了。
|
||||||
|
|
||||||
下面是一个更复杂的例子。
|
下面是一个更复杂的例子。
|
||||||
|
|
||||||
@ -308,11 +347,12 @@ function foo(x, y = function() { x = 2; }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
foo() // 3
|
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
|
```javascript
|
||||||
var x = 1;
|
var x = 1;
|
||||||
@ -323,6 +363,7 @@ function foo(x, y = function() { x = 2; }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
foo() // 2
|
foo() // 2
|
||||||
|
x // 1
|
||||||
```
|
```
|
||||||
|
|
||||||
### 应用
|
### 应用
|
||||||
@ -344,7 +385,7 @@ foo()
|
|||||||
|
|
||||||
上面代码的`foo`函数,如果调用的时候没有参数,就会调用默认值`throwIfMissing`函数,从而抛出一个错误。
|
上面代码的`foo`函数,如果调用的时候没有参数,就会调用默认值`throwIfMissing`函数,从而抛出一个错误。
|
||||||
|
|
||||||
从上面代码还可以看到,参数`mustBeProvided`的默认值等于`throwIfMissing`函数的运行结果(即函数名之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行(即如果参数已经赋值,默认值中的函数就不会运行),这与python语言不一样。
|
从上面代码还可以看到,参数`mustBeProvided`的默认值等于`throwIfMissing`函数的运行结果(注意函数名`throwIfMissing`之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行。如果参数已经赋值,默认值中的函数就不会运行。
|
||||||
|
|
||||||
另外,可以将参数默认值设为`undefined`,表明这个参数是可以省略的。
|
另外,可以将参数默认值设为`undefined`,表明这个参数是可以省略的。
|
||||||
|
|
||||||
@ -354,7 +395,7 @@ function foo(optional = undefined) { ··· }
|
|||||||
|
|
||||||
## rest 参数
|
## rest 参数
|
||||||
|
|
||||||
ES6引入rest参数(形式为“...变量名”),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
|
ES6 引入 rest 参数(形式为`...变量名`),用于获取函数的多余参数,这样就不需要使用`arguments`对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function add(...values) {
|
function add(...values) {
|
||||||
@ -370,9 +411,9 @@ function add(...values) {
|
|||||||
add(2, 5, 3) // 10
|
add(2, 5, 3) // 10
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码的add函数是一个求和函数,利用rest参数,可以向该函数传入任意数目的参数。
|
上面代码的`add`函数是一个求和函数,利用 rest 参数,可以向该函数传入任意数目的参数。
|
||||||
|
|
||||||
下面是一个rest参数代替arguments变量的例子。
|
下面是一个 rest 参数代替`arguments`变量的例子。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// arguments变量的写法
|
// arguments变量的写法
|
||||||
@ -386,7 +427,7 @@ const sortNumbers = (...numbers) => numbers.sort();
|
|||||||
|
|
||||||
上面代码的两种写法,比较后可以发现,rest 参数的写法更自然也更简洁。
|
上面代码的两种写法,比较后可以发现,rest 参数的写法更自然也更简洁。
|
||||||
|
|
||||||
rest参数中的变量代表一个数组,所以数组特有的方法都可以用于这个变量。下面是一个利用rest参数改写数组push方法的例子。
|
`arguments`对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用`Array.prototype.slice.call`先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。下面是一个利用 rest 参数改写数组`push`方法的例子。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function push(array, ...items) {
|
function push(array, ...items) {
|
||||||
@ -409,7 +450,7 @@ function f(a, ...b, c) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
函数的length属性,不包括rest参数。
|
函数的`length`属性,不包括 rest 参数。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
(function(a) {}).length // 1
|
(function(a) {}).length // 1
|
||||||
@ -417,283 +458,6 @@ function f(a, ...b, c) {
|
|||||||
(function(a, ...b) {}).length // 1
|
(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`转为真正的数组。
|
|
||||||
|
|
||||||
**(6)Map和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 开始,函数内部可以设定为严格模式。
|
从 ES5 开始,函数内部可以设定为严格模式。
|
||||||
@ -705,7 +469,7 @@ function doSomething(a, b) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
《ECMAScript 2016标准》做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。
|
ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 报错
|
// 报错
|
||||||
@ -735,7 +499,7 @@ const obj = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
这样规定的原因是,函数内部的严格模式,同时适用于函数体代码和函数参数代码。但是,函数执行的时候,先执行函数参数代码,然后再执行函数体代码。这样就有一个不合理的地方,只有从函数体代码之中,才能知道参数代码是否应该以严格模式执行,但是参数代码却应该先于函数体代码执行。
|
这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 报错
|
// 报错
|
||||||
@ -784,16 +548,16 @@ foo.name // "foo"
|
|||||||
需要注意的是,ES6 对这个属性的行为做出了一些修改。如果将一个匿名函数赋值给一个变量,ES5 的`name`属性,会返回空字符串,而 ES6 的`name`属性会返回实际的函数名。
|
需要注意的是,ES6 对这个属性的行为做出了一些修改。如果将一个匿名函数赋值给一个变量,ES5 的`name`属性,会返回空字符串,而 ES6 的`name`属性会返回实际的函数名。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var func1 = function () {};
|
var f = function () {};
|
||||||
|
|
||||||
// ES5
|
// ES5
|
||||||
func1.name // ""
|
f.name // ""
|
||||||
|
|
||||||
// ES6
|
// ES6
|
||||||
func1.name // "func1"
|
f.name // "f"
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,变量`func1`等于一个匿名函数,ES5和ES6的`name`属性返回的值不一样。
|
上面代码中,变量`f`等于一个匿名函数,ES5 和 ES6 的`name`属性返回的值不一样。
|
||||||
|
|
||||||
如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的`name`属性都返回这个具名函数原本的名字。
|
如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的`name`属性都返回这个具名函数原本的名字。
|
||||||
|
|
||||||
@ -807,13 +571,13 @@ bar.name // "baz"
|
|||||||
bar.name // "baz"
|
bar.name // "baz"
|
||||||
```
|
```
|
||||||
|
|
||||||
`Function`构造函数返回的函数实例,`name`属性的值为“anonymous”。
|
`Function`构造函数返回的函数实例,`name`属性的值为`anonymous`。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
(new Function).name // "anonymous"
|
(new Function).name // "anonymous"
|
||||||
```
|
```
|
||||||
|
|
||||||
`bind`返回的函数,`name`属性值会加上“bound ”前缀。
|
`bind`返回的函数,`name`属性值会加上`bound`前缀。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function foo() {};
|
function foo() {};
|
||||||
@ -860,10 +624,20 @@ var sum = function(num1, num2) {
|
|||||||
var sum = (num1, num2) => { return num1 + num2; }
|
var sum = (num1, num2) => { return num1 + num2; }
|
||||||
```
|
```
|
||||||
|
|
||||||
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号。
|
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
|
||||||
|
|
||||||
```javascript
|
```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`命令,否则会抛出一个错误。
|
(2)不可以当作构造函数,也就是说,不可以使用`new`命令,否则会抛出一个错误。
|
||||||
|
|
||||||
(3)不可以使用`arguments`对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。
|
(3)不可以使用`arguments`对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
|
||||||
|
|
||||||
(4)不可以使用`yield`命令,因此箭头函数不能用作 Generator 函数。
|
(4)不可以使用`yield`命令,因此箭头函数不能用作 Generator 函数。
|
||||||
|
|
||||||
@ -1137,11 +911,11 @@ var fix = f => (x => f(v => x(x)(v)))
|
|||||||
|
|
||||||
上面两种写法,几乎是一一对应的。由于 λ 演算对于计算机科学非常重要,这使得我们可以用 ES6 作为替代工具,探索计算机科学。
|
上面两种写法,几乎是一一对应的。由于 λ 演算对于计算机科学非常重要,这使得我们可以用 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
|
```javascript
|
||||||
foo::bar;
|
foo::bar;
|
||||||
@ -1170,7 +944,7 @@ let log = ::console.log;
|
|||||||
var log = console.log.bind(console);
|
var log = console.log.bind(console);
|
||||||
```
|
```
|
||||||
|
|
||||||
由于双冒号运算符返回的还是原对象,因此可以采用链式写法。
|
双冒号运算符的运算结果,还是一个对象,因此可以采用链式写法。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 例一
|
// 例一
|
||||||
@ -1201,7 +975,7 @@ function f(x){
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,函数f的最后一步是调用函数g,这就叫尾调用。
|
上面代码中,函数`f`的最后一步是调用函数`g`,这就叫尾调用。
|
||||||
|
|
||||||
以下三种情况,都不属于尾调用。
|
以下三种情况,都不属于尾调用。
|
||||||
|
|
||||||
@ -1223,7 +997,7 @@ function f(x){
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,情况一是调用函数g之后,还有赋值操作,所以不属于尾调用,即使语义完全一样。情况二也属于调用后还有操作,即使写在一行内。情况三等同于下面的代码。
|
上面代码中,情况一是调用函数`g`之后,还有赋值操作,所以不属于尾调用,即使语义完全一样。情况二也属于调用后还有操作,即使写在一行内。情况三等同于下面的代码。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function f(x){
|
function f(x){
|
||||||
@ -1243,13 +1017,13 @@ function f(x) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,函数m和n都属于尾调用,因为它们都是函数f的最后一步操作。
|
上面代码中,函数`m`和`n`都属于尾调用,因为它们都是函数`f`的最后一步操作。
|
||||||
|
|
||||||
### 尾调用优化
|
### 尾调用优化
|
||||||
|
|
||||||
尾调用之所以与其他调用不同,就在于它的特殊的调用位置。
|
尾调用之所以与其他调用不同,就在于它的特殊的调用位置。
|
||||||
|
|
||||||
我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数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(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),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。
|
这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。
|
||||||
|
|
||||||
@ -1304,7 +1078,7 @@ function factorial(n) {
|
|||||||
factorial(5) // 120
|
factorial(5) // 120
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。
|
上面代码是一个阶乘函数,计算`n`的阶乘,最多需要保存`n`个调用记录,复杂度 O(n) 。
|
||||||
|
|
||||||
如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。
|
如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。
|
||||||
|
|
||||||
@ -1317,9 +1091,9 @@ function factorial(n, total) {
|
|||||||
factorial(5, 1) // 120
|
factorial(5, 1) // 120
|
||||||
```
|
```
|
||||||
|
|
||||||
还有一个比较著名的例子,就是计算fibonacci 数列,也能充分说明尾递归优化的重要性
|
还有一个比较著名的例子,就是计算 Fibonacci 数列,也能充分说明尾递归优化的重要性。
|
||||||
|
|
||||||
如果是非尾递归的fibonacci 递归方法
|
非尾递归的 Fibonacci 数列实现如下。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function Fibonacci (n) {
|
function Fibonacci (n) {
|
||||||
@ -1328,13 +1102,12 @@ function Fibonacci (n) {
|
|||||||
return Fibonacci(n - 1) + Fibonacci(n - 2);
|
return Fibonacci(n - 1) + Fibonacci(n - 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
Fibonacci(10); // 89
|
Fibonacci(10) // 89
|
||||||
// Fibonacci(100)
|
Fibonacci(100) // 堆栈溢出
|
||||||
// Fibonacci(500)
|
Fibonacci(500) // 堆栈溢出
|
||||||
// 堆栈溢出了
|
|
||||||
```
|
```
|
||||||
|
|
||||||
如果我们使用尾递归优化过的fibonacci 递归算法
|
尾递归优化过的 Fibonacci 数列实现如下。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
|
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
|
||||||
@ -1348,11 +1121,11 @@ Fibonacci2(1000) // 7.0330367711422765e+208
|
|||||||
Fibonacci2(10000) // Infinity
|
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(5) // 120
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码通过一个正常形式的阶乘函数 factorial ,调用尾递归函数 tailFactorial ,看起来就正常多了。
|
上面代码通过一个正常形式的阶乘函数`factorial`,调用尾递归函数`tailFactorial`,看起来就正常多了。
|
||||||
|
|
||||||
函数式编程有一个概念,叫做柯里化(currying),意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化。
|
函数式编程有一个概念,叫做柯里化(currying),意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化。
|
||||||
|
|
||||||
@ -1390,7 +1163,7 @@ const factorial = currying(tailFactorial, 1);
|
|||||||
factorial(5) // 120
|
factorial(5) // 120
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码通过柯里化,将尾递归函数 tailFactorial 变为只接受1个参数的 factorial 。
|
上面代码通过柯里化,将尾递归函数`tailFactorial`变为只接受一个参数的`factorial`。
|
||||||
|
|
||||||
第二种方法就简单多了,就是采用 ES6 的函数默认值。
|
第二种方法就简单多了,就是采用 ES6 的函数默认值。
|
||||||
|
|
||||||
@ -1403,7 +1176,7 @@ function factorial(n, total = 1) {
|
|||||||
factorial(5) // 120
|
factorial(5) // 120
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,参数 total 有默认值1,所以调用时不用提供这个值。
|
上面代码中,参数`total`有默认值`1`,所以调用时不用提供这个值。
|
||||||
|
|
||||||
总结一下,递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言(比如 Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。
|
总结一下,递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言(比如 Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。
|
||||||
|
|
||||||
@ -1420,7 +1193,7 @@ ES6的尾调用优化只在严格模式下开启,正常模式是无效的。
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function restricted() {
|
function restricted() {
|
||||||
"use strict";
|
'use strict';
|
||||||
restricted.caller; // 报错
|
restricted.caller; // 报错
|
||||||
restricted.arguments; // 报错
|
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`后面加一个逗号,就会报错。
|
上面代码中,如果在`param2`或`bar`后面加一个逗号,就会报错。
|
||||||
|
|
||||||
这样的话,如果以后修改代码,想为函数`clownsEverywhere`添加第三个参数,就势必要在第二个参数后面添加一个逗号。这对版本管理系统来说,就会显示,添加逗号的那一行也发生了变动。这看上去有点冗余,因此新的语法允许定义和调用时,尾部直接有一个逗号。
|
如果像上面这样,将参数写成多行(即每个参数占据一行),以后修改代码的时候,想为函数`clownsEverywhere`添加第三个参数,或者调整参数的次序,就势必要在原来最后一个参数后面添加一个逗号。这对于版本管理系统来说,就会显示添加逗号的那一行也发生了变动。这看上去有点冗余,因此新的语法允许定义和调用时,尾部直接有一个逗号。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function clownsEverywhere(
|
function clownsEverywhere(
|
||||||
@ -1553,3 +1326,42 @@ clownsEverywhere(
|
|||||||
'bar',
|
'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
790
docs/generator-async.md
Normal 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 函数,在回调函数里面交回执行权。
|
||||||
|
|
||||||
|
(2)Promise 对象。将异步操作包装成 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`这个词出现的次数。
|
@ -1,16 +1,16 @@
|
|||||||
# Generator 函数
|
# Generator 函数的语法
|
||||||
|
|
||||||
## 简介
|
## 简介
|
||||||
|
|
||||||
### 基本概念
|
### 基本概念
|
||||||
|
|
||||||
Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。本章详细介绍Generator函数的语法和API,它的异步编程应用请看《异步操作》一章。
|
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。本章详细介绍 Generator 函数的语法和 API,它的异步编程应用请看《Generator 函数的异步应用》一章。
|
||||||
|
|
||||||
Generator函数有多种理解角度。从语法上,首先可以把它理解成,Generator函数是一个状态机,封装了多个内部状态。
|
Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
|
||||||
|
|
||||||
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
|
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
|
||||||
|
|
||||||
形式上,Generator函数是一个普通函数,但是有两个特征。一是,`function`关键字与函数名之间有一个星号;二是,函数体内部使用`yield`语句,定义不同的内部状态(yield语句在英语里的意思就是“产出”)。
|
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,`function`关键字与函数名之间有一个星号;二是,函数体内部使用`yield`表达式,定义不同的内部状态(`yield`在英语里的意思就是“产出”)。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function* helloWorldGenerator() {
|
function* helloWorldGenerator() {
|
||||||
@ -22,11 +22,11 @@ function* helloWorldGenerator() {
|
|||||||
var hw = helloWorldGenerator();
|
var hw = helloWorldGenerator();
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码定义了一个Generator函数`helloWorldGenerator`,它内部有两个`yield`语句“hello”和“world”,即该函数有三个状态:hello,world和return语句(结束执行)。
|
上面代码定义了一个 Generator 函数`helloWorldGenerator`,它内部有两个`yield`表达式(`hello`和`world`),即该函数有三个状态:hello,world 和 return 语句(结束执行)。
|
||||||
|
|
||||||
然后,Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。
|
然后,Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。
|
||||||
|
|
||||||
下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用`next`方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个`yield`语句(或`return`语句)为止。换言之,Generator函数是分段执行的,`yield`语句是暂停执行的标记,而`next`方法可以恢复执行。
|
下一步,必须调用遍历器对象的`next`方法,使得指针移向下一个状态。也就是说,每次调用`next`方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个`yield`表达式(或`return`语句)为止。换言之,Generator 函数是分段执行的,`yield`表达式是暂停执行的标记,而`next`方法可以恢复执行。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
hw.next()
|
hw.next()
|
||||||
@ -44,45 +44,42 @@ hw.next()
|
|||||||
|
|
||||||
上面代码一共调用了四次`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`关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。
|
ES6 没有规定,`function`关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function * foo(x, y) { ··· }
|
function * foo(x, y) { ··· }
|
||||||
|
|
||||||
function *foo(x, y) { ··· }
|
function *foo(x, y) { ··· }
|
||||||
|
|
||||||
function* foo(x, y) { ··· }
|
function* foo(x, y) { ··· }
|
||||||
|
|
||||||
function*foo(x, y) { ··· }
|
function*foo(x, y) { ··· }
|
||||||
```
|
```
|
||||||
|
|
||||||
由于 Generator 函数仍然是普通函数,所以一般的写法是上面的第三种,即星号紧跟在`function`关键字后面。本书也采用这种写法。
|
由于 Generator 函数仍然是普通函数,所以一般的写法是上面的第三种,即星号紧跟在`function`关键字后面。本书也采用这种写法。
|
||||||
|
|
||||||
### yield语句
|
### yield 表达式
|
||||||
|
|
||||||
由于Generator函数返回的遍历器对象,只有调用`next`方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。`yield`语句就是暂停标志。
|
由于 Generator 函数返回的遍历器对象,只有调用`next`方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。`yield`表达式就是暂停标志。
|
||||||
|
|
||||||
遍历器对象的`next`方法的运行逻辑如下。
|
遍历器对象的`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`。
|
(4)如果该函数没有`return`语句,则返回的对象的`value`属性值为`undefined`。
|
||||||
|
|
||||||
需要注意的是,`yield`语句后面的表达式,只有当调用`next`方法、内部指针指向该语句时才会执行,因此等于为JavaScript提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
|
需要注意的是,`yield`表达式后面的表达式,只有当调用`next`方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function* gen() {
|
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
|
```javascript
|
||||||
function* f() {
|
function* f() {
|
||||||
@ -110,7 +107,7 @@ setTimeout(function () {
|
|||||||
|
|
||||||
上面代码中,函数`f`如果是普通函数,在为变量`generator`赋值时就会执行。但是,函数`f`是一个 Generator 函数,就变成只有调用`next`方法时,函数`f`才会执行。
|
上面代码中,函数`f`如果是普通函数,在为变量`generator`赋值时就会执行。但是,函数`f`是一个 Generator 函数,就变成只有调用`next`方法时,函数`f`才会执行。
|
||||||
|
|
||||||
另外需要注意,`yield`语句不能用在普通函数中,否则会报错。
|
另外需要注意,`yield`表达式只能用在 Generator 函数里面,用在其他地方都会报错。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
(function (){
|
(function (){
|
||||||
@ -119,7 +116,7 @@ setTimeout(function () {
|
|||||||
// SyntaxError: Unexpected number
|
// SyntaxError: Unexpected number
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码在一个普通函数中使用`yield`语句,结果产生一个句法错误。
|
上面代码在一个普通函数中使用`yield`表达式,结果产生一个句法错误。
|
||||||
|
|
||||||
下面是另外一个例子。
|
下面是另外一个例子。
|
||||||
|
|
||||||
@ -133,7 +130,7 @@ var flat = function* (a) {
|
|||||||
} else {
|
} else {
|
||||||
yield item;
|
yield item;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
for (var f of flat(arr)){
|
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
|
```javascript
|
||||||
var arr = [1, [[2, 3], 4], [5, 6]];
|
var arr = [1, [[2, 3], 4], [5, 6]];
|
||||||
@ -164,21 +161,25 @@ for (var f of flat(arr)) {
|
|||||||
// 1, 2, 3, 4, 5, 6
|
// 1, 2, 3, 4, 5, 6
|
||||||
```
|
```
|
||||||
|
|
||||||
另外,`yield`语句如果用在一个表达式之中,必须放在圆括号里面。
|
另外,`yield`表达式如果用在另一个表达式之中,必须放在圆括号里面。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
function* demo() {
|
||||||
console.log('Hello' + yield); // SyntaxError
|
console.log('Hello' + yield); // SyntaxError
|
||||||
console.log('Hello' + yield 123); // SyntaxError
|
console.log('Hello' + yield 123); // SyntaxError
|
||||||
|
|
||||||
console.log('Hello' + (yield)); // OK
|
console.log('Hello' + (yield)); // OK
|
||||||
console.log('Hello' + (yield 123)); // OK
|
console.log('Hello' + (yield 123)); // OK
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`yield`语句用作函数参数或赋值表达式的右边,可以不加括号。
|
`yield`表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
function* demo() {
|
||||||
foo(yield 'a', yield 'b'); // OK
|
foo(yield 'a', yield 'b'); // OK
|
||||||
let input = yield; // OK
|
let input = yield; // OK
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 与 Iterator 接口的关系
|
### 与 Iterator 接口的关系
|
||||||
@ -217,7 +218,7 @@ g[Symbol.iterator]() === g
|
|||||||
|
|
||||||
## next 方法的参数
|
## next 方法的参数
|
||||||
|
|
||||||
`yield`句本身没有返回值,或者说总是返回`undefined`。`next`方法可以带一个参数,该参数就会被当作上一个`yield`语句的返回值。
|
`yield`表达式本身没有返回值,或者说总是返回`undefined`。`next`方法可以带一个参数,该参数就会被当作上一个`yield`表达式的返回值。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function* f() {
|
function* f() {
|
||||||
@ -234,7 +235,7 @@ g.next() // { value: 1, done: false }
|
|||||||
g.next(true) // { value: 0, 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 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
|
这个功能有很重要的语法意义。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`方法的时候不带参数,导致 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 函数外面再包一层。
|
如果想要第一次调用`next`方法时,就能够输入值,可以在 Generator 函数外面再包一层。
|
||||||
|
|
||||||
@ -286,27 +308,6 @@ wrapped().next('hello!')
|
|||||||
|
|
||||||
上面代码中,Generator 函数如果不用`wrapper`先包一层,是无法第一次调用`next`方法,就输入参数的。
|
上面代码中,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 循环
|
||||||
|
|
||||||
`for...of`循环可以自动遍历 Generator 函数时生成的`Iterator`对象,且此时不再需要调用`next`方法。
|
`for...of`循环可以自动遍历 Generator 函数时生成的`Iterator`对象,且此时不再需要调用`next`方法。
|
||||||
@ -327,7 +328,7 @@ for (let v of foo()) {
|
|||||||
// 1 2 3 4 5
|
// 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`循环,实现斐波那契数列的例子。
|
下面是一个利用 Generator 函数和`for...of`循环,实现斐波那契数列的例子。
|
||||||
|
|
||||||
@ -533,7 +534,7 @@ g.throw();
|
|||||||
|
|
||||||
上面代码中,`g.throw`抛出错误以后,没有任何`try...catch`代码块可以捕获这个错误,导致程序报错,中断执行。
|
上面代码中,`g.throw`抛出错误以后,没有任何`try...catch`代码块可以捕获这个错误,导致程序报错,中断执行。
|
||||||
|
|
||||||
`throw`方法被捕获以后,会附带执行下一条`yield`语句。也就是说,会附带执行一次`next`方法。
|
`throw`方法被捕获以后,会附带执行下一条`yield`表达式。也就是说,会附带执行一次`next`方法。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var gen = function* gen(){
|
var gen = function* gen(){
|
||||||
@ -576,7 +577,7 @@ try {
|
|||||||
|
|
||||||
上面代码中,`throw`命令抛出的错误不会影响到遍历器的状态,所以两次执行`next`方法,都进行了正确的操作。
|
上面代码中,`throw`命令抛出的错误不会影响到遍历器的状态,所以两次执行`next`方法,都进行了正确的操作。
|
||||||
|
|
||||||
这种函数体内捕获错误的机制,大大方便了对错误的处理。多个`yield`语句,可以只用一个`try...catch`代码块来捕获错误。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数内部写一个错误处理语句,现在只在Generator函数内部写一次`catch`语句就可以了。
|
这种函数体内捕获错误的机制,大大方便了对错误的处理。多个`yield`表达式,可以只用一个`try...catch`代码块来捕获错误。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数内部写一个错误处理语句,现在只在 Generator 函数内部写一次`catch`语句就可以了。
|
||||||
|
|
||||||
Generator 函数体外抛出的错误,可以在函数体内捕获;反过来,Generator 函数体内抛出的错误,也可以被函数体外的`catch`捕获。
|
Generator 函数体外抛出的错误,可以在函数体内捕获;反过来,Generator 函数体内抛出的错误,也可以被函数体外的`catch`捕获。
|
||||||
|
|
||||||
@ -695,19 +696,57 @@ function* numbers () {
|
|||||||
}
|
}
|
||||||
yield 6;
|
yield 6;
|
||||||
}
|
}
|
||||||
var g = numbers()
|
var g = numbers();
|
||||||
g.next() // { done: false, value: 1 }
|
g.next() // { value: 1, done: false }
|
||||||
g.next() // { done: false, value: 2 }
|
g.next() // { value: 2, done: false }
|
||||||
g.return(7) // { done: false, value: 4 }
|
g.return(7) // { value: 4, done: false }
|
||||||
g.next() // { done: false, value: 5 }
|
g.next() // { value: 5, done: false }
|
||||||
g.next() // { done: true, value: 7 }
|
g.next() // { value: 7, done: true }
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,调用`return`方法后,就开始执行`finally`代码块,然后等到`finally`代码块执行完,再执行`return`方法。
|
上面代码中,调用`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
|
```javascript
|
||||||
function* foo() {
|
function* foo() {
|
||||||
@ -730,7 +769,7 @@ for (let v of bar()){
|
|||||||
|
|
||||||
上面代码中,`foo`和`bar`都是 Generator 函数,在`bar`里面调用`foo`,是不会有效果的。
|
上面代码中,`foo`和`bar`都是 Generator 函数,在`bar`里面调用`foo`,是不会有效果的。
|
||||||
|
|
||||||
这个就需要用到`yield*`语句,用来在一个Generator函数里面执行另一个Generator函数。
|
这个就需要用到`yield*`表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function* bar() {
|
function* bar() {
|
||||||
@ -797,7 +836,7 @@ gen.next().value // "close"
|
|||||||
|
|
||||||
上面例子中,`outer2`使用了`yield*`,`outer1`没使用。结果就是,`outer1`返回一个遍历器对象,`outer2`返回该遍历器对象的内部值。
|
上面例子中,`outer2`使用了`yield*`,`outer1`没使用。结果就是,`outer1`返回一个遍历器对象,`outer2`返回该遍历器对象的内部值。
|
||||||
|
|
||||||
从语法角度看,如果`yield`命令后面跟的是一个遍历器对象,需要在`yield`命令后面加上星号,表明它返回的是一个遍历器对象。这被称为`yield*`语句。
|
从语法角度看,如果`yield`表达式后面跟的是一个遍历器对象,需要在`yield`表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为`yield*`表达式。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let delegatedIterator = (function* () {
|
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*`后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。
|
如果`yield*`后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。
|
||||||
|
|
||||||
@ -868,7 +907,7 @@ read.next().value // "hello"
|
|||||||
read.next().value // "h"
|
read.next().value // "h"
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,`yield`语句返回整个字符串,`yield*`语句返回单个字符。因为字符串具有Iterator接口,所以被`yield*`遍历。
|
上面代码中,`yield`表达式返回整个字符串,`yield*`语句返回单个字符。因为字符串具有 Iterator 接口,所以被`yield*`遍历。
|
||||||
|
|
||||||
如果被代理的 Generator 函数有`return`语句,那么就可以向代理它的 Generator 函数返回数据。
|
如果被代理的 Generator 函数有`return`语句,那么就可以向代理它的 Generator 函数返回数据。
|
||||||
|
|
||||||
@ -1078,7 +1117,7 @@ obj.b // 2
|
|||||||
obj.c // 3
|
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`,有没有办法将这两个对象统一呢?
|
上面代码中,执行的是遍历器对象`f`,但是生成的对象实例是`obj`,有没有办法将这两个对象统一呢?
|
||||||
|
|
||||||
@ -1129,7 +1168,7 @@ f.c // 3
|
|||||||
|
|
||||||
### Generator 与状态机
|
### Generator 与状态机
|
||||||
|
|
||||||
Generator是实现状态机的最佳结构。比如,下面的clock函数就是一个状态机。
|
Generator 是实现状态机的最佳结构。比如,下面的`clock`函数就是一个状态机。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var ticking = true;
|
var ticking = true;
|
||||||
@ -1142,7 +1181,7 @@ var clock = function() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码的clock函数一共有两种状态(Tick和Tock),每运行一次,就改变一次状态。这个函数如果用Generator实现,就是下面这样。
|
上面代码的`clock`函数一共有两种状态(`Tick`和`Tock`),每运行一次,就改变一次状态。这个函数如果用 Generator 实现,就是下面这样。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var clock = function* () {
|
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)异步操作的同步化表达
|
### (1)异步操作的同步化表达
|
||||||
|
|
||||||
Generator函数的暂停执行的效果,意味着可以把异步操作写在yield语句里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield语句下面,反正要等到调用next方法时再执行。所以,Generator函数的一个重要实际意义就是用来处理异步操作,改写回调函数。
|
Generator 函数的暂停执行的效果,意味着可以把异步操作写在`yield`表达式里面,等到调用`next`方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在`yield`表达式下面,反正要等到调用`next`方法时再执行。所以,Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function* loadUI() {
|
function* loadUI() {
|
||||||
@ -1199,7 +1262,7 @@ loader.next()
|
|||||||
loader.next()
|
loader.next()
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码表示,第一次调用loadUI函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用next方法,则会显示Loading界面,并且异步加载数据。等到数据加载完成,再一次使用next方法,则会隐藏Loading界面。可以看到,这种写法的好处是所有Loading界面的逻辑,都被封装在一个函数,按部就班非常清晰。
|
上面代码中,第一次调用`loadUI`函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用`next`方法,则会显示`Loading`界面(`showLoadingScreen`),并且异步加载数据(`loadUIDataAsynchronously`)。等到数据加载完成,再一次使用`next`方法,则会隐藏`Loading`界面。可以看到,这种写法的好处是所有`Loading`界面的逻辑,都被封装在一个函数,按部就班非常清晰。
|
||||||
|
|
||||||
Ajax 是典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。
|
Ajax 是典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。
|
||||||
|
|
||||||
@ -1220,7 +1283,7 @@ var it = main();
|
|||||||
it.next();
|
it.next();
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码的main函数,就是通过Ajax操作获取数据。可以看到,除了多了一个yield,它几乎与同步操作的写法完全一样。注意,makeAjaxCall函数中的next方法,必须加上response参数,因为yield语句构成的表达式,本身是没有值的,总是等于undefined。
|
上面代码的`main`函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个`yield`,它几乎与同步操作的写法完全一样。注意,`makeAjaxCall`函数中的`next`方法,必须加上`response`参数,因为`yield`表达式,本身是没有值的,总是等于`undefined`。
|
||||||
|
|
||||||
下面是另一个例子,通过 Generator 函数逐行读取文本文件。
|
下面是另一个例子,通过 Generator 函数逐行读取文本文件。
|
||||||
|
|
||||||
@ -1237,7 +1300,7 @@ function* numbers() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码打开文本文件,使用yield语句可以手动逐行读取文件。
|
上面代码打开文本文件,使用`yield`表达式可以手动逐行读取文件。
|
||||||
|
|
||||||
### (2)控制流管理
|
### (2)控制流管理
|
||||||
|
|
||||||
@ -1431,5 +1494,4 @@ function doStuff() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
上面的函数,可以用一模一样的for...of循环处理!两相一比较,就不难看出Generator使得数据或者操作,具备了类似数组的接口。
|
上面的函数,可以用一模一样的`for...of`循环处理!两相一比较,就不难看出 Generator 使得数据或者操作,具备了类似数组的接口。
|
||||||
|
|
||||||
|
121
docs/intro.md
121
docs/intro.md
@ -14,9 +14,9 @@ ECMAScript 6.0(以下简称ES6)是JavaScript语言的下一代标准,已
|
|||||||
|
|
||||||
## ES6 与 ECMAScript 2015 的关系
|
## ES6 与 ECMAScript 2015 的关系
|
||||||
|
|
||||||
媒体里面经常可以看到”ECMAScript 2015“这个词,它与ES6是什么关系呢?
|
ECMAScript 2015(简称 ES2015)这个词,也是经常可以看到的。它与 ES6 是什么关系呢?
|
||||||
|
|
||||||
2011年,ECMAScript 5.1版发布后,就开始制定6.0版了。因此,”ES6”这个词的原意,就是指JavaScript语言的下一个版本。
|
2011 年,ECMAScript 5.1 版发布后,就开始制定 6.0 版了。因此,ES6 这个词的原意,就是指 JavaScript 语言的下一个版本。
|
||||||
|
|
||||||
但是,因为这个版本引入的语法功能太多,而且制定过程当中,还有很多组织和个人不断提交新功能。事情很快就变得清楚了,不可能在一个版本里面包括所有将要引入的功能。常规的做法是先发布 6.0 版,过一段时间再发 6.1 版,然后是 6.2 版、6.3 版等等。
|
但是,因为这个版本引入的语法功能太多,而且制定过程当中,还有很多组织和个人不断提交新功能。事情很快就变得清楚了,不可能在一个版本里面包括所有将要引入的功能。常规的做法是先发布 6.0 版,过一段时间再发 6.1 版,然后是 6.2 版、6.3 版等等。
|
||||||
|
|
||||||
@ -24,13 +24,15 @@ ECMAScript 6.0(以下简称ES6)是JavaScript语言的下一代标准,已
|
|||||||
|
|
||||||
标准委员会最终决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。接下来的时间,就在这个版本的基础上做改动,直到下一年的 6 月份,草案就自然变成了新一年的版本。这样一来,就不需要以前的版本号了,只要用年份标记就可以了。
|
标准委员会最终决定,标准在每年的 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 0 - Strawman(展示阶段)
|
||||||
- Stage 1 - Proposal(征求意见阶段)
|
- Stage 1 - Proposal(征求意见阶段)
|
||||||
@ -38,9 +40,9 @@ ES6的第一个版本,就这样在2015年6月发布了,正式名称就是《
|
|||||||
- Stage 3 - Candidate(候选人阶段)
|
- Stage 3 - Candidate(候选人阶段)
|
||||||
- Stage 4 - Finished(定案阶段)
|
- 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 的历史
|
## 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)代替。
|
Node 是 JavaScript 的服务器运行环境(runtime)。它对 ES6 的支持度更高。除了那些默认打开的功能,还有一些语法功能已经实现了,但是默认没有打开。使用下面的命令,可以查看 Node 已经实现的 ES6 特性。
|
||||||
|
|
||||||
安装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特性。
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ node --v8-options | grep harmony
|
$ 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
|
```bash
|
||||||
$ npm install -g es-checker
|
$ npm install -g es-checker
|
||||||
@ -147,7 +106,7 @@ input.map(function (item) {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
上面的原始代码用了箭头函数,这个特性还没有得到广泛支持,Babel将其转为普通函数,就能在现有的JavaScript环境执行了。
|
上面的原始代码用了箭头函数,Babel 将其转为普通函数,就能在不支持箭头函数的 JavaScript 环境执行了。
|
||||||
|
|
||||||
### 配置文件`.babelrc`
|
### 配置文件`.babelrc`
|
||||||
|
|
||||||
@ -165,13 +124,13 @@ Babel的配置文件是`.babelrc`,存放在项目的根目录下。使用Babel
|
|||||||
`presets`字段设定转码规则,官方提供以下的规则集,你可以根据需要安装。
|
`presets`字段设定转码规则,官方提供以下的规则集,你可以根据需要安装。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# ES2015转码规则
|
# 最新转码规则
|
||||||
$ npm install --save-dev babel-preset-es2015
|
$ npm install --save-dev babel-preset-latest
|
||||||
|
|
||||||
# react 转码规则
|
# react 转码规则
|
||||||
$ npm install --save-dev babel-preset-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-0
|
||||||
$ npm install --save-dev babel-preset-stage-1
|
$ npm install --save-dev babel-preset-stage-1
|
||||||
$ npm install --save-dev babel-preset-stage-2
|
$ npm install --save-dev babel-preset-stage-2
|
||||||
@ -183,7 +142,7 @@ $ npm install --save-dev babel-preset-stage-3
|
|||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
"presets": [
|
"presets": [
|
||||||
"es2015",
|
"latest",
|
||||||
"react",
|
"react",
|
||||||
"stage-2"
|
"stage-2"
|
||||||
],
|
],
|
||||||
@ -351,7 +310,7 @@ babel.transformFromAst(ast, code, options);
|
|||||||
var es6Code = 'let x = n => n + 1';
|
var es6Code = 'let x = n => n + 1';
|
||||||
var es5Code = require('babel-core')
|
var es5Code = require('babel-core')
|
||||||
.transform(es6Code, {
|
.transform(es6Code, {
|
||||||
presets: ['es2015']
|
presets: ['latest']
|
||||||
})
|
})
|
||||||
.code;
|
.code;
|
||||||
// '"use strict";\n\nvar x = function x(n) {\n return n + 1;\n};'
|
// '"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-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`,为当前环境提供一个垫片。
|
举例来说,ES6 在`Array`对象上新增了`Array.from`方法。Babel 就不会转码这个方法。如果想让这个方法运行,必须使用`babel-polyfill`,为当前环境提供一个垫片。
|
||||||
|
|
||||||
@ -383,26 +342,7 @@ Babel默认不转码的API非常多,详细清单可以查看`babel-plugin-tran
|
|||||||
|
|
||||||
### 浏览器环境
|
### 浏览器环境
|
||||||
|
|
||||||
Babel也可以用于浏览器环境。但是,从Babel 6.0开始,不再直接提供浏览器版本,而是要用构建工具构建出来。如果你没有或不想使用构建工具,可以通过安装5.x版本的`babel-core`模块获取。
|
Babel 也可以用于浏览器环境。但是,从 Babel 6.0 开始,不再直接提供浏览器版本,而是要用构建工具构建出来。如果你没有或不想使用构建工具,可以使用[babel-standalone](https://github.com/Daniel15/babel-standalone)模块提供的浏览器版本,将其插入网页。
|
||||||
|
|
||||||
```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)模块提供的浏览器版本,将其插入网页。
|
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.4.4/babel.min.js"></script>
|
<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>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
注意,网页中实时将ES6代码转为ES5,对性能会有影响。生产环境需要加载已经转码完成的脚本。
|
注意,网页实时将 ES6 代码转为 ES5,对性能会有影响。生产环境需要加载已经转码完成的脚本。
|
||||||
|
|
||||||
下面是如何将代码打包成浏览器可以使用的脚本,以`Babel`配合`Browserify`为例。首先,安装`babelify`模块。
|
下面是如何将代码打包成浏览器可以使用的脚本,以`Babel`配合`Browserify`为例。首先,安装`babelify`模块。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ npm install --save-dev babelify babel-preset-es2015
|
$ npm install --save-dev babelify babel-preset-latest
|
||||||
```
|
```
|
||||||
|
|
||||||
然后,再用命令行转换 ES6 脚本。
|
然后,再用命令行转换 ES6 脚本。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ browserify script.js -o bundle.js \
|
$ browserify script.js -o bundle.js \
|
||||||
-t [ babelify --presets [ es2015 ] ]
|
-t [ babelify --presets [ latest ] ]
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码将 ES6 脚本`script.js`,转为`bundle.js`,浏览器直接加载后者就可以了。
|
上面代码将 ES6 脚本`script.js`,转为`bundle.js`,浏览器直接加载后者就可以了。
|
||||||
@ -433,7 +373,7 @@ $ browserify script.js -o bundle.js \
|
|||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
"browserify": {
|
"browserify": {
|
||||||
"transform": [["babelify", { "presets": ["es2015"] }]]
|
"transform": [["babelify", { "presets": ["latest"] }]]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -527,7 +467,7 @@ Traceur允许将ES6代码直接插入网页。首先,必须在网页头部加
|
|||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
正常情况下,上面代码会在控制台打印出9。
|
正常情况下,上面代码会在控制台打印出`9`。
|
||||||
|
|
||||||
如果想对 Traceur 的行为有精确控制,可以采用下面参数配置的写法。
|
如果想对 Traceur 的行为有精确控制,可以采用下面参数配置的写法。
|
||||||
|
|
||||||
@ -555,7 +495,7 @@ Traceur允许将ES6代码直接插入网页。首先,必须在网页头部加
|
|||||||
</script>
|
</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
|
```bash
|
||||||
$ npm install -g traceur
|
$ npm install -g traceur
|
||||||
@ -596,7 +536,7 @@ $ npm install -g traceur
|
|||||||
|
|
||||||
安装成功后,就可以在命令行下使用 Traceur 了。
|
安装成功后,就可以在命令行下使用 Traceur 了。
|
||||||
|
|
||||||
Traceur直接运行es6脚本文件,会在标准输出显示运行结果,以前面的`calc.js`为例。
|
Traceur 直接运行 ES6 脚本文件,会在标准输出显示运行结果,以前面的`calc.js`为例。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ traceur calc.js
|
$ 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
|
```javascript
|
||||||
var traceur = require('traceur');
|
var traceur = require('traceur');
|
||||||
@ -646,4 +586,3 @@ fs.writeFileSync('out.js', result.js);
|
|||||||
// sourceMap 属性对应 map 文件
|
// sourceMap 属性对应 map 文件
|
||||||
fs.writeFileSync('out.js.map', result.sourceMap);
|
fs.writeFileSync('out.js.map', result.sourceMap);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
152
docs/iterator.md
152
docs/iterator.md
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Iterator(遍历器)的概念
|
## Iterator(遍历器)的概念
|
||||||
|
|
||||||
JavaScript原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6又添加了Map和Set。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是Map,Map的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。
|
JavaScript 原有的表示“集合”的数据结构,主要是数组(`Array`)和对象(`Object`),ES6 又添加了`Map`和`Set`。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是`Map`,`Map`的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。
|
||||||
|
|
||||||
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
|
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
|
||||||
|
|
||||||
@ -69,9 +69,9 @@ function makeIterator(array) {
|
|||||||
```javascript
|
```javascript
|
||||||
var it = idMaker();
|
var it = idMaker();
|
||||||
|
|
||||||
it.next().value // '0'
|
it.next().value // 0
|
||||||
it.next().value // '1'
|
it.next().value // 1
|
||||||
it.next().value // '2'
|
it.next().value // 2
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
function idMaker() {
|
function idMaker() {
|
||||||
@ -87,9 +87,7 @@ function idMaker() {
|
|||||||
|
|
||||||
上面的例子中,遍历器生成函数`idMaker`,返回一个遍历器对象(即指针对象)。但是并没有对应的数据结构,或者说,遍历器对象自己描述了一个数据结构出来。
|
上面的例子中,遍历器生成函数`idMaker`,返回一个遍历器对象(即指针对象)。但是并没有对应的数据结构,或者说,遍历器对象自己描述了一个数据结构出来。
|
||||||
|
|
||||||
在ES6中,有些数据结构原生具备Iterator接口(比如数组),即不用任何处理,就可以被`for...of`循环遍历,有些就不行(比如对象)。原因在于,这些数据结构原生部署了`Symbol.iterator`属性(详见下文),另外一些数据结构没有。凡是部署了`Symbol.iterator`属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。
|
如果使用 TypeScript 的写法,遍历器接口(Iterable)、指针对象(Iterator)和`next`方法返回值的规格可以描述如下。
|
||||||
|
|
||||||
如果使用TypeScript的写法,遍历器接口(Iterable)、指针对象(Iterator)和next方法返回值的规格可以描述如下。
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
interface Iterable {
|
interface Iterable {
|
||||||
@ -106,13 +104,13 @@ interface IterationResult {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 数据结构的默认Iterator接口
|
## 默认 Iterator 接口
|
||||||
|
|
||||||
Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即`for...of`循环(详见下文)。当使用`for...of`循环遍历某种数据结构时,该循环会自动去寻找 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
|
```javascript
|
||||||
const obj = {
|
const obj = {
|
||||||
@ -131,7 +129,19 @@ const obj = {
|
|||||||
|
|
||||||
上面代码中,对象`obj`是可遍历的(iterable),因为具有`Symbol.iterator`属性。执行这个属性,会返回一个遍历器对象。该对象的根本特征就是具有`next`方法。每次调用`next`方法,都会返回一个代表当前成员的信息对象,具有`value`和`done`两个属性。
|
上面代码中,对象`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
|
```javascript
|
||||||
let arr = ['a', 'b', 'c'];
|
let arr = ['a', 'b', 'c'];
|
||||||
@ -145,11 +155,11 @@ iter.next() // { value: undefined, done: true }
|
|||||||
|
|
||||||
上面代码中,变量`arr`是一个数组,原生就具有遍历器接口,部署在`arr`的`Symbol.iterator`属性上面。所以,调用这个属性,就得到遍历器对象。
|
上面代码中,变量`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 原生提供了。
|
对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了。
|
||||||
|
|
||||||
一个对象如果要有可被`for...of`循环调用的Iterator接口,就必须在`Symbol.iterator`的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。
|
一个对象如果要具备可被`for...of`循环调用的 Iterator 接口,就必须在`Symbol.iterator`的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
class RangeIterator {
|
class RangeIterator {
|
||||||
@ -165,9 +175,8 @@ class RangeIterator {
|
|||||||
if (value < this.stop) {
|
if (value < this.stop) {
|
||||||
this.value++;
|
this.value++;
|
||||||
return {done: false, value: 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)) {
|
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() {
|
Obj.prototype[Symbol.iterator] = function() {
|
||||||
var iterator = {
|
var iterator = { next: next };
|
||||||
next: next
|
|
||||||
};
|
|
||||||
|
|
||||||
var current = this;
|
var current = this;
|
||||||
|
|
||||||
@ -201,14 +208,9 @@ Obj.prototype[Symbol.iterator] = function() {
|
|||||||
if (current) {
|
if (current) {
|
||||||
var value = current.value;
|
var value = current.value;
|
||||||
current = current.next;
|
current = current.next;
|
||||||
return {
|
return { done: false, value: value };
|
||||||
done: false,
|
|
||||||
value: value
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
return {
|
return { done: true };
|
||||||
done: true
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return iterator;
|
return iterator;
|
||||||
@ -222,11 +224,8 @@ one.next = two;
|
|||||||
two.next = three;
|
two.next = three;
|
||||||
|
|
||||||
for (var i of one){
|
for (var i of one){
|
||||||
console.log(i);
|
console.log(i); // 1, 2, 3
|
||||||
}
|
}
|
||||||
// 1
|
|
||||||
// 2
|
|
||||||
// 3
|
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码首先在构造函数的原型链上部署`Symbol.iterator`方法,调用该方法会返回遍历器对象`iterator`,调用该对象的`next`方法,在返回一个值的同时,自动将内部指针移到下一个实例。
|
上面代码首先在构造函数的原型链上部署`Symbol.iterator`方法,调用该方法会返回遍历器对象`iterator`,调用该对象的`next`方法,在返回一个值的同时,自动将内部指针移到下一个实例。
|
||||||
@ -255,7 +254,7 @@ let obj = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
对于类似数组的对象(存在数值键名和length属性),部署Iterator接口,有一个简便方法,就是`Symbol.iterator`方法直接引用数组的Iterator接口。
|
对于类似数组的对象(存在数值键名和`length`属性),部署 Iterator 接口,有一个简便方法,就是`Symbol.iterator`方法直接引用数组的 Iterator 接口。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
|
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
|
||||||
@ -265,7 +264,9 @@ NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];
|
|||||||
[...document.querySelectorAll('div')] // 可以执行了
|
[...document.querySelectorAll('div')] // 可以执行了
|
||||||
```
|
```
|
||||||
|
|
||||||
下面是类似数组的对象调用数组的`Symbol.iterator`方法的例子。
|
NodeList 对象是类似数组的对象,本来就具有遍历接口,可以直接遍历。上面代码中,我们将它的遍历接口改成数组的`Symbol.iterator`属性,可以看到没有任何影响。
|
||||||
|
|
||||||
|
下面是另一个类似数组的对象调用数组的`Symbol.iterator`方法的例子。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let iterable = {
|
let iterable = {
|
||||||
@ -305,7 +306,7 @@ obj[Symbol.iterator] = () => 1;
|
|||||||
[...obj] // TypeError: [] is not a function
|
[...obj] // TypeError: [] is not a function
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,变量obj的Symbol.iterator方法对应的不是遍历器生成函数,因此报错。
|
上面代码中,变量`obj`的`Symbol.iterator`方法对应的不是遍历器生成函数,因此报错。
|
||||||
|
|
||||||
有了遍历器接口,数据结构就可以用`for...of`循环遍历(详见下文),也可以使用`while`循环遍历。
|
有了遍历器接口,数据结构就可以用`for...of`循环遍历(详见下文),也可以使用`while`循环遍历。
|
||||||
|
|
||||||
@ -341,7 +342,7 @@ let [first, ...rest] = set;
|
|||||||
|
|
||||||
**(2)扩展运算符**
|
**(2)扩展运算符**
|
||||||
|
|
||||||
扩展运算符(...)也会调用默认的iterator接口。
|
扩展运算符(...)也会调用默认的 Iterator 接口。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 例一
|
// 例一
|
||||||
@ -362,9 +363,9 @@ let arr = ['b', 'c'];
|
|||||||
let arr = [...iterable];
|
let arr = [...iterable];
|
||||||
```
|
```
|
||||||
|
|
||||||
**(3)yield* **
|
**(3)yield\***
|
||||||
|
|
||||||
yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
|
`yield*`后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let generator = function* () {
|
let generator = function* () {
|
||||||
@ -443,13 +444,13 @@ str // "hi"
|
|||||||
`Symbol.iterator`方法的最简单实现,还是使用下一章要介绍的 Generator 函数。
|
`Symbol.iterator`方法的最简单实现,还是使用下一章要介绍的 Generator 函数。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var myIterable = {};
|
let myIterable = {
|
||||||
|
[Symbol.iterator]: function* () {
|
||||||
myIterable[Symbol.iterator] = function* () {
|
|
||||||
yield 1;
|
yield 1;
|
||||||
yield 2;
|
yield 2;
|
||||||
yield 3;
|
yield 3;
|
||||||
};
|
}
|
||||||
|
}
|
||||||
[...myIterable] // [1, 2, 3]
|
[...myIterable] // [1, 2, 3]
|
||||||
|
|
||||||
// 或者采用下面的简洁写法
|
// 或者采用下面的简洁写法
|
||||||
@ -464,8 +465,8 @@ let obj = {
|
|||||||
for (let x of obj) {
|
for (let x of obj) {
|
||||||
console.log(x);
|
console.log(x);
|
||||||
}
|
}
|
||||||
// hello
|
// "hello"
|
||||||
// world
|
// "world"
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,`Symbol.iterator`方法几乎不用部署任何代码,只要用 yield 命令给出每一步的返回值即可。
|
上面代码中,`Symbol.iterator`方法几乎不用部署任何代码,只要用 yield 命令给出每一步的返回值即可。
|
||||||
@ -480,10 +481,7 @@ for (let x of obj) {
|
|||||||
function readLinesSync(file) {
|
function readLinesSync(file) {
|
||||||
return {
|
return {
|
||||||
next() {
|
next() {
|
||||||
if (file.isAtEndOfFile()) {
|
return { done: false };
|
||||||
file.close();
|
|
||||||
return { done: true };
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
return() {
|
return() {
|
||||||
file.close();
|
file.close();
|
||||||
@ -493,15 +491,30 @@ function readLinesSync(file) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,函数`readLinesSync`接受一个文件对象作为参数,返回一个遍历器对象,其中除了`next`方法,还部署了`return`方法。下面,我们让文件的遍历提前返回,这样就会触发执行`return`方法。
|
上面代码中,函数`readLinesSync`接受一个文件对象作为参数,返回一个遍历器对象,其中除了`next`方法,还部署了`return`方法。下面的三种情况,都会触发执行`return`方法。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
// 情况一
|
||||||
for (let line of readLinesSync(fileName)) {
|
for (let line of readLinesSync(fileName)) {
|
||||||
console.log(line);
|
console.log(line);
|
||||||
break;
|
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 规格决定的。
|
注意,`return`方法必须返回一个对象,这是 Generator 规格决定的。
|
||||||
|
|
||||||
`throw`方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。请参阅《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`循环也不一样。
|
`for...of`循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟`for...in`循环也不一样。
|
||||||
|
|
||||||
@ -625,7 +638,7 @@ for (let [key, value] of map) {
|
|||||||
|
|
||||||
有些数据结构是在现有数据结构的基础上,计算生成的。比如,ES6 的数组、Set、Map 都部署了以下三个方法,调用后都返回遍历器对象。
|
有些数据结构是在现有数据结构的基础上,计算生成的。比如,ES6 的数组、Set、Map 都部署了以下三个方法,调用后都返回遍历器对象。
|
||||||
|
|
||||||
- `entries()` 返回一个遍历器对象,用来遍历`[键名, 键值]`组成的数组。对于数组,键名就是索引值;对于Set,键名与键值相同。Map结构的iterator接口,默认就是调用entries方法。
|
- `entries()` 返回一个遍历器对象,用来遍历`[键名, 键值]`组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的 Iterator 接口,默认就是调用`entries`方法。
|
||||||
- `keys()` 返回一个遍历器对象,用来遍历所有的键名。
|
- `keys()` 返回一个遍历器对象,用来遍历所有的键名。
|
||||||
- `values()` 返回一个遍历器对象,用来遍历所有的键值。
|
- `values()` 返回一个遍历器对象,用来遍历所有的键值。
|
||||||
|
|
||||||
@ -643,7 +656,7 @@ for (let pair of arr.entries()) {
|
|||||||
|
|
||||||
### 类似数组的对象
|
### 类似数组的对象
|
||||||
|
|
||||||
类似数组的对象包括好几类。下面是`for...of`循环用于字符串、DOM NodeList对象、arguments对象的例子。
|
类似数组的对象包括好几类。下面是`for...of`循环用于字符串、DOM NodeList 对象、`arguments`对象的例子。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 字符串
|
// 字符串
|
||||||
@ -681,7 +694,7 @@ for (let x of 'a\uD83D\uDC0A') {
|
|||||||
// '\uD83D\uDC0A'
|
// '\uD83D\uDC0A'
|
||||||
```
|
```
|
||||||
|
|
||||||
并不是所有类似数组的对象都具有iterator接口,一个简便的解决方法,就是使用Array.from方法将其转为数组。
|
并不是所有类似数组的对象都具有 Iterator 接口,一个简便的解决方法,就是使用`Array.from`方法将其转为数组。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let arrayLike = { length: 2, 0: 'a', 1: 'b' };
|
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
|
```javascript
|
||||||
var es6 = {
|
let es6 = {
|
||||||
edition: 6,
|
edition: 6,
|
||||||
committee: "TC39",
|
committee: "TC39",
|
||||||
standard: "ECMA-262"
|
standard: "ECMA-262"
|
||||||
@ -718,7 +731,7 @@ for (let e in es6) {
|
|||||||
for (let e of es6) {
|
for (let e of es6) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
// TypeError: es6 is not iterable
|
// TypeError: es6[Symbol.iterator] is not a function
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码表示,对于普通的对象,`for...in`循环可以遍历键名,`for...of`循环会报错。
|
上面代码表示,对于普通的对象,`for...in`循环可以遍历键名,`for...of`循环会报错。
|
||||||
@ -727,17 +740,10 @@ for (let e of es6) {
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
for (var key of Object.keys(someObject)) {
|
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 函数将对象重新包装一下。
|
另一个方法是使用 Generator 函数将对象重新包装一下。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
@ -748,7 +754,7 @@ function* entries(obj) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let [key, value] of entries(obj)) {
|
for (let [key, value] of entries(obj)) {
|
||||||
console.log(key, "->", value);
|
console.log(key, '->', value);
|
||||||
}
|
}
|
||||||
// a -> 1
|
// a -> 1
|
||||||
// b -> 2
|
// b -> 2
|
||||||
@ -757,7 +763,7 @@ for (let [key, value] of entries(obj)) {
|
|||||||
|
|
||||||
### 与其他遍历语法的比较
|
### 与其他遍历语法的比较
|
||||||
|
|
||||||
以数组为例,JavaScript提供多种遍历语法。最原始的写法就是for循环。
|
以数组为例,JavaScript 提供多种遍历语法。最原始的写法就是`for`循环。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
for (var index = 0; index < myArray.length; index++) {
|
for (var index = 0; index < myArray.length; index++) {
|
||||||
@ -765,7 +771,7 @@ for (var index = 0; index < myArray.length; index++) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
这种写法比较麻烦,因此数组提供内置的forEach方法。
|
这种写法比较麻烦,因此数组提供内置的`forEach`方法。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
myArray.forEach(function (value) {
|
myArray.forEach(function (value) {
|
||||||
@ -773,7 +779,7 @@ myArray.forEach(function (value) {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
这种写法的问题在于,无法中途跳出`forEach`循环,break命令或return命令都不能奏效。
|
这种写法的问题在于,无法中途跳出`forEach`循环,`break`命令或`return`命令都不能奏效。
|
||||||
|
|
||||||
`for...in`循环可以遍历数组的键名。
|
`for...in`循环可以遍历数组的键名。
|
||||||
|
|
||||||
@ -783,11 +789,11 @@ for (var index in myArray) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
for...in循环有几个缺点。
|
`for...in`循环有几个缺点。
|
||||||
|
|
||||||
- 数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。
|
- 数组的键名是数字,但是`for...in`循环是以字符串作为键名“0”、“1”、“2”等等。
|
||||||
- for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
|
- `for...in`循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
|
||||||
- 某些情况下,for...in循环会以任意顺序遍历键名。
|
- 某些情况下,`for...in`循环会以任意顺序遍历键名。
|
||||||
|
|
||||||
总之,`for...in`循环主要是为遍历对象而设计的,不适用于遍历数组。
|
总之,`for...in`循环主要是为遍历对象而设计的,不适用于遍历数组。
|
||||||
|
|
||||||
@ -799,8 +805,8 @@ for (let value of myArray) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- 有着同for...in一样的简洁语法,但是没有for...in那些缺点。
|
- 有着同`for...in`一样的简洁语法,但是没有`for...in`那些缺点。
|
||||||
- 不同用于forEach方法,它可以与break、continue和return配合使用。
|
- 不同于`forEach`方法,它可以与`break`、`continue`和`return`配合使用。
|
||||||
- 提供了遍历所有数据结构的统一操作接口。
|
- 提供了遍历所有数据结构的统一操作接口。
|
||||||
|
|
||||||
下面是一个使用 break 语句,跳出`for...of`循环的例子。
|
下面是一个使用 break 语句,跳出`for...of`循环的例子。
|
||||||
@ -813,4 +819,4 @@ for (var n of fibonacci) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
上面的例子,会输出斐波纳契数列小于等于1000的项。如果当前项大于1000,就会使用break语句跳出`for...of`循环。
|
上面的例子,会输出斐波纳契数列小于等于 1000 的项。如果当前项大于 1000,就会使用`break`语句跳出`for...of`循环。
|
||||||
|
143
docs/let.md
143
docs/let.md
@ -21,7 +21,9 @@ b // 1
|
|||||||
`for`循环的计数器,就很合适使用`let`命令。
|
`for`循环的计数器,就很合适使用`let`命令。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
for (let i = 0; i < 10; i++) {}
|
for (let i = 0; i < 10; i++) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
console.log(i);
|
console.log(i);
|
||||||
// ReferenceError: i is not defined
|
// ReferenceError: i is not defined
|
||||||
@ -29,7 +31,7 @@ console.log(i);
|
|||||||
|
|
||||||
上面代码中,计数器`i`只在`for`循环体内有效,在循环体外引用就会报错。
|
上面代码中,计数器`i`只在`for`循环体内有效,在循环体外引用就会报错。
|
||||||
|
|
||||||
下面的代码如果使用`var`,最后输出的是10。
|
下面的代码如果使用`var`,最后输出的是`10`。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var a = [];
|
var a = [];
|
||||||
@ -41,7 +43,7 @@ for (var i = 0; i < 10; i++) {
|
|||||||
a[6](); // 10
|
a[6](); // 10
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,变量`i`是`var`声明的,在全局范围内都有效。所以每一次循环,新的`i`值都会覆盖旧值,导致最后输出的是最后一轮的`i`的值。
|
上面代码中,变量`i`是`var`命令声明的,在全局范围内都有效,所以全局只有一个变量`i`。每一次循环,变量`i`的值都会发生改变,而循环内被赋给数组`a`的函数内部的`console.log(i)`,里面的`i`指向的就是全局的`i`。也就是说,所有数组`a`的成员里面的`i`,指向的都是同一个`i`,导致运行时输出的是最后一轮的`i`的值,也就是 10。
|
||||||
|
|
||||||
如果使用`let`,声明的变量仅在块级作用域内有效,最后输出的是 6。
|
如果使用`let`,声明的变量仅在块级作用域内有效,最后输出的是 6。
|
||||||
|
|
||||||
@ -55,17 +57,35 @@ for (let i = 0; i < 10; i++) {
|
|||||||
a[6](); // 6
|
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
|
```javascript
|
||||||
|
// var 的情况
|
||||||
console.log(foo); // 输出undefined
|
console.log(foo); // 输出undefined
|
||||||
console.log(bar); // 报错ReferenceError
|
|
||||||
|
|
||||||
var foo = 2;
|
var foo = 2;
|
||||||
|
|
||||||
|
// let 的情况
|
||||||
|
console.log(bar); // 报错ReferenceError
|
||||||
let bar = 2;
|
let bar = 2;
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -88,7 +108,7 @@ if (true) {
|
|||||||
|
|
||||||
ES6 明确规定,如果区块中存在`let`和`const`命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
|
ES6 明确规定,如果区块中存在`let`和`const`命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
|
||||||
|
|
||||||
总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称TDZ)。
|
总之,在代码块内,使用`let`命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
if (true) {
|
if (true) {
|
||||||
@ -142,23 +162,36 @@ function bar(x = 2, y = x) {
|
|||||||
bar(); // [2, 2]
|
bar(); // [2, 2]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
另外,下面的代码也会报错,与`var`的行为不同。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 不报错
|
||||||
|
var x = x;
|
||||||
|
|
||||||
|
// 报错
|
||||||
|
let x = x;
|
||||||
|
// ReferenceError: x is not defined
|
||||||
|
```
|
||||||
|
|
||||||
|
上面代码报错,也是因为暂时性死区。使用`let`声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量`x`的声明语句还没有执行完成前,就去取`x`的值,导致报错”x 未定义“。
|
||||||
|
|
||||||
ES6 规定暂时性死区和`let`、`const`语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。
|
ES6 规定暂时性死区和`let`、`const`语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。
|
||||||
|
|
||||||
总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
|
总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
|
||||||
|
|
||||||
### 不允许重复声明
|
### 不允许重复声明
|
||||||
|
|
||||||
let不允许在相同作用域内,重复声明同一个变量。
|
`let`不允许在相同作用域内,重复声明同一个变量。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 报错
|
// 报错
|
||||||
function () {
|
function func() {
|
||||||
let a = 10;
|
let a = 10;
|
||||||
var a = 1;
|
var a = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 报错
|
// 报错
|
||||||
function () {
|
function func() {
|
||||||
let a = 10;
|
let a = 10;
|
||||||
let a = 1;
|
let a = 1;
|
||||||
}
|
}
|
||||||
@ -192,14 +225,14 @@ var tmp = new Date();
|
|||||||
function f() {
|
function f() {
|
||||||
console.log(tmp);
|
console.log(tmp);
|
||||||
if (false) {
|
if (false) {
|
||||||
var tmp = "hello world";
|
var tmp = 'hello world';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
f(); // undefined
|
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
|
console.log(i); // 5
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
|
上面代码中,变量`i`只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
|
||||||
|
|
||||||
### ES6 的块级作用域
|
### ES6 的块级作用域
|
||||||
|
|
||||||
@ -229,7 +262,7 @@ function f1() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
上面的函数有两个代码块,都声明了变量`n`,运行后输出5。这表示外层代码块不受内层代码块的影响。如果使用`var`定义变量`n`,最后输出的值就是10。
|
上面的函数有两个代码块,都声明了变量`n`,运行后输出 5。这表示外层代码块不受内层代码块的影响。如果两次都使用`var`定义变量`n`,最后输出的值才是 10。
|
||||||
|
|
||||||
ES6 允许块级作用域的任意嵌套。
|
ES6 允许块级作用域的任意嵌套。
|
||||||
|
|
||||||
@ -273,7 +306,7 @@ ES6允许块级作用域的任意嵌套。
|
|||||||
|
|
||||||
### 块级作用域与函数声明
|
### 块级作用域与函数声明
|
||||||
|
|
||||||
函数能不能在块级作用域之中声明,是一个相当令人混淆的问题。
|
函数能不能在块级作用域之中声明?这是一个相当令人混淆的问题。
|
||||||
|
|
||||||
ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
|
ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
|
||||||
|
|
||||||
@ -287,37 +320,19 @@ if (true) {
|
|||||||
try {
|
try {
|
||||||
function f() {}
|
function f() {}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
// ...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码的两种函数声明,根据ES5的规定都是非法的。
|
上面两种函数声明,根据 ES5 的规定都是非法的。
|
||||||
|
|
||||||
但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。不过,“严格模式”下还是会报错。
|
但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。
|
||||||
|
|
||||||
```javascript
|
ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于`let`,在块级作用域之外不可引用。
|
||||||
// ES5严格模式
|
|
||||||
'use strict';
|
|
||||||
if (true) {
|
|
||||||
function f() {}
|
|
||||||
}
|
|
||||||
// 报错
|
|
||||||
```
|
|
||||||
|
|
||||||
ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// ES6严格模式
|
|
||||||
'use strict';
|
|
||||||
if (true) {
|
|
||||||
function f() {}
|
|
||||||
}
|
|
||||||
// 不报错
|
|
||||||
```
|
|
||||||
|
|
||||||
ES6 规定,块级作用域之中,函数声明语句的行为类似于`let`,在块级作用域之外不可引用。
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function f() { console.log('I am outside!'); }
|
function f() { console.log('I am outside!'); }
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
if (false) {
|
if (false) {
|
||||||
// 重复声明一次函数f
|
// 重复声明一次函数f
|
||||||
@ -331,8 +346,9 @@ function f() { console.log('I am outside!'); }
|
|||||||
上面代码在 ES5 中运行,会得到“I am inside!”,因为在`if`内声明的函数`f`会被提升到函数头部,实际运行的代码如下。
|
上面代码在 ES5 中运行,会得到“I am inside!”,因为在`if`内声明的函数`f`会被提升到函数头部,实际运行的代码如下。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// ES5版本
|
// ES5 环境
|
||||||
function f() { console.log('I am outside!'); }
|
function f() { console.log('I am outside!'); }
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
function f() { console.log('I am inside!'); }
|
function f() { console.log('I am inside!'); }
|
||||||
if (false) {
|
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 在[附录 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版本
|
|
||||||
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)。
|
|
||||||
|
|
||||||
- 允许在块级作用域内声明函数。
|
- 允许在块级作用域内声明函数。
|
||||||
- 函数声明类似于`var`,即会提升到全局作用域或函数作用域的头部。
|
- 函数声明类似于`var`,即会提升到全局作用域或函数作用域的头部。
|
||||||
@ -359,11 +367,12 @@ function f() { console.log('I am outside!'); }
|
|||||||
|
|
||||||
注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作`let`处理。
|
注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作`let`处理。
|
||||||
|
|
||||||
前面那段代码,在 Chrome 环境下运行会报错。
|
根据这三条规则,在浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于`var`声明的变量。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// ES6的浏览器环境
|
// 浏览器的 ES6 环境
|
||||||
function f() { console.log('I am outside!'); }
|
function f() { console.log('I am outside!'); }
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
if (false) {
|
if (false) {
|
||||||
// 重复声明一次函数f
|
// 重复声明一次函数f
|
||||||
@ -375,10 +384,10 @@ function f() { console.log('I am outside!'); }
|
|||||||
// Uncaught TypeError: f is not a function
|
// Uncaught TypeError: f is not a function
|
||||||
```
|
```
|
||||||
|
|
||||||
上面的代码报错,是因为实际运行的是下面的代码。
|
上面的代码在符合 ES6 的浏览器中,都会报错,因为实际运行的是下面的代码。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// ES6的浏览器环境
|
// 浏览器的 ES6 环境
|
||||||
function f() { console.log('I am outside!'); }
|
function f() { console.log('I am outside!'); }
|
||||||
(function () {
|
(function () {
|
||||||
var f = undefined;
|
var f = undefined;
|
||||||
@ -439,7 +448,7 @@ if (true)
|
|||||||
|
|
||||||
上面代码中,块级作用域将两个语句封装在一起。但是,在块级作用域以外,没有办法得到`t`的值,因为块级作用域不返回值,除非`t`是全局变量。
|
上面代码中,块级作用域将两个语句封装在一起。但是,在块级作用域以外,没有办法得到`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
|
```javascript
|
||||||
let x = do {
|
let x = do {
|
||||||
@ -448,10 +457,12 @@ let x = do {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,变量`x`会得到整个块级作用域的返回值。
|
上面代码中,变量`x`会得到整个块级作用域的返回值(`t * t + 1`)。
|
||||||
|
|
||||||
## const 命令
|
## const 命令
|
||||||
|
|
||||||
|
### 基本用法
|
||||||
|
|
||||||
`const`声明一个只读的常量。一旦声明,常量的值就不能改变。
|
`const`声明一个只读的常量。一旦声明,常量的值就不能改变。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
@ -464,7 +475,7 @@ PI = 3;
|
|||||||
|
|
||||||
上面代码表明改变常量的值会报错。
|
上面代码表明改变常量的值会报错。
|
||||||
|
|
||||||
`const`声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。
|
`const`声明的变量不得改变值,这意味着,`const`一旦声明变量,就必须立即初始化,不能留到以后赋值。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const foo;
|
const foo;
|
||||||
@ -505,15 +516,18 @@ const message = "Goodbye!";
|
|||||||
const age = 30;
|
const age = 30;
|
||||||
```
|
```
|
||||||
|
|
||||||
对于复合类型的变量,变量名不指向数据,而是指向数据所在的地址。`const`命令只是保证变量名指向的地址不变,并不保证该地址的数据不变,所以将一个对象声明为常量必须非常小心。
|
### 本质
|
||||||
|
|
||||||
|
`const`实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针,`const`只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const foo = {};
|
const foo = {};
|
||||||
|
|
||||||
|
// 为 foo 添加一个属性,可以成功
|
||||||
foo.prop = 123;
|
foo.prop = 123;
|
||||||
|
foo.prop // 123
|
||||||
|
|
||||||
foo.prop
|
// 将 foo 指向另一个对象,就会报错
|
||||||
// 123
|
|
||||||
|
|
||||||
foo = {}; // TypeError: "foo" is read-only
|
foo = {}; // TypeError: "foo" is read-only
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -521,7 +535,7 @@ foo = {}; // TypeError: "foo" is read-only
|
|||||||
|
|
||||||
下面是另一个例子。
|
下面是另一个例子。
|
||||||
|
|
||||||
```js
|
```javascript
|
||||||
const a = [];
|
const a = [];
|
||||||
a.push('Hello'); // 可执行
|
a.push('Hello'); // 可执行
|
||||||
a.length = 0; // 可执行
|
a.length = 0; // 可执行
|
||||||
@ -547,7 +561,7 @@ foo.prop = 123;
|
|||||||
```javascript
|
```javascript
|
||||||
var constantize = (obj) => {
|
var constantize = (obj) => {
|
||||||
Object.freeze(obj);
|
Object.freeze(obj);
|
||||||
Object.keys(obj).forEach( (key, value) => {
|
Object.keys(obj).forEach( (key, i) => {
|
||||||
if ( typeof obj[key] === 'object' ) {
|
if ( typeof obj[key] === 'object' ) {
|
||||||
constantize( obj[key] );
|
constantize( obj[key] );
|
||||||
}
|
}
|
||||||
@ -555,6 +569,8 @@ var constantize = (obj) => {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### ES6 声明变量的六种方法
|
||||||
|
|
||||||
ES5 只有两种声明变量的方法:`var`命令和`function`命令。ES6 除了添加`let`和`const`命令,后面章节还会提到,另外两种声明变量的方法:`import`命令和`class`命令。所以,ES6 一共有 6 种声明变量的方法。
|
ES5 只有两种声明变量的方法:`var`命令和`function`命令。ES6 除了添加`let`和`const`命令,后面章节还会提到,另外两种声明变量的方法:`import`命令和`class`命令。所以,ES6 一共有 6 种声明变量的方法。
|
||||||
|
|
||||||
## 顶层对象的属性
|
## 顶层对象的属性
|
||||||
@ -646,4 +662,3 @@ const global = getGlobal();
|
|||||||
```
|
```
|
||||||
|
|
||||||
上面代码将顶层对象放入变量`global`。
|
上面代码将顶层对象放入变量`global`。
|
||||||
|
|
||||||
|
814
docs/module-loader.md
Normal file
814
docs/module-loader.md
Normal 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`方法指定回调函数。
|
728
docs/module.md
728
docs/module.md
@ -1,8 +1,10 @@
|
|||||||
# Module
|
# Module 的语法
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的`require`、Python 的`import`,甚至就连 CSS 都有`@import`,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。
|
历史上,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 模块就是对象,输入时必须查找对象属性。
|
ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
|
||||||
|
|
||||||
@ -12,7 +14,9 @@ let { stat, exists, readFile } = require('fs');
|
|||||||
|
|
||||||
// 等同于
|
// 等同于
|
||||||
let _fs = 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 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
|
上面代码的实质是整体加载`fs`模块(即加载`fs`的所有方法),生成一个对象(`_fs`),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
|
||||||
@ -31,9 +35,11 @@ import { stat, exists, readFile } from 'fs';
|
|||||||
除了静态加载带来的各种好处,ES6 模块还有以下好处。
|
除了静态加载带来的各种好处,ES6 模块还有以下好处。
|
||||||
|
|
||||||
- 不再需要`UMD`模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
|
- 不再需要`UMD`模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
|
||||||
- 将来浏览器的新 API 就能用模块格式提供,不再必要做成全局变量或者`navigator`对象的属性。
|
- 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者`navigator`对象的属性。
|
||||||
- 不再需要对象作为命名空间(比如`Math`对象),未来这些功能可以通过模块提供。
|
- 不再需要对象作为命名空间(比如`Math`对象),未来这些功能可以通过模块提供。
|
||||||
|
|
||||||
|
本章介绍 ES6 模块的语法,下一章介绍如何在浏览器和 Node 之中,加载 ES6 模块。
|
||||||
|
|
||||||
## 严格模式
|
## 严格模式
|
||||||
|
|
||||||
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上`"use strict";`。
|
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上`"use strict";`。
|
||||||
@ -58,6 +64,8 @@ ES6 的模块自动采用严格模式,不管你有没有在模块头部加上`
|
|||||||
|
|
||||||
上面这些限制,模块都必须遵守。由于严格模式是 ES5 引入的,不属于 ES6,所以请参阅相关 ES5 书籍,本书不再详细介绍了。
|
上面这些限制,模块都必须遵守。由于严格模式是 ES5 引入的,不属于 ES6,所以请参阅相关 ES5 书籍,本书不再详细介绍了。
|
||||||
|
|
||||||
|
其中,尤其需要注意`this`的限制。ES6 模块之中,顶层的`this`指向`undefined`,即不应该在顶层代码使用`this`。
|
||||||
|
|
||||||
## export 命令
|
## export 命令
|
||||||
|
|
||||||
模块功能主要由两个命令构成:`export`和`import`。`export`命令用于规定模块的对外接口,`import`命令用于输入其他模块提供的功能。
|
模块功能主要由两个命令构成:`export`和`import`。`export`命令用于规定模块的对外接口,`import`命令用于输入其他模块提供的功能。
|
||||||
@ -86,7 +94,7 @@ export {firstName, lastName, year};
|
|||||||
|
|
||||||
上面代码在`export`命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在`var`语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。
|
上面代码在`export`命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在`var`语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。
|
||||||
|
|
||||||
export命令除了输出变量,还可以输出函数或类(class)。
|
`export`命令除了输出变量,还可以输出函数或类(class)。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
export function multiply(x, y) {
|
export function multiply(x, y) {
|
||||||
@ -163,7 +171,7 @@ setTimeout(() => foo = 'baz', 500);
|
|||||||
|
|
||||||
上面代码输出变量`foo`,值为`bar`,500 毫秒之后变成`baz`。
|
上面代码输出变量`foo`,值为`bar`,500 毫秒之后变成`baz`。
|
||||||
|
|
||||||
这一点与CommonJS规范完全不同。CommonJS模块输出的是值的缓存,不存在动态更新,详见下文《ES6模块加载的实质》一节。
|
这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新,详见下文《Module 的加载实现》一节。
|
||||||
|
|
||||||
最后,`export`命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的`import`命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
|
最后,`export`命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的`import`命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
|
||||||
|
|
||||||
@ -197,7 +205,7 @@ function setName(element) {
|
|||||||
import { lastName as surname } from './profile';
|
import { lastName as surname } from './profile';
|
||||||
```
|
```
|
||||||
|
|
||||||
`import`后面的`from`指定模块文件的位置,可以是相对路径,也可以是绝对路径,`.js`路径可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
|
`import`后面的`from`指定模块文件的位置,可以是相对路径,也可以是绝对路径,`.js`后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import {myMethod} from 'util';
|
import {myMethod} from 'util';
|
||||||
@ -262,6 +270,14 @@ import { foo, bar } from 'my_module';
|
|||||||
|
|
||||||
上面代码中,虽然`foo`和`bar`在两个语句中加载,但是它们对应的是同一个`my_module`实例。也就是说,`import`语句是 Singleton 模式。
|
上面代码中,虽然`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));
|
console.log('圆周长:' + circle.circumference(14));
|
||||||
```
|
```
|
||||||
|
|
||||||
|
注意,模块整体加载所在的那个对象(上例是`circle`),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import * as circle from './circle';
|
||||||
|
|
||||||
|
// 下面两行都是不允许的
|
||||||
|
circle.foo = 'hello';
|
||||||
|
circle.area = function () {};
|
||||||
|
```
|
||||||
|
|
||||||
## export default 命令
|
## export default 命令
|
||||||
|
|
||||||
从前面的例子可以看出,使用`import`命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。
|
从前面的例子可以看出,使用`import`命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。
|
||||||
@ -378,9 +404,9 @@ export {add as default};
|
|||||||
// export default add;
|
// export default add;
|
||||||
|
|
||||||
// app.js
|
// 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`的变量,所以它后面不能跟变量声明语句。
|
正是因为`export default`命令其实只是输出一个叫做`default`的变量,所以它后面不能跟变量声明语句。
|
||||||
@ -399,16 +425,28 @@ export default var a = 1;
|
|||||||
|
|
||||||
上面代码中,`export default a`的含义是将变量`a`的值赋给变量`default`。所以,最后一种写法会报错。
|
上面代码中,`export default a`的含义是将变量`a`的值赋给变量`default`。所以,最后一种写法会报错。
|
||||||
|
|
||||||
|
同样地,因为`export default`本质是将该命令后面的值,赋给`default`变量以后再默认,所以直接将一个值写在`export default`之后。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 正确
|
||||||
|
export default 42;
|
||||||
|
|
||||||
|
// 报错
|
||||||
|
export 42;
|
||||||
|
```
|
||||||
|
|
||||||
|
上面代码中,后一句报错是因为没有指定对外的接口,而前一句指定外对接口为`default`。
|
||||||
|
|
||||||
有了`export default`命令,输入模块时就非常直观了,以输入 lodash 模块为例。
|
有了`export default`命令,输入模块时就非常直观了,以输入 lodash 模块为例。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
```
|
```
|
||||||
|
|
||||||
如果想在一条`import`语句中,同时输入默认方法和其他变量,可以写成下面这样。
|
如果想在一条`import`语句中,同时输入默认方法和其他接口,可以写成下面这样。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import _, { each } from 'lodash';
|
import _, { each, each as forEach } from 'lodash';
|
||||||
```
|
```
|
||||||
|
|
||||||
对应上面代码的`export`语句如下。
|
对应上面代码的`export`语句如下。
|
||||||
@ -417,20 +455,16 @@ import _, { each } from 'lodash';
|
|||||||
export default function (obj) {
|
export default function (obj) {
|
||||||
// ···
|
// ···
|
||||||
}
|
}
|
||||||
|
|
||||||
export function each(obj, iterator, context) {
|
export function each(obj, iterator, context) {
|
||||||
// ···
|
// ···
|
||||||
}
|
}
|
||||||
|
|
||||||
export { each as forEach };
|
export { each as forEach };
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码的最后一行的意思是,暴露出`forEach`接口,默认指向`each`接口,即`forEach`和`each`指向同一个方法。
|
上面代码的最后一行的意思是,暴露出`forEach`接口,默认指向`each`接口,即`forEach`和`each`指向同一个方法。
|
||||||
|
|
||||||
如果要输出默认的值,只需将值跟在`export default`之后即可。
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
export default 42;
|
|
||||||
```
|
|
||||||
|
|
||||||
`export default`也可以用来输出类。
|
`export default`也可以用来输出类。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
@ -451,7 +485,7 @@ export { foo, bar } from 'my_module';
|
|||||||
|
|
||||||
// 等同于
|
// 等同于
|
||||||
import { foo, bar } from 'my_module';
|
import { foo, bar } from 'my_module';
|
||||||
export { foo, boo};
|
export { foo, bar };
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,`export`和`import`语句可以结合在一起,写成一行。
|
上面代码中,`export`和`import`语句可以结合在一起,写成一行。
|
||||||
@ -488,14 +522,20 @@ export default es6;
|
|||||||
export { default as es6 } from './someModule';
|
export { default as es6 } from './someModule';
|
||||||
```
|
```
|
||||||
|
|
||||||
另外,ES7有一个[提案](https://github.com/leebyron/ecmascript-more-export-from),简化先输入后输出的写法,拿掉输出时的大括号。
|
下面三种`import`语句,没有对应的复合写法。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 现行的写法
|
import * as someIdentifier from "someModule";
|
||||||
export {v} from 'mod';
|
import someIdentifier from "someModule";
|
||||||
|
import someIdentifier, { namedIdentifier } from "someModule";
|
||||||
|
```
|
||||||
|
|
||||||
// 提案的写法
|
为了做到形式的对称,现在有[提案](https://github.com/leebyron/ecmascript-export-default-from),提出补上这三种复合写法。
|
||||||
export v from 'mod';
|
|
||||||
|
```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`方法。
|
上面代码中的`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
|
```javascript
|
||||||
// constants.js 模块
|
// constants.js 模块
|
||||||
@ -1053,12 +625,14 @@ export {users} from './users';
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// script.js
|
// script.js
|
||||||
import {db, users} from './constants';
|
import {db, users} from './index';
|
||||||
```
|
```
|
||||||
|
|
||||||
## import()
|
## import()
|
||||||
|
|
||||||
上面说过了,`import`语句会被JavaScript引擎静态分析,先于模块内的其他模块执行(叫做”连接“更合适)。所以,下面的代码会报错。
|
### 简介
|
||||||
|
|
||||||
|
前面介绍过,`import`命令会被 JavaScript 引擎静态分析,先于模块内的其他模块执行(叫做”连接“更合适)。所以,下面的代码会报错。
|
||||||
|
|
||||||
```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
|
```javascript
|
||||||
const path = './' + fileName;
|
const path = './' + fileName;
|
||||||
@ -1084,7 +658,7 @@ const myModual = require(path);
|
|||||||
import(specifier)
|
import(specifier)
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,`import`函数的参数`specifier`,指定所要加载的模块的位置。`import`语句能够接受什么参数,`import()`函数就能接受什么参数,两者区别主要是后者为动态加载。
|
上面代码中,`import`函数的参数`specifier`,指定所要加载的模块的位置。`import`命令能够接受什么参数,`import()`函数就能接受什么参数,两者区别主要是后者为动态加载。
|
||||||
|
|
||||||
`import()`返回一个 Promise 对象。下面是一个例子。
|
`import()`返回一个 Promise 对象。下面是一个例子。
|
||||||
|
|
||||||
@ -1104,75 +678,109 @@ import(`./section-modules/${someVariable}.js`)
|
|||||||
|
|
||||||
`import()`类似于 Node 的`require`方法,区别主要是前者是异步加载,后者是同步加载。
|
`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 模块的写法,从而在浏览器中使用。
|
`import()`可以在需要的时候,再加载某个模块。
|
||||||
|
|
||||||
首先,安装这个转玛器。
|
|
||||||
|
|
||||||
```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
|
```javascript
|
||||||
// app/es6-file.js:
|
button.addEventListener('click', event => {
|
||||||
|
import('./dialogBox.js')
|
||||||
export class q {
|
.then(dialogBox => {
|
||||||
constructor() {
|
dialogBox.open();
|
||||||
this.es6 = 'hello';
|
})
|
||||||
}
|
.catch(error => {
|
||||||
}
|
/* Error handling */
|
||||||
```
|
})
|
||||||
|
|
||||||
然后,在网页内加载这个模块文件。
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script>
|
|
||||||
|
|
||||||
System.import('app/es6-file').then(function(m) {
|
|
||||||
console.log(new m.q().es6); // hello
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</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();
|
||||||
|
```
|
||||||
|
222
docs/number.md
222
docs/number.md
@ -33,7 +33,7 @@ Number('0o10') // 8
|
|||||||
|
|
||||||
## Number.isFinite(), Number.isNaN()
|
## Number.isFinite(), Number.isNaN()
|
||||||
|
|
||||||
ES6在Number对象上,新提供了`Number.isFinite()`和`Number.isNaN()`两个方法。
|
ES6 在`Number`对象上,新提供了`Number.isFinite()`和`Number.isNaN()`两个方法。
|
||||||
|
|
||||||
`Number.isFinite()`用来检查一个数值是否为有限的(finite)。
|
`Number.isFinite()`用来检查一个数值是否为有限的(finite)。
|
||||||
|
|
||||||
@ -94,7 +94,7 @@ ES5通过下面的代码,部署`Number.isNaN()`。
|
|||||||
})(this);
|
})(this);
|
||||||
```
|
```
|
||||||
|
|
||||||
它们与传统的全局方法`isFinite()`和`isNaN()`的区别在于,传统方法先调用`Number()`将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,非数值一律返回`false`。
|
它们与传统的全局方法`isFinite()`和`isNaN()`的区别在于,传统方法先调用`Number()`将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,`Number.isFinite()`对于非数值一律返回`false`, `Number.isNaN()`只有对于`NaN`才返回`true`,非`NaN`一律返回`false`。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
isFinite(25) // true
|
isFinite(25) // true
|
||||||
@ -106,11 +106,12 @@ isNaN(NaN) // true
|
|||||||
isNaN("NaN") // true
|
isNaN("NaN") // true
|
||||||
Number.isNaN(NaN) // true
|
Number.isNaN(NaN) // true
|
||||||
Number.isNaN("NaN") // false
|
Number.isNaN("NaN") // false
|
||||||
|
Number.isNaN(1) // false
|
||||||
```
|
```
|
||||||
|
|
||||||
## Number.parseInt(), Number.parseFloat()
|
## Number.parseInt(), Number.parseFloat()
|
||||||
|
|
||||||
ES6将全局方法`parseInt()`和`parseFloat()`,移植到Number对象上面,行为完全保持不变。
|
ES6 将全局方法`parseInt()`和`parseFloat()`,移植到`Number`对象上面,行为完全保持不变。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// ES5的写法
|
// ES5的写法
|
||||||
@ -150,8 +151,8 @@ ES5可以通过下面的代码,部署`Number.isInteger()`。
|
|||||||
|
|
||||||
Object.defineProperty(Number, 'isInteger', {
|
Object.defineProperty(Number, 'isInteger', {
|
||||||
value: function isInteger(value) {
|
value: function isInteger(value) {
|
||||||
return typeof value === 'number' && isFinite(value) &&
|
return typeof value === 'number' &&
|
||||||
value > -9007199254740992 && value < 9007199254740992 &&
|
isFinite(value) &&
|
||||||
floor(value) === value;
|
floor(value) === value;
|
||||||
},
|
},
|
||||||
configurable: true,
|
configurable: true,
|
||||||
@ -163,15 +164,21 @@ ES5可以通过下面的代码,部署`Number.isInteger()`。
|
|||||||
|
|
||||||
## Number.EPSILON
|
## Number.EPSILON
|
||||||
|
|
||||||
ES6在Number对象上面,新增一个极小的常量`Number.EPSILON`。
|
ES6 在`Number`对象上面,新增一个极小的常量`Number.EPSILON`。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。
|
||||||
|
|
||||||
|
对于 64 位浮点数来说,大于 1 的最小浮点数相当于二进制的`1.00..001`,小数点后面有连续 51 个零。这个值减去 1 之后,就等于 2 的-52 次方。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
Number.EPSILON === Math.pow(2, -52)
|
||||||
|
// true
|
||||||
Number.EPSILON
|
Number.EPSILON
|
||||||
// 2.220446049250313e-16
|
// 2.220446049250313e-16
|
||||||
Number.EPSILON.toFixed(20)
|
Number.EPSILON.toFixed(20)
|
||||||
// '0.00000000000000022204'
|
// "0.00000000000000022204"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`Number.EPSILON`实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。
|
||||||
|
|
||||||
引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。
|
引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
@ -185,23 +192,31 @@ Number.EPSILON.toFixed(20)
|
|||||||
// '0.00000000000000005551'
|
// '0.00000000000000005551'
|
||||||
```
|
```
|
||||||
|
|
||||||
但是如果这个误差能够小于`Number.EPSILON`,我们就可以认为得到了正确结果。
|
上面代码解释了,为什么比较`0.1 + 0.2`与`0.3`得到的结果是`false`。
|
||||||
|
|
||||||
```javascript
|
```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
|
// true
|
||||||
```
|
```
|
||||||
|
|
||||||
因此,`Number.EPSILON`的实质是一个可以接受的误差范围。
|
因此,`Number.EPSILON`的实质是一个可以接受的最小误差范围。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function withinErrorMargin (left, right) {
|
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
|
0.1 + 0.2 === 0.3 // false
|
||||||
withinErrorMargin(0.2 + 0.2, 0.3)
|
withinErrorMargin(0.1 + 0.2, 0.3) // true
|
||||||
// false
|
|
||||||
|
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`方法将其先转为数值。
|
对于非数值,`Math.trunc`内部使用`Number`方法将其先转为数值。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
Math.trunc('123.456')
|
Math.trunc('123.456') // 123
|
||||||
// 123
|
Math.trunc(true) //1
|
||||||
|
Math.trunc(false) // 0
|
||||||
|
Math.trunc(null) // 0
|
||||||
```
|
```
|
||||||
|
|
||||||
对于空值和无法截取整数的值,返回NaN。
|
对于空值和无法截取整数的值,返回`NaN`。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
Math.trunc(NaN); // NaN
|
Math.trunc(NaN); // NaN
|
||||||
Math.trunc('foo'); // NaN
|
Math.trunc('foo'); // NaN
|
||||||
Math.trunc(); // NaN
|
Math.trunc(); // NaN
|
||||||
|
Math.trunc(undefined) // NaN
|
||||||
```
|
```
|
||||||
|
|
||||||
对于没有部署这个方法的环境,可以用下面的代码模拟。
|
对于没有部署这个方法的环境,可以用下面的代码模拟。
|
||||||
@ -352,15 +370,15 @@ Math.trunc = Math.trunc || function(x) {
|
|||||||
|
|
||||||
### Math.sign()
|
### Math.sign()
|
||||||
|
|
||||||
`Math.sign`方法用来判断一个数到底是正数、负数、还是零。
|
`Math.sign`方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。
|
||||||
|
|
||||||
它会返回五种值。
|
它会返回五种值。
|
||||||
|
|
||||||
- 参数为正数,返回+1;
|
- 参数为正数,返回`+1`;
|
||||||
- 参数为负数,返回-1;
|
- 参数为负数,返回`-1`;
|
||||||
- 参数为0,返回0;
|
- 参数为 0,返回`0`;
|
||||||
- 参数为-0,返回-0;
|
- 参数为-0,返回`-0`;
|
||||||
- 其他值,返回NaN。
|
- 其他值,返回`NaN`。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
Math.sign(-5) // -1
|
Math.sign(-5) // -1
|
||||||
@ -368,8 +386,19 @@ Math.sign(5) // +1
|
|||||||
Math.sign(0) // +0
|
Math.sign(0) // +0
|
||||||
Math.sign(-0) // -0
|
Math.sign(-0) // -0
|
||||||
Math.sign(NaN) // NaN
|
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 个前导 0;1 的二进制形式是`0b1`,只占 1 位,所以 32 位之中有 31 个前导 0;1000 的二进制形式是`0b1111101000`,一共有 10 位,所以 32 位之中有 22 个前导 0。
|
上面代码中,0 的二进制形式全为 0,所以有 32 个前导 0;1 的二进制形式是`0b1`,只占 1 位,所以 32 位之中有 31 个前导 0;1000 的二进制形式是`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`方法直接相关。
|
左移运算符(`<<`)与`Math.clz32`方法直接相关。
|
||||||
|
|
||||||
@ -602,9 +631,9 @@ Math.log2 = Math.log2 || function(x) {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### 三角函数方法
|
### 双曲函数方法
|
||||||
|
|
||||||
ES6新增了6个三角函数方法。
|
ES6 新增了 6 个双曲函数方法。
|
||||||
|
|
||||||
- `Math.sinh(x)` 返回`x`的双曲正弦(hyperbolic sine)
|
- `Math.sinh(x)` 返回`x`的双曲正弦(hyperbolic sine)
|
||||||
- `Math.cosh(x)` 返回`x`的双曲余弦(hyperbolic cosine)
|
- `Math.cosh(x)` 返回`x`的双曲余弦(hyperbolic cosine)
|
||||||
@ -613,9 +642,41 @@ ES6新增了6个三角函数方法。
|
|||||||
- `Math.acosh(x)` 返回`x`的反双曲余弦(inverse hyperbolic cosine)
|
- `Math.acosh(x)` 返回`x`的反双曲余弦(inverse hyperbolic cosine)
|
||||||
- `Math.atanh(x)` 返回`x`的反双曲正切(inverse hyperbolic tangent)
|
- `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
|
```javascript
|
||||||
2 ** 2 // 4
|
2 ** 2 // 4
|
||||||
@ -625,11 +686,108 @@ ES7新增了一个指数运算符(`**`),目前Babel转码器已经支持
|
|||||||
指数运算符可以与等号结合,形成一个新的赋值运算符(`**=`)。
|
指数运算符可以与等号结合,形成一个新的赋值运算符(`**=`)。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let a = 2;
|
let a = 1.5;
|
||||||
a **= 2;
|
a **= 2;
|
||||||
// 等同于 a = a * a;
|
// 等同于 a = a * a;
|
||||||
|
|
||||||
let b = 3;
|
let b = 4;
|
||||||
b **= 3;
|
b **= 3;
|
||||||
// 等同于 b = b * b * b;
|
// 等同于 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
|
||||||
|
```
|
||||||
|
1317
docs/object.md
1317
docs/object.md
File diff suppressed because it is too large
Load Diff
300
docs/promise.md
300
docs/promise.md
@ -8,24 +8,26 @@ Promise是异步编程的一种解决方案,比传统的解决方案——回
|
|||||||
|
|
||||||
`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`也有一些缺点。首先,无法取消`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
|
```javascript
|
||||||
var promise = new Promise(function(resolve, reject) {
|
const promise = new Promise(function(resolve, reject) {
|
||||||
// ... some code
|
// ... some code
|
||||||
|
|
||||||
if (/* 异步操作成功 */){
|
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
|
```javascript
|
||||||
promise.then(function(value) {
|
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
|
```javascript
|
||||||
function timeout(ms) {
|
function timeout(ms) {
|
||||||
@ -66,7 +68,7 @@ timeout(100).then((value) => {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,`timeout`方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间(`ms`参数)以后,Promise实例的状态变为Resolved,就会触发`then`方法绑定的回调函数。
|
上面代码中,`timeout`方法返回一个`Promise`实例,表示一段时间以后才会发生的结果。过了指定的时间(`ms`参数)以后,`Promise`实例的状态变为`resolved`,就会触发`then`方法绑定的回调函数。
|
||||||
|
|
||||||
Promise 新建后就会立即执行。
|
Promise 新建后就会立即执行。
|
||||||
|
|
||||||
@ -77,24 +79,24 @@ let promise = new Promise(function(resolve, reject) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
promise.then(function() {
|
promise.then(function() {
|
||||||
console.log('Resolved.');
|
console.log('resolved.');
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Hi!');
|
console.log('Hi!');
|
||||||
|
|
||||||
// Promise
|
// Promise
|
||||||
// Hi!
|
// Hi!
|
||||||
// Resolved
|
// resolved
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,Promise新建后立即执行,所以首先输出的是“Promise”。然后,`then`方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以“Resolved”最后输出。
|
上面代码中,Promise 新建后立即执行,所以首先输出的是`Promise`。然后,`then`方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以`resolved`最后输出。
|
||||||
|
|
||||||
下面是异步加载图片的例子。
|
下面是异步加载图片的例子。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function loadImageAsync(url) {
|
function loadImageAsync(url) {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
var image = new Image();
|
const image = new Image();
|
||||||
|
|
||||||
image.onload = function() {
|
image.onload = function() {
|
||||||
resolve(image);
|
resolve(image);
|
||||||
@ -109,21 +111,14 @@ function loadImageAsync(url) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,使用Promise包装了一个图片加载的异步操作。如果加载成功,就调用`resolve`方法,否则就调用`reject`方法。
|
上面代码中,使用`Promise`包装了一个图片加载的异步操作。如果加载成功,就调用`resolve`方法,否则就调用`reject`方法。
|
||||||
|
|
||||||
下面是一个用Promise对象实现的Ajax操作的例子。
|
下面是一个用`Promise`对象实现的 Ajax 操作的例子。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var getJSON = function(url) {
|
const getJSON = function(url) {
|
||||||
var promise = new Promise(function(resolve, reject){
|
const promise = new Promise(function(resolve, reject){
|
||||||
var client = new XMLHttpRequest();
|
const handler = function() {
|
||||||
client.open("GET", url);
|
|
||||||
client.onreadystatechange = handler;
|
|
||||||
client.responseType = "json";
|
|
||||||
client.setRequestHeader("Accept", "application/json");
|
|
||||||
client.send();
|
|
||||||
|
|
||||||
function handler() {
|
|
||||||
if (this.readyState !== 4) {
|
if (this.readyState !== 4) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -133,6 +128,13 @@ var getJSON = function(url) {
|
|||||||
reject(new Error(this.statusText));
|
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;
|
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
|
```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);
|
resolve(p1);
|
||||||
})
|
})
|
||||||
@ -162,14 +164,14 @@ var p2 = new Promise(function (resolve, reject) {
|
|||||||
|
|
||||||
上面代码中,`p1`和`p2`都是 Promise 的实例,但是`p2`的`resolve`方法将`p1`作为参数,即一个异步操作的结果是返回另一个异步操作。
|
上面代码中,`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
|
```javascript
|
||||||
var p1 = new Promise(function (resolve, reject) {
|
const p1 = new Promise(function (resolve, reject) {
|
||||||
setTimeout(() => reject(new Error('fail')), 3000)
|
setTimeout(() => reject(new Error('fail')), 3000)
|
||||||
})
|
})
|
||||||
|
|
||||||
var p2 = new Promise(function (resolve, reject) {
|
const p2 = new Promise(function (resolve, reject) {
|
||||||
setTimeout(() => resolve(p1), 1000)
|
setTimeout(() => resolve(p1), 1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -179,13 +181,38 @@ p2
|
|||||||
// Error: fail
|
// Error: fail
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,`p1`是一个Promise,3秒之后变为`rejected`。`p2`的状态在1秒之后改变,`resolve`方法返回的是`p1`。此时,由于`p2`返回的是另一个Promise,所以后面的`then`语句都变成针对后者(`p1`)。又过了2秒,`p1`变为`rejected`,导致触发`catch`方法指定的回调函数。
|
上面代码中,`p1`是一个 Promise,3 秒之后变为`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.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
|
```javascript
|
||||||
getJSON("/posts.json").then(function(json) {
|
getJSON("/posts.json").then(function(json) {
|
||||||
@ -197,19 +224,19 @@ getJSON("/posts.json").then(function(json) {
|
|||||||
|
|
||||||
上面的代码使用`then`方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。
|
上面的代码使用`then`方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。
|
||||||
|
|
||||||
采用链式的`then`,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。
|
采用链式的`then`,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个`Promise`对象(即有异步操作),这时后一个回调函数,就会等待该`Promise`对象的状态发生变化,才会被调用。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
getJSON("/post/1.json").then(function(post) {
|
getJSON("/post/1.json").then(function(post) {
|
||||||
return getJSON(post.commentURL);
|
return getJSON(post.commentURL);
|
||||||
}).then(function funcA(comments) {
|
}).then(function funcA(comments) {
|
||||||
console.log("Resolved: ", comments);
|
console.log("resolved: ", comments);
|
||||||
}, function funcB(err){
|
}, 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(
|
getJSON("/post/1.json").then(
|
||||||
post => getJSON(post.commentURL)
|
post => getJSON(post.commentURL)
|
||||||
).then(
|
).then(
|
||||||
comments => console.log("Resolved: ", comments),
|
comments => console.log("resolved: ", comments),
|
||||||
err => console.log("Rejected: ", err)
|
err => console.log("rejected: ", err)
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -227,7 +254,7 @@ getJSON("/post/1.json").then(
|
|||||||
`Promise.prototype.catch`方法是`.then(null, rejection)`的别名,用于指定发生错误时的回调函数。
|
`Promise.prototype.catch`方法是`.then(null, rejection)`的别名,用于指定发生错误时的回调函数。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
getJSON("/posts.json").then(function(posts) {
|
getJSON('/posts.json').then(function(posts) {
|
||||||
// ...
|
// ...
|
||||||
}).catch(function(error) {
|
}).catch(function(error) {
|
||||||
// 处理 getJSON 和 前一个回调函数运行时发生的错误
|
// 处理 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
|
```javascript
|
||||||
p.then((val) => console.log("fulfilled:", val))
|
p.then((val) => console.log('fulfilled:', val))
|
||||||
.catch((err) => console.log("rejected:", err));
|
.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));
|
.then(null, (err) => console.log("rejected:", err));
|
||||||
```
|
```
|
||||||
|
|
||||||
下面是一个例子。
|
下面是一个例子。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var promise = new Promise(function(resolve, reject) {
|
const promise = new Promise(function(resolve, reject) {
|
||||||
throw new Error('test');
|
throw new Error('test');
|
||||||
});
|
});
|
||||||
promise.catch(function(error) {
|
promise.catch(function(error) {
|
||||||
@ -262,7 +289,7 @@ promise.catch(function(error) {
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 写法一
|
// 写法一
|
||||||
var promise = new Promise(function(resolve, reject) {
|
const promise = new Promise(function(resolve, reject) {
|
||||||
try {
|
try {
|
||||||
throw new Error('test');
|
throw new Error('test');
|
||||||
} catch(e) {
|
} 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'));
|
reject(new Error('test'));
|
||||||
});
|
});
|
||||||
promise.catch(function(error) {
|
promise.catch(function(error) {
|
||||||
@ -284,10 +311,10 @@ promise.catch(function(error) {
|
|||||||
|
|
||||||
比较上面两种写法,可以发现`reject`方法的作用,等同于抛出错误。
|
比较上面两种写法,可以发现`reject`方法的作用,等同于抛出错误。
|
||||||
|
|
||||||
如果Promise状态已经变成`Resolved`,再抛出错误是无效的。
|
如果 Promise 状态已经变成`resolved`,再抛出错误是无效的。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var promise = new Promise(function(resolve, reject) {
|
const promise = new Promise(function(resolve, reject) {
|
||||||
resolve('ok');
|
resolve('ok');
|
||||||
throw new Error('test');
|
throw new Error('test');
|
||||||
});
|
});
|
||||||
@ -297,12 +324,12 @@ promise
|
|||||||
// ok
|
// ok
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,Promise在`resolve`语句后面,再抛出错误,不会被捕获,等于没有抛出。
|
上面代码中,Promise 在`resolve`语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。
|
||||||
|
|
||||||
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个`catch`语句捕获。
|
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个`catch`语句捕获。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
getJSON("/post/1.json").then(function(post) {
|
getJSON('/post/1.json').then(function(post) {
|
||||||
return getJSON(post.commentURL);
|
return getJSON(post.commentURL);
|
||||||
}).then(function(comments) {
|
}).then(function(comments) {
|
||||||
// some code
|
// some code
|
||||||
@ -339,7 +366,7 @@ promise
|
|||||||
跟传统的`try/catch`代码块不同的是,如果没有使用`catch`方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。
|
跟传统的`try/catch`代码块不同的是,如果没有使用`catch`方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var someAsyncThing = function() {
|
const someAsyncThing = function() {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
// 下面一行会报错,因为x没有声明
|
// 下面一行会报错,因为x没有声明
|
||||||
resolve(x + 2);
|
resolve(x + 2);
|
||||||
@ -349,13 +376,31 @@ var someAsyncThing = function() {
|
|||||||
someAsyncThing().then(function() {
|
someAsyncThing().then(function() {
|
||||||
console.log('everything is great');
|
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
|
```javascript
|
||||||
var promise = new Promise(function(resolve, reject) {
|
process.on('unhandledRejection', function (err, p) {
|
||||||
resolve("ok");
|
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)
|
setTimeout(function () { throw new Error('test') }, 0)
|
||||||
});
|
});
|
||||||
promise.then(function (value) { console.log(value) });
|
promise.then(function (value) { console.log(value) });
|
||||||
@ -363,22 +408,12 @@ promise.then(function(value) { console.log(value) });
|
|||||||
// Uncaught Error: test
|
// Uncaught Error: test
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,Promise指定在下一轮“事件循环”再抛出错误,结果由于没有指定使用`try...catch`语句,就冒泡到最外层,成了未捕获的错误。因为此时,Promise的函数体已经运行结束了,所以这个错误是在Promise函数体外抛出的。
|
上面代码中,Promise 指定在下一轮“事件循环”再抛出错误。到了那个时候,Promise 的运行已经结束了,所以这个错误是在 Promise 函数体外抛出的,会冒泡到最外层,成了未捕获的错误。
|
||||||
|
|
||||||
Node.js有一个`unhandledRejection`事件,专门监听未捕获的`reject`错误。
|
一般总是建议,Promise 对象后面要跟`catch`方法,这样可以处理 Promise 内部发生的错误。`catch`方法返回的还是一个 Promise 对象,因此后面还可以接着调用`then`方法。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
process.on('unhandledRejection', function (err, p) {
|
const someAsyncThing = function() {
|
||||||
console.error(err.stack)
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
上面代码中,`unhandledRejection`事件的监听函数有两个参数,第一个是错误对象,第二个是报错的Promise实例,它可以用来了解发生错误的环境信息。。
|
|
||||||
|
|
||||||
需要注意的是,`catch`方法返回的还是一个Promise对象,因此后面还可以接着调用`then`方法。
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
var someAsyncThing = function() {
|
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
// 下面一行会报错,因为x没有声明
|
// 下面一行会报错,因为x没有声明
|
||||||
resolve(x + 2);
|
resolve(x + 2);
|
||||||
@ -414,7 +449,7 @@ Promise.resolve()
|
|||||||
`catch`方法之中,还能再抛出错误。
|
`catch`方法之中,还能再抛出错误。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var someAsyncThing = function() {
|
const someAsyncThing = function() {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
// 下面一行会报错,因为x没有声明
|
// 下面一行会报错,因为x没有声明
|
||||||
resolve(x + 2);
|
resolve(x + 2);
|
||||||
@ -456,10 +491,10 @@ someAsyncThing().then(function() {
|
|||||||
`Promise.all`方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
|
`Promise.all`方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
|
||||||
|
|
||||||
```javascript
|
```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`决定,分成两种情况。
|
`p`的状态由`p1`、`p2`、`p3`决定,分成两种情况。
|
||||||
|
|
||||||
@ -471,8 +506,8 @@ var p = Promise.all([p1, p2, p3]);
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 生成一个Promise对象的数组
|
// 生成一个Promise对象的数组
|
||||||
var promises = [2, 3, 5, 7, 11, 13].map(function (id) {
|
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
|
||||||
return getJSON("/post/" + id + ".json");
|
return getJSON('/post/' + id + ".json");
|
||||||
});
|
});
|
||||||
|
|
||||||
Promise.all(promises).then(function (posts) {
|
Promise.all(promises).then(function (posts) {
|
||||||
@ -489,7 +524,7 @@ Promise.all(promises).then(function (posts) {
|
|||||||
```javascript
|
```javascript
|
||||||
const databasePromise = connectDatabase();
|
const databasePromise = connectDatabase();
|
||||||
|
|
||||||
const booksPromise = databaseProimse
|
const booksPromise = databasePromise
|
||||||
.then(findAllBooks);
|
.then(findAllBooks);
|
||||||
|
|
||||||
const userPromise = databasePromise
|
const userPromise = databasePromise
|
||||||
@ -504,12 +539,54 @@ Promise.all([
|
|||||||
|
|
||||||
上面代码中,`booksPromise`和`userPromise`是两个异步操作,只有等到它们的结果都返回了,才会触发`pickTopRecommentations`这个回调函数。
|
上面代码中,`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.race`方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
|
`Promise.race`方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var p = Promise.race([p1, p2, p3]);
|
const p = Promise.race([p1, p2, p3]);
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,只要`p1`、`p2`、`p3`之中有一个实例率先改变状态,`p`的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给`p`的回调函数。
|
上面代码中,只要`p1`、`p2`、`p3`之中有一个实例率先改变状态,`p`的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给`p`的回调函数。
|
||||||
@ -519,14 +596,14 @@ var p = Promise.race([p1, p2, p3]);
|
|||||||
下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为`reject`,否则变为`resolve`。
|
下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为`reject`,否则变为`resolve`。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var p = Promise.race([
|
const p = Promise.race([
|
||||||
fetch('/resource-that-may-take-a-while'),
|
fetch('/resource-that-may-take-a-while'),
|
||||||
new Promise(function (resolve, reject) {
|
new Promise(function (resolve, reject) {
|
||||||
setTimeout(() => reject(new Error('request timeout')), 5000)
|
setTimeout(() => reject(new Error('request timeout')), 5000)
|
||||||
})
|
})
|
||||||
])
|
]);
|
||||||
p.then(response => console.log(response))
|
p.then(response => console.log(response));
|
||||||
p.catch(error => console.log(error))
|
p.catch(error => console.log(error));
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,如果 5 秒之内`fetch`方法无法返回结果,变量`p`的状态就会变为`rejected`,从而触发`catch`方法指定的回调函数。
|
上面代码中,如果 5 秒之内`fetch`方法无法返回结果,变量`p`的状态就会变为`rejected`,从而触发`catch`方法指定的回调函数。
|
||||||
@ -536,7 +613,7 @@ p.catch(error => console.log(error))
|
|||||||
有时需要将现有对象转为 Promise 对象,`Promise.resolve`方法就起到这个作用。
|
有时需要将现有对象转为 Promise 对象,`Promise.resolve`方法就起到这个作用。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var jsPromise = Promise.resolve($.ajax('/whatever.json'));
|
const jsPromise = Promise.resolve($.ajax('/whatever.json'));
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码将 jQuery 生成的`deferred`对象,转为一个新的 Promise 对象。
|
上面代码将 jQuery 生成的`deferred`对象,转为一个新的 Promise 对象。
|
||||||
@ -586,10 +663,10 @@ p1.then(function(value) {
|
|||||||
|
|
||||||
**(3)参数不是具有`then`方法的对象,或根本就不是对象**
|
**(3)参数不是具有`then`方法的对象,或根本就不是对象**
|
||||||
|
|
||||||
如果参数是一个原始值,或者是一个不具有`then`方法的对象,则`Promise.resolve`方法返回一个新的Promise对象,状态为`Resolved`。
|
如果参数是一个原始值,或者是一个不具有`then`方法的对象,则`Promise.resolve`方法返回一个新的 Promise 对象,状态为`resolved`。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var p = Promise.resolve('Hello');
|
const p = Promise.resolve('Hello');
|
||||||
|
|
||||||
p.then(function (s){
|
p.then(function (s){
|
||||||
console.log(s)
|
console.log(s)
|
||||||
@ -597,16 +674,16 @@ p.then(function (s){
|
|||||||
// Hello
|
// Hello
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码生成一个新的Promise对象的实例`p`。由于字符串`Hello`不属于异步操作(判断方法是它不是具有then方法的对象),返回Promise实例的状态从一生成就是`Resolved`,所以回调函数会立即执行。`Promise.resolve`方法的参数,会同时传给回调函数。
|
上面代码生成一个新的 Promise 对象的实例`p`。由于字符串`Hello`不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是`resolved`,所以回调函数会立即执行。`Promise.resolve`方法的参数,会同时传给回调函数。
|
||||||
|
|
||||||
**(4)不带有任何参数**
|
**(4)不带有任何参数**
|
||||||
|
|
||||||
`Promise.resolve`方法允许调用时不带参数,直接返回一个`Resolved`状态的Promise对象。
|
`Promise.resolve`方法允许调用时不带参数,直接返回一个`resolved`状态的 Promise 对象。
|
||||||
|
|
||||||
所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用`Promise.resolve`方法。
|
所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用`Promise.resolve`方法。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var p = Promise.resolve();
|
const p = Promise.resolve();
|
||||||
|
|
||||||
p.then(function () {
|
p.then(function () {
|
||||||
// ...
|
// ...
|
||||||
@ -633,16 +710,16 @@ console.log('one');
|
|||||||
// three
|
// three
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,`setTimeout(fn, 0)`在下一轮“事件循环”开始时执行,`Promise.resolve()`在本轮“事件循环”结束时执行,`console.log(’one‘)`则是立即执行,因此最先输出。
|
上面代码中,`setTimeout(fn, 0)`在下一轮“事件循环”开始时执行,`Promise.resolve()`在本轮“事件循环”结束时执行,`console.log('one')`则是立即执行,因此最先输出。
|
||||||
|
|
||||||
## Promise.reject()
|
## Promise.reject()
|
||||||
|
|
||||||
`Promise.reject(reason)`方法也会返回一个新的Promise实例,该实例的状态为`rejected`。它的参数用法与`Promise.resolve`方法完全一致。
|
`Promise.reject(reason)`方法也会返回一个新的 Promise 实例,该实例的状态为`rejected`。
|
||||||
|
|
||||||
```javascript
|
```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) {
|
p.then(null, function (s) {
|
||||||
console.log(s)
|
console.log(s)
|
||||||
@ -652,6 +729,24 @@ p.then(null, function (s){
|
|||||||
|
|
||||||
上面代码生成一个 Promise 对象的实例`p`,状态为`rejected`,回调函数会立即执行。
|
上面代码生成一个 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 之中、但很有用的方法。
|
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()
|
### finally()
|
||||||
|
|
||||||
@ -719,7 +814,7 @@ Promise.prototype.finally = function (callback) {
|
|||||||
```javascript
|
```javascript
|
||||||
const preloadImage = function (path) {
|
const preloadImage = function (path) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
var image = new Image();
|
const image = new Image();
|
||||||
image.onload = resolve;
|
image.onload = resolve;
|
||||||
image.onerror = reject;
|
image.onerror = reject;
|
||||||
image.src = path;
|
image.src = path;
|
||||||
@ -738,9 +833,9 @@ function getFoo () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var g = function* () {
|
const g = function* () {
|
||||||
try {
|
try {
|
||||||
var foo = yield getFoo();
|
const foo = yield getFoo();
|
||||||
console.log(foo);
|
console.log(foo);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
@ -748,7 +843,7 @@ var g = function* () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function run (generator) {
|
function run (generator) {
|
||||||
var it = generator();
|
const it = generator();
|
||||||
|
|
||||||
function go(result) {
|
function go(result) {
|
||||||
if (result.done) return result.value;
|
if (result.done) return result.value;
|
||||||
@ -776,7 +871,7 @@ run(g);
|
|||||||
Promise.resolve().then(f)
|
Promise.resolve().then(f)
|
||||||
```
|
```
|
||||||
|
|
||||||
上面的写法有一个缺点,就是如果`f`是同步函数,那么它会在下一轮事件循环执行。
|
上面的写法有一个缺点,就是如果`f`是同步函数,那么它会在本轮事件循环的末尾执行。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const f = () => console.log('now');
|
const f = () => console.log('now');
|
||||||
@ -798,7 +893,7 @@ console.log('next');
|
|||||||
// next
|
// next
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,第一行是一个立即执行的匿名函数,会立即执行里面的`async`函数,因此如果`f`是同步的,就会得到同步的结果;如果`f`是异步的,就可以用`then`指定下一步,就像下面的写法。
|
上面代码中,第二行是一个立即执行的匿名函数,会立即执行里面的`async`函数,因此如果`f`是同步的,就会得到同步的结果;如果`f`是异步的,就可以用`then`指定下一步,就像下面的写法。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
(async () => f())()
|
(async () => f())()
|
||||||
@ -881,4 +976,3 @@ Promise.try(database.users.get({id: userId}))
|
|||||||
```
|
```
|
||||||
|
|
||||||
事实上,`Promise.try`就是模拟`try`代码块,就像`promise.catch`模拟的是`catch`代码块。
|
事实上,`Promise.try`就是模拟`try`代码块,就像`promise.catch`模拟的是`catch`代码块。
|
||||||
|
|
||||||
|
515
docs/proxy.md
515
docs/proxy.md
@ -1,6 +1,6 @@
|
|||||||
# Proxy 和 Reflect
|
# Proxy
|
||||||
|
|
||||||
## Proxy 概述
|
## 概述
|
||||||
|
|
||||||
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
|
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ proxy.a = 'b';
|
|||||||
target.a // "b"
|
target.a // "b"
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,`handler`是一个空对象,没有任何拦截效果,访问`handler`就等同于访问`target`。
|
上面代码中,`handler`是一个空对象,没有任何拦截效果,访问`proxy`就等同于访问`target`。
|
||||||
|
|
||||||
一个技巧是将 Proxy 对象,设置到`object.proxy`属性,从而可以在`object`对象上调用。
|
一个技巧是将 Proxy 对象,设置到`object.proxy`属性,从而可以在`object`对象上调用。
|
||||||
|
|
||||||
@ -118,68 +118,26 @@ var fproxy = new Proxy(function(x, y) {
|
|||||||
fproxy(1, 2) // 1
|
fproxy(1, 2) // 1
|
||||||
new fproxy(1, 2) // {value: 2}
|
new fproxy(1, 2) // {value: 2}
|
||||||
fproxy.prototype === Object.prototype // true
|
fproxy.prototype === Object.prototype // true
|
||||||
fproxy.foo // "Hello, foo"
|
fproxy.foo === "Hello, foo" // true
|
||||||
```
|
```
|
||||||
|
|
||||||
下面是 Proxy 支持的拦截操作一览。
|
|
||||||
|
|
||||||
对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。
|
对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。
|
||||||
|
|
||||||
**(1)get(target, propKey, receiver)**
|
下面是 Proxy 支持的拦截操作一览,一共 13 种。
|
||||||
|
|
||||||
拦截对象属性的读取,比如`proxy.foo`和`proxy['foo']`。
|
- **get(target, propKey, receiver)**:拦截对象属性的读取,比如`proxy.foo`和`proxy['foo']`。
|
||||||
|
- **set(target, propKey, value, receiver)**:拦截对象属性的设置,比如`proxy.foo = v`或`proxy['foo'] = v`,返回一个布尔值。
|
||||||
最后一个参数`receiver`是一个对象,可选,参见下面`Reflect.get`的部分。
|
- **has(target, propKey)**:拦截`propKey in proxy`的操作,返回一个布尔值。
|
||||||
|
- **deleteProperty(target, propKey)**:拦截`delete proxy[propKey]`的操作,返回一个布尔值。
|
||||||
**(2)set(target, propKey, value, receiver)**
|
- **ownKeys(target)**:拦截`Object.getOwnPropertyNames(proxy)`、`Object.getOwnPropertySymbols(proxy)`、`Object.keys(proxy)`,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而`Object.keys()`的返回结果仅包括目标对象自身的可遍历属性。
|
||||||
|
- **getOwnPropertyDescriptor(target, propKey)**:拦截`Object.getOwnPropertyDescriptor(proxy, propKey)`,返回属性的描述对象。
|
||||||
拦截对象属性的设置,比如`proxy.foo = v`或`proxy['foo'] = v`,返回一个布尔值。
|
- **defineProperty(target, propKey, propDesc)**:拦截`Object.defineProperty(proxy, propKey, propDesc)`、`Object.defineProperties(proxy, propDescs)`,返回一个布尔值。
|
||||||
|
- **preventExtensions(target)**:拦截`Object.preventExtensions(proxy)`,返回一个布尔值。
|
||||||
**(3)has(target, propKey)**
|
- **getPrototypeOf(target)**:拦截`Object.getPrototypeOf(proxy)`,返回一个对象。
|
||||||
|
- **isExtensible(target)**:拦截`Object.isExtensible(proxy)`,返回一个布尔值。
|
||||||
拦截`propKey in proxy`的操作,以及对象的`hasOwnProperty`方法,返回一个布尔值。
|
- **setPrototypeOf(target, proto)**:拦截`Object.setPrototypeOf(proxy, proto)`,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
|
||||||
|
- **apply(target, object, args)**:拦截 Proxy 实例作为函数调用的操作,比如`proxy(...args)`、`proxy.call(object, ...args)`、`proxy.apply(...)`。
|
||||||
**(4)deleteProperty(target, propKey)**
|
- **construct(target, args)**:拦截 Proxy 实例作为构造函数调用的操作,比如`new proxy(...args)`。
|
||||||
|
|
||||||
拦截`delete proxy[propKey]`的操作,返回一个布尔值。
|
|
||||||
|
|
||||||
**(5)ownKeys(target)**
|
|
||||||
|
|
||||||
拦截`Object.getOwnPropertyNames(proxy)`、`Object.getOwnPropertySymbols(proxy)`、`Object.keys(proxy)`,返回一个数组。该方法返回对象所有自身的属性,而`Object.keys()`仅返回对象可遍历的属性。
|
|
||||||
|
|
||||||
**(6)getOwnPropertyDescriptor(target, propKey)**
|
|
||||||
|
|
||||||
拦截`Object.getOwnPropertyDescriptor(proxy, propKey)`,返回属性的描述对象。
|
|
||||||
|
|
||||||
**(7)defineProperty(target, propKey, propDesc)**
|
|
||||||
|
|
||||||
拦截`Object.defineProperty(proxy, propKey, propDesc)`、`Object.defineProperties(proxy, propDescs)`,返回一个布尔值。
|
|
||||||
|
|
||||||
**(8)preventExtensions(target)**
|
|
||||||
|
|
||||||
拦截`Object.preventExtensions(proxy)`,返回一个布尔值。
|
|
||||||
|
|
||||||
**(9)getPrototypeOf(target)**
|
|
||||||
|
|
||||||
拦截`Object.getPrototypeOf(proxy)`,返回一个对象。
|
|
||||||
|
|
||||||
**(10)isExtensible(target)**
|
|
||||||
|
|
||||||
拦截`Object.isExtensible(proxy)`,返回一个布尔值。
|
|
||||||
|
|
||||||
**(11)setPrototypeOf(target, proto)**
|
|
||||||
|
|
||||||
拦截`Object.setPrototypeOf(proxy, proto)`,返回一个布尔值。
|
|
||||||
|
|
||||||
如果目标对象是函数,那么还有两种额外操作可以拦截。
|
|
||||||
|
|
||||||
**(12)apply(target, object, args)**
|
|
||||||
|
|
||||||
拦截 Proxy 实例作为函数调用的操作,比如`proxy(...args)`、`proxy.call(object, ...args)`、`proxy.apply(...)`。
|
|
||||||
|
|
||||||
**(13)construct(target, args)**
|
|
||||||
|
|
||||||
拦截 Proxy 实例作为构造函数调用的操作,比如`new proxy(...args)`。
|
|
||||||
|
|
||||||
## Proxy 实例的方法
|
## Proxy 实例的方法
|
||||||
|
|
||||||
@ -187,7 +145,9 @@ fproxy.foo // "Hello, foo"
|
|||||||
|
|
||||||
### get()
|
### get()
|
||||||
|
|
||||||
`get`方法用于拦截某个属性的读取操作。上文已经有一个例子,下面是另一个拦截读取操作的例子。
|
`get`方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(即`this`关键字指向的那个对象),其中最后一个参数可选。
|
||||||
|
|
||||||
|
`get`方法的用法,上文已经有一个例子,下面是另一个拦截读取操作的例子。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var person = {
|
var person = {
|
||||||
@ -221,7 +181,7 @@ let proto = new Proxy({}, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let obj = Object.create(proto);
|
let obj = Object.create(proto);
|
||||||
obj.xxx // "GET xxx"
|
obj.foo // "GET foo"
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,拦截操作定义在`Prototype`对象上面,所以如果读取`obj`对象继承的属性时,拦截会生效。
|
上面代码中,拦截操作定义在`Prototype`对象上面,所以如果读取`obj`对象继承的属性时,拦截会生效。
|
||||||
@ -317,9 +277,45 @@ const el = dom.div({},
|
|||||||
document.body.appendChild(el);
|
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`方法用来拦截某个属性的赋值操作。
|
`set`方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。
|
||||||
|
|
||||||
假定`Person`对象有一个`age`属性,该属性应该是一个不大于 200 的整数,那么可以使用`Proxy`保证`age`的属性值符合要求。
|
假定`Person`对象有一个`age`属性,该属性应该是一个不大于 200 的整数,那么可以使用`Proxy`保证`age`的属性值符合要求。
|
||||||
|
|
||||||
@ -335,7 +331,7 @@ let validator = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对于age以外的属性,直接保存
|
// 对于满足条件的 age 属性以及其他属性,直接保存
|
||||||
obj[prop] = value;
|
obj[prop] = value;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -349,12 +345,12 @@ person.age = 'young' // 报错
|
|||||||
person.age = 300 // 报错
|
person.age = 300 // 报错
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,由于设置了存值函数`set`,任何不符合要求的`age`属性赋值,都会抛出一个错误。利用`set`方法,还可以数据绑定,即每当对象发生变化时,会自动更新DOM。
|
上面代码中,由于设置了存值函数`set`,任何不符合要求的`age`属性赋值,都会抛出一个错误,这是数据验证的一种实现方法。利用`set`方法,还可以数据绑定,即每当对象发生变化时,会自动更新 DOM。
|
||||||
|
|
||||||
有时,我们会在对象上面设置内部属性,属性名的第一个字符使用下划线开头,表示这些属性不应该被外部使用。结合`get`和`set`方法,就可以做到防止这些内部属性被外部读写。
|
有时,我们会在对象上面设置内部属性,属性名的第一个字符使用下划线开头,表示这些属性不应该被外部使用。结合`get`和`set`方法,就可以做到防止这些内部属性被外部读写。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var handler = {
|
const handler = {
|
||||||
get (target, key) {
|
get (target, key) {
|
||||||
invariant(key, 'get');
|
invariant(key, 'get');
|
||||||
return target[key];
|
return target[key];
|
||||||
@ -370,8 +366,8 @@ function invariant (key, action) {
|
|||||||
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
|
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var target = {};
|
const target = {};
|
||||||
var proxy = new Proxy(target, handler);
|
const proxy = new Proxy(target, handler);
|
||||||
proxy._prop
|
proxy._prop
|
||||||
// Error: Invalid attempt to get private "_prop" property
|
// Error: Invalid attempt to get private "_prop" property
|
||||||
proxy._prop = 'c'
|
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()
|
||||||
|
|
||||||
`apply`方法拦截函数的调用、call和apply操作。
|
`apply`方法拦截函数的调用、`call`和`apply`操作。
|
||||||
|
|
||||||
|
`apply`方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(`this`)和目标对象的参数数组。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var handler = {
|
var handler = {
|
||||||
@ -392,8 +407,6 @@ var handler = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
`apply`方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(`this`)和目标对象的参数数组。
|
|
||||||
|
|
||||||
下面是一个例子。
|
下面是一个例子。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
@ -464,6 +477,7 @@ var proxy = new Proxy(target, handler);
|
|||||||
```javascript
|
```javascript
|
||||||
var obj = { a: 10 };
|
var obj = { a: 10 };
|
||||||
Object.preventExtensions(obj);
|
Object.preventExtensions(obj);
|
||||||
|
|
||||||
var p = new Proxy(obj, {
|
var p = new Proxy(obj, {
|
||||||
has: function(target, prop) {
|
has: function(target, prop) {
|
||||||
return false;
|
return false;
|
||||||
@ -473,7 +487,7 @@ var p = new Proxy(obj, {
|
|||||||
'a' in p // TypeError is thrown
|
'a' in p // TypeError is thrown
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,`obj`对象禁止扩展,结果使用`has`拦截就会报错。
|
上面代码中,`obj`对象禁止扩展,结果使用`has`拦截就会报错。也就是说,如果某个属性不可配置(或者目标对象不可扩展),则`has`方法就不得“隐藏”(即返回`false`)目标对象的该属性。
|
||||||
|
|
||||||
值得注意的是,`has`方法拦截的是`HasProperty`操作,而不是`HasOwnProperty`操作,即`has`方法不判断一个属性是对象自身的属性,还是继承的属性。
|
值得注意的是,`has`方法拦截的是`HasProperty`操作,而不是`HasOwnProperty`操作,即`has`方法不判断一个属性是对象自身的属性,还是继承的属性。
|
||||||
|
|
||||||
@ -516,7 +530,7 @@ for (let b in oproxy2) {
|
|||||||
// 99
|
// 99
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,`has`拦截只对`in`循环生效,对`for...in`循环不生效,导致不符合要求的属性没有被排除在`for...in`循环之外。
|
上面代码中,`has`拦截只对`in`运算符生效,对`for...in`循环不生效,导致不符合要求的属性没有被排除在`for...in`循环之外。
|
||||||
|
|
||||||
### construct()
|
### construct()
|
||||||
|
|
||||||
@ -545,7 +559,7 @@ var p = new Proxy(function() {}, {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
new p(1).value
|
(new p(1)).value
|
||||||
// "called: 1"
|
// "called: 1"
|
||||||
// 10
|
// 10
|
||||||
```
|
```
|
||||||
@ -587,6 +601,8 @@ delete proxy._prop
|
|||||||
|
|
||||||
上面代码中,`deleteProperty`方法拦截了`delete`操作符,删除第一个字符为下划线的属性会报错。
|
上面代码中,`deleteProperty`方法拦截了`delete`操作符,删除第一个字符为下划线的属性会报错。
|
||||||
|
|
||||||
|
注意,目标对象自身的不可配置(configurable)的属性,不能被`deleteProperty`方法删除,否则报错。
|
||||||
|
|
||||||
### defineProperty()
|
### defineProperty()
|
||||||
|
|
||||||
`defineProperty`方法拦截了`Object.defineProperty`操作。
|
`defineProperty`方法拦截了`Object.defineProperty`操作。
|
||||||
@ -605,9 +621,11 @@ proxy.foo = 'bar'
|
|||||||
|
|
||||||
上面代码中,`defineProperty`方法返回`false`,导致添加新属性会抛出错误。
|
上面代码中,`defineProperty`方法返回`false`,导致添加新属性会抛出错误。
|
||||||
|
|
||||||
|
注意,如果目标对象不可扩展(extensible),则`defineProperty`不能增加目标对象上不存在的属性,否则会报错。另外,如果目标对象的某个属性不可写(writable)或不可配置(configurable),则`defineProperty`方法不得改变这两个设置。
|
||||||
|
|
||||||
### getOwnPropertyDescriptor()
|
### getOwnPropertyDescriptor()
|
||||||
|
|
||||||
`getOwnPropertyDescriptor`方法拦截`Object.getOwnPropertyDescriptor`,返回一个属性描述对象或者`undefined`。
|
`getOwnPropertyDescriptor`方法拦截`Object.getOwnPropertyDescriptor()`,返回一个属性描述对象或者`undefined`。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var handler = {
|
var handler = {
|
||||||
@ -632,13 +650,13 @@ Object.getOwnPropertyDescriptor(proxy, 'baz')
|
|||||||
|
|
||||||
### getPrototypeOf()
|
### getPrototypeOf()
|
||||||
|
|
||||||
`getPrototypeOf`方法主要用来拦截`Object.getPrototypeOf()`运算符,以及其他一些操作。
|
`getPrototypeOf`方法主要用来拦截获取对象原型。具体来说,拦截下面这些操作。
|
||||||
|
|
||||||
- `Object.prototype.__proto__`
|
- `Object.prototype.__proto__`
|
||||||
- `Object.prototype.isPrototypeOf()`
|
- `Object.prototype.isPrototypeOf()`
|
||||||
- `Object.getPrototypeOf()`
|
- `Object.getPrototypeOf()`
|
||||||
- `Reflect.getPrototypeOf()`
|
- `Reflect.getPrototypeOf()`
|
||||||
- `instanceof`运算符
|
- `instanceof`
|
||||||
|
|
||||||
下面是一个例子。
|
下面是一个例子。
|
||||||
|
|
||||||
@ -654,6 +672,8 @@ Object.getPrototypeOf(p) === proto // true
|
|||||||
|
|
||||||
上面代码中,`getPrototypeOf`方法拦截`Object.getPrototypeOf()`,返回`proto`对象。
|
上面代码中,`getPrototypeOf`方法拦截`Object.getPrototypeOf()`,返回`proto`对象。
|
||||||
|
|
||||||
|
注意,`getPrototypeOf`方法的返回值必须是对象或者`null`,否则报错。另外,如果目标对象不可扩展(extensible), `getPrototypeOf`方法必须返回目标对象的原型对象。
|
||||||
|
|
||||||
### isExtensible()
|
### isExtensible()
|
||||||
|
|
||||||
`isExtensible`方法拦截`Object.isExtensible`操作。
|
`isExtensible`方法拦截`Object.isExtensible`操作。
|
||||||
@ -673,7 +693,9 @@ Object.isExtensible(p)
|
|||||||
|
|
||||||
上面代码设置了`isExtensible`方法,在调用`Object.isExtensible`时会输出`called`。
|
上面代码设置了`isExtensible`方法,在调用`Object.isExtensible`时会输出`called`。
|
||||||
|
|
||||||
这个方法有一个强限制,如果不能满足下面的条件,就会抛出错误。
|
注意,该方法只能返回布尔值,否则返回值会被自动转为布尔值。
|
||||||
|
|
||||||
|
这个方法有一个强限制,它的返回值必须与目标对象的`isExtensible`属性保持一致,否则就会抛出错误。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
Object.isExtensible(proxy) === Object.isExtensible(target)
|
Object.isExtensible(proxy) === Object.isExtensible(target)
|
||||||
@ -693,24 +715,34 @@ Object.isExtensible(p) // 报错
|
|||||||
|
|
||||||
### ownKeys()
|
### ownKeys()
|
||||||
|
|
||||||
`ownKeys`方法用来拦截`Object.keys()`操作。
|
`ownKeys`方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作。
|
||||||
|
|
||||||
|
- `Object.getOwnPropertyNames()`
|
||||||
|
- `Object.getOwnPropertySymbols()`
|
||||||
|
- `Object.keys()`
|
||||||
|
|
||||||
|
下面是拦截`Object.keys()`的例子。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let target = {};
|
let target = {
|
||||||
|
a: 1,
|
||||||
|
b: 2,
|
||||||
|
c: 3
|
||||||
|
};
|
||||||
|
|
||||||
let handler = {
|
let handler = {
|
||||||
ownKeys(target) {
|
ownKeys(target) {
|
||||||
return ['hello', 'world'];
|
return ['a'];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let proxy = new Proxy(target, handler);
|
let proxy = new Proxy(target, handler);
|
||||||
|
|
||||||
Object.keys(proxy)
|
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"
|
// "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()
|
||||||
|
|
||||||
`preventExtensions`方法拦截`Object.preventExtensions()`。该方法必须返回一个布尔值。
|
`preventExtensions`方法拦截`Object.preventExtensions()`。该方法必须返回一个布尔值,否则会被自动转为布尔值。
|
||||||
|
|
||||||
这个方法有一个限制,只有当`Object.isExtensible(proxy)`为`false`(即不可扩展)时,`proxy.preventExtensions`才能返回`true`,否则会报错。
|
这个方法有一个限制,只有目标对象不可扩展时(即`Object.isExtensible(proxy)`为`false`),`proxy.preventExtensions`才能返回`true`,否则会报错。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var p = new Proxy({}, {
|
var p = new Proxy({}, {
|
||||||
@ -757,7 +897,7 @@ Object.preventExtensions(p) // 报错
|
|||||||
```javascript
|
```javascript
|
||||||
var p = new Proxy({}, {
|
var p = new Proxy({}, {
|
||||||
preventExtensions: function(target) {
|
preventExtensions: function(target) {
|
||||||
console.log("called");
|
console.log('called');
|
||||||
Object.preventExtensions(target);
|
Object.preventExtensions(target);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -783,15 +923,17 @@ var handler = {
|
|||||||
var proto = {};
|
var proto = {};
|
||||||
var target = function () {};
|
var target = function () {};
|
||||||
var proxy = new Proxy(target, handler);
|
var proxy = new Proxy(target, handler);
|
||||||
proxy.setPrototypeOf(proxy, proto);
|
Object.setPrototypeOf(proxy, proto);
|
||||||
// Error: Changing the prototype is forbidden
|
// Error: Changing the prototype is forbidden
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,只要修改`target`的原型对象,就会报错。
|
上面代码中,只要修改`target`的原型对象,就会报错。
|
||||||
|
|
||||||
|
注意,该方法只能返回布尔值,否则会被自动转为布尔值。另外,如果目标对象不可扩展(extensible),`setPrototypeOf`方法不得改变目标对象的原型。
|
||||||
|
|
||||||
## Proxy.revocable()
|
## Proxy.revocable()
|
||||||
|
|
||||||
Proxy.revocable方法返回一个可取消的Proxy实例。
|
`Proxy.revocable`方法返回一个可取消的 Proxy 实例。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let target = {};
|
let target = {};
|
||||||
@ -808,6 +950,8 @@ proxy.foo // TypeError: Revoked
|
|||||||
|
|
||||||
`Proxy.revocable`方法返回一个对象,该对象的`proxy`属性是`Proxy`实例,`revoke`属性是一个函数,可以取消`Proxy`实例。上面代码中,当执行`revoke`函数之后,再访问`Proxy`实例,就会抛出一个错误。
|
`Proxy.revocable`方法返回一个对象,该对象的`proxy`属性是`Proxy`实例,`revoke`属性是一个函数,可以取消`Proxy`实例。上面代码中,当执行`revoke`函数之后,再访问`Proxy`实例,就会抛出一个错误。
|
||||||
|
|
||||||
|
`Proxy.revocable`的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。
|
||||||
|
|
||||||
## this 问题
|
## this 问题
|
||||||
|
|
||||||
虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的`this`关键字会指向 Proxy 代理。
|
虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的`this`关键字会指向 Proxy 代理。
|
||||||
@ -879,200 +1023,29 @@ const proxy = new Proxy(target, handler);
|
|||||||
proxy.getDate() // 1
|
proxy.getDate() // 1
|
||||||
```
|
```
|
||||||
|
|
||||||
## Reflect概述
|
## 实例:Web 服务的客户端
|
||||||
|
|
||||||
`Reflect`对象与`Proxy`对象一样,也是ES6为了操作对象而提供的新API。`Reflect`对象的设计目的有这样几个。
|
Proxy 对象可以拦截目标对象的任意属性,这使得它很合适用来写 Web 服务的客户端。
|
||||||
|
|
||||||
(1) 将`Object`对象的一些明显属于语言内部的方法(比如`Object.defineProperty`),放到`Reflect`对象上。现阶段,某些方法同时在`Object`和`Reflect`对象上部署,未来的新方法将只部署在`Reflect`对象上。
|
|
||||||
|
|
||||||
(2) 修改某些Object方法的返回结果,让其变得更合理。比如,`Object.defineProperty(obj, name, desc)`在无法定义属性时,会抛出一个错误,而`Reflect.defineProperty(obj, name, desc)`则会返回`false`。
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 老写法
|
const service = createWebService('http://example.com/data');
|
||||||
try {
|
|
||||||
Object.defineProperty(target, property, attributes);
|
|
||||||
// success
|
|
||||||
} catch (e) {
|
|
||||||
// failure
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新写法
|
service.employees().then(json => {
|
||||||
if (Reflect.defineProperty(target, property, attributes)) {
|
const employees = JSON.parse(json);
|
||||||
// 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`方法将值赋值给对象的属性,然后再部署额外的功能。
|
上面代码新建了一个 Web 服务的接口,这个接口返回各种数据。Proxy 可以拦截这个对象的任意属性,所以不用为每一种数据写一个适配方法,只要写一个 Proxy 拦截就可以了。
|
||||||
|
|
||||||
下面是另一个例子。
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var loggedObj = new Proxy(obj, {
|
function createWebService(baseUrl) {
|
||||||
get(target, name) {
|
return new Proxy({}, {
|
||||||
console.log('get', target, name);
|
get(target, propKey, receiver) {
|
||||||
return Reflect.get(target, name);
|
return () => httpGet(baseUrl+'/' + propKey);
|
||||||
},
|
|
||||||
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对象的方法
|
|
||||||
|
|
||||||
`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`对象的方法是一一对应的。下面是对其中几个方法的解释。
|
|
||||||
|
|
||||||
**(1)Reflect.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);
|
|
||||||
```
|
|
||||||
|
|
||||||
**(2)Reflect.set(target, name, value, receiver)**
|
|
||||||
|
|
||||||
设置`target`对象的`name`属性等于`value`。如果`name`属性设置了赋值函数,则赋值函数的`this`绑定`receiver`。
|
|
||||||
|
|
||||||
**(3)Reflect.has(obj, name)**
|
|
||||||
|
|
||||||
等同于`name in obj`。
|
|
||||||
|
|
||||||
**(4)Reflect.deleteProperty(obj, name)**
|
|
||||||
|
|
||||||
等同于`delete obj[name]`。
|
|
||||||
|
|
||||||
**(5)Reflect.construct(target, args)**
|
|
||||||
|
|
||||||
等同于`new target(...args)`,这提供了一种不使用`new`,来调用构造函数的方法。
|
|
||||||
|
|
||||||
**(6)Reflect.getPrototypeOf(obj)**
|
|
||||||
|
|
||||||
读取对象的`__proto__`属性,对应`Object.getPrototypeOf(obj)`。
|
|
||||||
|
|
||||||
**(7)Reflect.setPrototypeOf(obj, newProto)**
|
|
||||||
|
|
||||||
设置对象的`__proto__`属性,对应`Object.setPrototypeOf(obj, newProto)`。
|
|
||||||
|
|
||||||
**(8)Reflect.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 层。
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
- [ECMAScript Daily](https://ecmascript-daily.github.io/): TC39 委员会的动态
|
- [ECMAScript Daily](https://ecmascript-daily.github.io/): TC39 委员会的动态
|
||||||
- [The TC39 Process](https://tc39.github.io/process-document/): 提案进入正式规格的流程
|
- [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: 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 修饰符
|
- 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 正则特性的详细介绍
|
- 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):介绍后行断言
|
- 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)
|
- 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 指向,必须非常小心
|
- 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): 如何自己实现尾递归优化
|
- 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/): 使用参数默认值时,不能在函数内部显式开启严格模式
|
- 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)
|
- 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)
|
- 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 实现观察者模式
|
- 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 对象
|
## 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 的关系
|
- 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)
|
- 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): 异步遍历器的详细介绍
|
- 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
|
## Class
|
||||||
|
|
||||||
@ -206,6 +212,10 @@
|
|||||||
- Jason Orendorff, [ES6 In Depth: Modules](https://hacks.mozilla.org/2015/08/es6-in-depth-modules/): ES6 模块设计思想的介绍
|
- 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 模块的设计思想
|
- 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)
|
- 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)
|
- 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)
|
- 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, [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
|
## SIMD
|
||||||
|
|
||||||
@ -236,4 +249,3 @@
|
|||||||
- SystemJS, [SystemJS](https://github.com/systemjs/systemjs): 在浏览器中加载 AMD、CJS、ES6 模块的一个垫片库
|
- 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 垫片库清单
|
- 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 的转码器
|
- Facebook, [regenerator](https://github.com/facebook/regenerator): 将 Generator 函数转为 ES5 的转码器
|
||||||
|
|
||||||
|
519
docs/reflect.md
Normal file
519
docs/reflect.md
Normal 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`之中,会自动执行所有观察者。
|
238
docs/regex.md
238
docs/regex.md
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## RegExp 构造函数
|
## RegExp 构造函数
|
||||||
|
|
||||||
在ES5中,RegExp构造函数的参数有两种情况。
|
在 ES5 中,`RegExp`构造函数的参数有两种情况。
|
||||||
|
|
||||||
第一种情况是,参数是字符串,这时第二个参数表示正则表达式的修饰符(flag)。
|
第一种情况是,参数是字符串,这时第二个参数表示正则表达式的修饰符(flag)。
|
||||||
|
|
||||||
@ -20,14 +20,14 @@ var regex = new RegExp(/xyz/i);
|
|||||||
var regex = /xyz/i;
|
var regex = /xyz/i;
|
||||||
```
|
```
|
||||||
|
|
||||||
但是,ES5不允许此时使用第二个参数,添加修饰符,否则会报错。
|
但是,ES5 不允许此时使用第二个参数添加修饰符,否则会报错。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var regex = new RegExp(/xyz/, 'i');
|
var regex = new RegExp(/xyz/, 'i');
|
||||||
// Uncaught TypeError: Cannot supply flags when constructing one RegExp from another
|
// Uncaught TypeError: Cannot supply flags when constructing one RegExp from another
|
||||||
```
|
```
|
||||||
|
|
||||||
ES6改变了这种行为。如果RegExp构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。
|
ES6 改变了这种行为。如果`RegExp`构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
new RegExp(/abc/ig, 'i').flags
|
new RegExp(/abc/ig, 'i').flags
|
||||||
@ -40,7 +40,7 @@ new RegExp(/abc/ig, 'i').flags
|
|||||||
|
|
||||||
字符串对象共有 4 个方法,可以使用正则表达式:`match()`、`replace()`、`search()`和`split()`。
|
字符串对象共有 4 个方法,可以使用正则表达式:`match()`、`replace()`、`search()`和`split()`。
|
||||||
|
|
||||||
ES6将这4个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。
|
ES6 将这 4 个方法,在语言内部全部调用`RegExp`的实例方法,从而做到所有与正则相关的方法,全都定义在`RegExp`对象上。
|
||||||
|
|
||||||
- `String.prototype.match` 调用 `RegExp.prototype[Symbol.match]`
|
- `String.prototype.match` 调用 `RegExp.prototype[Symbol.match]`
|
||||||
- `String.prototype.replace` 调用 `RegExp.prototype[Symbol.replace]`
|
- `String.prototype.replace` 调用 `RegExp.prototype[Symbol.replace]`
|
||||||
@ -52,10 +52,8 @@ ES6将这4个方法,在语言内部全部调用RegExp的实例方法,从而
|
|||||||
ES6 对正则表达式添加了`u`修饰符,含义为“Unicode 模式”,用来正确处理大于`\uFFFF`的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码。
|
ES6 对正则表达式添加了`u`修饰符,含义为“Unicode 模式”,用来正确处理大于`\uFFFF`的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
/^\uD83D/u.test('\uD83D\uDC2A')
|
/^\uD83D/u.test('\uD83D\uDC2A') // false
|
||||||
// false
|
/^\uD83D/.test('\uD83D\uDC2A') // true
|
||||||
/^\uD83D/.test('\uD83D\uDC2A')
|
|
||||||
// true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,`\uD83D\uDC2A`是一个四个字节的 UTF-16 编码,代表一个字符。但是,ES5 不支持四个字节的 UTF-16 编码,会将其识别为两个字符,导致第二行代码结果为`true`。加了`u`修饰符以后,ES6 就会识别其为一个字符,所以第一行代码结果为`false`。
|
上面代码中,`\uD83D\uDC2A`是一个四个字节的 UTF-16 编码,代表一个字符。但是,ES5 不支持四个字节的 UTF-16 编码,会将其识别为两个字符,导致第二行代码结果为`true`。加了`u`修饰符以后,ES6 就会识别其为一个字符,所以第一行代码结果为`false`。
|
||||||
@ -77,7 +75,7 @@ var s = '𠮷';
|
|||||||
|
|
||||||
**(2)Unicode 字符表示法**
|
**(2)Unicode 字符表示法**
|
||||||
|
|
||||||
ES6新增了使用大括号表示Unicode字符,这种表示法在正则表达式中必须加上`u`修饰符,才能识别。
|
ES6 新增了使用大括号表示 Unicode 字符,这种表示法在正则表达式中必须加上`u`修饰符,才能识别当中的大括号,否则会被解读为量词。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
/\u{61}/.test('a') // false
|
/\u{61}/.test('a') // false
|
||||||
@ -98,14 +96,6 @@ ES6新增了使用大括号表示Unicode字符,这种表示法在正则表达
|
|||||||
/𠮷{2}/u.test('𠮷𠮷') // true
|
/𠮷{2}/u.test('𠮷𠮷') // true
|
||||||
```
|
```
|
||||||
|
|
||||||
另外,只有在使用`u`修饰符的情况下,Unicode表达式当中的大括号才会被正确解读,否则会被解读为量词。
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
/^\u{3}$/.test('uuu') // true
|
|
||||||
```
|
|
||||||
|
|
||||||
上面代码中,由于正则表达式没有`u`修饰符,所以大括号被解读为量词。加上`u`修饰符,就会被解读为Unicode表达式。
|
|
||||||
|
|
||||||
**(4)预定义模式**
|
**(4)预定义模式**
|
||||||
|
|
||||||
`u`修饰符也影响到预定义模式,能否正确识别码点大于`0xFFFF`的 Unicode 字符。
|
`u`修饰符也影响到预定义模式,能否正确识别码点大于`0xFFFF`的 Unicode 字符。
|
||||||
@ -140,7 +130,7 @@ codePointLength(s) // 2
|
|||||||
/[a-z]/iu.test('\u212A') // true
|
/[a-z]/iu.test('\u212A') // true
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,不加`u`修饰符,就无法识别非规范的K字符。
|
上面代码中,不加`u`修饰符,就无法识别非规范的`K`字符。
|
||||||
|
|
||||||
## y 修饰符
|
## y 修饰符
|
||||||
|
|
||||||
@ -217,7 +207,7 @@ match.index // 3
|
|||||||
REGEX.lastIndex // 4
|
REGEX.lastIndex // 4
|
||||||
```
|
```
|
||||||
|
|
||||||
进一步说,`y`修饰符号隐含了头部匹配的标志`^`。
|
实际上,`y`修饰符号隐含了头部匹配的标志`^`。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
/b/y.exec('aba')
|
/b/y.exec('aba')
|
||||||
@ -226,28 +216,6 @@ REGEX.lastIndex // 4
|
|||||||
|
|
||||||
上面代码由于不能保证头部匹配,所以返回`null`。`y`修饰符的设计本意,就是让头部匹配的标志`^`在全局匹配中都有效。
|
上面代码由于不能保证头部匹配,所以返回`null`。`y`修饰符的设计本意,就是让头部匹配的标志`^`在全局匹配中都有效。
|
||||||
|
|
||||||
在`split`方法中使用`y`修饰符,原字符串必须以分隔符开头。这也意味着,只要匹配成功,数组的第一个成员肯定是空字符串。
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 没有找到匹配
|
|
||||||
'x##'.split(/#/y)
|
|
||||||
// [ 'x##' ]
|
|
||||||
|
|
||||||
// 找到两个匹配
|
|
||||||
'##x'.split(/#/y)
|
|
||||||
// [ '', '', 'x' ]
|
|
||||||
```
|
|
||||||
|
|
||||||
后续的分隔符只有紧跟前面的分隔符,才会被识别。
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
'#x#'.split(/#/y)
|
|
||||||
// [ '', 'x#' ]
|
|
||||||
|
|
||||||
'##'.split(/#/y)
|
|
||||||
// [ '', '', '' ]
|
|
||||||
```
|
|
||||||
|
|
||||||
下面是字符串对象的`replace`方法的例子。
|
下面是字符串对象的`replace`方法的例子。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
@ -255,7 +223,7 @@ const REGEX = /a/gy;
|
|||||||
'aaxa'.replace(REGEX, '-') // '--xa'
|
'aaxa'.replace(REGEX, '-') // '--xa'
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,最后一个`a`因为不是出现下一次匹配的头部,所以不会被替换。
|
上面代码中,最后一个`a`因为不是出现在下一次匹配的头部,所以不会被替换。
|
||||||
|
|
||||||
单单一个`y`修饰符对`match`方法,只能返回第一个匹配,必须与`g`修饰符联用,才能返回所有匹配。
|
单单一个`y`修饰符对`match`方法,只能返回第一个匹配,必须与`g`修饰符联用,才能返回所有匹配。
|
||||||
|
|
||||||
@ -321,51 +289,6 @@ ES6为正则表达式新增了`flags`属性,会返回正则表达式的修饰
|
|||||||
// 'gi'
|
// '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 模式
|
## s 修饰符:dotAll 模式
|
||||||
|
|
||||||
正则表达式中,点(`.`)是一个特殊字符,代表任意的单个字符,但是行终止符(line terminator character)除外。
|
正则表达式中,点(`.`)是一个特殊字符,代表任意的单个字符,但是行终止符(line terminator character)除外。
|
||||||
@ -413,9 +336,7 @@ re.flags // 's'
|
|||||||
|
|
||||||
## 后行断言
|
## 后行断言
|
||||||
|
|
||||||
JavaScript语言的正则表达式,只支持先行断言(lookahead)和先行否定断言(negative lookahead),不支持后行断言(lookbehind)和后行否定断言(negative lookbehind)。
|
JavaScript 语言的正则表达式,只支持先行断言(lookahead)和先行否定断言(negative lookahead),不支持后行断言(lookbehind)和后行否定断言(negative lookbehind)。目前,有一个[提案](https://github.com/goyakin/es-regexp-lookbehind),引入后行断言,V8 引擎 4.9 版已经支持。
|
||||||
|
|
||||||
目前,有一个[提案](https://github.com/goyakin/es-regexp-lookbehind),在ES7加入后行断言。V8引擎4.9版已经支持,Chrome浏览器49版打开”experimental JavaScript features“开关(地址栏键入`about:flags`),就可以使用这项功能。
|
|
||||||
|
|
||||||
”先行断言“指的是,`x`只有在`y`前面才匹配,必须写成`/x(?=y)/`。比如,只匹配百分号之前的数字,要写成`/\d+(?=%)/`。”先行否定断言“指的是,`x`只有不在`y`前面才匹配,必须写成`/x(?!y)/`。比如,只匹配不在百分号之前的数字,要写成`/\d+(?!%)/`。
|
”先行断言“指的是,`x`只有在`y`前面才匹配,必须写成`/x(?=y)/`。比如,只匹配百分号之前的数字,要写成`/\d+(?=%)/`。”先行否定断言“指的是,`x`只有不在`y`前面才匹配,必须写成`/x(?!y)/`。比如,只匹配不在百分号之前的数字,要写成`/\d+(?!%)/`。
|
||||||
|
|
||||||
@ -424,18 +345,28 @@ JavaScript语言的正则表达式,只支持先行断言(lookahead)和先
|
|||||||
/\d+(?!%)/.exec('that’s all 44 of them') // ["44"]
|
/\d+(?!%)/.exec('that’s 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
|
```javascript
|
||||||
/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill') // ["100"]
|
/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill') // ["100"]
|
||||||
/(?<!\$)\d+/.exec('it’s is worth about €90') // ["90"]
|
/(?<!\$)\d+/.exec('it’s 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"]
|
/(?<=\1d(o))r/.exec('hodor') // ["r", "o"]
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,如果后行断言的反斜杠引用(`\1`)放在括号的后面,就不会得到匹配结果,必须放在前面才可以。
|
上面代码中,如果后行断言的反斜杠引用(`\1`)放在括号的后面,就不会得到匹配结果,必须放在前面才可以。因为后行断言是先从左到右扫描,发现匹配以后再回过头,从右到左完成反斜杠引用。
|
||||||
|
|
||||||
## Unicode 属性类
|
## Unicode 属性类
|
||||||
|
|
||||||
@ -461,7 +392,7 @@ JavaScript语言的正则表达式,只支持先行断言(lookahead)和先
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const regexGreekSymbol = /\p{Script=Greek}/u;
|
const regexGreekSymbol = /\p{Script=Greek}/u;
|
||||||
regexGreekSymbol.test('π') // u
|
regexGreekSymbol.test('π') // true
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,`\p{Script=Greek}`指定匹配一个希腊文字母,所以匹配`π`成功。
|
上面代码中,`\p{Script=Greek}`指定匹配一个希腊文字母,所以匹配`π`成功。
|
||||||
@ -508,10 +439,123 @@ regex.test('ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ') // true
|
|||||||
[\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}]
|
||||||
|
|
||||||
// 匹配各种文字的所有非字母的字符,等同于 Unicode 版的 \W
|
// 匹配各种文字的所有非字母的字符,等同于 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;
|
const regexArrows = /^\p{Block=Arrows}+$/u;
|
||||||
regexArrows.test('←↑→↓↔↕↖↗↘↙⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇧⇩') // true
|
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
|
||||||
|
```
|
||||||
|
427
docs/set-map.md
427
docs/set-map.md
@ -9,9 +9,9 @@ ES6提供了新的数据结构Set。它类似于数组,但是成员的值都
|
|||||||
Set 本身是一个构造函数,用来生成 Set 数据结构。
|
Set 本身是一个构造函数,用来生成 Set 数据结构。
|
||||||
|
|
||||||
```javascript
|
```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) {
|
for (let i of s) {
|
||||||
console.log(i);
|
console.log(i);
|
||||||
@ -21,16 +21,16 @@ for (let i of s) {
|
|||||||
|
|
||||||
上面代码通过`add`方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。
|
上面代码通过`add`方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。
|
||||||
|
|
||||||
Set函数可以接受一个数组(或类似数组的对象)作为参数,用来初始化。
|
Set 函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 例一
|
// 例一
|
||||||
var set = new Set([1, 2, 3, 4, 4]);
|
const set = new Set([1, 2, 3, 4, 4]);
|
||||||
[...set]
|
[...set]
|
||||||
// [1, 2, 3, 4]
|
// [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
|
items.size // 5
|
||||||
|
|
||||||
// 例三
|
// 例三
|
||||||
@ -38,7 +38,7 @@ function divs () {
|
|||||||
return [...document.querySelectorAll('div')];
|
return [...document.querySelectorAll('div')];
|
||||||
}
|
}
|
||||||
|
|
||||||
var set = new Set(divs());
|
const set = new Set(divs());
|
||||||
set.size // 56
|
set.size // 56
|
||||||
|
|
||||||
// 类似于
|
// 类似于
|
||||||
@ -116,7 +116,7 @@ s.has(2) // false
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 对象的写法
|
// 对象的写法
|
||||||
var properties = {
|
const properties = {
|
||||||
'width': 1,
|
'width': 1,
|
||||||
'height': 1
|
'height': 1
|
||||||
};
|
};
|
||||||
@ -126,7 +126,7 @@ if (properties[someName]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set的写法
|
// Set的写法
|
||||||
var properties = new Set();
|
const properties = new Set();
|
||||||
|
|
||||||
properties.add('width');
|
properties.add('width');
|
||||||
properties.add('height');
|
properties.add('height');
|
||||||
@ -139,8 +139,8 @@ if (properties.has(someName)) {
|
|||||||
`Array.from`方法可以将 Set 结构转为数组。
|
`Array.from`方法可以将 Set 结构转为数组。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var items = new Set([1, 2, 3, 4, 5]);
|
const items = new Set([1, 2, 3, 4, 5]);
|
||||||
var array = Array.from(items);
|
const array = Array.from(items);
|
||||||
```
|
```
|
||||||
|
|
||||||
这就提供了去除数组重复成员的另一种方法。
|
这就提供了去除数组重复成员的另一种方法。
|
||||||
@ -166,7 +166,7 @@ Set结构的实例有四个遍历方法,可以用于遍历成员。
|
|||||||
|
|
||||||
**(1)`keys()`,`values()`,`entries()`**
|
**(1)`keys()`,`values()`,`entries()`**
|
||||||
|
|
||||||
`key`方法、`value`方法、`entries`方法返回的都是遍历器对象(详见《Iterator对象》一章)。由于Set结构没有键名,只有键值(或者说键名和键值是同一个值),所以`key`方法和`value`方法的行为完全一致。
|
`keys`方法、`values`方法、`entries`方法返回的都是遍历器对象(详见《Iterator 对象》一章)。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以`keys`方法和`values`方法的行为完全一致。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let set = new Set(['red', 'green', 'blue']);
|
let set = new Set(['red', 'green', 'blue']);
|
||||||
@ -217,17 +217,19 @@ for (let x of set) {
|
|||||||
|
|
||||||
**(2)`forEach()`**
|
**(2)`forEach()`**
|
||||||
|
|
||||||
Set结构的实例的`forEach`方法,用于对每个成员执行某种操作,没有返回值。
|
Set 结构的实例与数组一样,也拥有`forEach`方法,用于对每个成员执行某种操作,没有返回值。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let set = new Set([1, 2, 3]);
|
set = new Set([1, 4, 9]);
|
||||||
set.forEach((value, key) => console.log(value * 2) )
|
set.forEach((value, key) => console.log(key + ' : ' + value))
|
||||||
// 2
|
// 1 : 1
|
||||||
// 4
|
// 4 : 4
|
||||||
// 6
|
// 9 : 9
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码说明,`forEach`方法的参数就是一个处理函数。该函数的参数依次为键值、键名、集合本身(上例省略了该参数)。另外,`forEach`方法还可以有第二个参数,表示绑定的this对象。
|
上面代码说明,`forEach`方法的参数就是一个处理函数。该函数的参数与数组的`forEach`一致,依次为键值、键名、集合本身(上例省略了该参数)。这里需要注意,Set 结构的键名就是键值(两者是同一个值),因此第一个参数与第二个参数的值永远都是一样的。
|
||||||
|
|
||||||
|
另外,`forEach`方法还可以有第二个参数,表示绑定处理函数内部的`this`对象。
|
||||||
|
|
||||||
**(3)遍历的应用**
|
**(3)遍历的应用**
|
||||||
|
|
||||||
@ -247,7 +249,7 @@ let unique = [...new Set(arr)];
|
|||||||
// [3, 5, 2]
|
// [3, 5, 2]
|
||||||
```
|
```
|
||||||
|
|
||||||
而且,数组的`map`和`filter`方法也可以用于Set了。
|
而且,数组的`map`和`filter`方法也可以间接用于 Set 了。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let set = new Set([1, 2, 3]);
|
let set = new Set([1, 2, 3]);
|
||||||
@ -296,14 +298,14 @@ set = new Set(Array.from(set, val => val * 2));
|
|||||||
|
|
||||||
## WeakSet
|
## WeakSet
|
||||||
|
|
||||||
|
### 含义
|
||||||
|
|
||||||
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
|
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
|
||||||
|
|
||||||
首先,WeakSet 的成员只能是对象,而不能是其他类型的值。
|
首先,WeakSet 的成员只能是对象,而不能是其他类型的值。
|
||||||
|
|
||||||
其次,WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于WeakSet之中。这个特点意味着,无法引用WeakSet的成员,因此WeakSet是不可遍历的。
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var ws = new WeakSet();
|
const ws = new WeakSet();
|
||||||
ws.add(1)
|
ws.add(1)
|
||||||
// TypeError: Invalid value used in weak set
|
// TypeError: Invalid value used in weak set
|
||||||
ws.add(Symbol())
|
ws.add(Symbol())
|
||||||
@ -312,17 +314,28 @@ ws.add(Symbol())
|
|||||||
|
|
||||||
上面代码试图向 WeakSet 添加一个数值和`Symbol`值,结果报错,因为 WeakSet 只能放置对象。
|
上面代码试图向 WeakSet 添加一个数值和`Symbol`值,结果报错,因为 WeakSet 只能放置对象。
|
||||||
|
|
||||||
|
其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
|
||||||
|
|
||||||
|
这是因为垃圾回收机制依赖引用计数,如果一个值的引用次数不为`0`,垃圾回收机制就不会释放这块内存。结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。WeakSet 里面的引用,都不计入垃圾回收机制,所以就不存在这个问题。因此,WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。
|
||||||
|
|
||||||
|
由于上面这个特点,WeakSet 的成员是不适合引用的,因为它会随时消失。另外,由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。
|
||||||
|
|
||||||
|
这些特点同样适用于本章后面要介绍的 WeakMap 结构。
|
||||||
|
|
||||||
|
### 语法
|
||||||
|
|
||||||
WeakSet 是一个构造函数,可以使用`new`命令,创建 WeakSet 数据结构。
|
WeakSet 是一个构造函数,可以使用`new`命令,创建 WeakSet 数据结构。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var ws = new WeakSet();
|
const ws = new WeakSet();
|
||||||
```
|
```
|
||||||
|
|
||||||
作为构造函数,WeakSet可以接受一个数组或类似数组的对象作为参数。(实际上,任何具有iterable接口的对象,都可以作为WeakSet的参数。)该数组的所有成员,都会自动成为WeakSet实例对象的成员。
|
作为构造函数,WeakSet 可以接受一个数组或类似数组的对象作为参数。(实际上,任何具有 Iterable 接口的对象,都可以作为 WeakSet 的参数。)该数组的所有成员,都会自动成为 WeakSet 实例对象的成员。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var a = [[1,2], [3,4]];
|
const a = [[1, 2], [3, 4]];
|
||||||
var ws = new WeakSet(a);
|
const ws = new WeakSet(a);
|
||||||
|
// WeakSet {[1, 2], [3, 4]}
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,`a`是一个数组,它有两个成员,也都是数组。将`a`作为 WeakSet 构造函数的参数,`a`的成员会自动成为 WeakSet 的成员。
|
上面代码中,`a`是一个数组,它有两个成员,也都是数组。将`a`作为 WeakSet 构造函数的参数,`a`的成员会自动成为 WeakSet 的成员。
|
||||||
@ -330,8 +343,8 @@ var ws = new WeakSet(a);
|
|||||||
注意,是`a`数组的成员成为 WeakSet 的成员,而不是`a`数组本身。这意味着,数组的成员只能是对象。
|
注意,是`a`数组的成员成为 WeakSet 的成员,而不是`a`数组本身。这意味着,数组的成员只能是对象。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var b = [3, 4];
|
const b = [3, 4];
|
||||||
var ws = new WeakSet(b);
|
const ws = new WeakSet(b);
|
||||||
// Uncaught TypeError: Invalid value used in weak set(…)
|
// Uncaught TypeError: Invalid value used in weak set(…)
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -346,9 +359,9 @@ WeakSet结构有以下三个方法。
|
|||||||
下面是一个例子。
|
下面是一个例子。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var ws = new WeakSet();
|
const ws = new WeakSet();
|
||||||
var obj = {};
|
const obj = {};
|
||||||
var foo = {};
|
const foo = {};
|
||||||
|
|
||||||
ws.add(window);
|
ws.add(window);
|
||||||
ws.add(obj);
|
ws.add(obj);
|
||||||
@ -394,13 +407,13 @@ class Foo {
|
|||||||
|
|
||||||
## Map
|
## Map
|
||||||
|
|
||||||
### Map结构的目的和基本用法
|
### 含义和基本用法
|
||||||
|
|
||||||
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
|
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var data = {};
|
const data = {};
|
||||||
var element = document.getElementById('myDiv');
|
const element = document.getElementById('myDiv');
|
||||||
|
|
||||||
data[element] = 'metadata';
|
data[element] = 'metadata';
|
||||||
data['[object HTMLDivElement]'] // "metadata"
|
data['[object HTMLDivElement]'] // "metadata"
|
||||||
@ -411,8 +424,8 @@ data['[object HTMLDivElement]'] // "metadata"
|
|||||||
为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
|
为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var m = new Map();
|
const m = new Map();
|
||||||
var o = {p: 'Hello World'};
|
const o = {p: 'Hello World'};
|
||||||
|
|
||||||
m.set(o, 'content')
|
m.set(o, 'content')
|
||||||
m.get(o) // "content"
|
m.get(o) // "content"
|
||||||
@ -422,12 +435,12 @@ m.delete(o) // true
|
|||||||
m.has(o) // false
|
m.has(o) // false
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码使用`set`方法,将对象`o`当作`m`的一个键,然后又使用`get`方法读取这个键,接着使用`delete`方法删除了这个键。
|
上面代码使用 Map 结构的`set`方法,将对象`o`当作`m`的一个键,然后又使用`get`方法读取这个键,接着使用`delete`方法删除了这个键。
|
||||||
|
|
||||||
作为构造函数,Map也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
|
上面的例子展示了如何向 Map 添加成员。作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var map = new Map([
|
const map = new Map([
|
||||||
['name', '张三'],
|
['name', '张三'],
|
||||||
['title', 'Author']
|
['title', 'Author']
|
||||||
]);
|
]);
|
||||||
@ -441,33 +454,42 @@ map.get('title') // "Author"
|
|||||||
|
|
||||||
上面代码在新建 Map 实例时,就指定了两个键`name`和`title`。
|
上面代码在新建 Map 实例时,就指定了两个键`name`和`title`。
|
||||||
|
|
||||||
Map构造函数接受数组作为参数,实际上执行的是下面的算法。
|
`Map`构造函数接受数组作为参数,实际上执行的是下面的算法。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var items = [
|
const items = [
|
||||||
['name', '张三'],
|
['name', '张三'],
|
||||||
['title', 'Author']
|
['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
|
```javascript
|
||||||
var m = new Map([
|
const set = new Set([
|
||||||
[true, 'foo'],
|
['foo', 1],
|
||||||
['true', 'bar']
|
['bar', 2]
|
||||||
]);
|
]);
|
||||||
|
const m1 = new Map(set);
|
||||||
|
m1.get('foo') // 1
|
||||||
|
|
||||||
m.get(true) // 'foo'
|
const m2 = new Map([['baz', 3]]);
|
||||||
m.get('true') // 'bar'
|
const m3 = new Map(m2);
|
||||||
|
m3.get('baz') // 3
|
||||||
```
|
```
|
||||||
|
|
||||||
|
上面代码中,我们分别使用 Set 对象和 Map 对象,当作`Map`构造函数的参数,结果都生成了新的 Map 对象。
|
||||||
|
|
||||||
如果对同一个键多次赋值,后面的值将覆盖前面的值。
|
如果对同一个键多次赋值,后面的值将覆盖前面的值。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let map = new Map();
|
const map = new Map();
|
||||||
|
|
||||||
map
|
map
|
||||||
.set(1, 'aaa')
|
.set(1, 'aaa')
|
||||||
@ -488,7 +510,7 @@ new Map().get('asfddfsasadf')
|
|||||||
注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。
|
注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var map = new Map();
|
const map = new Map();
|
||||||
|
|
||||||
map.set(['a'], 555);
|
map.set(['a'], 555);
|
||||||
map.get(['a']) // undefined
|
map.get(['a']) // undefined
|
||||||
@ -499,10 +521,10 @@ map.get(['a']) // undefined
|
|||||||
同理,同样的值的两个实例,在 Map 结构中被视为两个键。
|
同理,同样的值的两个实例,在 Map 结构中被视为两个键。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var map = new Map();
|
const map = new Map();
|
||||||
|
|
||||||
var k1 = ['a'];
|
const k1 = ['a'];
|
||||||
var k2 = ['a'];
|
const k2 = ['a'];
|
||||||
|
|
||||||
map
|
map
|
||||||
.set(k1, 111)
|
.set(k1, 111)
|
||||||
@ -516,16 +538,24 @@ map.get(k2) // 222
|
|||||||
|
|
||||||
由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。
|
由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。
|
||||||
|
|
||||||
如果Map的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map将其视为一个键,包括`0`和`-0`。另外,虽然`NaN`不严格相等于自身,但Map将其视为同一个键。
|
如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如`0`和`-0`就是一个键,布尔值`true`和字符串`true`则是两个不同的键。另外,`undefined`和`null`也是两个不同的键。虽然`NaN`不严格相等于自身,但 Map 将其视为同一个键。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let map = new Map();
|
let map = new Map();
|
||||||
|
|
||||||
map.set(NaN, 123);
|
|
||||||
map.get(NaN) // 123
|
|
||||||
|
|
||||||
map.set(-0, 123);
|
map.set(-0, 123);
|
||||||
map.get(+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 结构的成员总数。
|
`size`属性返回 Map 结构的成员总数。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let map = new Map();
|
const map = new Map();
|
||||||
map.set('foo', true);
|
map.set('foo', true);
|
||||||
map.set('bar', false);
|
map.set('bar', false);
|
||||||
|
|
||||||
@ -546,17 +576,17 @@ map.size // 2
|
|||||||
|
|
||||||
**(2)set(key, value)**
|
**(2)set(key, value)**
|
||||||
|
|
||||||
`set`方法设置`key`所对应的键值,然后返回整个Map结构。如果`key`已经有值,则键值会被更新,否则就新生成该键。
|
`set`方法设置键名`key`对应的键值为`value`,然后返回整个 Map 结构。如果`key`已经有值,则键值会被更新,否则就新生成该键。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var m = new Map();
|
const m = new Map();
|
||||||
|
|
||||||
m.set("edition", 6) // 键是字符串
|
m.set('edition', 6) // 键是字符串
|
||||||
m.set(262, "standard") // 键是数值
|
m.set(262, 'standard') // 键是数值
|
||||||
m.set(undefined, "nah") // 键是undefined
|
m.set(undefined, 'nah') // 键是 undefined
|
||||||
```
|
```
|
||||||
|
|
||||||
`set`方法返回的是Map本身,因此可以采用链式写法。
|
`set`方法返回的是当前的`Map`对象,因此可以采用链式写法。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let map = new Map()
|
let map = new Map()
|
||||||
@ -570,43 +600,44 @@ let map = new Map()
|
|||||||
`get`方法读取`key`对应的键值,如果找不到`key`,返回`undefined`。
|
`get`方法读取`key`对应的键值,如果找不到`key`,返回`undefined`。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var m = new Map();
|
const m = new Map();
|
||||||
|
|
||||||
var hello = function() {console.log("hello");}
|
const hello = function() {console.log('hello');};
|
||||||
m.set(hello, "Hello ES6!") // 键是函数
|
m.set(hello, 'Hello ES6!') // 键是函数
|
||||||
|
|
||||||
m.get(hello) // Hello ES6!
|
m.get(hello) // Hello ES6!
|
||||||
```
|
```
|
||||||
|
|
||||||
**(4)has(key)**
|
**(4)has(key)**
|
||||||
|
|
||||||
`has`方法返回一个布尔值,表示某个键是否在Map数据结构中。
|
`has`方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var m = new Map();
|
const m = new Map();
|
||||||
|
|
||||||
m.set("edition", 6);
|
m.set('edition', 6);
|
||||||
m.set(262, "standard");
|
m.set(262, 'standard');
|
||||||
m.set(undefined, "nah");
|
m.set(undefined, 'nah');
|
||||||
|
|
||||||
m.has("edition") // true
|
m.has('edition') // true
|
||||||
m.has("years") // false
|
m.has('years') // false
|
||||||
m.has(262) // true
|
m.has(262) // true
|
||||||
m.has(undefined) // true
|
m.has(undefined) // true
|
||||||
```
|
```
|
||||||
|
|
||||||
**(5)delete(key)**
|
**(5)delete(key)**
|
||||||
|
|
||||||
`delete`方法删除某个键,返回true。如果删除失败,返回false。
|
`delete`方法删除某个键,返回`true`。如果删除失败,返回`false`。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var m = new Map();
|
const m = new Map();
|
||||||
m.set(undefined, "nah");
|
m.set(undefined, 'nah');
|
||||||
m.has(undefined) // true
|
m.has(undefined) // true
|
||||||
|
|
||||||
m.delete(undefined)
|
m.delete(undefined)
|
||||||
m.has(undefined) // false
|
m.has(undefined) // false
|
||||||
```
|
```
|
||||||
|
|
||||||
**(6)clear()**
|
**(6)clear()**
|
||||||
|
|
||||||
`clear`方法清除所有成员,没有返回值。
|
`clear`方法清除所有成员,没有返回值。
|
||||||
@ -623,7 +654,7 @@ map.size // 0
|
|||||||
|
|
||||||
### 遍历方法
|
### 遍历方法
|
||||||
|
|
||||||
Map原生提供三个遍历器生成函数和一个遍历方法。
|
Map 结构原生提供三个遍历器生成函数和一个遍历方法。
|
||||||
|
|
||||||
- `keys()`:返回键名的遍历器。
|
- `keys()`:返回键名的遍历器。
|
||||||
- `values()`:返回键值的遍历器。
|
- `values()`:返回键值的遍历器。
|
||||||
@ -632,10 +663,8 @@ Map原生提供三个遍历器生成函数和一个遍历方法。
|
|||||||
|
|
||||||
需要特别注意的是,Map 的遍历顺序就是插入顺序。
|
需要特别注意的是,Map 的遍历顺序就是插入顺序。
|
||||||
|
|
||||||
下面是使用实例。
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let map = new Map([
|
const map = new Map([
|
||||||
['F', 'no'],
|
['F', 'no'],
|
||||||
['T', 'yes'],
|
['T', 'yes'],
|
||||||
]);
|
]);
|
||||||
@ -662,11 +691,15 @@ for (let item of map.entries()) {
|
|||||||
for (let [key, value] of map.entries()) {
|
for (let [key, value] of map.entries()) {
|
||||||
console.log(key, value);
|
console.log(key, value);
|
||||||
}
|
}
|
||||||
|
// "F" "no"
|
||||||
|
// "T" "yes"
|
||||||
|
|
||||||
// 等同于使用map.entries()
|
// 等同于使用map.entries()
|
||||||
for (let [key, value] of map) {
|
for (let [key, value] of map) {
|
||||||
console.log(key, value);
|
console.log(key, value);
|
||||||
}
|
}
|
||||||
|
// "F" "no"
|
||||||
|
// "T" "yes"
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码最后的那个例子,表示 Map 结构的默认遍历器接口(`Symbol.iterator`属性),就是`entries`方法。
|
上面代码最后的那个例子,表示 Map 结构的默认遍历器接口(`Symbol.iterator`属性),就是`entries`方法。
|
||||||
@ -676,10 +709,10 @@ map[Symbol.iterator] === map.entries
|
|||||||
// true
|
// true
|
||||||
```
|
```
|
||||||
|
|
||||||
Map结构转为数组结构,比较快速的方法是结合使用扩展运算符(`...`)。
|
Map 结构转为数组结构,比较快速的方法是使用扩展运算符(`...`)。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let map = new Map([
|
const map = new Map([
|
||||||
[1, 'one'],
|
[1, 'one'],
|
||||||
[2, 'two'],
|
[2, 'two'],
|
||||||
[3, 'three'],
|
[3, 'three'],
|
||||||
@ -701,17 +734,17 @@ let map = new Map([
|
|||||||
结合数组的`map`方法、`filter`方法,可以实现 Map 的遍历和过滤(Map 本身没有`map`和`filter`方法)。
|
结合数组的`map`方法、`filter`方法,可以实现 Map 的遍历和过滤(Map 本身没有`map`和`filter`方法)。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let map0 = new Map()
|
const map0 = new Map()
|
||||||
.set(1, 'a')
|
.set(1, 'a')
|
||||||
.set(2, 'b')
|
.set(2, 'b')
|
||||||
.set(3, 'c');
|
.set(3, 'c');
|
||||||
|
|
||||||
let map1 = new Map(
|
const map1 = new Map(
|
||||||
[...map0].filter(([k, v]) => k < 3)
|
[...map0].filter(([k, v]) => k < 3)
|
||||||
);
|
);
|
||||||
// 产生 Map 结构 {1 => 'a', 2 => 'b'}
|
// 产生 Map 结构 {1 => 'a', 2 => 'b'}
|
||||||
|
|
||||||
let map2 = new Map(
|
const map2 = new Map(
|
||||||
[...map0].map(([k, v]) => [k * 2, '_' + v])
|
[...map0].map(([k, v]) => [k * 2, '_' + v])
|
||||||
);
|
);
|
||||||
// 产生 Map 结构 {2 => '_a', 4 => '_b', 6 => '_c'}
|
// 产生 Map 结构 {2 => '_a', 4 => '_b', 6 => '_c'}
|
||||||
@ -728,7 +761,7 @@ map.forEach(function(value, key, map) {
|
|||||||
`forEach`方法还可以接受第二个参数,用来绑定`this`。
|
`forEach`方法还可以接受第二个参数,用来绑定`this`。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var reporter = {
|
const reporter = {
|
||||||
report: function(key, value) {
|
report: function(key, value) {
|
||||||
console.log("Key: %s, Value: %s", key, value);
|
console.log("Key: %s, Value: %s", key, value);
|
||||||
}
|
}
|
||||||
@ -745,21 +778,29 @@ map.forEach(function(value, key, map) {
|
|||||||
|
|
||||||
**(1)Map 转为数组**
|
**(1)Map 转为数组**
|
||||||
|
|
||||||
前面已经提过,Map转为数组最方便的方法,就是使用扩展运算符(...)。
|
前面已经提过,Map 转为数组最方便的方法,就是使用扩展运算符(`...`)。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
|
const myMap = new Map()
|
||||||
|
.set(true, 7)
|
||||||
|
.set({foo: 3}, ['abc']);
|
||||||
[...myMap]
|
[...myMap]
|
||||||
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
|
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
|
||||||
```
|
```
|
||||||
|
|
||||||
**(2)数组 转为 Map**
|
**(2)数组 转为 Map**
|
||||||
|
|
||||||
将数组转入Map构造函数,就可以转为Map。
|
将数组传入 Map 构造函数,就可以转为 Map。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
new Map([[true, 7], [{foo: 3}, ['abc']]])
|
new Map([
|
||||||
// Map {true => 7, Object {foo: 3} => ['abc']}
|
[true, 7],
|
||||||
|
[{foo: 3}, ['abc']]
|
||||||
|
])
|
||||||
|
// Map {
|
||||||
|
// true => 7,
|
||||||
|
// Object {foo: 3} => ['abc']
|
||||||
|
// }
|
||||||
```
|
```
|
||||||
|
|
||||||
**(3)Map 转为对象**
|
**(3)Map 转为对象**
|
||||||
@ -775,7 +816,9 @@ function strMapToObj(strMap) {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
let myMap = new Map().set('yes', true).set('no', false);
|
const myMap = new Map()
|
||||||
|
.set('yes', true)
|
||||||
|
.set('no', false);
|
||||||
strMapToObj(myMap)
|
strMapToObj(myMap)
|
||||||
// { yes: true, no: false }
|
// { yes: true, no: false }
|
||||||
```
|
```
|
||||||
@ -792,7 +835,7 @@ function objToStrMap(obj) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
objToStrMap({yes: true, no: false})
|
objToStrMap({yes: true, no: false})
|
||||||
// [ [ 'yes', true ], [ 'no', false ] ]
|
// Map {"yes" => true, "no" => false}
|
||||||
```
|
```
|
||||||
|
|
||||||
**(5)Map 转为 JSON**
|
**(5)Map 转为 JSON**
|
||||||
@ -847,48 +890,188 @@ jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
|
|||||||
|
|
||||||
## WeakMap
|
## WeakMap
|
||||||
|
|
||||||
`WeakMap`结构与`Map`结构基本类似,唯一的区别是它只接受对象作为键名(`null`除外),不接受其他类型的值作为键名,而且键名所指向的对象,不计入垃圾回收机制。
|
### 含义
|
||||||
|
|
||||||
|
`WeakMap`结构与`Map`结构类似,也是用于生成键值对的集合。
|
||||||
|
|
||||||
```javascript
|
```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)
|
map.set(1, 2)
|
||||||
// TypeError: 1 is not an object!
|
// TypeError: 1 is not an object!
|
||||||
map.set(Symbol(), 2)
|
map.set(Symbol(), 2)
|
||||||
// TypeError: Invalid value used as weak map key
|
// 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
|
```javascript
|
||||||
var wm = new WeakMap();
|
const e1 = document.getElementById('foo');
|
||||||
var element = document.querySelector(".element");
|
const e2 = document.getElementById('bar');
|
||||||
|
const arr = [
|
||||||
wm.set(element, "Original");
|
[e1, 'foo 元素'],
|
||||||
wm.get(element) // "Original"
|
[e2, 'bar 元素'],
|
||||||
|
];
|
||||||
element.parentNode.removeChild(element);
|
|
||||||
element = null;
|
|
||||||
wm.get(element) // undefined
|
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,变量`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
|
```javascript
|
||||||
var wm = new WeakMap();
|
// 不需要 e1 和 e2 的时候
|
||||||
|
// 必须手动删除引用
|
||||||
wm.size
|
arr [0] = null;
|
||||||
// undefined
|
arr [1] = null;
|
||||||
|
|
||||||
wm.forEach
|
|
||||||
// undefined
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
上面这样的写法显然很不方便。一旦忘了写,就会造成内存泄露。
|
||||||
|
|
||||||
|
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 节点作为键名。下面是一个例子。
|
前文说过,WeakMap 应用的典型场合就是 DOM 节点作为键名。下面是一个例子。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
@ -908,8 +1091,8 @@ myElement.addEventListener('click', function() {
|
|||||||
WeakMap 的另一个用处是部署私有属性。
|
WeakMap 的另一个用处是部署私有属性。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let _counter = new WeakMap();
|
const _counter = new WeakMap();
|
||||||
let _action = new WeakMap();
|
const _action = new WeakMap();
|
||||||
|
|
||||||
class Countdown {
|
class Countdown {
|
||||||
constructor(counter, action) {
|
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()
|
||||||
c.dec()
|
c.dec()
|
||||||
// DONE
|
// DONE
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,Countdown类的两个内部属性`_counter`和`_action`,是实例的弱引用,所以如果删除实例,它们也就随之消失,不会造成内存泄漏。
|
上面代码中,`Countdown`类的两个内部属性`_counter`和`_action`,是实例的弱引用,所以如果删除实例,它们也就随之消失,不会造成内存泄漏。
|
||||||
|
@ -43,7 +43,7 @@ v + w = 〈v1, …, vn〉+ 〈w1, …, wn〉
|
|||||||
|
|
||||||
上面代码中,`v`和`w`是两个多元矢量。它们的加运算,在 SIMD 下是一个指令、而不是 n 个指令完成的,这就大大提高了效率。这对于 3D 动画、图像处理、信号处理、数值处理、加密等运算是非常重要的。比如,Canvas 的`getImageData()`会将图像文件读成一个二进制数组,SIMD 就很适合对于这种数组的处理。
|
上面代码中,`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]
|
// 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()
|
### SIMD.%type%.mul(),SIMD.%type%.div(),SIMD.%type%.sqrt()
|
||||||
|
|
||||||
@ -713,4 +713,3 @@ function average(list) {
|
|||||||
```
|
```
|
||||||
|
|
||||||
上面代码先是每隔四位,将所有的值读入一个 SIMD,然后立刻累加。然后,得到累加值四个通道的总和,再除以`n`就可以了。
|
上面代码先是每隔四位,将所有的值读入一个 SIMD,然后立刻累加。然后,得到累加值四个通道的总和,再除以`n`就可以了。
|
||||||
|
|
||||||
|
54
docs/spec.md
54
docs/spec.md
@ -40,19 +40,19 @@ ECMAScript 6规格的26章之中,第1章到第3章是对文件本身的介绍
|
|||||||
|
|
||||||
> 1. ReturnIfAbrupt(x).
|
> 1. ReturnIfAbrupt(x).
|
||||||
> 1. ReturnIfAbrupt(y).
|
> 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`.
|
> 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 `null` and `y` is `undefined`, return `true`.
|
||||||
> 1. If `x` is `undefined` and `y` is `null`, 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)`.
|
> 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`.
|
> 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(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(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)`.
|
> 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`.
|
> return the result of the comparison `ToPrimitive(x) == y`.
|
||||||
> 1. Return `false`.
|
> 1. Return `false`.
|
||||||
|
|
||||||
@ -145,17 +145,17 @@ a2.map(n => 1) // [, , ,]
|
|||||||
> 1. Let `A` be `ArraySpeciesCreate(O, len)`.
|
> 1. Let `A` be `ArraySpeciesCreate(O, len)`.
|
||||||
> 1. `ReturnIfAbrupt(A)`.
|
> 1. `ReturnIfAbrupt(A)`.
|
||||||
> 1. Let `k` be 0.
|
> 1. Let `k` be 0.
|
||||||
> 1. Repeat, while `k` < `len`
|
> 1. Repeat, while `k` < `len`\
|
||||||
> a. Let `Pk` be `ToString(k)`.
|
> a. Let `Pk` be `ToString(k)`.\
|
||||||
> b. Let `kPresent` be `HasProperty(O, Pk)`.
|
> b. Let `kPresent` be `HasProperty(O, Pk)`.\
|
||||||
> c. `ReturnIfAbrupt(kPresent)`.
|
> c. `ReturnIfAbrupt(kPresent)`.\
|
||||||
> d. If `kPresent` is `true`, then
|
> d. If `kPresent` is `true`, then\
|
||||||
> d-1. Let `kValue` be `Get(O, Pk)`.
|
> d-1. Let `kValue` be `Get(O, Pk)`.\
|
||||||
> d-2. `ReturnIfAbrupt(kValue)`.
|
> d-2. `ReturnIfAbrupt(kValue)`.\
|
||||||
> d-3. Let `mappedValue` be `Call(callbackfn, T, «kValue, k, O»)`.
|
> d-3. Let `mappedValue` be `Call(callbackfn, T, «kValue, k, O»)`.\
|
||||||
> d-4. `ReturnIfAbrupt(mappedValue)`.
|
> d-4. `ReturnIfAbrupt(mappedValue)`.\
|
||||||
> d-5. Let `status` be `CreateDataPropertyOrThrow (A, Pk, mappedValue)`.
|
> d-5. Let `status` be `CreateDataPropertyOrThrow (A, Pk, mappedValue)`.\
|
||||||
> d-6. `ReturnIfAbrupt(status)`.
|
> d-6. `ReturnIfAbrupt(status)`.\
|
||||||
> e. Increase `k` by 1.
|
> e. Increase `k` by 1.
|
||||||
> 1. Return `A`.
|
> 1. Return `A`.
|
||||||
|
|
||||||
@ -170,17 +170,17 @@ a2.map(n => 1) // [, , ,]
|
|||||||
> 1. 生成一个新的数组`A`,跟当前数组的`length`属性保持一致
|
> 1. 生成一个新的数组`A`,跟当前数组的`length`属性保持一致
|
||||||
> 1. 如果报错就返回
|
> 1. 如果报错就返回
|
||||||
> 1. 设定`k`等于 0
|
> 1. 设定`k`等于 0
|
||||||
> 1. 只要`k`小于当前数组的`length`属性,就重复下面步骤
|
> 1. 只要`k`小于当前数组的`length`属性,就重复下面步骤\
|
||||||
> a. 设定`Pk`等于`ToString(k)`,即将`K`转为字符串
|
> a. 设定`Pk`等于`ToString(k)`,即将`K`转为字符串\
|
||||||
> b. 设定`kPresent`等于`HasProperty(O, Pk)`,即求当前数组有没有指定属性
|
> b. 设定`kPresent`等于`HasProperty(O, Pk)`,即求当前数组有没有指定属性\
|
||||||
> c. 如果报错就返回
|
> c. 如果报错就返回\
|
||||||
> d. 如果`kPresent`等于`true`,则进行下面步骤
|
> d. 如果`kPresent`等于`true`,则进行下面步骤\
|
||||||
> d-1. 设定`kValue`等于`Get(O, Pk)`,取出当前数组的指定属性
|
> d-1. 设定`kValue`等于`Get(O, Pk)`,取出当前数组的指定属性\
|
||||||
> d-2. 如果报错就返回
|
> d-2. 如果报错就返回\
|
||||||
> d-3. 设定`mappedValue`等于`Call(callbackfn, T, «kValue, k, O»)`,即执行回调函数
|
> d-3. 设定`mappedValue`等于`Call(callbackfn, T, «kValue, k, O»)`,即执行回调函数\
|
||||||
> d-4. 如果报错就返回
|
> d-4. 如果报错就返回\
|
||||||
> d-5. 设定`status`等于`CreateDataPropertyOrThrow (A, Pk, mappedValue)`,即将回调函数的值放入`A`数组的指定位置
|
> d-5. 设定`status`等于`CreateDataPropertyOrThrow (A, Pk, mappedValue)`,即将回调函数的值放入`A`数组的指定位置\
|
||||||
> d-6. 如果报错就返回
|
> d-6. 如果报错就返回\
|
||||||
> e. `k`增加 1
|
> e. `k`增加 1
|
||||||
> 1. 返回`A`
|
> 1. 返回`A`
|
||||||
|
|
||||||
|
119
docs/string.md
119
docs/string.md
@ -4,14 +4,14 @@ ES6加强了对Unicode的支持,并且扩展了字符串对象。
|
|||||||
|
|
||||||
## 字符的 Unicode 表示法
|
## 字符的 Unicode 表示法
|
||||||
|
|
||||||
JavaScript允许采用`\uxxxx`形式表示一个字符,其中“xxxx”表示字符的码点。
|
JavaScript 允许采用`\uxxxx`形式表示一个字符,其中`xxxx`表示字符的 Unicode 码点。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
"\u0061"
|
"\u0061"
|
||||||
// "a"
|
// "a"
|
||||||
```
|
```
|
||||||
|
|
||||||
但是,这种表示法只限于`\u0000`——`\uFFFF`之间的字符。超出这个范围的字符,必须用两个双字节的形式表达。
|
但是,这种表示法只限于码点在`\u0000`~`\uFFFF`之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
"\uD842\uDFB7"
|
"\uD842\uDFB7"
|
||||||
@ -65,12 +65,12 @@ s.charCodeAt(0) // 55362
|
|||||||
s.charCodeAt(1) // 57271
|
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 个字节储存的字符,返回一个字符的码点。
|
ES6 提供了`codePointAt`方法,能够正确处理 4 个字节储存的字符,返回一个字符的码点。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var s = '𠮷a';
|
let s = '𠮷a';
|
||||||
|
|
||||||
s.codePointAt(0) // 134071
|
s.codePointAt(0) // 134071
|
||||||
s.codePointAt(1) // 57271
|
s.codePointAt(1) // 57271
|
||||||
@ -85,7 +85,7 @@ s.codePointAt(2) // 97
|
|||||||
`codePointAt`方法返回的是码点的十进制值,如果想要十六进制的值,可以使用`toString`方法转换一下。
|
`codePointAt`方法返回的是码点的十进制值,如果想要十六进制的值,可以使用`toString`方法转换一下。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var s = '𠮷a';
|
let s = '𠮷a';
|
||||||
|
|
||||||
s.codePointAt(0).toString(16) // "20bb7"
|
s.codePointAt(0).toString(16) // "20bb7"
|
||||||
s.codePointAt(2).toString(16) // "61"
|
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 字符。
|
你可能注意到了,`codePointAt`方法的参数,仍然是不正确的。比如,上面代码中,字符`a`在字符串`s`的正确位置序号应该是 1,但是必须向`codePointAt`方法传入 2。解决这个问题的一个办法是使用`for...of`循环,因为它会正确识别 32 位的 UTF-16 字符。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var s = '𠮷a';
|
let s = '𠮷a';
|
||||||
for (let ch of s) {
|
for (let ch of s) {
|
||||||
console.log(ch.codePointAt(0).toString(16));
|
console.log(ch.codePointAt(0).toString(16));
|
||||||
}
|
}
|
||||||
@ -124,7 +124,7 @@ String.fromCharCode(0x20BB7)
|
|||||||
|
|
||||||
上面代码中,`String.fromCharCode`不能识别大于`0xFFFF`的码点,所以`0x20BB7`就发生了溢出,最高位`2`被舍弃了,最后返回码点`U+0BB7`对应的字符,而不是码点`U+20BB7`对应的字符。
|
上面代码中,`String.fromCharCode`不能识别大于`0xFFFF`的码点,所以`0x20BB7`就发生了溢出,最高位`2`被舍弃了,最后返回码点`U+0BB7`对应的字符,而不是码点`U+20BB7`对应的字符。
|
||||||
|
|
||||||
ES6提供了`String.fromCodePoint`方法,可以识别`0xFFFF`的字符,弥补了`String.fromCharCode`方法的不足。在作用上,正好与`codePointAt`方法相反。
|
ES6 提供了`String.fromCodePoint`方法,可以识别大于`0xFFFF`的字符,弥补了`String.fromCharCode`方法的不足。在作用上,正好与`codePointAt`方法相反。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
String.fromCodePoint(0x20BB7)
|
String.fromCodePoint(0x20BB7)
|
||||||
@ -153,7 +153,7 @@ for (let codePoint of 'foo') {
|
|||||||
除了遍历字符串,这个遍历器最大的优点是可以识别大于`0xFFFF`的码点,传统的`for`循环无法识别这样的码点。
|
除了遍历字符串,这个遍历器最大的优点是可以识别大于`0xFFFF`的码点,传统的`for`循环无法识别这样的码点。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var text = String.fromCodePoint(0x20BB7);
|
let text = String.fromCodePoint(0x20BB7);
|
||||||
|
|
||||||
for (let i = 0; i < text.length; i++) {
|
for (let i = 0; i < text.length; i++) {
|
||||||
console.log(text[i]);
|
console.log(text[i]);
|
||||||
@ -232,11 +232,11 @@ ES6提供字符串实例的`normalize()`方法,用来将字符的不同表示
|
|||||||
传统上,JavaScript 只有`indexOf`方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。
|
传统上,JavaScript 只有`indexOf`方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。
|
||||||
|
|
||||||
- **includes()**:返回布尔值,表示是否找到了参数字符串。
|
- **includes()**:返回布尔值,表示是否找到了参数字符串。
|
||||||
- **startsWith()**:返回布尔值,表示参数字符串是否在源字符串的头部。
|
- **startsWith()**:返回布尔值,表示参数字符串是否在原字符串的头部。
|
||||||
- **endsWith()**:返回布尔值,表示参数字符串是否在源字符串的尾部。
|
- **endsWith()**:返回布尔值,表示参数字符串是否在原字符串的尾部。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var s = 'Hello world!';
|
let s = 'Hello world!';
|
||||||
|
|
||||||
s.startsWith('Hello') // true
|
s.startsWith('Hello') // true
|
||||||
s.endsWith('!') // true
|
s.endsWith('!') // true
|
||||||
@ -246,7 +246,7 @@ s.includes('o') // true
|
|||||||
这三个方法都支持第二个参数,表示开始搜索的位置。
|
这三个方法都支持第二个参数,表示开始搜索的位置。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var s = 'Hello world!';
|
let s = 'Hello world!';
|
||||||
|
|
||||||
s.startsWith('world', 6) // true
|
s.startsWith('world', 6) // true
|
||||||
s.endsWith('Hello', 5) // true
|
s.endsWith('Hello', 5) // true
|
||||||
@ -301,7 +301,7 @@ s.includes('Hello', 6) // false
|
|||||||
|
|
||||||
## padStart(),padEnd()
|
## padStart(),padEnd()
|
||||||
|
|
||||||
ES7推出了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。`padStart`用于头部补全,`padEnd`用于尾部补全。
|
ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。`padStart()`用于头部补全,`padEnd()`用于尾部补全。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
'x'.padStart(5, 'ab') // 'ababx'
|
'x'.padStart(5, 'ab') // 'ababx'
|
||||||
@ -327,7 +327,7 @@ ES7推出了字符串补全长度的功能。如果某个字符串不够指定
|
|||||||
// '0123456abc'
|
// '0123456abc'
|
||||||
```
|
```
|
||||||
|
|
||||||
如果省略第二个参数,则会用空格补全长度。
|
如果省略第二个参数,默认使用空格补全长度。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
'x'.padStart(4) // ' x'
|
'x'.padStart(4) // ' x'
|
||||||
@ -386,14 +386,14 @@ console.log(`string text line 1
|
|||||||
string text line 2`);
|
string text line 2`);
|
||||||
|
|
||||||
// 字符串中嵌入变量
|
// 字符串中嵌入变量
|
||||||
var name = "Bob", time = "today";
|
let name = "Bob", time = "today";
|
||||||
`Hello ${name}, how are you ${time}?`
|
`Hello ${name}, how are you ${time}?`
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中的模板字符串,都是用反引号表示。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。
|
上面代码中的模板字符串,都是用反引号表示。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var greeting = `\`Yo\` World!`;
|
let greeting = `\`Yo\` World!`;
|
||||||
```
|
```
|
||||||
|
|
||||||
如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。
|
如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。
|
||||||
@ -409,7 +409,6 @@ $('#list').html(`
|
|||||||
|
|
||||||
上面代码中,所有模板字符串的空格和换行,都是被保留的,比如`<ul>`标签前面会有一个换行。如果你不想要这个换行,可以使用`trim`方法消除它。
|
上面代码中,所有模板字符串的空格和换行,都是被保留的,比如`<ul>`标签前面会有一个换行。如果你不想要这个换行,可以使用`trim`方法消除它。
|
||||||
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
$('#list').html(`
|
$('#list').html(`
|
||||||
<ul>
|
<ul>
|
||||||
@ -439,8 +438,8 @@ function authorize(user, action) {
|
|||||||
大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。
|
大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var x = 1;
|
let x = 1;
|
||||||
var y = 2;
|
let y = 2;
|
||||||
|
|
||||||
`${x} + ${y} = ${x + y}`
|
`${x} + ${y} = ${x + y}`
|
||||||
// "1 + 2 = 3"
|
// "1 + 2 = 3"
|
||||||
@ -448,9 +447,9 @@ var y = 2;
|
|||||||
`${x} + ${y * 2} = ${x + y * 2}`
|
`${x} + ${y * 2} = ${x + y * 2}`
|
||||||
// "1 + 4 = 5"
|
// "1 + 4 = 5"
|
||||||
|
|
||||||
var obj = {x: 1, y: 2};
|
let obj = {x: 1, y: 2};
|
||||||
`${obj.x + obj.y}`
|
`${obj.x + obj.y}`
|
||||||
// 3
|
// "3"
|
||||||
```
|
```
|
||||||
|
|
||||||
模板字符串之中还能调用函数。
|
模板字符串之中还能调用函数。
|
||||||
@ -470,7 +469,7 @@ function fn() {
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 变量place没有声明
|
// 变量place没有声明
|
||||||
var msg = `Hello, ${place}`;
|
let msg = `Hello, ${place}`;
|
||||||
// 报错
|
// 报错
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -533,9 +532,9 @@ func('Jack') // "Hello Jack!"
|
|||||||
下面,我们来看一个通过模板字符串,生成正式模板的实例。
|
下面,我们来看一个通过模板字符串,生成正式模板的实例。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var template = `
|
let template = `
|
||||||
<ul>
|
<ul>
|
||||||
<% for(var i=0; i < data.supplies.length; i++) { %>
|
<% for(let i=0; i < data.supplies.length; i++) { %>
|
||||||
<li><%= data.supplies[i] %></li>
|
<li><%= data.supplies[i] %></li>
|
||||||
<% } %>
|
<% } %>
|
||||||
</ul>
|
</ul>
|
||||||
@ -550,7 +549,7 @@ var template = `
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
echo('<ul>');
|
echo('<ul>');
|
||||||
for(var i=0; i < data.supplies.length; i++) {
|
for(let i=0; i < data.supplies.length; i++) {
|
||||||
echo('<li>');
|
echo('<li>');
|
||||||
echo(data.supplies[i]);
|
echo(data.supplies[i]);
|
||||||
echo('</li>');
|
echo('</li>');
|
||||||
@ -561,8 +560,8 @@ echo('</ul>');
|
|||||||
这个转换使用正则表达式就行了。
|
这个转换使用正则表达式就行了。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var evalExpr = /<%=(.+?)%>/g;
|
let evalExpr = /<%=(.+?)%>/g;
|
||||||
var expr = /<%([\s\S]+?)%>/g;
|
let expr = /<%([\s\S]+?)%>/g;
|
||||||
|
|
||||||
template = template
|
template = template
|
||||||
.replace(evalExpr, '`); \n echo( $1 ); \n echo(`')
|
.replace(evalExpr, '`); \n echo( $1 ); \n echo(`')
|
||||||
@ -574,9 +573,9 @@ template = 'echo(`' + template + '`);';
|
|||||||
然后,将`template`封装在一个函数里面返回,就可以了。
|
然后,将`template`封装在一个函数里面返回,就可以了。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var script =
|
let script =
|
||||||
`(function parse(data){
|
`(function parse(data){
|
||||||
var output = "";
|
let output = "";
|
||||||
|
|
||||||
function echo(html){
|
function echo(html){
|
||||||
output += html;
|
output += html;
|
||||||
@ -594,8 +593,8 @@ return script;
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function compile(template){
|
function compile(template){
|
||||||
var evalExpr = /<%=(.+?)%>/g;
|
const evalExpr = /<%=(.+?)%>/g;
|
||||||
var expr = /<%([\s\S]+?)%>/g;
|
const expr = /<%([\s\S]+?)%>/g;
|
||||||
|
|
||||||
template = template
|
template = template
|
||||||
.replace(evalExpr, '`); \n echo( $1 ); \n echo(`')
|
.replace(evalExpr, '`); \n echo( $1 ); \n echo(`')
|
||||||
@ -603,9 +602,9 @@ function compile(template){
|
|||||||
|
|
||||||
template = 'echo(`' + template + '`);';
|
template = 'echo(`' + template + '`);';
|
||||||
|
|
||||||
var script =
|
let script =
|
||||||
`(function parse(data){
|
`(function parse(data){
|
||||||
var output = "";
|
let output = "";
|
||||||
|
|
||||||
function echo(html){
|
function echo(html){
|
||||||
output += html;
|
output += html;
|
||||||
@ -623,7 +622,7 @@ function compile(template){
|
|||||||
`compile`函数的用法如下。
|
`compile`函数的用法如下。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var parse = eval(compile(template));
|
let parse = eval(compile(template));
|
||||||
div.innerHTML = parse({ supplies: [ "broom", "mop", "cleaner" ] });
|
div.innerHTML = parse({ supplies: [ "broom", "mop", "cleaner" ] });
|
||||||
// <ul>
|
// <ul>
|
||||||
// <li>broom</li>
|
// <li>broom</li>
|
||||||
@ -647,8 +646,8 @@ alert(123)
|
|||||||
但是,如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。
|
但是,如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var a = 5;
|
let a = 5;
|
||||||
var b = 10;
|
let b = 10;
|
||||||
|
|
||||||
tag`Hello ${ a + b } world ${ a * b }`;
|
tag`Hello ${ a + b } world ${ a * b }`;
|
||||||
// 等同于
|
// 等同于
|
||||||
@ -690,8 +689,8 @@ tag(['Hello ', ' world ', ''], 15, 50)
|
|||||||
我们可以按照需要编写`tag`函数的代码。下面是`tag`函数的一种写法,以及运行结果。
|
我们可以按照需要编写`tag`函数的代码。下面是`tag`函数的一种写法,以及运行结果。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var a = 5;
|
let a = 5;
|
||||||
var b = 10;
|
let b = 10;
|
||||||
|
|
||||||
function tag(s, v1, v2) {
|
function tag(s, v1, v2) {
|
||||||
console.log(s[0]);
|
console.log(s[0]);
|
||||||
@ -715,12 +714,12 @@ tag`Hello ${ a + b } world ${ a * b}`;
|
|||||||
下面是一个更复杂的例子。
|
下面是一个更复杂的例子。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var total = 30;
|
let total = 30;
|
||||||
var msg = passthru`The total is ${total} (${total*1.05} with tax)`;
|
let msg = passthru`The total is ${total} (${total*1.05} with tax)`;
|
||||||
|
|
||||||
function passthru(literals) {
|
function passthru(literals) {
|
||||||
var result = '';
|
let result = '';
|
||||||
var i = 0;
|
let i = 0;
|
||||||
|
|
||||||
while (i < literals.length) {
|
while (i < literals.length) {
|
||||||
result += literals[i++];
|
result += literals[i++];
|
||||||
@ -741,8 +740,9 @@ msg // "The total is 30 (31.5 with tax)"
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function passthru(literals, ...values) {
|
function passthru(literals, ...values) {
|
||||||
var output = "";
|
let output = "";
|
||||||
for (var index = 0; index < values.length; index++) {
|
let index;
|
||||||
|
for (index = 0; index < values.length; index++) {
|
||||||
output += literals[index] + values[index];
|
output += literals[index] + values[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -754,13 +754,13 @@ function passthru(literals, ...values) {
|
|||||||
“标签模板”的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容。
|
“标签模板”的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var message =
|
let message =
|
||||||
SaferHTML`<p>${sender} has sent you a message.</p>`;
|
SaferHTML`<p>${sender} has sent you a message.</p>`;
|
||||||
|
|
||||||
function SaferHTML(templateData) {
|
function SaferHTML(templateData) {
|
||||||
var s = templateData[0];
|
let s = templateData[0];
|
||||||
for (var i = 1; i < arguments.length; i++) {
|
for (let i = 1; i < arguments.length; i++) {
|
||||||
var arg = String(arguments[i]);
|
let arg = String(arguments[i]);
|
||||||
|
|
||||||
// Escape special characters in the substitution.
|
// Escape special characters in the substitution.
|
||||||
s += arg.replace(/&/g, "&")
|
s += arg.replace(/&/g, "&")
|
||||||
@ -777,14 +777,13 @@ function SaferHTML(templateData) {
|
|||||||
上面代码中,`sender`变量往往是用户提供的,经过`SaferHTML`函数处理,里面的特殊字符都会被转义。
|
上面代码中,`sender`变量往往是用户提供的,经过`SaferHTML`函数处理,里面的特殊字符都会被转义。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var sender = '<script>alert("abc")</script>'; // 恶意代码
|
let sender = '<script>alert("abc")</script>'; // 恶意代码
|
||||||
var message = SaferHTML`<p>${sender} has sent you a message.</p>`;
|
let message = SaferHTML`<p>${sender} has sent you a message.</p>`;
|
||||||
|
|
||||||
message
|
message
|
||||||
// <p><script>alert("abc")</script> has sent you a message.</p>
|
// <p><script>alert("abc")</script> has sent you a message.</p>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
标签模板的另一个应用,就是多语言转换(国际化处理)。
|
标签模板的另一个应用,就是多语言转换(国际化处理)。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
@ -797,7 +796,7 @@ i18n`Welcome to ${siteName}, you are visitor number ${visitorNumber}!`
|
|||||||
```javascript
|
```javascript
|
||||||
// 下面的hashTemplate函数
|
// 下面的hashTemplate函数
|
||||||
// 是一个自定义的模板处理函数
|
// 是一个自定义的模板处理函数
|
||||||
var libraryHtml = hashTemplate`
|
let libraryHtml = hashTemplate`
|
||||||
<ul>
|
<ul>
|
||||||
#for book in ${myBooks}
|
#for book in ${myBooks}
|
||||||
<li><i>#{book.title}</i> by #{book.author}</li>
|
<li><i>#{book.title}</i> by #{book.author}</li>
|
||||||
@ -851,7 +850,8 @@ tag`First line\nSecond line`
|
|||||||
|
|
||||||
function tag(strings) {
|
function tag(strings) {
|
||||||
console.log(strings.raw[0]);
|
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
|
```javascript
|
||||||
String.raw = function (strings, ...values) {
|
String.raw = function (strings, ...values) {
|
||||||
var output = "";
|
let output = "";
|
||||||
for (var index = 0; index < values.length; index++) {
|
let index;
|
||||||
|
for (index = 0; index < values.length; index++) {
|
||||||
output += strings.raw[index] + values[index];
|
output += strings.raw[index] + values[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -906,9 +907,9 @@ String.raw({ raw: ['t','e','s','t'] }, 0, 1, 2);
|
|||||||
|
|
||||||
## 模板字符串的限制
|
## 模板字符串的限制
|
||||||
|
|
||||||
前面提到标签模板里面,可以内嵌其他语言。但是,模板字符串默认会将字符串转义,因此导致了无法嵌入其他语言。
|
前面提到标签模板里面,可以内嵌其他语言。但是,模板字符串默认会将字符串转义,导致无法嵌入其他语言。
|
||||||
|
|
||||||
举例来说,在标签模板里面可以嵌入Latex语言。
|
举例来说,标签模板里面可以嵌入 LaTEX 语言。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function latex(strings) {
|
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`属性上面可以得到原始字符串。
|
为了解决这个问题,现在有一个[提案](https://tc39.github.io/proposal-template-literal-revision/),放松对标签模板里面的字符串转义的限制。如果遇到不合法的字符串转义,就返回`undefined`,而不是报错,并且从`raw`属性上面可以得到原始字符串。
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
本章探讨如何将 ES6 的新语法,运用到编码实践之中,与传统的 JavaScript 语法结合在一起,写出合理的、易于阅读和维护的代码。
|
本章探讨如何将 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`,尤其是在全局环境,不应该设置变量,只应设置常量。
|
在`let`和`const`之间,建议优先使用`const`,尤其是在全局环境,不应该设置变量,只应设置常量。
|
||||||
|
|
||||||
`const`优于`let`有几个原因。一个是`const`可以提醒阅读程序的人,这个变量不应该改变;另一个是`const`比较符合函数式编程思想,运算不改变值,只是新建值,而且这样也有利于将来的分布式运算;最后一个原因是 JavaScript 编译器会对`const`进行优化,所以多使用`const`,有利于提供程序的运行效率,也就是说`let`和`const`的本质区别,其实是编译器内部的处理不同。
|
`const`优于`let`有几个原因。一个是`const`可以提醒阅读程序的人,这个变量不应该改变;另一个是`const`比较符合函数式编程思想,运算不改变值,只是新建值,而且这样也有利于将来的分布式运算;最后一个原因是 JavaScript 编译器会对`const`进行优化,所以多使用`const`,有利于提高程序的运行效率,也就是说`let`和`const`的本质区别,其实是编译器内部的处理不同。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// bad
|
// 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的写法
|
// ES6的写法
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const Breadcrumbs = React.createClass({
|
class Breadcrumbs extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return <nav />;
|
return <nav />;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
export default Breadcrumbs
|
export default Breadcrumbs;
|
||||||
```
|
```
|
||||||
|
|
||||||
如果模块只有一个输出值,就使用`export default`,如果模块有多个输出值,就不使用`export default`,不要`export default`与普通的`export`同时使用。
|
如果模块只有一个输出值,就使用`export default`,如果模块有多个输出值,就不使用`export default`,`export default`与普通的`export`不要同时使用。
|
||||||
|
|
||||||
不要在模块输入中使用通配符。因为这样可以确保你的模块之中,有一个默认输出(export default)。
|
不要在模块输入中使用通配符。因为这样可以确保你的模块之中,有一个默认输出(export default)。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// bad
|
// bad
|
||||||
import * as myObject './importModule';
|
import * as myObject from './importModule';
|
||||||
|
|
||||||
// good
|
// good
|
||||||
import myObject from './importModule';
|
import myObject from './importModule';
|
||||||
@ -478,10 +478,11 @@ ESLint是一个语法规则和代码风格的检查工具,可以用来保证
|
|||||||
$ npm i -g eslint
|
$ npm i -g eslint
|
||||||
```
|
```
|
||||||
|
|
||||||
然后,安装Airbnb语法规则。
|
然后,安装 Airbnb 语法规则,以及 import、a11y、react 插件。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ npm i -g eslint-config-airbnb
|
$ npm i -g eslint-config-airbnb
|
||||||
|
$ npm i -g eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react
|
||||||
```
|
```
|
||||||
|
|
||||||
最后,在项目的根目录下新建一个`.eslintrc`文件,配置 ESLint。
|
最后,在项目的根目录下新建一个`.eslintrc`文件,配置 ESLint。
|
||||||
@ -507,16 +508,18 @@ function greet() {
|
|||||||
greet();
|
greet();
|
||||||
```
|
```
|
||||||
|
|
||||||
使用ESLint检查这个文件。
|
使用 ESLint 检查这个文件,就会报出错误。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ eslint index.js
|
$ eslint index.js
|
||||||
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
|
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 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
|
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 个空格。
|
||||||
|
200
docs/symbol.md
200
docs/symbol.md
@ -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 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
|
Symbol 值通过`Symbol`函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
|
||||||
|
|
||||||
@ -22,8 +22,8 @@ typeof s
|
|||||||
`Symbol`函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
|
`Symbol`函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var s1 = Symbol('foo');
|
let s1 = Symbol('foo');
|
||||||
var s2 = Symbol('bar');
|
let s2 = Symbol('bar');
|
||||||
|
|
||||||
s1 // Symbol(foo)
|
s1 // Symbol(foo)
|
||||||
s2 // Symbol(bar)
|
s2 // Symbol(bar)
|
||||||
@ -50,14 +50,14 @@ sym // Symbol(abc)
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 没有参数的情况
|
// 没有参数的情况
|
||||||
var s1 = Symbol();
|
let s1 = Symbol();
|
||||||
var s2 = Symbol();
|
let s2 = Symbol();
|
||||||
|
|
||||||
s1 === s2 // false
|
s1 === s2 // false
|
||||||
|
|
||||||
// 有参数的情况
|
// 有参数的情况
|
||||||
var s1 = Symbol('foo');
|
let s1 = Symbol('foo');
|
||||||
var s2 = Symbol('foo');
|
let s2 = Symbol('foo');
|
||||||
|
|
||||||
s1 === s2 // false
|
s1 === s2 // false
|
||||||
```
|
```
|
||||||
@ -67,7 +67,7 @@ s1 === s2 // false
|
|||||||
Symbol 值不能与其他类型的值进行运算,会报错。
|
Symbol 值不能与其他类型的值进行运算,会报错。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var sym = Symbol('My symbol');
|
let sym = Symbol('My symbol');
|
||||||
|
|
||||||
"your symbol is " + sym
|
"your symbol is " + sym
|
||||||
// TypeError: can't convert symbol to string
|
// TypeError: can't convert symbol to string
|
||||||
@ -78,7 +78,7 @@ var sym = Symbol('My symbol');
|
|||||||
但是,Symbol 值可以显式转为字符串。
|
但是,Symbol 值可以显式转为字符串。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var sym = Symbol('My symbol');
|
let sym = Symbol('My symbol');
|
||||||
|
|
||||||
String(sym) // 'Symbol(My symbol)'
|
String(sym) // 'Symbol(My symbol)'
|
||||||
sym.toString() // 'Symbol(My symbol)'
|
sym.toString() // 'Symbol(My symbol)'
|
||||||
@ -87,7 +87,7 @@ sym.toString() // 'Symbol(My symbol)'
|
|||||||
另外,Symbol 值也可以转为布尔值,但是不能转为数值。
|
另外,Symbol 值也可以转为布尔值,但是不能转为数值。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var sym = Symbol();
|
let sym = Symbol();
|
||||||
Boolean(sym) // true
|
Boolean(sym) // true
|
||||||
!sym // false
|
!sym // false
|
||||||
|
|
||||||
@ -104,19 +104,19 @@ sym + 2 // TypeError
|
|||||||
由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。
|
由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var mySymbol = Symbol();
|
let mySymbol = Symbol();
|
||||||
|
|
||||||
// 第一种写法
|
// 第一种写法
|
||||||
var a = {};
|
let a = {};
|
||||||
a[mySymbol] = 'Hello!';
|
a[mySymbol] = 'Hello!';
|
||||||
|
|
||||||
// 第二种写法
|
// 第二种写法
|
||||||
var a = {
|
let a = {
|
||||||
[mySymbol]: 'Hello!'
|
[mySymbol]: 'Hello!'
|
||||||
};
|
};
|
||||||
|
|
||||||
// 第三种写法
|
// 第三种写法
|
||||||
var a = {};
|
let a = {};
|
||||||
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
|
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
|
||||||
|
|
||||||
// 以上写法都得到同样结果
|
// 以上写法都得到同样结果
|
||||||
@ -128,8 +128,8 @@ a[mySymbol] // "Hello!"
|
|||||||
注意,Symbol 值作为对象属性名时,不能用点运算符。
|
注意,Symbol 值作为对象属性名时,不能用点运算符。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var mySymbol = Symbol();
|
const mySymbol = Symbol();
|
||||||
var a = {};
|
const a = {};
|
||||||
|
|
||||||
a.mySymbol = 'Hello!';
|
a.mySymbol = 'Hello!';
|
||||||
a[mySymbol] // undefined
|
a[mySymbol] // undefined
|
||||||
@ -196,11 +196,11 @@ function getComplement(color) {
|
|||||||
|
|
||||||
## 实例:消除魔术字符串
|
## 实例:消除魔术字符串
|
||||||
|
|
||||||
魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,该由含义清晰的变量代替。
|
魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function getArea(shape, options) {
|
function getArea(shape, options) {
|
||||||
var area = 0;
|
let area = 0;
|
||||||
|
|
||||||
switch (shape) {
|
switch (shape) {
|
||||||
case 'Triangle': // 魔术字符串
|
case 'Triangle': // 魔术字符串
|
||||||
@ -215,17 +215,17 @@ function getArea(shape, options) {
|
|||||||
getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串
|
getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,字符串“Triangle”就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。
|
上面代码中,字符串`Triangle`就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。
|
||||||
|
|
||||||
常用的消除魔术字符串的方法,就是把它写成一个变量。
|
常用的消除魔术字符串的方法,就是把它写成一个变量。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var shapeType = {
|
const shapeType = {
|
||||||
triangle: 'Triangle'
|
triangle: 'Triangle'
|
||||||
};
|
};
|
||||||
|
|
||||||
function getArea(shape, options) {
|
function getArea(shape, options) {
|
||||||
var area = 0;
|
let area = 0;
|
||||||
switch (shape) {
|
switch (shape) {
|
||||||
case shapeType.triangle:
|
case shapeType.triangle:
|
||||||
area = .5 * options.width * options.height;
|
area = .5 * options.width * options.height;
|
||||||
@ -237,7 +237,7 @@ function getArea(shape, options) {
|
|||||||
getArea(shapeType.triangle, { width: 100, height: 100 });
|
getArea(shapeType.triangle, { width: 100, height: 100 });
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码中,我们把“Triangle”写成`shapeType`对象的`triangle`属性,这样就消除了强耦合。
|
上面代码中,我们把`Triangle`写成`shapeType`对象的`triangle`属性,这样就消除了强耦合。
|
||||||
|
|
||||||
如果仔细分析,可以发现`shapeType.triangle`等于哪个值并不重要,只要确保不会跟其他`shapeType`属性的值冲突即可。因此,这里就很适合改用 Symbol 值。
|
如果仔细分析,可以发现`shapeType.triangle`等于哪个值并不重要,只要确保不会跟其他`shapeType`属性的值冲突即可。因此,这里就很适合改用 Symbol 值。
|
||||||
|
|
||||||
@ -256,14 +256,14 @@ Symbol 作为属性名,该属性不会出现在`for...in`、`for...of`循环
|
|||||||
`Object.getOwnPropertySymbols`方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
|
`Object.getOwnPropertySymbols`方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var obj = {};
|
const obj = {};
|
||||||
var a = Symbol('a');
|
let a = Symbol('a');
|
||||||
var b = Symbol('b');
|
let b = Symbol('b');
|
||||||
|
|
||||||
obj[a] = 'Hello';
|
obj[a] = 'Hello';
|
||||||
obj[b] = 'World';
|
obj[b] = 'World';
|
||||||
|
|
||||||
var objectSymbols = Object.getOwnPropertySymbols(obj);
|
const objectSymbols = Object.getOwnPropertySymbols(obj);
|
||||||
|
|
||||||
objectSymbols
|
objectSymbols
|
||||||
// [Symbol(a), Symbol(b)]
|
// [Symbol(a), Symbol(b)]
|
||||||
@ -272,15 +272,15 @@ objectSymbols
|
|||||||
下面是另一个例子,`Object.getOwnPropertySymbols`方法与`for...in`循环、`Object.getOwnPropertyNames`方法进行对比的例子。
|
下面是另一个例子,`Object.getOwnPropertySymbols`方法与`for...in`循环、`Object.getOwnPropertyNames`方法进行对比的例子。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var obj = {};
|
const obj = {};
|
||||||
|
|
||||||
var foo = Symbol("foo");
|
let foo = Symbol("foo");
|
||||||
|
|
||||||
Object.defineProperty(obj, foo, {
|
Object.defineProperty(obj, foo, {
|
||||||
value: "foobar",
|
value: "foobar",
|
||||||
});
|
});
|
||||||
|
|
||||||
for (var i in obj) {
|
for (let i in obj) {
|
||||||
console.log(i); // 无输出
|
console.log(i); // 无输出
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,13 +303,13 @@ let obj = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Reflect.ownKeys(obj)
|
Reflect.ownKeys(obj)
|
||||||
// [Symbol(my_key), 'enum', 'nonEnum']
|
// ["enum", "nonEnum", Symbol(my_key)]
|
||||||
```
|
```
|
||||||
|
|
||||||
由于以 Symbol 值作为名称的属性,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。
|
由于以 Symbol 值作为名称的属性,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var size = Symbol('size');
|
let size = Symbol('size');
|
||||||
|
|
||||||
class Collection {
|
class Collection {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -326,7 +326,7 @@ class Collection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var x = new Collection();
|
let x = new Collection();
|
||||||
Collection.sizeOf(x) // 0
|
Collection.sizeOf(x) // 0
|
||||||
|
|
||||||
x.add('foo');
|
x.add('foo');
|
||||||
@ -344,8 +344,8 @@ Object.getOwnPropertySymbols(x) // [Symbol(size)]
|
|||||||
有时,我们希望重新使用同一个 Symbol 值,`Symbol.for`方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。
|
有时,我们希望重新使用同一个 Symbol 值,`Symbol.for`方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var s1 = Symbol.for('foo');
|
let s1 = Symbol.for('foo');
|
||||||
var s2 = Symbol.for('foo');
|
let s2 = Symbol.for('foo');
|
||||||
|
|
||||||
s1 === s2 // true
|
s1 === s2 // true
|
||||||
```
|
```
|
||||||
@ -367,10 +367,10 @@ Symbol("bar") === Symbol("bar")
|
|||||||
`Symbol.keyFor`方法返回一个已登记的 Symbol 类型值的`key`。
|
`Symbol.keyFor`方法返回一个已登记的 Symbol 类型值的`key`。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var s1 = Symbol.for("foo");
|
let s1 = Symbol.for("foo");
|
||||||
Symbol.keyFor(s1) // "foo"
|
Symbol.keyFor(s1) // "foo"
|
||||||
|
|
||||||
var s2 = Symbol("foo");
|
let s2 = Symbol("foo");
|
||||||
Symbol.keyFor(s2) // undefined
|
Symbol.keyFor(s2) // undefined
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -413,7 +413,7 @@ module.exports = global._foo;
|
|||||||
然后,加载上面的`mod.js`。
|
然后,加载上面的`mod.js`。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var a = require('./mod.js');
|
const a = require('./mod.js');
|
||||||
console.log(a.foo);
|
console.log(a.foo);
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -422,7 +422,7 @@ console.log(a.foo);
|
|||||||
但是,这里有一个问题,全局变量`global._foo`是可写的,任何文件都可以修改。
|
但是,这里有一个问题,全局变量`global._foo`是可写的,任何文件都可以修改。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var a = require('./mod.js');
|
const a = require('./mod.js');
|
||||||
global._foo = 123;
|
global._foo = 123;
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -448,7 +448,7 @@ module.exports = global[FOO_KEY];
|
|||||||
上面代码中,可以保证`global[FOO_KEY]`不会被无意间覆盖,但还是可以被改写。
|
上面代码中,可以保证`global[FOO_KEY]`不会被无意间覆盖,但还是可以被改写。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var a = require('./mod.js');
|
const a = require('./mod.js');
|
||||||
global[Symbol.for('foo')] = 123;
|
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
|
1 instanceof Even // false
|
||||||
2 instanceof Even // true
|
2 instanceof Even // true
|
||||||
12345 instanceof Even // false
|
12345 instanceof Even // false
|
||||||
@ -499,7 +506,7 @@ class Even {
|
|||||||
|
|
||||||
### Symbol.isConcatSpreadable
|
### Symbol.isConcatSpreadable
|
||||||
|
|
||||||
对象的`Symbol.isConcatSpreadable`属性等于一个布尔值,表示该对象使用`Array.prototype.concat()`时,是否可以展开。
|
对象的`Symbol.isConcatSpreadable`属性等于一个布尔值,表示该对象用于`Array.prototype.concat()`时,是否可以展开。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
let arr1 = ['c', 'd'];
|
let arr1 = ['c', 'd'];
|
||||||
@ -511,9 +518,9 @@ arr2[Symbol.isConcatSpreadable] = false;
|
|||||||
['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']
|
['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
|
```javascript
|
||||||
let obj = {length: 2, 0: 'c', 1: 'd'};
|
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']
|
['a', 'b'].concat(obj, 'e') // ['a', 'b', 'c', 'd', 'e']
|
||||||
```
|
```
|
||||||
|
|
||||||
对于一个类来说,`Symbol.isConcatSpreadable`属性必须写成实例的属性。
|
`Symbol.isConcatSpreadable`属性也可以定义在类里面。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
class A1 extends Array {
|
class A1 extends Array {
|
||||||
@ -535,7 +542,9 @@ class A1 extends Array {
|
|||||||
class A2 extends Array {
|
class A2 extends Array {
|
||||||
constructor(args) {
|
constructor(args) {
|
||||||
super(args);
|
super(args);
|
||||||
this[Symbol.isConcatSpreadable] = false;
|
}
|
||||||
|
get [Symbol.isConcatSpreadable] () {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let a1 = new A1();
|
let a1 = new A1();
|
||||||
@ -550,11 +559,31 @@ a2[1] = 6;
|
|||||||
|
|
||||||
上面代码中,类`A1`是可展开的,类`A2`是不可展开的,所以使用`concat`时有不一样的结果。
|
上面代码中,类`A1`是可展开的,类`A2`是不可展开的,所以使用`concat`时有不一样的结果。
|
||||||
|
|
||||||
|
注意,`Symbol.isConcatSpreadable`的位置差异,`A1`是定义在实例上,`A2`是定义在类本身,效果相同。
|
||||||
|
|
||||||
### Symbol.species
|
### 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
|
```javascript
|
||||||
static get [Symbol.species]() {
|
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
|
||||||
|
|
||||||
对象的`Symbol.match`属性,指向一个函数。当执行`str.match(myObject)`时,如果该属性存在,会调用它,返回该方法的返回值。
|
对象的`Symbol.match`属性,指向一个函数。当执行`str.match(myObject)`时,如果该属性存在,会调用它,返回该方法的返回值。
|
||||||
@ -631,12 +694,43 @@ String.prototype.split(separator, limit)
|
|||||||
separator[Symbol.split](this, 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
|
||||||
|
|
||||||
对象的`Symbol.iterator`属性,指向该对象的默认遍历器方法。
|
对象的`Symbol.iterator`属性,指向该对象的默认遍历器方法。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
var myIterable = {};
|
const myIterable = {};
|
||||||
myIterable[Symbol.iterator] = function* () {
|
myIterable[Symbol.iterator] = function* () {
|
||||||
yield 1;
|
yield 1;
|
||||||
yield 2;
|
yield 2;
|
||||||
@ -717,7 +811,7 @@ class Collection {
|
|||||||
return 'xxx';
|
return 'xxx';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var x = new Collection();
|
let x = new Collection();
|
||||||
Object.prototype.toString.call(x) // "[object xxx]"
|
Object.prototype.toString.call(x) // "[object xxx]"
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -752,15 +846,16 @@ Array.prototype[Symbol.unscopables]
|
|||||||
// entries: true,
|
// entries: true,
|
||||||
// fill: true,
|
// fill: true,
|
||||||
// find: true,
|
// find: true,
|
||||||
// findIndex: true,
|
// findIndex: true,
|
||||||
|
// includes: true,
|
||||||
// keys: true
|
// keys: true
|
||||||
// }
|
// }
|
||||||
|
|
||||||
Object.keys(Array.prototype[Symbol.unscopables])
|
Object.keys(Array.prototype[Symbol.unscopables])
|
||||||
// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'keys']
|
// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'includes', 'keys']
|
||||||
```
|
```
|
||||||
|
|
||||||
上面代码说明,数组有6个属性,会被with命令排除。
|
上面代码说明,数组有 7 个属性,会被`with`命令排除。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 没有 unscopables 时
|
// 没有 unscopables 时
|
||||||
@ -789,3 +884,4 @@ with (MyClass.prototype) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
上面代码通过指定`Symbol.unscopables`属性,使得`with`语法块不会在当前作用域寻找`foo`属性,即`foo`将指向外层作用域的变量。
|
||||||
|
BIN
images/cover-3rd.jpg
Normal file
BIN
images/cover-3rd.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 476 KiB |
BIN
images/cover_thumbnail_3rd.jpg
Normal file
BIN
images/cover_thumbnail_3rd.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<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">
|
<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="app/bower_components/normalize-css/normalize.css">
|
||||||
<link rel="stylesheet" href="css/app.css">
|
<link rel="stylesheet" href="css/app.css">
|
||||||
@ -23,7 +23,7 @@
|
|||||||
<div id="back_to_top">back to top</div>
|
<div id="back_to_top">back to top</div>
|
||||||
<div id="edit">edit</div>
|
<div id="edit">edit</div>
|
||||||
<div id="loading">Loading ...</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 id="flip"><div id="pageup">上一章</div><div id="pagedown">下一章</div></div>
|
||||||
<div class="progress-indicator-2"></div>
|
<div class="progress-indicator-2"></div>
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ function init_back_to_top_button() {
|
|||||||
|
|
||||||
function goTop(e) {
|
function goTop(e) {
|
||||||
if(e) e.preventDefault();
|
if(e) e.preventDefault();
|
||||||
$('html body').animate({
|
$('html, body').animate({
|
||||||
scrollTop: 0
|
scrollTop: 0
|
||||||
}, 200);
|
}, 200);
|
||||||
history.pushState(null, null, '#' + location.hash.split('#')[1]);
|
history.pushState(null, null, '#' + location.hash.split('#')[1]);
|
||||||
|
21
sidebar.md
21
sidebar.md
@ -12,23 +12,26 @@
|
|||||||
1. [字符串的扩展](#docs/string)
|
1. [字符串的扩展](#docs/string)
|
||||||
1. [正则的扩展](#docs/regex)
|
1. [正则的扩展](#docs/regex)
|
||||||
1. [数值的扩展](#docs/number)
|
1. [数值的扩展](#docs/number)
|
||||||
1. [数组的扩展](#docs/array)
|
|
||||||
1. [函数的扩展](#docs/function)
|
1. [函数的扩展](#docs/function)
|
||||||
|
1. [数组的扩展](#docs/array)
|
||||||
1. [对象的扩展](#docs/object)
|
1. [对象的扩展](#docs/object)
|
||||||
1. [Symbol](#docs/symbol)
|
1. [Symbol](#docs/symbol)
|
||||||
1. [Set 和 Map 数据结构](#docs/set-map)
|
1. [Set 和 Map 数据结构](#docs/set-map)
|
||||||
1. [Proxy和Reflect](#docs/proxy)
|
1. [Proxy](#docs/proxy)
|
||||||
1. [Iterator和for...of循环](#docs/iterator)
|
1. [Reflect](#docs/reflect)
|
||||||
1. [Generator函数](#docs/generator)
|
|
||||||
1. [Promise 对象](#docs/promise)
|
1. [Promise 对象](#docs/promise)
|
||||||
1. [异步操作和Async函数](#docs/async)
|
1. [Iterator 和 for...of 循环](#docs/iterator)
|
||||||
1. [Class](#docs/class)
|
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. [Decorator](#docs/decorator)
|
||||||
1. [Module](#docs/module)
|
1. [Module 的语法](#docs/module)
|
||||||
|
1. [Module 的加载实现](#docs/module-loader)
|
||||||
1. [编程风格](#docs/style)
|
1. [编程风格](#docs/style)
|
||||||
1. [读懂规格](#docs/spec)
|
1. [读懂规格](#docs/spec)
|
||||||
1. [二进制数组](#docs/arraybuffer)
|
1. [ArrayBuffer](#docs/arraybuffer)
|
||||||
1. [SIMD](#docs/simd)
|
|
||||||
1. [参考链接](#docs/reference)
|
1. [参考链接](#docs/reference)
|
||||||
|
|
||||||
## 其他
|
## 其他
|
||||||
|
Loading…
x
Reference in New Issue
Block a user