mirror of
https://github.com/ruanyf/es6tutorial.git
synced 2025-05-24 18:32:22 +00:00
Merge branch 'gh-pages' into gh-pages
This commit is contained in:
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/
|
||||
|
||||
|
||||
|
21
README.md
21
README.md
@ -1,21 +1,22 @@
|
||||
# ECMAScript 6入门
|
||||
# ECMAScript 6 入门
|
||||
|
||||
《ECMAScript 6入门》是一本开源的JavaScript语言教程,全面介绍ECMAScript 6新引入的语法特性。
|
||||
《ECMAScript 6 入门》是一本开源的 JavaScript 语言教程,全面介绍 ECMAScript 6 新引入的语法特性。
|
||||
|
||||
[](images/cover-2nd.jpg)
|
||||
[](images/cover-3rd.jpg)
|
||||
|
||||
本书覆盖 ES6/ES7 与 ES5 的所有不同之处,对涉及的语法知识给予详细介绍,并给出大量简洁易懂的示例代码。
|
||||
本书覆盖 ES6 与上一个版本 ES5 的所有不同之处,对涉及的语法知识给予详细介绍,并给出大量简洁易懂的示例代码。
|
||||
|
||||
本书为中级难度,适合已经掌握 ES5 的读者,用来了解这门语言的最新发展;也可当作参考手册,查寻新增的语法点。
|
||||
|
||||
全书已由电子工业出版社出版,目前是第二版,书名为《ES6 标准入门》。纸版是基于网站内容排版印刷的。
|
||||
全书已由电子工业出版社出版,2017年9月推出了第三版,书名为《ES6 标准入门》。纸版是基于网站内容排版印刷的。
|
||||
|
||||
感谢张春雨编辑支持我将全书开源的做法。如果您认可这本书,建议购买纸版。这样可以使出版社不因出版开源书籍而亏钱,进而鼓励更多的作者开源自己的书籍。
|
||||
感谢张春雨编辑支持我将全书开源的做法。如果您认可这本书,建议购买纸版。这样可以使出版社不因出版开源书籍而亏钱,进而鼓励更多的作者开源自己的书籍。下面是第三版的购买地址。
|
||||
|
||||
- [京东](http://item.jd.com/11849235.html)
|
||||
- [当当](http://product.dangdang.com/23840431.html)
|
||||
- [亚马逊](http://www.amazon.cn/ES6-%E6%A0%87%E5%87%86%E5%85%A5%E9%97%A8-%E9%98%AE%E4%B8%80%E5%B3%B0/dp/B01A18WWAG/)
|
||||
- [China-pub](http://product.china-pub.com/4904712)
|
||||
- [淘宝](https://s.taobao.com/search?q=ES6%E6%A0%87%E5%87%86%E5%85%A5%E9%97%A8+%E7%AC%AC3%E7%89%88)
|
||||
- [京东](https://search.jd.com/Search?keyword=ES6%E6%A0%87%E5%87%86%E5%85%A5%E9%97%A8%20%E7%AC%AC3%E7%89%88&enc=utf-8&wq=ES6%E6%A0%87%E5%87%86%E5%85%A5%E9%97%A8%20%E7%AC%AC3%E7%89%88)
|
||||
- [当当](http://product.dangdang.com/25156888.html)
|
||||
- [亚马逊](https://www.amazon.cn/ES6%E6%A0%87%E5%87%86%E5%85%A5%E9%97%A8-%E9%98%AE%E4%B8%80%E5%B3%B0/dp/B0755547ZZ)
|
||||
- [China-pub](http://product.china-pub.com/6504650)
|
||||
|
||||
### 版权许可
|
||||
|
||||
|
@ -120,6 +120,7 @@ input[type=search] {
|
||||
height: 18px;
|
||||
text-align: left;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input.searchButton {
|
||||
|
404
docs/array.md
404
docs/array.md
@ -1,8 +1,328 @@
|
||||
# 数组的扩展
|
||||
|
||||
## 扩展运算符
|
||||
|
||||
### 含义
|
||||
|
||||
扩展运算符(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-like object)和可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map)。
|
||||
`Array.from`方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
|
||||
|
||||
下面是一个类似数组的对象,`Array.from`将它转为真正的数组。
|
||||
|
||||
@ -21,13 +341,13 @@ var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
|
||||
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
|
||||
```
|
||||
|
||||
实际应用中,常见的类似数组的对象是DOM操作返回的NodeList集合,以及函数内部的`arguments`对象。`Array.from`都可以将它们转为真正的数组。
|
||||
实际应用中,常见的类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的`arguments`对象。`Array.from`都可以将它们转为真正的数组。
|
||||
|
||||
```javascript
|
||||
// NodeList对象
|
||||
let ps = document.querySelectorAll('p');
|
||||
Array.from(ps).forEach(function (p) {
|
||||
console.log(p);
|
||||
Array.from(ps).filter(p => {
|
||||
return p.textContent.length > 100;
|
||||
});
|
||||
|
||||
// arguments对象
|
||||
@ -37,9 +357,9 @@ function foo() {
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,`querySelectorAll`方法返回的是一个类似数组的对象,只有将这个对象转为真正的数组,才能使用`forEach`方法。
|
||||
上面代码中,`querySelectorAll`方法返回的是一个类似数组的对象,可以将这个对象转为真正的数组,再使用`forEach`方法。
|
||||
|
||||
只要是部署了Iterator接口的数据结构,`Array.from`都能将其转为数组。
|
||||
只要是部署了 Iterator 接口的数据结构,`Array.from`都能将其转为数组。
|
||||
|
||||
```javascript
|
||||
Array.from('hello')
|
||||
@ -49,7 +369,7 @@ let namesSet = new Set(['a', 'b'])
|
||||
Array.from(namesSet) // ['a', 'b']
|
||||
```
|
||||
|
||||
上面代码中,字符串和Set结构都具有Iterator接口,因此可以被`Array.from`转为真正的数组。
|
||||
上面代码中,字符串和 Set 结构都具有 Iterator 接口,因此可以被`Array.from`转为真正的数组。
|
||||
|
||||
如果参数是一个真正的数组,`Array.from`会返回一个一模一样的新数组。
|
||||
|
||||
@ -63,14 +383,14 @@ Array.from([1, 2, 3])
|
||||
```javascript
|
||||
// arguments对象
|
||||
function foo() {
|
||||
var args = [...arguments];
|
||||
const args = [...arguments];
|
||||
}
|
||||
|
||||
// NodeList对象
|
||||
[...document.querySelectorAll('div')]
|
||||
```
|
||||
|
||||
扩展运算符背后调用的是遍历器接口(`Symbol.iterator`),如果一个对象没有部署这个接口,就无法转换。`Array.from`方法则是还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有`length`属性。因此,任何有`length`属性的对象,都可以通过`Array.from`方法转为数组,而此时扩展运算符就无法转换。
|
||||
扩展运算符背后调用的是遍历器接口(`Symbol.iterator`),如果一个对象没有部署这个接口,就无法转换。`Array.from`方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有`length`属性。因此,任何有`length`属性的对象,都可以通过`Array.from`方法转为数组,而此时扩展运算符就无法转换。
|
||||
|
||||
```javascript
|
||||
Array.from({ length: 3 });
|
||||
@ -98,7 +418,7 @@ Array.from([1, 2, 3], (x) => x * x)
|
||||
// [1, 4, 9]
|
||||
```
|
||||
|
||||
下面的例子是取出一组DOM节点的文本内容。
|
||||
下面的例子是取出一组 DOM 节点的文本内容。
|
||||
|
||||
```javascript
|
||||
let spans = document.querySelectorAll('span.name');
|
||||
@ -138,7 +458,7 @@ Array.from({ length: 2 }, () => 'jack')
|
||||
|
||||
上面代码中,`Array.from`的第一个参数指定了第二个参数运行的次数。这种特性可以让该方法的用法变得非常灵活。
|
||||
|
||||
`Array.from()`的另一个应用是,将字符串转为数组,然后返回字符串的长度。因为它能正确处理各种Unicode字符,可以避免JavaScript将大于`\uFFFF`的Unicode字符,算作两个字符的bug。
|
||||
`Array.from()`的另一个应用是,将字符串转为数组,然后返回字符串的长度。因为它能正确处理各种 Unicode 字符,可以避免 JavaScript 将大于`\uFFFF`的 Unicode 字符,算作两个字符的 bug。
|
||||
|
||||
```javascript
|
||||
function countSymbols(string) {
|
||||
@ -164,7 +484,7 @@ Array(3) // [, , ,]
|
||||
Array(3, 11, 8) // [3, 11, 8]
|
||||
```
|
||||
|
||||
上面代码中,`Array`方法没有参数、一个参数、三个参数时,返回结果都不一样。只有当参数个数不少于2个时,`Array()`才会返回由参数组成的新数组。参数个数只有一个时,实际上是指定数组的长度。
|
||||
上面代码中,`Array`方法没有参数、一个参数、三个参数时,返回结果都不一样。只有当参数个数不少于 2 个时,`Array()`才会返回由参数组成的新数组。参数个数只有一个时,实际上是指定数组的长度。
|
||||
|
||||
`Array.of`基本上可以用来替代`Array()`或`new Array()`,并且不存在由于参数不同而导致的重载。它的行为非常统一。
|
||||
|
||||
@ -185,7 +505,7 @@ function ArrayOf(){
|
||||
}
|
||||
```
|
||||
|
||||
## 数组实例的copyWithin()
|
||||
## 数组实例的 copyWithin()
|
||||
|
||||
数组实例的`copyWithin`方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
|
||||
|
||||
@ -196,7 +516,7 @@ Array.prototype.copyWithin(target, start = 0, end = this.length)
|
||||
它接受三个参数。
|
||||
|
||||
- target(必需):从该位置开始替换数据。
|
||||
- start(可选):从该位置开始读取数据,默认为0。如果为负值,表示倒数。
|
||||
- start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数。
|
||||
- end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。
|
||||
|
||||
这三个参数都应该是数值,如果不是,会自动转为数值。
|
||||
@ -206,7 +526,7 @@ Array.prototype.copyWithin(target, start = 0, end = this.length)
|
||||
// [4, 5, 3, 4, 5]
|
||||
```
|
||||
|
||||
上面代码表示将从3号位直到数组结束的成员(4和5),复制到从0号位开始的位置,结果覆盖了原来的1和2。
|
||||
上面代码表示将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2。
|
||||
|
||||
下面是更多例子。
|
||||
|
||||
@ -224,17 +544,17 @@ Array.prototype.copyWithin(target, start = 0, end = this.length)
|
||||
// {0: 1, 3: 1, length: 5}
|
||||
|
||||
// 将2号位到数组结束,复制到0号位
|
||||
var i32a = new Int32Array([1, 2, 3, 4, 5]);
|
||||
let i32a = new Int32Array([1, 2, 3, 4, 5]);
|
||||
i32a.copyWithin(0, 2);
|
||||
// Int32Array [3, 4, 5, 4, 5]
|
||||
|
||||
// 对于没有部署TypedArray的copyWithin方法的平台
|
||||
// 对于没有部署 TypedArray 的 copyWithin 方法的平台
|
||||
// 需要采用下面的写法
|
||||
[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);
|
||||
// Int32Array [4, 2, 3, 4, 5]
|
||||
```
|
||||
|
||||
## 数组实例的find()和findIndex()
|
||||
## 数组实例的 find() 和 findIndex()
|
||||
|
||||
数组实例的`find`方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为`true`的成员,然后返回该成员。如果没有符合条件的成员,则返回`undefined`。
|
||||
|
||||
@ -243,7 +563,7 @@ i32a.copyWithin(0, 2);
|
||||
// -5
|
||||
```
|
||||
|
||||
上面代码找出数组中第一个小于0的成员。
|
||||
上面代码找出数组中第一个小于 0 的成员。
|
||||
|
||||
```javascript
|
||||
[1, 5, 10, 15].find(function(value, index, arr) {
|
||||
@ -263,7 +583,7 @@ i32a.copyWithin(0, 2);
|
||||
|
||||
这两个方法都可以接受第二个参数,用来绑定回调函数的`this`对象。
|
||||
|
||||
另外,这两个方法都可以发现`NaN`,弥补了数组的`IndexOf`方法的不足。
|
||||
另外,这两个方法都可以发现`NaN`,弥补了数组的`indexOf`方法的不足。
|
||||
|
||||
```javascript
|
||||
[NaN].indexOf(NaN)
|
||||
@ -275,7 +595,7 @@ i32a.copyWithin(0, 2);
|
||||
|
||||
上面代码中,`indexOf`方法无法识别数组的`NaN`成员,但是`findIndex`方法可以借助`Object.is`方法做到。
|
||||
|
||||
## 数组实例的fill()
|
||||
## 数组实例的 fill()
|
||||
|
||||
`fill`方法使用给定值,填充一个数组。
|
||||
|
||||
@ -296,11 +616,11 @@ new Array(3).fill(7)
|
||||
// ['a', 7, 'c']
|
||||
```
|
||||
|
||||
上面代码表示,`fill`方法从1号位开始,向原数组填充7,到2号位之前结束。
|
||||
上面代码表示,`fill`方法从 1 号位开始,向原数组填充 7,到 2 号位之前结束。
|
||||
|
||||
## 数组实例的entries(),keys()和values()
|
||||
## 数组实例的 entries(),keys() 和 values()
|
||||
|
||||
ES6提供三个新的方法——`entries()`,`keys()`和`values()`——用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用`for...of`循环进行遍历,唯一的区别是`keys()`是对键名的遍历、`values()`是对键值的遍历,`entries()`是对键值对的遍历。
|
||||
ES6 提供三个新的方法——`entries()`,`keys()`和`values()`——用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用`for...of`循环进行遍历,唯一的区别是`keys()`是对键名的遍历、`values()`是对键值的遍历,`entries()`是对键值对的遍历。
|
||||
|
||||
```javascript
|
||||
for (let index of ['a', 'b'].keys()) {
|
||||
@ -332,17 +652,17 @@ console.log(entries.next().value); // [1, 'b']
|
||||
console.log(entries.next().value); // [2, 'c']
|
||||
```
|
||||
|
||||
## 数组实例的includes()
|
||||
## 数组实例的 includes()
|
||||
|
||||
`Array.prototype.includes`方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的`includes`方法类似。该方法属于ES7,但Babel转码器已经支持。
|
||||
`Array.prototype.includes`方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的`includes`方法类似。ES2016 引入了该方法。
|
||||
|
||||
```javascript
|
||||
[1, 2, 3].includes(2); // true
|
||||
[1, 2, 3].includes(4); // false
|
||||
[1, 2, NaN].includes(NaN); // true
|
||||
[1, 2, 3].includes(2) // true
|
||||
[1, 2, 3].includes(4) // false
|
||||
[1, 2, NaN].includes(NaN) // true
|
||||
```
|
||||
|
||||
该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。
|
||||
该方法的第二个参数表示搜索的起始位置,默认为`0`。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为`-4`,但数组长度为`3`),则会重置为从`0`开始。
|
||||
|
||||
```javascript
|
||||
[1, 2, 3].includes(3, 3); // false
|
||||
@ -357,7 +677,7 @@ if (arr.indexOf(el) !== -1) {
|
||||
}
|
||||
```
|
||||
|
||||
`indexOf`方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相当运算符(===)进行判断,这会导致对`NaN`的误判。
|
||||
`indexOf`方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于`-1`,表达起来不够直观。二是,它内部使用严格相等运算符(`===`)进行判断,这会导致对`NaN`的误判。
|
||||
|
||||
```javascript
|
||||
[NaN].indexOf(NaN)
|
||||
@ -379,13 +699,13 @@ const contains = (() =>
|
||||
? (arr, value) => arr.includes(value)
|
||||
: (arr, value) => arr.some(el => el === value)
|
||||
)();
|
||||
contains(["foo", "bar"], "baz"); // => false
|
||||
contains(['foo', 'bar'], 'baz'); // => false
|
||||
```
|
||||
|
||||
另外,Map和Set数据结构有一个`has`方法,需要注意与`includes`区分。
|
||||
另外,Map 和 Set 数据结构有一个`has`方法,需要注意与`includes`区分。
|
||||
|
||||
- Map结构的`has`方法,是用来查找键名的,比如`Map.prototype.has(key)`、`WeakMap.prototype.has(key)`、`Reflect.has(target, propertyKey)`。
|
||||
- Set结构的`has`方法,是用来查找值的,比如`Set.prototype.has(value)`、`WeakSet.prototype.has(value)`。
|
||||
- Map 结构的`has`方法,是用来查找键名的,比如`Map.prototype.has(key)`、`WeakMap.prototype.has(key)`、`Reflect.has(target, propertyKey)`。
|
||||
- Set 结构的`has`方法,是用来查找值的,比如`Set.prototype.has(value)`、`WeakSet.prototype.has(value)`。
|
||||
|
||||
## 数组的空位
|
||||
|
||||
@ -395,7 +715,7 @@ contains(["foo", "bar"], "baz"); // => false
|
||||
Array(3) // [, , ,]
|
||||
```
|
||||
|
||||
上面代码中,`Array(3)`返回一个具有3个空位的数组。
|
||||
上面代码中,`Array(3)`返回一个具有 3 个空位的数组。
|
||||
|
||||
注意,空位不是`undefined`,一个位置的值等于`undefined`,依然是有值的。空位是没有任何值,`in`运算符可以说明这一点。
|
||||
|
||||
@ -404,11 +724,11 @@ Array(3) // [, , ,]
|
||||
0 in [, , ,] // false
|
||||
```
|
||||
|
||||
上面代码说明,第一个数组的0号位置是有值的,第二个数组的0号位置没有值。
|
||||
上面代码说明,第一个数组的 0 号位置是有值的,第二个数组的 0 号位置没有值。
|
||||
|
||||
ES5对空位的处理,已经很不一致了,大多数情况下会忽略空位。
|
||||
ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。
|
||||
|
||||
- `forEach()`, `filter()`, `every()` 和`some()`都会跳过空位。
|
||||
- `forEach()`, `filter()`, `reduce()`, `every()` 和`some()`都会跳过空位。
|
||||
- `map()`会跳过空位,但会保留这个值
|
||||
- `join()`和`toString()`会将空位视为`undefined`,而`undefined`和`null`会被处理成空字符串。
|
||||
|
||||
@ -422,6 +742,9 @@ ES5对空位的处理,已经很不一致了,大多数情况下会忽略空
|
||||
// every方法
|
||||
[,'a'].every(x => x==='a') // true
|
||||
|
||||
// reduce方法
|
||||
[1,,2].reduce((x,y) => return x+y) // 3
|
||||
|
||||
// some方法
|
||||
[,'a'].some(x => x !== 'a') // false
|
||||
|
||||
@ -435,7 +758,7 @@ ES5对空位的处理,已经很不一致了,大多数情况下会忽略空
|
||||
[,'a',undefined,null].toString() // ",a,,"
|
||||
```
|
||||
|
||||
ES6则是明确将空位转为`undefined`。
|
||||
ES6 则是明确将空位转为`undefined`。
|
||||
|
||||
`Array.from`方法会将数组的空位,转为`undefined`,也就是说,这个方法不会忽略空位。
|
||||
|
||||
@ -496,4 +819,3 @@ for (let i of arr) {
|
||||
```
|
||||
|
||||
由于空位的处理规则非常不统一,所以建议避免出现空位。
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
1515
docs/async.md
1515
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) {
|
||||
// ...
|
||||
}
|
||||
```
|
904
docs/class.md
904
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
|
||||
@testable
|
||||
class MyTestableClass {
|
||||
// ...
|
||||
}
|
||||
|
||||
function testable(target) {
|
||||
target.isTestable = true;
|
||||
}
|
||||
|
||||
@testable
|
||||
class MyTestableClass {}
|
||||
|
||||
console.log(MyTestableClass.isTestable) // true
|
||||
MyTestableClass.isTestable // true
|
||||
```
|
||||
|
||||
上面代码中,`@testable`就是一个修饰器。它修改了`MyTestableClass`这个类的行为,为它加上了静态属性`isTestable`。
|
||||
上面代码中,`@testable`就是一个修饰器。它修改了`MyTestableClass`这个类的行为,为它加上了静态属性`isTestable`。`testable`函数的参数`target`是`MyTestableClass`类本身。
|
||||
|
||||
基本上,修饰器的行为就是下面这样。
|
||||
|
||||
@ -31,9 +31,7 @@ class A {}
|
||||
A = decorator(A) || A;
|
||||
```
|
||||
|
||||
也就是说,修饰器本质就是编译时执行的函数。
|
||||
|
||||
修饰器函数的第一个参数,就是所要修饰的目标类。
|
||||
也就是说,修饰器是一个对类进行处理的函数。修饰器函数的第一个参数,就是所要修饰的目标类。
|
||||
|
||||
```javascript
|
||||
function testable(target) {
|
||||
@ -63,6 +61,8 @@ MyClass.isTestable // false
|
||||
|
||||
上面代码中,修饰器`testable`可以接受参数,这就等于可以修改修饰器的行为。
|
||||
|
||||
注意,修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。
|
||||
|
||||
前面的例子是为类添加一个静态属性,如果想添加实例属性,可以通过目标类的`prototype`对象操作。
|
||||
|
||||
```javascript
|
||||
@ -118,6 +118,23 @@ let obj = new MyClass();
|
||||
obj.foo() // 'foo'
|
||||
```
|
||||
|
||||
实际开发中,React 与 Redux 库结合使用时,常常需要写成下面这样。
|
||||
|
||||
```javascript
|
||||
class MyReactComponent extends React.Component {}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
|
||||
```
|
||||
|
||||
有了装饰器,就可以改写上面的代码。
|
||||
|
||||
```javascript
|
||||
@connect(mapStateToProps, mapDispatchToProps)
|
||||
export default class MyReactComponent extends React.Component {}
|
||||
```
|
||||
|
||||
相对来说,后一种写法看上去更容易理解。
|
||||
|
||||
## 方法的修饰
|
||||
|
||||
修饰器不仅可以修饰类,还可以修饰类的属性。
|
||||
@ -131,7 +148,7 @@ class Person {
|
||||
|
||||
上面代码中,修饰器`readonly`用来修饰“类”的`name`方法。
|
||||
|
||||
此时,修饰器函数一共可以接受三个参数,第一个参数是所要修饰的目标对象,第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象。
|
||||
修饰器函数`readonly`一共可以接受三个参数。
|
||||
|
||||
```javascript
|
||||
function readonly(target, name, descriptor){
|
||||
@ -151,7 +168,9 @@ readonly(Person.prototype, 'name', descriptor);
|
||||
Object.defineProperty(Person.prototype, 'name', descriptor);
|
||||
```
|
||||
|
||||
上面代码说明,修饰器(readonly)会修改属性的描述对象(descriptor),然后被修改的描述对象再用来定义属性。
|
||||
修饰器第一个参数是类的原型对象,上例是`Person.prototype`,修饰器的本意是要“修饰”类的实例,但是这个时候实例还没生成,所以只能去修饰原型(这不同于类的修饰,那种情况时`target`参数指的是类本身);第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象。
|
||||
|
||||
另外,上面代码说明,修饰器(readonly)会修改属性的描述对象(descriptor),然后被修改的描述对象再用来定义属性。
|
||||
|
||||
下面是另一个例子,修改属性描述对象的`enumerable`属性,使得该属性不可遍历。
|
||||
|
||||
@ -209,12 +228,32 @@ class Person {
|
||||
|
||||
从上面代码中,我们一眼就能看出,`Person`类是可测试的,而`name`方法是只读和不可枚举的。
|
||||
|
||||
下面是使用 Decorator 写法的[组件](https://github.com/ionic-team/stencil),看上去一目了然。
|
||||
|
||||
```javascript
|
||||
@Component({
|
||||
tag: 'my-component',
|
||||
styleUrl: 'my-component.scss'
|
||||
})
|
||||
export class MyComponent {
|
||||
@Prop() first: string;
|
||||
@Prop() last: string;
|
||||
@State() isVisible: boolean = true;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<p>Hello, my name is {this.first} {this.last}</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果同一个方法有多个修饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行。
|
||||
|
||||
```javascript
|
||||
function dec(id){
|
||||
console.log('evaluated', id);
|
||||
return (target, property, descriptor) => console.log('executed', id);
|
||||
console.log('evaluated', id);
|
||||
return (target, property, descriptor) => console.log('executed', id);
|
||||
}
|
||||
|
||||
class Example {
|
||||
@ -230,7 +269,7 @@ class Example {
|
||||
|
||||
上面代码中,外层修饰器`@dec(1)`先进入,但是内层修饰器`@dec(2)`先执行。
|
||||
|
||||
除了注释,修饰器还能用来类型检查。所以,对于类来说,这项功能相当有用。从长期来看,它将是JavaScript代码静态分析的重要工具。
|
||||
除了注释,修饰器还能用来类型检查。所以,对于类来说,这项功能相当有用。从长期来看,它将是 JavaScript 代码静态分析的重要工具。
|
||||
|
||||
## 为什么修饰器不能用于函数?
|
||||
|
||||
@ -248,16 +287,16 @@ function foo() {
|
||||
}
|
||||
```
|
||||
|
||||
上面的代码,意图是执行后`counter`等于1,但是实际上结果是`counter`等于0。因为函数提升,使得实际执行的代码是下面这样。
|
||||
上面的代码,意图是执行后`counter`等于 1,但是实际上结果是`counter`等于 0。因为函数提升,使得实际执行的代码是下面这样。
|
||||
|
||||
```javascript
|
||||
var counter;
|
||||
var add;
|
||||
|
||||
@add
|
||||
function foo() {
|
||||
}
|
||||
|
||||
var counter;
|
||||
var add;
|
||||
|
||||
counter = 0;
|
||||
|
||||
add = function () {
|
||||
@ -289,6 +328,25 @@ readOnly = require("some-decorator");
|
||||
|
||||
总之,由于存在函数提升,使得修饰器不能用于函数。类是不会提升的,所以就没有这方面的问题。
|
||||
|
||||
另一方面,如果一定要修饰函数,可以采用高阶函数的形式直接执行。
|
||||
|
||||
```javascript
|
||||
function doSomething(name) {
|
||||
console.log('Hello, ' + name);
|
||||
}
|
||||
|
||||
function loggingDecorator(wrapped) {
|
||||
return function() {
|
||||
console.log('Starting');
|
||||
const result = wrapped.apply(this, arguments);
|
||||
console.log('Finished');
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const wrapped = loggingDecorator(doSomething);
|
||||
```
|
||||
|
||||
## core-decorators.js
|
||||
|
||||
[core-decorators.js](https://github.com/jayphelps/core-decorators.js)是一个第三方模块,提供了几个常见的修饰器,通过它可以更好地理解修饰器。
|
||||
@ -394,7 +452,7 @@ person.facepalmHarder();
|
||||
|
||||
**(5)@suppressWarnings**
|
||||
|
||||
`suppressWarnings`修饰器抑制`decorated`修饰器导致的`console.warn()`调用。但是,异步代码发出的调用除外。
|
||||
`suppressWarnings`修饰器抑制`deprecated`修饰器导致的`console.warn()`调用。但是,异步代码发出的调用除外。
|
||||
|
||||
```javascript
|
||||
import { suppressWarnings } from 'core-decorators';
|
||||
@ -420,15 +478,23 @@ person.facepalmWithoutWarning();
|
||||
我们可以使用修饰器,使得对象的方法被调用时,自动发出一个事件。
|
||||
|
||||
```javascript
|
||||
import postal from "postal/lib/postal.lodash";
|
||||
const postal = require("postal/lib/postal.lodash");
|
||||
|
||||
export default function publish(topic, channel) {
|
||||
const channelName = channel || '/';
|
||||
const msgChannel = postal.channel(channelName);
|
||||
msgChannel.subscribe(topic, v => {
|
||||
console.log('频道: ', channelName);
|
||||
console.log('事件: ', topic);
|
||||
console.log('数据: ', v);
|
||||
});
|
||||
|
||||
return function(target, name, descriptor) {
|
||||
const fn = descriptor.value;
|
||||
|
||||
descriptor.value = function() {
|
||||
let value = fn.apply(this, arguments);
|
||||
postal.channel(channel || target.channel || "/").publish(topic, value);
|
||||
msgChannel.publish(topic, value);
|
||||
};
|
||||
};
|
||||
}
|
||||
@ -439,29 +505,37 @@ export default function publish(topic, channel) {
|
||||
它的用法如下。
|
||||
|
||||
```javascript
|
||||
import publish from "path/to/decorators/publish";
|
||||
// index.js
|
||||
import publish from './publish';
|
||||
|
||||
class FooComponent {
|
||||
@publish("foo.some.message", "component")
|
||||
@publish('foo.some.message', 'component')
|
||||
someMethod() {
|
||||
return {
|
||||
my: "data"
|
||||
};
|
||||
return { my: 'data' };
|
||||
}
|
||||
@publish("foo.some.other")
|
||||
@publish('foo.some.other')
|
||||
anotherMethod() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
let foo = new FooComponent();
|
||||
|
||||
foo.someMethod();
|
||||
foo.anotherMethod();
|
||||
```
|
||||
|
||||
以后,只要调用`someMethod`或者`anotherMethod`,就会自动发出一个事件。
|
||||
|
||||
```javascript
|
||||
let foo = new FooComponent();
|
||||
```bash
|
||||
$ bash-node index.js
|
||||
频道: component
|
||||
事件: foo.some.message
|
||||
数据: { my: 'data' }
|
||||
|
||||
foo.someMethod() // 在"component"频道发布"foo.some.message"事件,附带的数据是{ my: "data" }
|
||||
foo.anotherMethod() // 在"/"频道发布"foo.some.other"事件,不附带数据
|
||||
频道: /
|
||||
事件: foo.some.other
|
||||
数据: undefined
|
||||
```
|
||||
|
||||
## Mixin
|
||||
@ -485,7 +559,7 @@ obj.foo() // 'foo'
|
||||
|
||||
上面代码之中,对象`Foo`有一个`foo`方法,通过`Object.assign`方法,可以将`foo`方法“混入”`MyClass`类,导致`MyClass`的实例`obj`对象都具有`foo`方法。这就是“混入”模式的一个简单实现。
|
||||
|
||||
下面,我们部署一个通用脚本`mixins.js`,将mixin写成一个修饰器。
|
||||
下面,我们部署一个通用脚本`mixins.js`,将 Mixin 写成一个修饰器。
|
||||
|
||||
```javascript
|
||||
export function mixins(...list) {
|
||||
@ -511,9 +585,9 @@ let obj = new MyClass();
|
||||
obj.foo() // "foo"
|
||||
```
|
||||
|
||||
通过mixins这个修饰器,实现了在MyClass类上面“混入”Foo对象的`foo`方法。
|
||||
通过`mixins`这个修饰器,实现了在`MyClass`类上面“混入”`Foo`对象的`foo`方法。
|
||||
|
||||
不过,上面的方法会改写`MyClass`类的`prototype`对象,如果不喜欢这一点,也可以通过类的继承实现mixin。
|
||||
不过,上面的方法会改写`MyClass`类的`prototype`对象,如果不喜欢这一点,也可以通过类的继承实现 Mixin。
|
||||
|
||||
```javascript
|
||||
class MyClass extends MyBaseClass {
|
||||
@ -595,9 +669,9 @@ new C().foo()
|
||||
|
||||
## Trait
|
||||
|
||||
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
|
||||
import { traits } from 'traits-decorator';
|
||||
@ -618,9 +692,9 @@ obj.foo() // foo
|
||||
obj.bar() // bar
|
||||
```
|
||||
|
||||
上面代码中,通过traits修饰器,在`MyClass`类上面“混入”了`TFoo`类的`foo`方法和`TBar`对象的`bar`方法。
|
||||
上面代码中,通过`traits`修饰器,在`MyClass`类上面“混入”了`TFoo`类的`foo`方法和`TBar`对象的`bar`方法。
|
||||
|
||||
Trait不允许“混入”同名方法。
|
||||
Trait 不允许“混入”同名方法。
|
||||
|
||||
```javascript
|
||||
import { traits } from 'traits-decorator';
|
||||
@ -642,9 +716,9 @@ class MyClass { }
|
||||
// Error: Method named: foo is defined twice.
|
||||
```
|
||||
|
||||
上面代码中,TFoo和TBar都有foo方法,结果traits修饰器报错。
|
||||
上面代码中,`TFoo`和`TBar`都有`foo`方法,结果`traits`修饰器报错。
|
||||
|
||||
一种解决方法是排除TBar的foo方法。
|
||||
一种解决方法是排除`TBar`的`foo`方法。
|
||||
|
||||
```javascript
|
||||
import { traits, excludes } from 'traits-decorator';
|
||||
@ -666,9 +740,9 @@ obj.foo() // foo
|
||||
obj.bar() // bar
|
||||
```
|
||||
|
||||
上面代码使用绑定运算符(::)在TBar上排除foo方法,混入时就不会报错了。
|
||||
上面代码使用绑定运算符(::)在`TBar`上排除`foo`方法,混入时就不会报错了。
|
||||
|
||||
另一种方法是为TBar的foo方法起一个别名。
|
||||
另一种方法是为`TBar`的`foo`方法起一个别名。
|
||||
|
||||
```javascript
|
||||
import { traits, alias } from 'traits-decorator';
|
||||
@ -691,27 +765,27 @@ obj.aliasFoo() // foo
|
||||
obj.bar() // bar
|
||||
```
|
||||
|
||||
上面代码为TBar的foo方法起了别名aliasFoo,于是MyClass也可以混入TBar的foo方法了。
|
||||
上面代码为`TBar`的`foo`方法起了别名`aliasFoo`,于是`MyClass`也可以混入`TBar`的`foo`方法了。
|
||||
|
||||
alias和excludes方法,可以结合起来使用。
|
||||
`alias`和`excludes`方法,可以结合起来使用。
|
||||
|
||||
```javascript
|
||||
@traits(TExample::excludes('foo','bar')::alias({baz:'exampleBaz'}))
|
||||
class MyClass {}
|
||||
```
|
||||
|
||||
上面代码排除了TExample的foo方法和bar方法,为baz方法起了别名exampleBaz。
|
||||
上面代码排除`了TExample`的`foo`方法和`bar`方法,为`baz`方法起了别名`exampleBaz`。
|
||||
|
||||
as方法则为上面的代码提供了另一种写法。
|
||||
`as`方法则为上面的代码提供了另一种写法。
|
||||
|
||||
```javascript
|
||||
@traits(TExample::as({excludes:['foo', 'bar'], alias: {baz: 'exampleBaz'}}))
|
||||
class MyClass {}
|
||||
```
|
||||
|
||||
## Babel转码器的支持
|
||||
## Babel 转码器的支持
|
||||
|
||||
目前,Babel转码器已经支持Decorator。
|
||||
目前,Babel 转码器已经支持 Decorator。
|
||||
|
||||
首先,安装`babel-core`和`babel-plugin-transform-decorators`。由于后者包括在`babel-preset-stage-0`之中,所以改为安装`babel-preset-stage-0`亦可。
|
||||
|
||||
@ -727,7 +801,7 @@ $ npm install babel-core babel-plugin-transform-decorators
|
||||
}
|
||||
```
|
||||
|
||||
这时,Babel就可以对Decorator转码了。
|
||||
这时,Babel 就可以对 Decorator 转码了。
|
||||
|
||||
脚本中打开的命令如下。
|
||||
|
||||
@ -735,4 +809,4 @@ $ npm install babel-core babel-plugin-transform-decorators
|
||||
babel.transform("code", {plugins: ["transform-decorators"]})
|
||||
```
|
||||
|
||||
Babel的官方网站提供一个[在线转码器](https://babeljs.io/repl/),只要勾选Experimental,就能支持Decorator的在线转码。
|
||||
Babel 的官方网站提供一个[在线转码器](https://babeljs.io/repl/),只要勾选 Experimental,就能支持 Decorator 的在线转码。
|
||||
|
@ -4,20 +4,20 @@
|
||||
|
||||
### 基本用法
|
||||
|
||||
ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
|
||||
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
|
||||
|
||||
以前,为变量赋值,只能直接指定值。
|
||||
|
||||
```javascript
|
||||
var a = 1;
|
||||
var b = 2;
|
||||
var c = 3;
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
let c = 3;
|
||||
```
|
||||
|
||||
ES6允许写成下面这样。
|
||||
ES6 允许写成下面这样。
|
||||
|
||||
```javascript
|
||||
var [a, b, c] = [1, 2, 3];
|
||||
let [a, b, c] = [1, 2, 3];
|
||||
```
|
||||
|
||||
上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。
|
||||
@ -50,8 +50,8 @@ z // []
|
||||
如果解构不成功,变量的值就等于`undefined`。
|
||||
|
||||
```javascript
|
||||
var [foo] = [];
|
||||
var [bar, foo] = [1];
|
||||
let [foo] = [];
|
||||
let [bar, foo] = [1];
|
||||
```
|
||||
|
||||
以上两种情况都属于解构不成功,`foo`的值都会等于`undefined`。
|
||||
@ -83,60 +83,52 @@ let [foo] = null;
|
||||
let [foo] = {};
|
||||
```
|
||||
|
||||
上面的表达式都会报错,因为等号右边的值,要么转为对象以后不具备Iterator接口(前五个表达式),要么本身就不具备Iterator接口(最后一个表达式)。
|
||||
上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。
|
||||
|
||||
解构赋值不仅适用于var命令,也适用于let和const命令。
|
||||
对于 Set 结构,也可以使用数组的解构赋值。
|
||||
|
||||
```javascript
|
||||
var [v1, v2, ..., vN ] = array;
|
||||
let [v1, v2, ..., vN ] = array;
|
||||
const [v1, v2, ..., vN ] = array;
|
||||
```
|
||||
|
||||
对于Set结构,也可以使用数组的解构赋值。
|
||||
|
||||
```javascript
|
||||
let [x, y, z] = new Set(["a", "b", "c"]);
|
||||
let [x, y, z] = new Set(['a', 'b', 'c']);
|
||||
x // "a"
|
||||
```
|
||||
|
||||
事实上,只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值。
|
||||
事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。
|
||||
|
||||
```javascript
|
||||
function* fibs() {
|
||||
var a = 0;
|
||||
var b = 1;
|
||||
let a = 0;
|
||||
let b = 1;
|
||||
while (true) {
|
||||
yield a;
|
||||
[a, b] = [b, a + b];
|
||||
}
|
||||
}
|
||||
|
||||
var [first, second, third, fourth, fifth, sixth] = fibs();
|
||||
let [first, second, third, fourth, fifth, sixth] = fibs();
|
||||
sixth // 5
|
||||
```
|
||||
|
||||
上面代码中,`fibs`是一个Generator函数,原生具有Iterator接口。解构赋值会依次从这个接口获取值。
|
||||
上面代码中,`fibs`是一个 Generator 函数(参见《Generator 函数》一章),原生具有 Iterator 接口。解构赋值会依次从这个接口获取值。
|
||||
|
||||
### 默认值
|
||||
|
||||
解构赋值允许指定默认值。
|
||||
|
||||
```javascript
|
||||
var [foo = true] = [];
|
||||
let [foo = true] = [];
|
||||
foo // true
|
||||
|
||||
[x, y = 'b'] = ['a']; // x='a', y='b'
|
||||
[x, y = 'b'] = ['a', undefined]; // x='a', y='b'
|
||||
let [x, y = 'b'] = ['a']; // x='a', y='b'
|
||||
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
|
||||
```
|
||||
|
||||
注意,ES6内部使用严格相等运算符(`===`),判断一个位置是否有值。而且,只有当一个数组成员严格等于`undefined`,默认值才会生效。
|
||||
|
||||
```javascript
|
||||
var [x = 1] = [undefined];
|
||||
let [x = 1] = [undefined];
|
||||
x // 1
|
||||
|
||||
var [x = 1] = [null];
|
||||
let [x = 1] = [null];
|
||||
x // null
|
||||
```
|
||||
|
||||
@ -179,7 +171,7 @@ let [x = y, y = 1] = []; // ReferenceError: y is not defined
|
||||
解构不仅可以用于数组,还可以用于对象。
|
||||
|
||||
```javascript
|
||||
var { foo, bar } = { foo: "aaa", bar: "bbb" };
|
||||
let { foo, bar } = { foo: "aaa", bar: "bbb" };
|
||||
foo // "aaa"
|
||||
bar // "bbb"
|
||||
```
|
||||
@ -187,11 +179,11 @@ bar // "bbb"
|
||||
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
|
||||
|
||||
```javascript
|
||||
var { bar, foo } = { foo: "aaa", bar: "bbb" };
|
||||
let { bar, foo } = { foo: "aaa", bar: "bbb" };
|
||||
foo // "aaa"
|
||||
bar // "bbb"
|
||||
|
||||
var { baz } = { foo: "aaa", bar: "bbb" };
|
||||
let { baz } = { foo: "aaa", bar: "bbb" };
|
||||
baz // undefined
|
||||
```
|
||||
|
||||
@ -200,7 +192,7 @@ baz // undefined
|
||||
如果变量名与属性名不一致,必须写成下面这样。
|
||||
|
||||
```javascript
|
||||
var { foo: baz } = { foo: 'aaa', bar: 'bbb' };
|
||||
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
|
||||
baz // "aaa"
|
||||
|
||||
let obj = { first: 'hello', last: 'world' };
|
||||
@ -212,60 +204,54 @@ l // 'world'
|
||||
这实际上说明,对象的解构赋值是下面形式的简写(参见《对象的扩展》一章)。
|
||||
|
||||
```javascript
|
||||
var { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
|
||||
let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
|
||||
```
|
||||
|
||||
也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
|
||||
|
||||
```javascript
|
||||
var { foo: baz } = { foo: "aaa", bar: "bbb" };
|
||||
let { foo: baz } = { foo: "aaa", bar: "bbb" };
|
||||
baz // "aaa"
|
||||
foo // error: foo is not defined
|
||||
```
|
||||
|
||||
上面代码中,`foo`是匹配的模式,`baz`才是变量。真正被赋值的是变量`baz`,而不是模式`foo`。
|
||||
|
||||
注意,采用这种写法时,变量的声明和赋值是一体的。对于`let`和`const`来说,变量不能重新声明,所以一旦赋值的变量以前声明过,就会报错。
|
||||
与数组一样,解构也可以用于嵌套结构的对象。
|
||||
|
||||
```javascript
|
||||
let foo;
|
||||
let {foo} = {foo: 1}; // SyntaxError: Duplicate declaration "foo"
|
||||
|
||||
let baz;
|
||||
let {bar: baz} = {bar: 1}; // SyntaxError: Duplicate declaration "baz"
|
||||
```
|
||||
|
||||
上面代码中,解构赋值的变量都会重新声明,所以报错了。不过,因为`var`命令允许重新声明,所以这个错误只会在使用`let`和`const`命令时出现。如果没有第二个`let`命令,上面的代码就不会报错。
|
||||
|
||||
```javascript
|
||||
let foo;
|
||||
({foo} = {foo: 1}); // 成功
|
||||
|
||||
let baz;
|
||||
({bar: baz} = {bar: 1}); // 成功
|
||||
```
|
||||
|
||||
上面代码中,`let`命令下面一行的圆括号是必须的,否则会报错。因为解析器会将起首的大括号,理解成一个代码块,而不是赋值语句。
|
||||
|
||||
和数组一样,解构也可以用于嵌套结构的对象。
|
||||
|
||||
```javascript
|
||||
var obj = {
|
||||
let obj = {
|
||||
p: [
|
||||
'Hello',
|
||||
{ y: 'World' }
|
||||
]
|
||||
};
|
||||
|
||||
var { p: [x, { y }] } = obj;
|
||||
let { p: [x, { y }] } = obj;
|
||||
x // "Hello"
|
||||
y // "World"
|
||||
```
|
||||
|
||||
注意,这时`p`是模式,不是变量,因此不会被赋值。
|
||||
注意,这时`p`是模式,不是变量,因此不会被赋值。如果`p`也要作为变量赋值,可以写成下面这样。
|
||||
|
||||
```javascript
|
||||
var node = {
|
||||
let obj = {
|
||||
p: [
|
||||
'Hello',
|
||||
{ y: 'World' }
|
||||
]
|
||||
};
|
||||
|
||||
let { p, p: [x, { y }] } = obj;
|
||||
x // "Hello"
|
||||
y // "World"
|
||||
p // ["Hello", {y: "World"}]
|
||||
```
|
||||
|
||||
下面是另一个例子。
|
||||
|
||||
```javascript
|
||||
const node = {
|
||||
loc: {
|
||||
start: {
|
||||
line: 1,
|
||||
@ -274,13 +260,13 @@ var node = {
|
||||
}
|
||||
};
|
||||
|
||||
var { loc: { start: { line }} } = node;
|
||||
let { loc, loc: { start }, loc: { start: { line }} } = node;
|
||||
line // 1
|
||||
loc // error: loc is undefined
|
||||
start // error: start is undefined
|
||||
loc // Object {start: Object}
|
||||
start // Object {line: 1, column: 5}
|
||||
```
|
||||
|
||||
上面代码中,只有`line`是变量,`loc`和`start`都是模式,不会被赋值。
|
||||
上面代码有三次解构赋值,分别是对`loc`、`start`、`line`三个属性的解构赋值。注意,最后一次对`line`属性的解构赋值之中,只有`line`是变量,`loc`和`start`都是模式,不是变量。
|
||||
|
||||
下面是嵌套赋值的例子。
|
||||
|
||||
@ -304,10 +290,10 @@ var {x, y = 5} = {x: 1};
|
||||
x // 1
|
||||
y // 5
|
||||
|
||||
var {x:y = 3} = {};
|
||||
var {x: y = 3} = {};
|
||||
y // 3
|
||||
|
||||
var {x:y = 3} = {x: 5};
|
||||
var {x: y = 3} = {x: 5};
|
||||
y // 5
|
||||
|
||||
var { message: msg = 'Something went wrong' } = {};
|
||||
@ -329,7 +315,7 @@ x // null
|
||||
如果解构失败,变量的值等于`undefined`。
|
||||
|
||||
```javascript
|
||||
var {foo} = {bar: 'baz'};
|
||||
let {foo} = {bar: 'baz'};
|
||||
foo // undefined
|
||||
```
|
||||
|
||||
@ -337,13 +323,13 @@ foo // undefined
|
||||
|
||||
```javascript
|
||||
// 报错
|
||||
var {foo: {bar}} = {baz: 'baz'};
|
||||
let {foo: {bar}} = {baz: 'baz'};
|
||||
```
|
||||
|
||||
上面代码中,等号左边对象的`foo`属性,对应一个子对象。该子对象的`bar`属性,解构时会报错。原因很简单,因为`foo`这时等于`undefined`,再取子属性就会报错,请看下面的代码。
|
||||
|
||||
```javascript
|
||||
var _tmp = {baz: 'baz'};
|
||||
let _tmp = {baz: 'baz'};
|
||||
_tmp.foo.bar // 报错
|
||||
```
|
||||
|
||||
@ -351,21 +337,22 @@ _tmp.foo.bar // 报错
|
||||
|
||||
```javascript
|
||||
// 错误的写法
|
||||
var x;
|
||||
let x;
|
||||
{x} = {x: 1};
|
||||
// SyntaxError: syntax error
|
||||
```
|
||||
|
||||
上面代码的写法会报错,因为JavaScript引擎会将`{x}`理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免JavaScript将其解释为代码块,才能解决这个问题。
|
||||
上面代码的写法会报错,因为 JavaScript 引擎会将`{x}`理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。
|
||||
|
||||
```javascript
|
||||
// 正确的写法
|
||||
let x;
|
||||
({x} = {x: 1});
|
||||
```
|
||||
|
||||
上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。关于圆括号与解构赋值的关系,参见下文。
|
||||
|
||||
解构赋值允许,等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。
|
||||
解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。
|
||||
|
||||
```javascript
|
||||
({} = [true, false]);
|
||||
@ -386,8 +373,8 @@ let { log, sin, cos } = Math;
|
||||
由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。
|
||||
|
||||
```javascript
|
||||
var arr = [1, 2, 3];
|
||||
var {0 : first, [arr.length - 1] : last} = arr;
|
||||
let arr = [1, 2, 3];
|
||||
let {0 : first, [arr.length - 1] : last} = arr;
|
||||
first // 1
|
||||
last // 3
|
||||
```
|
||||
@ -428,7 +415,7 @@ s === Boolean.prototype.toString // true
|
||||
|
||||
上面代码中,数值和布尔值的包装对象都有`toString`属性,因此变量`s`都能取到值。
|
||||
|
||||
解构赋值的规则是,只要等号右边的值不是对象,就先将其转为对象。由于`undefined`和`null`无法转为对象,所以对它们进行解构赋值,都会报错。
|
||||
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于`undefined`和`null`无法转为对象,所以对它们进行解构赋值,都会报错。
|
||||
|
||||
```javascript
|
||||
let { prop: x } = undefined; // TypeError
|
||||
@ -497,7 +484,7 @@ move(); // [0, 0]
|
||||
|
||||
解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。
|
||||
|
||||
由此带来的问题是,如果模式中出现圆括号怎么处理。ES6的规则是,只要有可能导致解构的歧义,就不得使用圆括号。
|
||||
由此带来的问题是,如果模式中出现圆括号怎么处理。ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号。
|
||||
|
||||
但是,这条规则实际上不那么容易辨别,处理起来相当麻烦。因此,建议只要有可能,就不要在模式中放置圆括号。
|
||||
|
||||
@ -505,32 +492,34 @@ move(); // [0, 0]
|
||||
|
||||
以下三种解构赋值不得使用圆括号。
|
||||
|
||||
(1)变量声明语句中,不能带有圆括号。
|
||||
(1)变量声明语句
|
||||
|
||||
```javascript
|
||||
// 全部报错
|
||||
var [(a)] = [1];
|
||||
let [(a)] = [1];
|
||||
|
||||
var {x: (c)} = {};
|
||||
var ({x: c}) = {};
|
||||
var {(x: c)} = {};
|
||||
var {(x): c} = {};
|
||||
let {x: (c)} = {};
|
||||
let ({x: c}) = {};
|
||||
let {(x: c)} = {};
|
||||
let {(x): c} = {};
|
||||
|
||||
var { o: ({ p: p }) } = { o: { p: 2 } };
|
||||
let { o: ({ p: p }) } = { o: { p: 2 } };
|
||||
```
|
||||
|
||||
上面三个语句都会报错,因为它们都是变量声明语句,模式不能使用圆括号。
|
||||
上面 6 个语句都会报错,因为它们都是变量声明语句,模式不能使用圆括号。
|
||||
|
||||
(2)函数参数中,模式不能带有圆括号。
|
||||
(2)函数参数
|
||||
|
||||
函数参数也属于变量声明,因此不能带有圆括号。
|
||||
|
||||
```javascript
|
||||
// 报错
|
||||
function f([(z)]) { return z; }
|
||||
// 报错
|
||||
function f([z,(x)]) { return x; }
|
||||
```
|
||||
|
||||
(3)赋值语句中,不能将整个模式,或嵌套模式中的一层,放在圆括号之中。
|
||||
(3)赋值语句的模式
|
||||
|
||||
```javascript
|
||||
// 全部报错
|
||||
@ -545,7 +534,7 @@ function f([(z)]) { return z; }
|
||||
[({ p: a }), { x: c }] = [{}, {}];
|
||||
```
|
||||
|
||||
上面代码将嵌套模式的一层,放在圆括号之中,导致报错。
|
||||
上面代码将一部分模式放在圆括号之中,导致报错。
|
||||
|
||||
### 可以使用圆括号的情况
|
||||
|
||||
@ -557,7 +546,7 @@ function f([(z)]) { return z; }
|
||||
[(parseInt.prop)] = [3]; // 正确
|
||||
```
|
||||
|
||||
上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是p,而不是d;第三行语句与第一行语句的性质一致。
|
||||
上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是`p`,而不是`d`;第三行语句与第一行语句的性质一致。
|
||||
|
||||
## 用途
|
||||
|
||||
@ -566,6 +555,9 @@ function f([(z)]) { return z; }
|
||||
**(1)交换变量的值**
|
||||
|
||||
```javascript
|
||||
let x = 1;
|
||||
let y = 2;
|
||||
|
||||
[x, y] = [y, x];
|
||||
```
|
||||
|
||||
@ -581,7 +573,7 @@ function f([(z)]) { return z; }
|
||||
function example() {
|
||||
return [1, 2, 3];
|
||||
}
|
||||
var [a, b, c] = example();
|
||||
let [a, b, c] = example();
|
||||
|
||||
// 返回一个对象
|
||||
|
||||
@ -591,7 +583,7 @@ function example() {
|
||||
bar: 2
|
||||
};
|
||||
}
|
||||
var { foo, bar } = example();
|
||||
let { foo, bar } = example();
|
||||
```
|
||||
|
||||
**(3)函数参数的定义**
|
||||
@ -608,12 +600,12 @@ function f({x, y, z}) { ... }
|
||||
f({z: 3, y: 2, x: 1});
|
||||
```
|
||||
|
||||
**(4)提取JSON数据**
|
||||
**(4)提取 JSON 数据**
|
||||
|
||||
解构赋值对提取JSON对象中的数据,尤其有用。
|
||||
解构赋值对提取 JSON 对象中的数据,尤其有用。
|
||||
|
||||
```javascript
|
||||
var jsonData = {
|
||||
let jsonData = {
|
||||
id: 42,
|
||||
status: "OK",
|
||||
data: [867, 5309]
|
||||
@ -625,7 +617,7 @@ console.log(id, status, number);
|
||||
// 42, "OK", [867, 5309]
|
||||
```
|
||||
|
||||
上面代码可以快速提取JSON数据的值。
|
||||
上面代码可以快速提取 JSON 数据的值。
|
||||
|
||||
**(5)函数参数的默认值**
|
||||
|
||||
@ -645,12 +637,12 @@ jQuery.ajax = function (url, {
|
||||
|
||||
指定参数的默认值,就避免了在函数体内部再写`var foo = config.foo || 'default foo';`这样的语句。
|
||||
|
||||
**(6)遍历Map结构**
|
||||
**(6)遍历 Map 结构**
|
||||
|
||||
任何部署了Iterator接口的对象,都可以用`for...of`循环遍历。Map结构原生支持Iterator接口,配合变量的解构赋值,获取键名和键值就非常方便。
|
||||
任何部署了 Iterator 接口的对象,都可以用`for...of`循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。
|
||||
|
||||
```javascript
|
||||
var map = new Map();
|
||||
const map = new Map();
|
||||
map.set('first', 'hello');
|
||||
map.set('second', 'world');
|
||||
|
||||
@ -677,7 +669,7 @@ for (let [,value] of map) {
|
||||
|
||||
**(7)输入模块的指定方法**
|
||||
|
||||
加载模块时,往往需要指定输入那些方法。解构赋值使得输入语句非常清晰。
|
||||
加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。
|
||||
|
||||
```javascript
|
||||
const { SourceMapConsumer, SourceNode } = require("source-map");
|
||||
|
@ -1,8 +1,8 @@
|
||||
# 函数式编程
|
||||
|
||||
JavaScript语言从一诞生,就具有函数式编程的烙印。它将函数作为一种独立的数据类型,与其他数据类型处于完全平等的地位。在JavaScript语言中,你可以采用面向对象编程,也可以采用函数式编程。有人甚至说,JavaScript是有史以来第一种被大规模采用的函数式编程语言。
|
||||
JavaScript 语言从一诞生,就具有函数式编程的烙印。它将函数作为一种独立的数据类型,与其他数据类型处于完全平等的地位。在 JavaScript 语言中,你可以采用面向对象编程,也可以采用函数式编程。有人甚至说,JavaScript 是有史以来第一种被大规模采用的函数式编程语言。
|
||||
|
||||
ES6的种种新增功能,使得函数式编程变得更方便、更强大。本章介绍ES6如何进行函数式编程。
|
||||
ES6 的种种新增功能,使得函数式编程变得更方便、更强大。本章介绍 ES6 如何进行函数式编程。
|
||||
|
||||
## 柯里化
|
||||
|
||||
@ -66,7 +66,7 @@ var flip = f.flip(three);
|
||||
flip(1, 2, 3); // => [2, 1, 3]
|
||||
```
|
||||
|
||||
上面代码中,如果按照正常的参数顺序,10除以5等于2。但是,参数倒置以后得到的新函数,结果就是5除以10,结果得到0.5。如果原函数有3个参数,则只颠倒前两个参数的位置。
|
||||
上面代码中,如果按照正常的参数顺序,10 除以 5 等于 2。但是,参数倒置以后得到的新函数,结果就是 5 除以 10,结果得到 0.5。如果原函数有 3 个参数,则只颠倒前两个参数的位置。
|
||||
|
||||
参数倒置的代码非常简单。
|
||||
|
||||
@ -94,7 +94,7 @@ until = f.until(condition, inc);
|
||||
until(3) // 5
|
||||
```
|
||||
|
||||
上面代码中,第一段的条件是执行到`x`大于100为止,所以`x`初值为0时,会一直执行到101。第二段的条件是执行到等于5为止,所以`x`最后的值是5。
|
||||
上面代码中,第一段的条件是执行到`x`大于 100 为止,所以`x`初值为 0 时,会一直执行到 101。第二段的条件是执行到等于 5 为止,所以`x`最后的值是 5。
|
||||
|
||||
执行边界的实现如下。
|
||||
|
||||
|
568
docs/function.md
568
docs/function.md
@ -4,7 +4,7 @@
|
||||
|
||||
### 基本用法
|
||||
|
||||
在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
|
||||
ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
|
||||
|
||||
```javascript
|
||||
function log(x, y) {
|
||||
@ -27,7 +27,7 @@ if (typeof y === 'undefined') {
|
||||
}
|
||||
```
|
||||
|
||||
ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。
|
||||
ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
|
||||
|
||||
```javascript
|
||||
function log(x, y = 'World') {
|
||||
@ -39,7 +39,7 @@ log('Hello', 'China') // Hello China
|
||||
log('Hello', '') // Hello
|
||||
```
|
||||
|
||||
可以看到,ES6的写法比ES5简洁许多,而且非常自然。下面是另一个例子。
|
||||
可以看到,ES6 的写法比 ES5 简洁许多,而且非常自然。下面是另一个例子。
|
||||
|
||||
```javascript
|
||||
function Point(x = 0, y = 0) {
|
||||
@ -47,11 +47,11 @@ function Point(x = 0, y = 0) {
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
var p = new Point();
|
||||
const p = new Point();
|
||||
p // { x: 0, y: 0 }
|
||||
```
|
||||
|
||||
除了简洁,ES6的写法还有两个好处:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。
|
||||
除了简洁,ES6 的写法还有两个好处:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。
|
||||
|
||||
参数变量是默认声明的,所以不能用`let`或`const`再次声明。
|
||||
|
||||
@ -64,6 +64,37 @@ function foo(x = 5) {
|
||||
|
||||
上面代码中,参数变量`x`是默认声明的,在函数体中,不能用`let`或`const`再次声明,否则会报错。
|
||||
|
||||
使用参数默认值时,函数不能有同名参数。
|
||||
|
||||
```javascript
|
||||
// 不报错
|
||||
function foo(x, x, y) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 报错
|
||||
function foo(x, x, y = 1) {
|
||||
// ...
|
||||
}
|
||||
// SyntaxError: Duplicate parameter name not allowed in this context
|
||||
```
|
||||
|
||||
另外,一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。
|
||||
|
||||
```javascript
|
||||
let x = 99;
|
||||
function foo(p = x + 1) {
|
||||
console.log(p);
|
||||
}
|
||||
|
||||
foo() // 100
|
||||
|
||||
x = 100;
|
||||
foo() // 101
|
||||
```
|
||||
|
||||
上面代码中,参数`p`的默认值是`x + 1`。这时,每次调用函数`foo`,都会重新计算`x + 1`,而不是默认`p`等于 100。
|
||||
|
||||
### 与解构赋值默认值结合使用
|
||||
|
||||
参数默认值可以与解构赋值的默认值,结合起来使用。
|
||||
@ -73,15 +104,25 @@ function foo({x, y = 5}) {
|
||||
console.log(x, y);
|
||||
}
|
||||
|
||||
foo({}) // undefined, 5
|
||||
foo({x: 1}) // 1, 5
|
||||
foo({x: 1, y: 2}) // 1, 2
|
||||
foo({}) // undefined 5
|
||||
foo({x: 1}) // 1 5
|
||||
foo({x: 1, y: 2}) // 1 2
|
||||
foo() // TypeError: Cannot read property 'x' of undefined
|
||||
```
|
||||
|
||||
上面代码使用了对象的解构赋值默认值,而没有使用函数参数的默认值。只有当函数`foo`的参数是一个对象时,变量`x`和`y`才会通过解构赋值而生成。如果函数`foo`调用时参数不是对象,变量`x`和`y`就不会生成,从而报错。如果参数对象没有`y`属性,`y`的默认值5才会生效。
|
||||
上面代码只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当函数`foo`的参数是一个对象时,变量`x`和`y`才会通过解构赋值生成。如果函数`foo`调用时没提供参数,变量`x`和`y`就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况。
|
||||
|
||||
下面是另一个对象的解构赋值默认值的例子。
|
||||
```javascript
|
||||
function foo({x, y = 5} = {}) {
|
||||
console.log(x, y);
|
||||
}
|
||||
|
||||
foo() // undefined 5
|
||||
```
|
||||
|
||||
上面代码指定,如果没有提供参数,函数`foo`的参数默认为一个空对象。
|
||||
|
||||
下面是另一个解构赋值默认值的例子。
|
||||
|
||||
```javascript
|
||||
function fetch(url, { body = '', method = 'GET', headers = {} }) {
|
||||
@ -95,12 +136,10 @@ fetch('http://example.com')
|
||||
// 报错
|
||||
```
|
||||
|
||||
上面代码中,如果函数`fetch`的第二个参数是一个对象,就可以为它的三个属性设置默认值。
|
||||
|
||||
上面的写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。
|
||||
上面代码中,如果函数`fetch`的第二个参数是一个对象,就可以为它的三个属性设置默认值。这种写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。
|
||||
|
||||
```javascript
|
||||
function fetch(url, { method = 'GET' } = {}) {
|
||||
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
|
||||
console.log(method);
|
||||
}
|
||||
|
||||
@ -110,7 +149,7 @@ fetch('http://example.com')
|
||||
|
||||
上面代码中,函数`fetch`没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量`method`才会取到默认值`GET`。
|
||||
|
||||
再请问下面两种写法有什么差别?
|
||||
作为练习,请问下面两种写法有什么差别?
|
||||
|
||||
```javascript
|
||||
// 写法一
|
||||
@ -131,15 +170,15 @@ function m2({x, y} = { x: 0, y: 0 }) {
|
||||
m1() // [0, 0]
|
||||
m2() // [0, 0]
|
||||
|
||||
// x和y都有值的情况
|
||||
// x 和 y 都有值的情况
|
||||
m1({x: 3, y: 8}) // [3, 8]
|
||||
m2({x: 3, y: 8}) // [3, 8]
|
||||
|
||||
// x有值,y无值的情况
|
||||
// x 有值,y 无值的情况
|
||||
m1({x: 3}) // [3, 0]
|
||||
m2({x: 3}) // [3, undefined]
|
||||
|
||||
// x和y都无值的情况
|
||||
// x 和 y 都无值的情况
|
||||
m1({}) // [0, 0];
|
||||
m2({}) // [undefined, undefined]
|
||||
|
||||
@ -188,7 +227,7 @@ foo(undefined, null)
|
||||
|
||||
上面代码中,`x`参数对应`undefined`,结果触发了默认值,`y`参数等于`null`,就没有触发默认值。
|
||||
|
||||
### 函数的length属性
|
||||
### 函数的 length 属性
|
||||
|
||||
指定了默认值以后,函数的`length`属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,`length`属性将失真。
|
||||
|
||||
@ -198,9 +237,9 @@ foo(undefined, null)
|
||||
(function (a, b, c = 5) {}).length // 2
|
||||
```
|
||||
|
||||
上面代码中,`length`属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。比如,上面最后一个函数,定义了3个参数,其中有一个参数`c`指定了默认值,因此`length`属性等于`3`减去`1`,最后得到`2`。
|
||||
上面代码中,`length`属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。比如,上面最后一个函数,定义了 3 个参数,其中有一个参数`c`指定了默认值,因此`length`属性等于`3`减去`1`,最后得到`2`。
|
||||
|
||||
这是因为`length`属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,rest参数也不会计入`length`属性。
|
||||
这是因为`length`属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,后文的 rest 参数也不会计入`length`属性。
|
||||
|
||||
```javascript
|
||||
(function(...args) {}).length // 0
|
||||
@ -215,7 +254,7 @@ foo(undefined, null)
|
||||
|
||||
### 作用域
|
||||
|
||||
一个需要注意的地方是,如果参数默认值是一个变量,则该变量所处的作用域,与其他变量的作用域规则是一样的,即先是当前函数的作用域,然后才是全局作用域。
|
||||
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
|
||||
|
||||
```javascript
|
||||
var x = 1;
|
||||
@ -227,9 +266,9 @@ function f(x, y = x) {
|
||||
f(2) // 2
|
||||
```
|
||||
|
||||
上面代码中,参数`y`的默认值等于`x`。调用时,由于函数作用域内部的变量`x`已经生成,所以`y`等于参数`x`,而不是全局变量`x`。
|
||||
上面代码中,参数`y`的默认值等于变量`x`。调用函数`f`时,参数形成一个单独的作用域。在这个作用域里面,默认值变量`x`指向第一个参数`x`,而不是全局变量`x`,所以输出是`2`。
|
||||
|
||||
如果调用时,函数作用域内部的变量`x`没有生成,结果就会不一样。
|
||||
再看下面的例子。
|
||||
|
||||
```javascript
|
||||
let x = 1;
|
||||
@ -242,7 +281,7 @@ function f(y = x) {
|
||||
f() // 1
|
||||
```
|
||||
|
||||
上面代码中,函数调用时,`y`的默认值变量`x`尚未在函数内部生成,所以`x`指向全局变量。
|
||||
上面代码中,函数`f`调用时,参数`y = x`形成一个单独的作用域。这个作用域里面,变量`x`本身没有定义,所以指向外层的全局变量`x`。函数调用时,函数体内部的局部变量`x`影响不到默认值变量`x`。
|
||||
|
||||
如果此时,全局变量`x`不存在,就会报错。
|
||||
|
||||
@ -267,22 +306,22 @@ function foo(x = x) {
|
||||
foo() // ReferenceError: x is not defined
|
||||
```
|
||||
|
||||
上面代码中,函数`foo`的参数`x`的默认值也是`x`。这时,默认值`x`的作用域是函数作用域,而不是全局作用域。由于在函数作用域中,存在变量`x`,但是默认值在`x`赋值之前先执行了,所以这时属于暂时性死区(参见《let和const命令》一章),任何对`x`的操作都会报错。
|
||||
上面代码中,参数`x = x`形成一个单独作用域。实际执行的是`let x = x`,由于暂时性死区的原因,这行代码会报错”x 未定义“。
|
||||
|
||||
如果参数的默认值是一个函数,该函数的作用域是其声明时所在的作用域。请看下面的例子。
|
||||
如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。请看下面的例子。
|
||||
|
||||
```javascript
|
||||
let foo = 'outer';
|
||||
|
||||
function bar(func = x => foo) {
|
||||
function bar(func = () => foo) {
|
||||
let foo = 'inner';
|
||||
console.log(func()); // outer
|
||||
console.log(func());
|
||||
}
|
||||
|
||||
bar();
|
||||
bar(); // outer
|
||||
```
|
||||
|
||||
上面代码中,函数`bar`的参数`func`的默认值是一个匿名函数,返回值为变量`foo`。这个匿名函数声明时,`bar`函数的作用域还没有形成,所以匿名函数里面的`foo`指向外层作用域的`foo`,输出`outer`。
|
||||
上面代码中,函数`bar`的参数`func`的默认值是一个匿名函数,返回值为变量`foo`。函数参数形成的单独作用域里面,并没有定义变量`foo`,所以`foo`指向外层的全局变量`foo`,因此输出`outer`。
|
||||
|
||||
如果写成下面这样,就会报错。
|
||||
|
||||
@ -295,7 +334,7 @@ function bar(func = () => foo) {
|
||||
bar() // ReferenceError: foo is not defined
|
||||
```
|
||||
|
||||
上面代码中,匿名函数里面的`foo`指向函数外层,但是函数外层并没有声明`foo`,所以就报错了。
|
||||
上面代码中,匿名函数里面的`foo`指向函数外层,但是函数外层并没有声明变量`foo`,所以就报错了。
|
||||
|
||||
下面是一个更复杂的例子。
|
||||
|
||||
@ -308,11 +347,12 @@ function foo(x, y = function() { x = 2; }) {
|
||||
}
|
||||
|
||||
foo() // 3
|
||||
x // 1
|
||||
```
|
||||
|
||||
上面代码中,函数`foo`的参数`y`的默认值是一个匿名函数。函数`foo`调用时,它的参数`x`的值为`undefined`,所以`y`函数内部的`x`一开始是`undefined`,后来被重新赋值`2`。但是,函数`foo`内部重新声明了一个`x`,值为`3`,这两个`x`是不一样的,互相不产生影响,因此最后输出`3`。
|
||||
上面代码中,函数`foo`的参数形成一个单独作用域。这个作用域里面,首先声明了变量`x`,然后声明了变量`y`,`y`的默认值是一个匿名函数。这个匿名函数内部的变量`x`,指向同一个作用域的第一个参数`x`。函数`foo`内部又声明了一个内部变量`x`,该变量与第一个参数`x`由于不是同一个作用域,所以不是同一个变量,因此执行`y`后,内部变量`x`和外部全局变量`x`的值都没变。
|
||||
|
||||
如果将`var x = 3`的`var`去除,两个`x`就是一样的,最后输出的就是`2`。
|
||||
如果将`var x = 3`的`var`去除,函数`foo`的内部变量`x`就指向第一个参数`x`,与匿名函数内部的`x`是一致的,所以最后输出的就是`2`,而外层的全局变量`x`依然不受影响。
|
||||
|
||||
```javascript
|
||||
var x = 1;
|
||||
@ -323,6 +363,7 @@ function foo(x, y = function() { x = 2; }) {
|
||||
}
|
||||
|
||||
foo() // 2
|
||||
x // 1
|
||||
```
|
||||
|
||||
### 应用
|
||||
@ -344,7 +385,7 @@ foo()
|
||||
|
||||
上面代码的`foo`函数,如果调用的时候没有参数,就会调用默认值`throwIfMissing`函数,从而抛出一个错误。
|
||||
|
||||
从上面代码还可以看到,参数`mustBeProvided`的默认值等于`throwIfMissing`函数的运行结果(即函数名之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行(即如果参数已经赋值,默认值中的函数就不会运行),这与python语言不一样。
|
||||
从上面代码还可以看到,参数`mustBeProvided`的默认值等于`throwIfMissing`函数的运行结果(注意函数名`throwIfMissing`之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行。如果参数已经赋值,默认值中的函数就不会运行。
|
||||
|
||||
另外,可以将参数默认值设为`undefined`,表明这个参数是可以省略的。
|
||||
|
||||
@ -352,9 +393,9 @@ foo()
|
||||
function foo(optional = undefined) { ··· }
|
||||
```
|
||||
|
||||
## rest参数
|
||||
## rest 参数
|
||||
|
||||
ES6引入rest参数(形式为“...变量名”),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
|
||||
ES6 引入 rest 参数(形式为`...变量名`),用于获取函数的多余参数,这样就不需要使用`arguments`对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
|
||||
|
||||
```javascript
|
||||
function add(...values) {
|
||||
@ -370,9 +411,9 @@ function add(...values) {
|
||||
add(2, 5, 3) // 10
|
||||
```
|
||||
|
||||
上面代码的add函数是一个求和函数,利用rest参数,可以向该函数传入任意数目的参数。
|
||||
上面代码的`add`函数是一个求和函数,利用 rest 参数,可以向该函数传入任意数目的参数。
|
||||
|
||||
下面是一个rest参数代替arguments变量的例子。
|
||||
下面是一个 rest 参数代替`arguments`变量的例子。
|
||||
|
||||
```javascript
|
||||
// arguments变量的写法
|
||||
@ -384,9 +425,9 @@ function sortNumbers() {
|
||||
const sortNumbers = (...numbers) => numbers.sort();
|
||||
```
|
||||
|
||||
上面代码的两种写法,比较后可以发现,rest参数的写法更自然也更简洁。
|
||||
上面代码的两种写法,比较后可以发现,rest 参数的写法更自然也更简洁。
|
||||
|
||||
rest参数中的变量代表一个数组,所以数组特有的方法都可以用于这个变量。下面是一个利用rest参数改写数组push方法的例子。
|
||||
`arguments`对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用`Array.prototype.slice.call`先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。下面是一个利用 rest 参数改写数组`push`方法的例子。
|
||||
|
||||
```javascript
|
||||
function push(array, ...items) {
|
||||
@ -400,7 +441,7 @@ var a = [];
|
||||
push(a, 1, 2, 3)
|
||||
```
|
||||
|
||||
注意,rest参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
|
||||
注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
|
||||
|
||||
```javascript
|
||||
// 报错
|
||||
@ -409,7 +450,7 @@ function f(a, ...b, c) {
|
||||
}
|
||||
```
|
||||
|
||||
函数的length属性,不包括rest参数。
|
||||
函数的`length`属性,不包括 rest 参数。
|
||||
|
||||
```javascript
|
||||
(function(a) {}).length // 1
|
||||
@ -417,286 +458,9 @@ function f(a, ...b, c) {
|
||||
(function(a, ...b) {}).length // 1
|
||||
```
|
||||
|
||||
## 扩展运算符
|
||||
|
||||
### 含义
|
||||
|
||||
扩展运算符(spread)是三个点(`...`)。它好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。
|
||||
|
||||
```javascript
|
||||
console.log(...[1, 2, 3])
|
||||
// 1 2 3
|
||||
|
||||
console.log(1, ...[2, 3, 4], 5)
|
||||
// 1 2 3 4 5
|
||||
|
||||
[...document.querySelectorAll('div')]
|
||||
// [<div>, <div>, <div>]
|
||||
```
|
||||
|
||||
该运算符主要用于函数调用。
|
||||
|
||||
```javascript
|
||||
function push(array, ...items) {
|
||||
array.push(...items);
|
||||
}
|
||||
|
||||
function add(x, y) {
|
||||
return x + y;
|
||||
}
|
||||
|
||||
var numbers = [4, 38];
|
||||
add(...numbers) // 42
|
||||
```
|
||||
|
||||
上面代码中,`array.push(...items)`和`add(...numbers)`这两行,都是函数的调用,它们的都使用了扩展运算符。该运算符将一个数组,变为参数序列。
|
||||
|
||||
扩展运算符与正常的函数参数可以结合使用,非常灵活。
|
||||
|
||||
```javascript
|
||||
function f(v, w, x, y, z) { }
|
||||
var args = [0, 1];
|
||||
f(-1, ...args, 2, ...[3]);
|
||||
```
|
||||
|
||||
### 替代数组的apply方法
|
||||
|
||||
由于扩展运算符可以展开数组,所以不再需要`apply`方法,将数组转为函数的参数了。
|
||||
|
||||
```javascript
|
||||
// ES5的写法
|
||||
function f(x, y, z) {
|
||||
// ...
|
||||
}
|
||||
var args = [0, 1, 2];
|
||||
f.apply(null, args);
|
||||
|
||||
// ES6的写法
|
||||
function f(x, y, z) {
|
||||
// ...
|
||||
}
|
||||
var args = [0, 1, 2];
|
||||
f(...args);
|
||||
```
|
||||
|
||||
下面是扩展运算符取代`apply`方法的一个实际的例子,应用`Math.max`方法,简化求出一个数组最大元素的写法。
|
||||
|
||||
```javascript
|
||||
// ES5的写法
|
||||
Math.max.apply(null, [14, 3, 77])
|
||||
|
||||
// ES6的写法
|
||||
Math.max(...[14, 3, 77])
|
||||
|
||||
// 等同于
|
||||
Math.max(14, 3, 77);
|
||||
```
|
||||
|
||||
上面代码表示,由于JavaScript不提供求数组最大元素的函数,所以只能套用`Math.max`函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用`Math.max`了。
|
||||
|
||||
另一个例子是通过`push`函数,将一个数组添加到另一个数组的尾部。
|
||||
|
||||
```javascript
|
||||
// ES5的写法
|
||||
var arr1 = [0, 1, 2];
|
||||
var arr2 = [3, 4, 5];
|
||||
Array.prototype.push.apply(arr1, arr2);
|
||||
|
||||
// ES6的写法
|
||||
var arr1 = [0, 1, 2];
|
||||
var arr2 = [3, 4, 5];
|
||||
arr1.push(...arr2);
|
||||
```
|
||||
|
||||
上面代码的ES5写法中,`push`方法的参数不能是数组,所以只好通过`apply`方法变通使用`push`方法。有了扩展运算符,就可以直接将数组传入`push`方法。
|
||||
|
||||
下面是另外一个例子。
|
||||
|
||||
```javascript
|
||||
// ES5
|
||||
new (Date.bind.apply(Date, [null, 2015, 1, 1]))
|
||||
// ES6
|
||||
new Date(...[2015, 1, 1]);
|
||||
```
|
||||
|
||||
### 扩展运算符的应用
|
||||
|
||||
**(1)合并数组**
|
||||
|
||||
扩展运算符提供了数组合并的新写法。
|
||||
|
||||
```javascript
|
||||
// ES5
|
||||
[1, 2].concat(more)
|
||||
// ES6
|
||||
[1, 2, ...more]
|
||||
|
||||
var arr1 = ['a', 'b'];
|
||||
var arr2 = ['c'];
|
||||
var arr3 = ['d', 'e'];
|
||||
|
||||
// ES5的合并数组
|
||||
arr1.concat(arr2, arr3);
|
||||
// [ 'a', 'b', 'c', 'd', 'e' ]
|
||||
|
||||
// ES6的合并数组
|
||||
[...arr1, ...arr2, ...arr3]
|
||||
// [ 'a', 'b', 'c', 'd', 'e' ]
|
||||
```
|
||||
|
||||
**(2)与解构赋值结合**
|
||||
|
||||
扩展运算符可以与解构赋值结合起来,用于生成数组。
|
||||
|
||||
```javascript
|
||||
// ES5
|
||||
a = list[0], rest = list.slice(1)
|
||||
// ES6
|
||||
[a, ...rest] = list
|
||||
```
|
||||
|
||||
下面是另外一些例子。
|
||||
|
||||
```javascript
|
||||
const [first, ...rest] = [1, 2, 3, 4, 5];
|
||||
first // 1
|
||||
rest // [2, 3, 4, 5]
|
||||
|
||||
const [first, ...rest] = [];
|
||||
first // undefined
|
||||
rest // []:
|
||||
|
||||
const [first, ...rest] = ["foo"];
|
||||
first // "foo"
|
||||
rest // []
|
||||
```
|
||||
|
||||
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
|
||||
|
||||
```javascript
|
||||
const [...butLast, last] = [1, 2, 3, 4, 5];
|
||||
// 报错
|
||||
|
||||
const [first, ...middle, last] = [1, 2, 3, 4, 5];
|
||||
// 报错
|
||||
```
|
||||
|
||||
**(3)函数的返回值**
|
||||
|
||||
JavaScript的函数只能返回一个值,如果需要返回多个值,只能返回数组或对象。扩展运算符提供了解决这个问题的一种变通方法。
|
||||
|
||||
```javascript
|
||||
var dateFields = readDateFields(database);
|
||||
var d = new Date(...dateFields);
|
||||
```
|
||||
|
||||
上面代码从数据库取出一行数据,通过扩展运算符,直接将其传入构造函数`Date`。
|
||||
|
||||
**(4)字符串**
|
||||
|
||||
扩展运算符还可以将字符串转为真正的数组。
|
||||
|
||||
```javascript
|
||||
[...'hello']
|
||||
// [ "h", "e", "l", "l", "o" ]
|
||||
```
|
||||
|
||||
上面的写法,有一个重要的好处,那就是能够正确识别32位的Unicode字符。
|
||||
|
||||
```javascript
|
||||
'x\uD83D\uDE80y'.length // 4
|
||||
[...'x\uD83D\uDE80y'].length // 3
|
||||
```
|
||||
|
||||
上面代码的第一种写法,JavaScript会将32位Unicode字符,识别为2个字符,采用扩展运算符就没有这个问题。因此,正确返回字符串长度的函数,可以像下面这样写。
|
||||
|
||||
```javascript
|
||||
function length(str) {
|
||||
return [...str].length;
|
||||
}
|
||||
|
||||
length('x\uD83D\uDE80y') // 3
|
||||
```
|
||||
|
||||
凡是涉及到操作32位Unicode字符的函数,都有这个问题。因此,最好都用扩展运算符改写。
|
||||
|
||||
```javascript
|
||||
let str = 'x\uD83D\uDE80y';
|
||||
|
||||
str.split('').reverse().join('')
|
||||
// 'y\uDE80\uD83Dx'
|
||||
|
||||
[...str].reverse().join('')
|
||||
// 'y\uD83D\uDE80x'
|
||||
```
|
||||
|
||||
上面代码中,如果不用扩展运算符,字符串的`reverse`操作就不正确。
|
||||
|
||||
**(5)实现了Iterator接口的对象**
|
||||
|
||||
任何Iterator接口的对象,都可以用扩展运算符转为真正的数组。
|
||||
|
||||
```javascript
|
||||
var nodeList = document.querySelectorAll('div');
|
||||
var array = [...nodeList];
|
||||
```
|
||||
|
||||
上面代码中,`querySelectorAll`方法返回的是一个`nodeList`对象。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于`NodeList`对象实现了Iterator接口。
|
||||
|
||||
对于那些没有部署Iterator接口的类似数组的对象,扩展运算符就无法将其转为真正的数组。
|
||||
|
||||
```javascript
|
||||
let arrayLike = {
|
||||
'0': 'a',
|
||||
'1': 'b',
|
||||
'2': 'c',
|
||||
length: 3
|
||||
};
|
||||
|
||||
// TypeError: Cannot spread non-iterable object.
|
||||
let arr = [...arrayLike];
|
||||
```
|
||||
|
||||
上面代码中,`arrayLike`是一个类似数组的对象,但是没有部署Iterator接口,扩展运算符就会报错。这时,可以改为使用`Array.from`方法将`arrayLike`转为真正的数组。
|
||||
|
||||
**(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 开始,函数内部可以设定为严格模式。
|
||||
|
||||
```javascript
|
||||
function doSomething(a, b) {
|
||||
@ -705,7 +469,7 @@ function doSomething(a, b) {
|
||||
}
|
||||
```
|
||||
|
||||
《ECMAScript 2016标准》做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。
|
||||
ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。
|
||||
|
||||
```javascript
|
||||
// 报错
|
||||
@ -735,7 +499,7 @@ const obj = {
|
||||
};
|
||||
```
|
||||
|
||||
这样规定的原因是,函数内部的严格模式,同时适用于函数体代码和函数参数代码。但是,函数执行的时候,先执行函数参数代码,然后再执行函数体代码。这样就有一个不合理的地方,只有从函数体代码之中,才能知道参数代码是否应该以严格模式执行,但是参数代码却应该先于函数体代码执行。
|
||||
这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。
|
||||
|
||||
```javascript
|
||||
// 报错
|
||||
@ -745,7 +509,7 @@ function doSomething(value = 070) {
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,参数`value`的默认值是八进制数`070`,但是严格模式下不能用前缀`0`表示八进制,所以应该报错。但是实际上,JavaScript引擎会先成功执行`value = 070`,然后进入函数体内部,发现需要用严格模式执行,这时才会报错。
|
||||
上面代码中,参数`value`的默认值是八进制数`070`,但是严格模式下不能用前缀`0`表示八进制,所以应该报错。但是实际上,JavaScript 引擎会先成功执行`value = 070`,然后进入函数体内部,发现需要用严格模式执行,这时才会报错。
|
||||
|
||||
虽然可以先解析函数体代码,再执行参数代码,但是这样无疑就增加了复杂性。因此,标准索性禁止了这种用法,只要参数使用了默认值、解构赋值、或者扩展运算符,就不能显式指定严格模式。
|
||||
|
||||
@ -770,7 +534,7 @@ const doSomething = (function () {
|
||||
}());
|
||||
```
|
||||
|
||||
## name属性
|
||||
## name 属性
|
||||
|
||||
函数的`name`属性,返回该函数的函数名。
|
||||
|
||||
@ -779,23 +543,23 @@ function foo() {}
|
||||
foo.name // "foo"
|
||||
```
|
||||
|
||||
这个属性早就被浏览器广泛支持,但是直到ES6,才将其写入了标准。
|
||||
这个属性早就被浏览器广泛支持,但是直到 ES6,才将其写入了标准。
|
||||
|
||||
需要注意的是,ES6对这个属性的行为做出了一些修改。如果将一个匿名函数赋值给一个变量,ES5的`name`属性,会返回空字符串,而ES6的`name`属性会返回实际的函数名。
|
||||
需要注意的是,ES6 对这个属性的行为做出了一些修改。如果将一个匿名函数赋值给一个变量,ES5 的`name`属性,会返回空字符串,而 ES6 的`name`属性会返回实际的函数名。
|
||||
|
||||
```javascript
|
||||
var func1 = function () {};
|
||||
var f = function () {};
|
||||
|
||||
// ES5
|
||||
func1.name // ""
|
||||
f.name // ""
|
||||
|
||||
// ES6
|
||||
func1.name // "func1"
|
||||
f.name // "f"
|
||||
```
|
||||
|
||||
上面代码中,变量`func1`等于一个匿名函数,ES5和ES6的`name`属性返回的值不一样。
|
||||
上面代码中,变量`f`等于一个匿名函数,ES5 和 ES6 的`name`属性返回的值不一样。
|
||||
|
||||
如果将一个具名函数赋值给一个变量,则ES5和ES6的`name`属性都返回这个具名函数原本的名字。
|
||||
如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的`name`属性都返回这个具名函数原本的名字。
|
||||
|
||||
```javascript
|
||||
const bar = function baz() {};
|
||||
@ -807,13 +571,13 @@ bar.name // "baz"
|
||||
bar.name // "baz"
|
||||
```
|
||||
|
||||
`Function`构造函数返回的函数实例,`name`属性的值为“anonymous”。
|
||||
`Function`构造函数返回的函数实例,`name`属性的值为`anonymous`。
|
||||
|
||||
```javascript
|
||||
(new Function).name // "anonymous"
|
||||
```
|
||||
|
||||
`bind`返回的函数,`name`属性值会加上“bound ”前缀。
|
||||
`bind`返回的函数,`name`属性值会加上`bound`前缀。
|
||||
|
||||
```javascript
|
||||
function foo() {};
|
||||
@ -826,7 +590,7 @@ foo.bind({}).name // "bound foo"
|
||||
|
||||
### 基本用法
|
||||
|
||||
ES6允许使用“箭头”(`=>`)定义函数。
|
||||
ES6 允许使用“箭头”(`=>`)定义函数。
|
||||
|
||||
```javascript
|
||||
var f = v => v;
|
||||
@ -860,10 +624,20 @@ var sum = function(num1, num2) {
|
||||
var sum = (num1, num2) => { return num1 + num2; }
|
||||
```
|
||||
|
||||
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号。
|
||||
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
|
||||
|
||||
```javascript
|
||||
var getTempItem = id => ({ id: id, name: "Temp" });
|
||||
// 报错
|
||||
let getTempItem = id => { id: id, name: "Temp" };
|
||||
|
||||
// 不报错
|
||||
let getTempItem = id => ({ id: id, name: "Temp" });
|
||||
```
|
||||
|
||||
如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。
|
||||
|
||||
```javascript
|
||||
let fn = () => void doesNotReturn();
|
||||
```
|
||||
|
||||
箭头函数可以与变量解构结合使用。
|
||||
@ -910,7 +684,7 @@ var result = values.sort(function (a, b) {
|
||||
var result = values.sort((a, b) => a - b);
|
||||
```
|
||||
|
||||
下面是rest参数与箭头函数结合的例子。
|
||||
下面是 rest 参数与箭头函数结合的例子。
|
||||
|
||||
```javascript
|
||||
const numbers = (...nums) => nums;
|
||||
@ -932,9 +706,9 @@ headAndTail(1, 2, 3, 4, 5)
|
||||
|
||||
(2)不可以当作构造函数,也就是说,不可以使用`new`命令,否则会抛出一个错误。
|
||||
|
||||
(3)不可以使用`arguments`对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。
|
||||
(3)不可以使用`arguments`对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
|
||||
|
||||
(4)不可以使用`yield`命令,因此箭头函数不能用作Generator函数。
|
||||
(4)不可以使用`yield`命令,因此箭头函数不能用作 Generator 函数。
|
||||
|
||||
上面四点中,第一点尤其值得注意。`this`对象的指向是可变的,但是在箭头函数中,它是固定的。
|
||||
|
||||
@ -951,7 +725,7 @@ foo.call({ id: 42 });
|
||||
// id: 42
|
||||
```
|
||||
|
||||
上面代码中,`setTimeout`的参数是一个箭头函数,这个箭头函数的定义生效是在`foo`函数生成时,而它的真正执行要等到100毫秒后。如果是普通函数,执行时`this`应该指向全局对象`window`,这时应该输出`21`。但是,箭头函数导致`this`总是指向函数定义生效时所在的对象(本例是`{id: 42}`),所以输出的是`42`。
|
||||
上面代码中,`setTimeout`的参数是一个箭头函数,这个箭头函数的定义生效是在`foo`函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时`this`应该指向全局对象`window`,这时应该输出`21`。但是,箭头函数导致`this`总是指向函数定义生效时所在的对象(本例是`{id: 42}`),所以输出的是`42`。
|
||||
|
||||
箭头函数可以让`setTimeout`里面的`this`,绑定定义时所在的作用域,而不是指向运行时所在的作用域。下面是另一个例子。
|
||||
|
||||
@ -975,9 +749,9 @@ setTimeout(() => console.log('s2: ', timer.s2), 3100);
|
||||
// s2: 0
|
||||
```
|
||||
|
||||
上面代码中,`Timer`函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的`this`绑定定义时所在的作用域(即`Timer`函数),后者的`this`指向运行时所在的作用域(即全局对象)。所以,3100毫秒之后,`timer.s1`被更新了3次,而`timer.s2`一次都没更新。
|
||||
上面代码中,`Timer`函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的`this`绑定定义时所在的作用域(即`Timer`函数),后者的`this`指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,`timer.s1`被更新了 3 次,而`timer.s2`一次都没更新。
|
||||
|
||||
箭头函数可以让`this`指向固定化,这种特性很有利于封装回调函数。下面是一个例子,DOM事件的回调函数封装在一个对象里面。
|
||||
箭头函数可以让`this`指向固定化,这种特性很有利于封装回调函数。下面是一个例子,DOM 事件的回调函数封装在一个对象里面。
|
||||
|
||||
```javascript
|
||||
var handler = {
|
||||
@ -998,7 +772,7 @@ var handler = {
|
||||
|
||||
`this`指向的固定化,并不是因为箭头函数内部有绑定`this`的机制,实际原因是箭头函数根本没有自己的`this`,导致内部的`this`就是外层代码块的`this`。正是因为它没有`this`,所以也就不能用作构造函数。
|
||||
|
||||
所以,箭头函数转成ES5的代码如下。
|
||||
所以,箭头函数转成 ES5 的代码如下。
|
||||
|
||||
```javascript
|
||||
// ES6
|
||||
@ -1018,7 +792,7 @@ function foo() {
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,转换后的ES5版本清楚地说明了,箭头函数里面根本没有自己的`this`,而是引用外层的`this`。
|
||||
上面代码中,转换后的 ES5 版本清楚地说明了,箭头函数里面根本没有自己的`this`,而是引用外层的`this`。
|
||||
|
||||
请问下面的代码之中有几个`this`?
|
||||
|
||||
@ -1070,11 +844,11 @@ foo(2, 4, 6, 8)
|
||||
|
||||
上面代码中,箭头函数没有自己的`this`,所以`bind`方法无效,内部的`this`指向外部的`this`。
|
||||
|
||||
长期以来,JavaScript语言的`this`对象一直是一个令人头痛的问题,在对象方法中使用`this`,必须非常小心。箭头函数”绑定”`this`,很大程度上解决了这个困扰。
|
||||
长期以来,JavaScript 语言的`this`对象一直是一个令人头痛的问题,在对象方法中使用`this`,必须非常小心。箭头函数”绑定”`this`,很大程度上解决了这个困扰。
|
||||
|
||||
### 嵌套的箭头函数
|
||||
|
||||
箭头函数内部,还可以再使用箭头函数。下面是一个ES5语法的多重嵌套函数。
|
||||
箭头函数内部,还可以再使用箭头函数。下面是一个 ES5 语法的多重嵌套函数。
|
||||
|
||||
```javascript
|
||||
function insert(value) {
|
||||
@ -1124,7 +898,7 @@ mult2(plus1(5))
|
||||
// 12
|
||||
```
|
||||
|
||||
箭头函数还有一个功能,就是可以很方便地改写λ演算。
|
||||
箭头函数还有一个功能,就是可以很方便地改写 λ 演算。
|
||||
|
||||
```javascript
|
||||
// λ演算的写法
|
||||
@ -1135,13 +909,13 @@ var fix = f => (x => f(v => x(x)(v)))
|
||||
(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
|
||||
foo::bar;
|
||||
@ -1170,7 +944,7 @@ let log = ::console.log;
|
||||
var log = console.log.bind(console);
|
||||
```
|
||||
|
||||
由于双冒号运算符返回的还是原对象,因此可以采用链式写法。
|
||||
双冒号运算符的运算结果,还是一个对象,因此可以采用链式写法。
|
||||
|
||||
```javascript
|
||||
// 例一
|
||||
@ -1201,7 +975,7 @@ function f(x){
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,函数f的最后一步是调用函数g,这就叫尾调用。
|
||||
上面代码中,函数`f`的最后一步是调用函数`g`,这就叫尾调用。
|
||||
|
||||
以下三种情况,都不属于尾调用。
|
||||
|
||||
@ -1223,7 +997,7 @@ function f(x){
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,情况一是调用函数g之后,还有赋值操作,所以不属于尾调用,即使语义完全一样。情况二也属于调用后还有操作,即使写在一行内。情况三等同于下面的代码。
|
||||
上面代码中,情况一是调用函数`g`之后,还有赋值操作,所以不属于尾调用,即使语义完全一样。情况二也属于调用后还有操作,即使写在一行内。情况三等同于下面的代码。
|
||||
|
||||
```javascript
|
||||
function f(x){
|
||||
@ -1243,13 +1017,13 @@ function f(x) {
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,函数m和n都属于尾调用,因为它们都是函数f的最后一步操作。
|
||||
上面代码中,函数`m`和`n`都属于尾调用,因为它们都是函数`f`的最后一步操作。
|
||||
|
||||
### 尾调用优化
|
||||
|
||||
尾调用之所以与其他调用不同,就在于它的特殊的调用位置。
|
||||
|
||||
我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到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不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除 f(x) 的调用帧,只保留 g(3) 的调用帧。
|
||||
上面代码中,如果函数`g`不是尾调用,函数`f`就需要保存内部变量`m`和`n`的值、`g`的调用位置等信息。但由于调用`g`之后,函数`f`就结束了,所以执行到最后一步,完全可以删除`f(x)`的调用帧,只保留`g(3)`的调用帧。
|
||||
|
||||
这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。
|
||||
|
||||
@ -1304,7 +1078,7 @@ function factorial(n) {
|
||||
factorial(5) // 120
|
||||
```
|
||||
|
||||
上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。
|
||||
上面代码是一个阶乘函数,计算`n`的阶乘,最多需要保存`n`个调用记录,复杂度 O(n) 。
|
||||
|
||||
如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。
|
||||
|
||||
@ -1317,9 +1091,9 @@ function factorial(n, total) {
|
||||
factorial(5, 1) // 120
|
||||
```
|
||||
|
||||
还有一个比较著名的例子,就是计算fibonacci 数列,也能充分说明尾递归优化的重要性
|
||||
还有一个比较著名的例子,就是计算 Fibonacci 数列,也能充分说明尾递归优化的重要性。
|
||||
|
||||
如果是非尾递归的fibonacci 递归方法
|
||||
非尾递归的 Fibonacci 数列实现如下。
|
||||
|
||||
```javascript
|
||||
function Fibonacci (n) {
|
||||
@ -1328,13 +1102,12 @@ function Fibonacci (n) {
|
||||
return Fibonacci(n - 1) + Fibonacci(n - 2);
|
||||
}
|
||||
|
||||
Fibonacci(10); // 89
|
||||
// Fibonacci(100)
|
||||
// Fibonacci(500)
|
||||
// 堆栈溢出了
|
||||
Fibonacci(10) // 89
|
||||
Fibonacci(100) // 堆栈溢出
|
||||
Fibonacci(500) // 堆栈溢出
|
||||
```
|
||||
|
||||
如果我们使用尾递归优化过的fibonacci 递归算法
|
||||
尾递归优化过的 Fibonacci 数列实现如下。
|
||||
|
||||
```javascript
|
||||
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
|
||||
@ -1348,11 +1121,11 @@ Fibonacci2(1000) // 7.0330367711422765e+208
|
||||
Fibonacci2(10000) // Infinity
|
||||
```
|
||||
|
||||
由此可见,“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6也是如此,第一次明确规定,所有ECMAScript的实现,都必须部署“尾调用优化”。这就是说,在ES6中,只要使用尾递归,就不会发生栈溢出,相对节省内存。
|
||||
由此可见,“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6 是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存。
|
||||
|
||||
### 递归函数的改写
|
||||
|
||||
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量 total ,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来,为什么计算5的阶乘,需要传入两个参数5和1?
|
||||
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量`total`,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来,为什么计算`5`的阶乘,需要传入两个参数`5`和`1`?
|
||||
|
||||
两个方法可以解决这个问题。方法一是在尾递归函数之外,再提供一个正常形式的函数。
|
||||
|
||||
@ -1369,7 +1142,7 @@ function factorial(n) {
|
||||
factorial(5) // 120
|
||||
```
|
||||
|
||||
上面代码通过一个正常形式的阶乘函数 factorial ,调用尾递归函数 tailFactorial ,看起来就正常多了。
|
||||
上面代码通过一个正常形式的阶乘函数`factorial`,调用尾递归函数`tailFactorial`,看起来就正常多了。
|
||||
|
||||
函数式编程有一个概念,叫做柯里化(currying),意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化。
|
||||
|
||||
@ -1390,9 +1163,9 @@ const factorial = currying(tailFactorial, 1);
|
||||
factorial(5) // 120
|
||||
```
|
||||
|
||||
上面代码通过柯里化,将尾递归函数 tailFactorial 变为只接受1个参数的 factorial 。
|
||||
上面代码通过柯里化,将尾递归函数`tailFactorial`变为只接受一个参数的`factorial`。
|
||||
|
||||
第二种方法就简单多了,就是采用ES6的函数默认值。
|
||||
第二种方法就简单多了,就是采用 ES6 的函数默认值。
|
||||
|
||||
```javascript
|
||||
function factorial(n, total = 1) {
|
||||
@ -1403,13 +1176,13 @@ function factorial(n, total = 1) {
|
||||
factorial(5) // 120
|
||||
```
|
||||
|
||||
上面代码中,参数 total 有默认值1,所以调用时不用提供这个值。
|
||||
上面代码中,参数`total`有默认值`1`,所以调用时不用提供这个值。
|
||||
|
||||
总结一下,递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言(比如Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。
|
||||
总结一下,递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言(比如 Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。
|
||||
|
||||
### 严格模式
|
||||
|
||||
ES6的尾调用优化只在严格模式下开启,正常模式是无效的。
|
||||
ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。
|
||||
|
||||
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。
|
||||
|
||||
@ -1420,7 +1193,7 @@ ES6的尾调用优化只在严格模式下开启,正常模式是无效的。
|
||||
|
||||
```javascript
|
||||
function restricted() {
|
||||
"use strict";
|
||||
'use strict';
|
||||
restricted.caller; // 报错
|
||||
restricted.arguments; // 报错
|
||||
}
|
||||
@ -1448,7 +1221,7 @@ sum(1, 100000)
|
||||
// Uncaught RangeError: Maximum call stack size exceeded(…)
|
||||
```
|
||||
|
||||
上面代码中,`sum`是一个递归函数,参数`x`是需要累加的值,参数`y`控制递归次数。一旦指定`sum`递归100000次,就会报错,提示超出调用栈的最大次数。
|
||||
上面代码中,`sum`是一个递归函数,参数`x`是需要累加的值,参数`y`控制递归次数。一旦指定`sum`递归 100000 次,就会报错,提示超出调用栈的最大次数。
|
||||
|
||||
蹦床函数(trampoline)可以将递归执行转为循环执行。
|
||||
|
||||
@ -1522,7 +1295,7 @@ sum(1, 100000)
|
||||
|
||||
## 函数参数的尾逗号
|
||||
|
||||
ECMAScript 2017将[允许](https://github.com/jeffmo/es-trailing-function-commas)函数的最后一个参数有尾逗号(trailing comma)。
|
||||
ES2017 [允许](https://github.com/jeffmo/es-trailing-function-commas)函数的最后一个参数有尾逗号(trailing comma)。
|
||||
|
||||
此前,函数定义和调用时,都不允许最后一个参数后面出现逗号。
|
||||
|
||||
@ -1540,7 +1313,7 @@ clownsEverywhere(
|
||||
|
||||
上面代码中,如果在`param2`或`bar`后面加一个逗号,就会报错。
|
||||
|
||||
这样的话,如果以后修改代码,想为函数`clownsEverywhere`添加第三个参数,就势必要在第二个参数后面添加一个逗号。这对版本管理系统来说,就会显示,添加逗号的那一行也发生了变动。这看上去有点冗余,因此新的语法允许定义和调用时,尾部直接有一个逗号。
|
||||
如果像上面这样,将参数写成多行(即每个参数占据一行),以后修改代码的时候,想为函数`clownsEverywhere`添加第三个参数,或者调整参数的次序,就势必要在原来最后一个参数后面添加一个逗号。这对于版本管理系统来说,就会显示添加逗号的那一行也发生了变动。这看上去有点冗余,因此新的语法允许定义和调用时,尾部直接有一个逗号。
|
||||
|
||||
```javascript
|
||||
function clownsEverywhere(
|
||||
@ -1553,3 +1326,42 @@ clownsEverywhere(
|
||||
'bar',
|
||||
);
|
||||
```
|
||||
|
||||
这样的规定也使得,函数参数与数组和对象的尾逗号规则,保持一致了。
|
||||
|
||||
## catch 语句的参数
|
||||
|
||||
目前,有一个[提案](https://github.com/tc39/proposal-optional-catch-binding),允许`try...catch`结构中的`catch`语句调用时不带有参数。这个提案跟参数有关,也放在这一章介绍。
|
||||
|
||||
传统的写法是`catch`语句必须带有参数,用来接收`try`代码块抛出的错误。
|
||||
|
||||
```javascript
|
||||
try {
|
||||
// ···
|
||||
} catch (error) {
|
||||
// ···
|
||||
}
|
||||
```
|
||||
|
||||
新的写法允许省略`catch`后面的参数,而不报错。
|
||||
|
||||
```javascript
|
||||
try {
|
||||
// ···
|
||||
} catch {
|
||||
// ···
|
||||
}
|
||||
```
|
||||
|
||||
新写法只在不需要错误实例的情况下有用,因此不及传统写法的用途广。
|
||||
|
||||
```javascript
|
||||
let jsonData;
|
||||
try {
|
||||
jsonData = JSON.parse(str);
|
||||
} catch {
|
||||
jsonData = DEFAULT_DATA;
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,`JSON.parse`报错只有一种可能:解析失败。因此,可以不需要抛出的错误实例。
|
||||
|
790
docs/generator-async.md
Normal file
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
|
||||
function* helloWorldGenerator() {
|
||||
@ -22,11 +22,11 @@ function* 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
|
||||
hw.next()
|
||||
@ -44,45 +44,42 @@ hw.next()
|
||||
|
||||
上面代码一共调用了四次`next`方法。
|
||||
|
||||
第一次调用,Generator函数开始执行,直到遇到第一个`yield`语句为止。`next`方法返回一个对象,它的`value`属性就是当前`yield`语句的值hello,`done`属性的值false,表示遍历还没有结束。
|
||||
第一次调用,Generator 函数开始执行,直到遇到第一个`yield`表达式为止。`next`方法返回一个对象,它的`value`属性就是当前`yield`表达式的值`hello`,`done`属性的值`false`,表示遍历还没有结束。
|
||||
|
||||
第二次调用,Generator函数从上次`yield`语句停下的地方,一直执行到下一个`yield`语句。`next`方法返回的对象的`value`属性就是当前`yield`语句的值world,`done`属性的值false,表示遍历还没有结束。
|
||||
第二次调用,Generator 函数从上次`yield`表达式停下的地方,一直执行到下一个`yield`表达式。`next`方法返回的对象的`value`属性就是当前`yield`表达式的值`world`,`done`属性的值`false`,表示遍历还没有结束。
|
||||
|
||||
第三次调用,Generator函数从上次`yield`语句停下的地方,一直执行到`return`语句(如果没有return语句,就执行到函数结束)。`next`方法返回的对象的`value`属性,就是紧跟在`return`语句后面的表达式的值(如果没有`return`语句,则`value`属性的值为undefined),`done`属性的值true,表示遍历已经结束。
|
||||
第三次调用,Generator 函数从上次`yield`表达式停下的地方,一直执行到`return`语句(如果没有`return`语句,就执行到函数结束)。`next`方法返回的对象的`value`属性,就是紧跟在`return`语句后面的表达式的值(如果没有`return`语句,则`value`属性的值为`undefined`),`done`属性的值`true`,表示遍历已经结束。
|
||||
|
||||
第四次调用,此时Generator函数已经运行完毕,`next`方法返回对象的`value`属性为undefined,`done`属性为true。以后再调用`next`方法,返回的都是这个值。
|
||||
第四次调用,此时 Generator 函数已经运行完毕,`next`方法返回对象的`value`属性为`undefined`,`done`属性为`true`。以后再调用`next`方法,返回的都是这个值。
|
||||
|
||||
总结一下,调用Generator函数,返回一个遍历器对象,代表Generator函数的内部指针。以后,每次调用遍历器对象的`next`方法,就会返回一个有着`value`和`done`两个属性的对象。`value`属性表示当前的内部状态的值,是`yield`语句后面那个表达式的值;`done`属性是一个布尔值,表示是否遍历结束。
|
||||
总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的`next`方法,就会返回一个有着`value`和`done`两个属性的对象。`value`属性表示当前的内部状态的值,是`yield`表达式后面那个表达式的值;`done`属性是一个布尔值,表示是否遍历结束。
|
||||
|
||||
ES6没有规定,`function`关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。
|
||||
ES6 没有规定,`function`关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。
|
||||
|
||||
```javascript
|
||||
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`方法的运行逻辑如下。
|
||||
|
||||
(1)遇到`yield`语句,就暂停执行后面的操作,并将紧跟在`yield`后面的那个表达式的值,作为返回的对象的`value`属性值。
|
||||
(1)遇到`yield`表达式,就暂停执行后面的操作,并将紧跟在`yield`后面的那个表达式的值,作为返回的对象的`value`属性值。
|
||||
|
||||
(2)下一次调用`next`方法时,再继续往下执行,直到遇到下一个`yield`语句。
|
||||
(2)下一次调用`next`方法时,再继续往下执行,直到遇到下一个`yield`表达式。
|
||||
|
||||
(3)如果没有再遇到新的`yield`语句,就一直运行到函数结束,直到`return`语句为止,并将`return`语句后面的表达式的值,作为返回的对象的`value`属性值。
|
||||
(3)如果没有再遇到新的`yield`表达式,就一直运行到函数结束,直到`return`语句为止,并将`return`语句后面的表达式的值,作为返回的对象的`value`属性值。
|
||||
|
||||
(4)如果该函数没有`return`语句,则返回的对象的`value`属性值为`undefined`。
|
||||
|
||||
需要注意的是,`yield`语句后面的表达式,只有当调用`next`方法、内部指针指向该语句时才会执行,因此等于为JavaScript提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
|
||||
需要注意的是,`yield`表达式后面的表达式,只有当调用`next`方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
|
||||
|
||||
```javascript
|
||||
function* gen() {
|
||||
@ -90,11 +87,11 @@ function* gen() {
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,yield后面的表达式`123 + 456`,不会立即求值,只会在`next`方法将指针移到这一句时,才会求值。
|
||||
上面代码中,`yield`后面的表达式`123 + 456`,不会立即求值,只会在`next`方法将指针移到这一句时,才会求值。
|
||||
|
||||
`yield`语句与`return`语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到`yield`,函数暂停执行,下一次再从该位置继续向后执行,而`return`语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)`return`语句,但是可以执行多次(或者说多个)`yield`语句。正常函数只能返回一个值,因为只能执行一次`return`;Generator函数可以返回一系列的值,因为可以有任意多个`yield`。从另一个角度看,也可以说Generator生成了一系列的值,这也就是它的名称的来历(在英语中,generator这个词是“生成器”的意思)。
|
||||
`yield`表达式与`return`语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到`yield`,函数暂停执行,下一次再从该位置继续向后执行,而`return`语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)`return`语句,但是可以执行多次(或者说多个)`yield`表达式。正常函数只能返回一个值,因为只能执行一次`return`;Generator 函数可以返回一系列的值,因为可以有任意多个`yield`。从另一个角度看,也可以说 Generator 生成了一系列的值,这也就是它的名称的来历(英语中,generator 这个词是“生成器”的意思)。
|
||||
|
||||
Generator函数可以不用`yield`语句,这时就变成了一个单纯的暂缓执行函数。
|
||||
Generator 函数可以不用`yield`表达式,这时就变成了一个单纯的暂缓执行函数。
|
||||
|
||||
```javascript
|
||||
function* f() {
|
||||
@ -108,9 +105,9 @@ setTimeout(function () {
|
||||
}, 2000);
|
||||
```
|
||||
|
||||
上面代码中,函数`f`如果是普通函数,在为变量`generator`赋值时就会执行。但是,函数`f`是一个Generator函数,就变成只有调用`next`方法时,函数`f`才会执行。
|
||||
上面代码中,函数`f`如果是普通函数,在为变量`generator`赋值时就会执行。但是,函数`f`是一个 Generator 函数,就变成只有调用`next`方法时,函数`f`才会执行。
|
||||
|
||||
另外需要注意,`yield`语句不能用在普通函数中,否则会报错。
|
||||
另外需要注意,`yield`表达式只能用在 Generator 函数里面,用在其他地方都会报错。
|
||||
|
||||
```javascript
|
||||
(function (){
|
||||
@ -119,7 +116,7 @@ setTimeout(function () {
|
||||
// SyntaxError: Unexpected number
|
||||
```
|
||||
|
||||
上面代码在一个普通函数中使用`yield`语句,结果产生一个句法错误。
|
||||
上面代码在一个普通函数中使用`yield`表达式,结果产生一个句法错误。
|
||||
|
||||
下面是另外一个例子。
|
||||
|
||||
@ -133,7 +130,7 @@ var flat = function* (a) {
|
||||
} else {
|
||||
yield item;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
for (var f of flat(arr)){
|
||||
@ -141,7 +138,7 @@ for (var f of flat(arr)){
|
||||
}
|
||||
```
|
||||
|
||||
上面代码也会产生句法错误,因为`forEach`方法的参数是一个普通函数,但是在里面使用了`yield`语句(这个函数里面还使用了`yield*`语句,这里可以不用理会,详细说明见后文)。一种修改方法是改用`for`循环。
|
||||
上面代码也会产生句法错误,因为`forEach`方法的参数是一个普通函数,但是在里面使用了`yield`表达式(这个函数里面还使用了`yield*`表达式,详细介绍见后文)。一种修改方法是改用`for`循环。
|
||||
|
||||
```javascript
|
||||
var arr = [1, [[2, 3], 4], [5, 6]];
|
||||
@ -164,28 +161,32 @@ for (var f of flat(arr)) {
|
||||
// 1, 2, 3, 4, 5, 6
|
||||
```
|
||||
|
||||
另外,`yield`语句如果用在一个表达式之中,必须放在圆括号里面。
|
||||
另外,`yield`表达式如果用在另一个表达式之中,必须放在圆括号里面。
|
||||
|
||||
```javascript
|
||||
console.log('Hello' + yield); // SyntaxError
|
||||
console.log('Hello' + yield 123); // SyntaxError
|
||||
function* demo() {
|
||||
console.log('Hello' + yield); // SyntaxError
|
||||
console.log('Hello' + yield 123); // SyntaxError
|
||||
|
||||
console.log('Hello' + (yield)); // OK
|
||||
console.log('Hello' + (yield 123)); // OK
|
||||
console.log('Hello' + (yield)); // OK
|
||||
console.log('Hello' + (yield 123)); // OK
|
||||
}
|
||||
```
|
||||
|
||||
`yield`语句用作函数参数或赋值表达式的右边,可以不加括号。
|
||||
`yield`表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
|
||||
|
||||
```javascript
|
||||
foo(yield 'a', yield 'b'); // OK
|
||||
let input = yield; // OK
|
||||
function* demo() {
|
||||
foo(yield 'a', yield 'b'); // OK
|
||||
let input = yield; // OK
|
||||
}
|
||||
```
|
||||
|
||||
### 与Iterator接口的关系
|
||||
### 与 Iterator 接口的关系
|
||||
|
||||
上一章说过,任意一个对象的`Symbol.iterator`方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。
|
||||
|
||||
由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的`Symbol.iterator`属性,从而使得该对象具有Iterator接口。
|
||||
由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的`Symbol.iterator`属性,从而使得该对象具有 Iterator 接口。
|
||||
|
||||
```javascript
|
||||
var myIterable = {};
|
||||
@ -198,9 +199,9 @@ myIterable[Symbol.iterator] = function* () {
|
||||
[...myIterable] // [1, 2, 3]
|
||||
```
|
||||
|
||||
上面代码中,Generator函数赋值给`Symbol.iterator`属性,从而使得`myIterable`对象具有了Iterator接口,可以被`...`运算符遍历了。
|
||||
上面代码中,Generator 函数赋值给`Symbol.iterator`属性,从而使得`myIterable`对象具有了 Iterator 接口,可以被`...`运算符遍历了。
|
||||
|
||||
Generator函数执行后,返回一个遍历器对象。该对象本身也具有`Symbol.iterator`属性,执行后返回自身。
|
||||
Generator 函数执行后,返回一个遍历器对象。该对象本身也具有`Symbol.iterator`属性,执行后返回自身。
|
||||
|
||||
```javascript
|
||||
function* gen(){
|
||||
@ -213,15 +214,15 @@ g[Symbol.iterator]() === g
|
||||
// true
|
||||
```
|
||||
|
||||
上面代码中,`gen`是一个Generator函数,调用它会生成一个遍历器对象`g`。它的`Symbol.iterator`属性,也是一个遍历器对象生成函数,执行后返回它自己。
|
||||
上面代码中,`gen`是一个 Generator 函数,调用它会生成一个遍历器对象`g`。它的`Symbol.iterator`属性,也是一个遍历器对象生成函数,执行后返回它自己。
|
||||
|
||||
## next方法的参数
|
||||
## next 方法的参数
|
||||
|
||||
`yield`句本身没有返回值,或者说总是返回`undefined`。`next`方法可以带一个参数,该参数就会被当作上一个`yield`语句的返回值。
|
||||
`yield`表达式本身没有返回值,或者说总是返回`undefined`。`next`方法可以带一个参数,该参数就会被当作上一个`yield`表达式的返回值。
|
||||
|
||||
```javascript
|
||||
function* f() {
|
||||
for(var i=0; true; i++) {
|
||||
for(var i = 0; true; i++) {
|
||||
var reset = yield i;
|
||||
if(reset) { i = -1; }
|
||||
}
|
||||
@ -234,9 +235,9 @@ g.next() // { value: 1, done: false }
|
||||
g.next(true) // { value: 0, done: false }
|
||||
```
|
||||
|
||||
上面代码先定义了一个可以无限运行的Generator函数`f`,如果`next`方法没有参数,每次运行到`yield`语句,变量`reset`的值总是`undefined`。当`next`方法带一个参数`true`时,当前的变量`reset`就被重置为这个参数(即`true`),因此`i`会等于-1,下一轮循环就会从-1开始递增。
|
||||
上面代码先定义了一个可以无限运行的 Generator 函数`f`,如果`next`方法没有参数,每次运行到`yield`表达式,变量`reset`的值总是`undefined`。当`next`方法带一个参数`true`时,变量`reset`就被重置为这个参数(即`true`),因此`i`会等于`-1`,下一轮循环就会从`-1`开始递增。
|
||||
|
||||
这个功能有很重要的语法意义。Generator函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过`next`方法的参数,就有办法在Generator函数开始运行之后,继续向函数体内部注入值。也就是说,可以在Generator函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
|
||||
这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过`next`方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
|
||||
|
||||
再看一个例子。
|
||||
|
||||
@ -258,13 +259,34 @@ b.next(12) // { value:8, done:false }
|
||||
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函数外面再包一层。
|
||||
再看一个通过`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 函数外面再包一层。
|
||||
|
||||
```javascript
|
||||
function wrapper(generatorFunction) {
|
||||
@ -284,32 +306,11 @@ wrapped().next('hello!')
|
||||
// First input: hello!
|
||||
```
|
||||
|
||||
上面代码中,Generator函数如果不用`wrapper`先包一层,是无法第一次调用`next`方法,就输入参数的。
|
||||
上面代码中,Generator 函数如果不用`wrapper`先包一层,是无法第一次调用`next`方法,就输入参数的。
|
||||
|
||||
再看一个通过`next`方法的参数,向Generator函数内部输入值的例子。
|
||||
## for...of 循环
|
||||
|
||||
```javascript
|
||||
function* dataConsumer() {
|
||||
console.log('Started');
|
||||
console.log(`1. ${yield}`);
|
||||
console.log(`2. ${yield}`);
|
||||
return 'result';
|
||||
}
|
||||
|
||||
let genObj = dataConsumer();
|
||||
genObj.next();
|
||||
// Started
|
||||
genObj.next('a')
|
||||
// 1. a
|
||||
genObj.next('b')
|
||||
// 2. b
|
||||
```
|
||||
|
||||
上面代码是一个很直观的例子,每次通过`next`方法向Generator函数输入值,然后打印出来。
|
||||
|
||||
## for...of循环
|
||||
|
||||
`for...of`循环可以自动遍历Generator函数时生成的`Iterator`对象,且此时不再需要调用`next`方法。
|
||||
`for...of`循环可以自动遍历 Generator 函数时生成的`Iterator`对象,且此时不再需要调用`next`方法。
|
||||
|
||||
```javascript
|
||||
function *foo() {
|
||||
@ -327,9 +328,9 @@ for (let v of foo()) {
|
||||
// 1 2 3 4 5
|
||||
```
|
||||
|
||||
上面代码使用`for...of`循环,依次显示5个`yield`语句的值。这里需要注意,一旦`next`方法的返回对象的`done`属性为`true`,`for...of`循环就会中止,且不包含该返回对象,所以上面代码的`return`语句返回的6,不包括在`for...of`循环之中。
|
||||
上面代码使用`for...of`循环,依次显示 5 个`yield`表达式的值。这里需要注意,一旦`next`方法的返回对象的`done`属性为`true`,`for...of`循环就会中止,且不包含该返回对象,所以上面代码的`return`语句返回的`6`,不包括在`for...of`循环之中。
|
||||
|
||||
下面是一个利用Generator函数和`for...of`循环,实现斐波那契数列的例子。
|
||||
下面是一个利用 Generator 函数和`for...of`循环,实现斐波那契数列的例子。
|
||||
|
||||
```javascript
|
||||
function* fibonacci() {
|
||||
@ -348,7 +349,7 @@ for (let n of fibonacci()) {
|
||||
|
||||
从上面代码可见,使用`for...of`语句时不需要使用`next`方法。
|
||||
|
||||
利用`for...of`循环,可以写出遍历任意对象(object)的方法。原生的JavaScript对象没有遍历接口,无法使用`for...of`循环,通过Generator函数为它加上这个接口,就可以用了。
|
||||
利用`for...of`循环,可以写出遍历任意对象(object)的方法。原生的 JavaScript 对象没有遍历接口,无法使用`for...of`循环,通过 Generator 函数为它加上这个接口,就可以用了。
|
||||
|
||||
```javascript
|
||||
function* objectEntries(obj) {
|
||||
@ -368,7 +369,7 @@ for (let [key, value] of objectEntries(jane)) {
|
||||
// last: Doe
|
||||
```
|
||||
|
||||
上面代码中,对象`jane`原生不具备Iterator接口,无法用`for...of`遍历。这时,我们通过Generator函数`objectEntries`为它加上遍历器接口,就可以用`for...of`遍历了。加上遍历器接口的另一种写法是,将Generator函数加到对象的`Symbol.iterator`属性上面。
|
||||
上面代码中,对象`jane`原生不具备 Iterator 接口,无法用`for...of`遍历。这时,我们通过 Generator 函数`objectEntries`为它加上遍历器接口,就可以用`for...of`遍历了。加上遍历器接口的另一种写法是,将 Generator 函数加到对象的`Symbol.iterator`属性上面。
|
||||
|
||||
```javascript
|
||||
function* objectEntries() {
|
||||
@ -390,7 +391,7 @@ for (let [key, value] of jane) {
|
||||
// last: Doe
|
||||
```
|
||||
|
||||
除了`for...of`循环以外,扩展运算符(`...`)、解构赋值和`Array.from`方法内部调用的,都是遍历器接口。这意味着,它们都可以将Generator函数返回的Iterator对象,作为参数。
|
||||
除了`for...of`循环以外,扩展运算符(`...`)、解构赋值和`Array.from`方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。
|
||||
|
||||
```javascript
|
||||
function* numbers () {
|
||||
@ -421,7 +422,7 @@ for (let n of numbers()) {
|
||||
|
||||
## Generator.prototype.throw()
|
||||
|
||||
Generator函数返回的遍历器对象,都有一个`throw`方法,可以在函数体外抛出错误,然后在Generator函数体内捕获。
|
||||
Generator 函数返回的遍历器对象,都有一个`throw`方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
|
||||
|
||||
```javascript
|
||||
var g = function* () {
|
||||
@ -445,7 +446,7 @@ try {
|
||||
// 外部捕获 b
|
||||
```
|
||||
|
||||
上面代码中,遍历器对象`i`连续抛出两个错误。第一个错误被Generator函数体内的`catch`语句捕获。`i`第二次抛出错误,由于Generator函数内部的`catch`语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了Generator函数体,被函数体外的`catch`语句捕获。
|
||||
上面代码中,遍历器对象`i`连续抛出两个错误。第一个错误被 Generator 函数体内的`catch`语句捕获。`i`第二次抛出错误,由于 Generator 函数内部的`catch`语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的`catch`语句捕获。
|
||||
|
||||
`throw`方法可以接受一个参数,该参数会被`catch`语句接收,建议抛出`Error`对象的实例。
|
||||
|
||||
@ -492,7 +493,7 @@ try {
|
||||
|
||||
上面代码之所以只捕获了`a`,是因为函数体外的`catch`语句块,捕获了抛出的`a`错误以后,就不会再继续`try`代码块里面剩余的语句了。
|
||||
|
||||
如果Generator函数内部没有部署`try...catch`代码块,那么`throw`方法抛出的错误,将被外部`try...catch`代码块捕获。
|
||||
如果 Generator 函数内部没有部署`try...catch`代码块,那么`throw`方法抛出的错误,将被外部`try...catch`代码块捕获。
|
||||
|
||||
```javascript
|
||||
var g = function* () {
|
||||
@ -514,9 +515,9 @@ try {
|
||||
// 外部捕获 a
|
||||
```
|
||||
|
||||
上面代码中,Generator函数`g`内部没有部署`try...catch`代码块,所以抛出的错误直接被外部`catch`代码块捕获。
|
||||
上面代码中,Generator 函数`g`内部没有部署`try...catch`代码块,所以抛出的错误直接被外部`catch`代码块捕获。
|
||||
|
||||
如果Generator函数内部和外部,都没有部署`try...catch`代码块,那么程序将报错,直接中断执行。
|
||||
如果 Generator 函数内部和外部,都没有部署`try...catch`代码块,那么程序将报错,直接中断执行。
|
||||
|
||||
```javascript
|
||||
var gen = function* gen(){
|
||||
@ -533,7 +534,7 @@ g.throw();
|
||||
|
||||
上面代码中,`g.throw`抛出错误以后,没有任何`try...catch`代码块可以捕获这个错误,导致程序报错,中断执行。
|
||||
|
||||
`throw`方法被捕获以后,会附带执行下一条`yield`语句。也就是说,会附带执行一次`next`方法。
|
||||
`throw`方法被捕获以后,会附带执行下一条`yield`表达式。也就是说,会附带执行一次`next`方法。
|
||||
|
||||
```javascript
|
||||
var gen = function* gen(){
|
||||
@ -552,7 +553,7 @@ g.throw() // b
|
||||
g.next() // c
|
||||
```
|
||||
|
||||
上面代码中,`g.throw`方法被捕获以后,自动执行了一次`next`方法,所以会打印`b`。另外,也可以看到,只要Generator函数内部部署了`try...catch`代码块,那么遍历器的`throw`方法抛出的错误,不影响下一次遍历。
|
||||
上面代码中,`g.throw`方法被捕获以后,自动执行了一次`next`方法,所以会打印`b`。另外,也可以看到,只要 Generator 函数内部部署了`try...catch`代码块,那么遍历器的`throw`方法抛出的错误,不影响下一次遍历。
|
||||
|
||||
另外,`throw`命令与`g.throw`方法是无关的,两者互不影响。
|
||||
|
||||
@ -576,12 +577,12 @@ try {
|
||||
|
||||
上面代码中,`throw`命令抛出的错误不会影响到遍历器的状态,所以两次执行`next`方法,都进行了正确的操作。
|
||||
|
||||
这种函数体内捕获错误的机制,大大方便了对错误的处理。多个`yield`语句,可以只用一个`try...catch`代码块来捕获错误。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数内部写一个错误处理语句,现在只在Generator函数内部写一次`catch`语句就可以了。
|
||||
这种函数体内捕获错误的机制,大大方便了对错误的处理。多个`yield`表达式,可以只用一个`try...catch`代码块来捕获错误。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数内部写一个错误处理语句,现在只在 Generator 函数内部写一次`catch`语句就可以了。
|
||||
|
||||
Generator函数体外抛出的错误,可以在函数体内捕获;反过来,Generator函数体内抛出的错误,也可以被函数体外的`catch`捕获。
|
||||
Generator 函数体外抛出的错误,可以在函数体内捕获;反过来,Generator 函数体内抛出的错误,也可以被函数体外的`catch`捕获。
|
||||
|
||||
```javascript
|
||||
function *foo() {
|
||||
function* foo() {
|
||||
var x = yield 3;
|
||||
var y = x.toUpperCase();
|
||||
yield y;
|
||||
@ -598,9 +599,9 @@ try {
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,第二个`next`方法向函数体内传入一个参数42,数值是没有`toUpperCase`方法的,所以会抛出一个TypeError错误,被函数体外的`catch`捕获。
|
||||
上面代码中,第二个`next`方法向函数体内传入一个参数 42,数值是没有`toUpperCase`方法的,所以会抛出一个 TypeError 错误,被函数体外的`catch`捕获。
|
||||
|
||||
一旦Generator执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用`next`方法,将返回一个`value`属性等于`undefined`、`done`属性等于`true`的对象,即JavaScript引擎认为这个Generator已经运行结束了。
|
||||
一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用`next`方法,将返回一个`value`属性等于`undefined`、`done`属性等于`true`的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。
|
||||
|
||||
```javascript
|
||||
function* g() {
|
||||
@ -644,11 +645,11 @@ log(g());
|
||||
// caller done
|
||||
```
|
||||
|
||||
上面代码一共三次运行`next`方法,第二次运行的时候会抛出错误,然后第三次运行的时候,Generator函数就已经结束了,不再执行下去了。
|
||||
上面代码一共三次运行`next`方法,第二次运行的时候会抛出错误,然后第三次运行的时候,Generator 函数就已经结束了,不再执行下去了。
|
||||
|
||||
## Generator.prototype.return()
|
||||
|
||||
Generator函数返回的遍历器对象,还有一个`return`方法,可以返回给定的值,并且终结遍历Generator函数。
|
||||
Generator 函数返回的遍历器对象,还有一个`return`方法,可以返回给定的值,并且终结遍历 Generator 函数。
|
||||
|
||||
```javascript
|
||||
function* gen() {
|
||||
@ -664,7 +665,7 @@ g.return('foo') // { value: "foo", done: true }
|
||||
g.next() // { value: undefined, done: true }
|
||||
```
|
||||
|
||||
上面代码中,遍历器对象`g`调用`return`方法后,返回值的`value`属性就是`return`方法的参数`foo`。并且,Generator函数的遍历就终止了,返回值的`done`属性为`true`,以后再调用`next`方法,`done`属性总是返回`true`。
|
||||
上面代码中,遍历器对象`g`调用`return`方法后,返回值的`value`属性就是`return`方法的参数`foo`。并且,Generator 函数的遍历就终止了,返回值的`done`属性为`true`,以后再调用`next`方法,`done`属性总是返回`true`。
|
||||
|
||||
如果`return`方法调用时,不提供参数,则返回值的`value`属性为`undefined`。
|
||||
|
||||
@ -681,7 +682,7 @@ g.next() // { value: 1, done: false }
|
||||
g.return() // { value: undefined, done: true }
|
||||
```
|
||||
|
||||
如果Generator函数内部有`try...finally`代码块,那么`return`方法会推迟到`finally`代码块执行完再执行。
|
||||
如果 Generator 函数内部有`try...finally`代码块,那么`return`方法会推迟到`finally`代码块执行完再执行。
|
||||
|
||||
```javascript
|
||||
function* numbers () {
|
||||
@ -695,19 +696,57 @@ function* numbers () {
|
||||
}
|
||||
yield 6;
|
||||
}
|
||||
var g = numbers()
|
||||
g.next() // { done: false, value: 1 }
|
||||
g.next() // { done: false, value: 2 }
|
||||
g.return(7) // { done: false, value: 4 }
|
||||
g.next() // { done: false, value: 5 }
|
||||
g.next() // { done: true, value: 7 }
|
||||
var g = numbers();
|
||||
g.next() // { value: 1, done: false }
|
||||
g.next() // { value: 2, done: false }
|
||||
g.return(7) // { value: 4, done: false }
|
||||
g.next() // { value: 5, done: false }
|
||||
g.next() // { value: 7, done: true }
|
||||
```
|
||||
|
||||
上面代码中,调用`return`方法后,就开始执行`finally`代码块,然后等到`finally`代码块执行完,再执行`return`方法。
|
||||
|
||||
## yield*语句
|
||||
## next()、throw()、return() 的共同点
|
||||
|
||||
如果在Generater函数内部,调用另一个Generator函数,默认情况下是没有效果的。
|
||||
网友 vision57 提出,`next()`、`throw()`、`return()`这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换`yield`表达式。
|
||||
|
||||
`next()`是将`yield`表达式替换成一个值。
|
||||
|
||||
```javascript
|
||||
const g = function* (x, y) {
|
||||
let result = yield x + y;
|
||||
return result;
|
||||
};
|
||||
|
||||
const gen = g(1, 2);
|
||||
gen.next(); // Object {value: 3, done: false}
|
||||
|
||||
gen.next(1); // Object {value: 1, done: true}
|
||||
// 相当于将 let result = yield x + y
|
||||
// 替换成 let result = 1;
|
||||
```
|
||||
|
||||
上面代码中,第二个`next(1)`方法就相当于将`yield`表达式替换成一个值`1`。如果`next`方法没有参数,就相当于替换成`undefined`。
|
||||
|
||||
`throw()`是将`yield`表达式替换成一个`throw`语句。
|
||||
|
||||
```javascript
|
||||
gen.throw(new Error('出错了')); // Uncaught Error: 出错了
|
||||
// 相当于将 let result = yield x + y
|
||||
// 替换成 let result = throw(new Error('出错了'));
|
||||
```
|
||||
|
||||
`return()`是将`yield`表达式替换成一个`return`语句。
|
||||
|
||||
```javascript
|
||||
gen.return(2); // Object {value: 2, done: true}
|
||||
// 相当于将 let result = yield x + y
|
||||
// 替换成 let result = return 2;
|
||||
```
|
||||
|
||||
## yield\* 表达式
|
||||
|
||||
如果在 Generator 函数内部,调用另一个 Generator 函数,默认情况下是没有效果的。
|
||||
|
||||
```javascript
|
||||
function* foo() {
|
||||
@ -728,9 +767,9 @@ for (let v of bar()){
|
||||
// "y"
|
||||
```
|
||||
|
||||
上面代码中,`foo`和`bar`都是Generator函数,在`bar`里面调用`foo`,是不会有效果的。
|
||||
上面代码中,`foo`和`bar`都是 Generator 函数,在`bar`里面调用`foo`,是不会有效果的。
|
||||
|
||||
这个就需要用到`yield*`语句,用来在一个Generator函数里面执行另一个Generator函数。
|
||||
这个就需要用到`yield*`表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。
|
||||
|
||||
```javascript
|
||||
function* bar() {
|
||||
@ -797,7 +836,7 @@ gen.next().value // "close"
|
||||
|
||||
上面例子中,`outer2`使用了`yield*`,`outer1`没使用。结果就是,`outer1`返回一个遍历器对象,`outer2`返回该遍历器对象的内部值。
|
||||
|
||||
从语法角度看,如果`yield`命令后面跟的是一个遍历器对象,需要在`yield`命令后面加上星号,表明它返回的是一个遍历器对象。这被称为`yield*`语句。
|
||||
从语法角度看,如果`yield`表达式后面跟的是一个遍历器对象,需要在`yield`表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为`yield*`表达式。
|
||||
|
||||
```javascript
|
||||
let delegatedIterator = (function* () {
|
||||
@ -820,9 +859,9 @@ for(let value of delegatingIterator) {
|
||||
// "Ok, bye."
|
||||
```
|
||||
|
||||
上面代码中,`delegatingIterator`是代理者,`delegatedIterator`是被代理者。由于`yield* delegatedIterator`语句得到的值,是一个遍历器,所以要用星号表示。运行结果就是使用一个遍历器,遍历了多个Generator函数,有递归的效果。
|
||||
上面代码中,`delegatingIterator`是代理者,`delegatedIterator`是被代理者。由于`yield* delegatedIterator`语句得到的值,是一个遍历器,所以要用星号表示。运行结果就是使用一个遍历器,遍历了多个 Generator 函数,有递归的效果。
|
||||
|
||||
`yield*`后面的Generator函数(没有`return`语句时),等同于在Generator函数内部,部署一个`for...of`循环。
|
||||
`yield*`后面的 Generator 函数(没有`return`语句时),等同于在 Generator 函数内部,部署一个`for...of`循环。
|
||||
|
||||
```javascript
|
||||
function* concat(iter1, iter2) {
|
||||
@ -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*`后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。
|
||||
|
||||
@ -856,7 +895,7 @@ gen().next() // { value:"a", done:false }
|
||||
|
||||
上面代码中,`yield`命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象。
|
||||
|
||||
实际上,任何数据结构只要有Iterator接口,就可以被`yield*`遍历。
|
||||
实际上,任何数据结构只要有 Iterator 接口,就可以被`yield*`遍历。
|
||||
|
||||
```javascript
|
||||
let read = (function* () {
|
||||
@ -868,9 +907,9 @@ read.next().value // "hello"
|
||||
read.next().value // "h"
|
||||
```
|
||||
|
||||
上面代码中,`yield`语句返回整个字符串,`yield*`语句返回单个字符。因为字符串具有Iterator接口,所以被`yield*`遍历。
|
||||
上面代码中,`yield`表达式返回整个字符串,`yield*`语句返回单个字符。因为字符串具有 Iterator 接口,所以被`yield*`遍历。
|
||||
|
||||
如果被代理的Generator函数有`return`语句,那么就可以向代理它的Generator函数返回数据。
|
||||
如果被代理的 Generator 函数有`return`语句,那么就可以向代理它的 Generator 函数返回数据。
|
||||
|
||||
```javascript
|
||||
function *foo() {
|
||||
@ -988,9 +1027,9 @@ result
|
||||
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']
|
||||
```
|
||||
|
||||
## 作为对象属性的Generator函数
|
||||
## 作为对象属性的 Generator 函数
|
||||
|
||||
如果一个对象的属性是Generator函数,可以简写成下面的形式。
|
||||
如果一个对象的属性是 Generator 函数,可以简写成下面的形式。
|
||||
|
||||
```javascript
|
||||
let obj = {
|
||||
@ -1000,7 +1039,7 @@ let obj = {
|
||||
};
|
||||
```
|
||||
|
||||
上面代码中,`myGeneratorMethod`属性前面有一个星号,表示这个属性是一个Generator函数。
|
||||
上面代码中,`myGeneratorMethod`属性前面有一个星号,表示这个属性是一个 Generator 函数。
|
||||
|
||||
它的完整形式如下,与上面的写法是等价的。
|
||||
|
||||
@ -1012,9 +1051,9 @@ let obj = {
|
||||
};
|
||||
```
|
||||
|
||||
## Generator函数的`this`
|
||||
## Generator 函数的`this`
|
||||
|
||||
Generator函数总是返回一个遍历器,ES6规定这个遍历器是Generator函数的实例,也继承了Generator函数的`prototype`对象上的方法。
|
||||
Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的`prototype`对象上的方法。
|
||||
|
||||
```javascript
|
||||
function* g() {}
|
||||
@ -1029,7 +1068,7 @@ obj instanceof g // true
|
||||
obj.hello() // 'hi!'
|
||||
```
|
||||
|
||||
上面代码表明,Generator函数`g`返回的遍历器`obj`,是`g`的实例,而且继承了`g.prototype`。但是,如果把`g`当作普通的构造函数,并不会生效,因为`g`返回的总是遍历器对象,而不是`this`对象。
|
||||
上面代码表明,Generator 函数`g`返回的遍历器`obj`,是`g`的实例,而且继承了`g.prototype`。但是,如果把`g`当作普通的构造函数,并不会生效,因为`g`返回的总是遍历器对象,而不是`this`对象。
|
||||
|
||||
```javascript
|
||||
function* g() {
|
||||
@ -1040,9 +1079,9 @@ let obj = g();
|
||||
obj.a // undefined
|
||||
```
|
||||
|
||||
上面代码中,Generator函数`g`在`this`对象上面添加了一个属性`a`,但是`obj`对象拿不到这个属性。
|
||||
上面代码中,Generator 函数`g`在`this`对象上面添加了一个属性`a`,但是`obj`对象拿不到这个属性。
|
||||
|
||||
Generator函数也不能跟`new`命令一起用,会报错。
|
||||
Generator 函数也不能跟`new`命令一起用,会报错。
|
||||
|
||||
```javascript
|
||||
function* F() {
|
||||
@ -1056,9 +1095,9 @@ new F()
|
||||
|
||||
上面代码中,`new`命令跟构造函数`F`一起使用,结果报错,因为`F`不是构造函数。
|
||||
|
||||
那么,有没有办法让Generator函数返回一个正常的对象实例,既可以用`next`方法,又可以获得正常的`this`?
|
||||
那么,有没有办法让 Generator 函数返回一个正常的对象实例,既可以用`next`方法,又可以获得正常的`this`?
|
||||
|
||||
下面是一个变通方法。首先,生成一个空对象,使用`call`方法绑定Generator函数内部的`this`。这样,构造函数调用以后,这个空对象就是Generator函数的实例对象了。
|
||||
下面是一个变通方法。首先,生成一个空对象,使用`call`方法绑定 Generator 函数内部的`this`。这样,构造函数调用以后,这个空对象就是 Generator 函数的实例对象了。
|
||||
|
||||
```javascript
|
||||
function* F() {
|
||||
@ -1078,7 +1117,7 @@ obj.b // 2
|
||||
obj.c // 3
|
||||
```
|
||||
|
||||
上面代码中,首先是`F`内部的`this`对象绑定`obj`对象,然后调用它,返回一个Iterator对象。这个对象执行三次`next`方法(因为`F`内部有两个`yield`语句),完成F内部所有代码的运行。这时,所有内部属性都绑定在`obj`对象上了,因此`obj`对象也就成了`F`的实例。
|
||||
上面代码中,首先是`F`内部的`this`对象绑定`obj`对象,然后调用它,返回一个 Iterator 对象。这个对象执行三次`next`方法(因为`F`内部有两个`yield`表达式),完成 F 内部所有代码的运行。这时,所有内部属性都绑定在`obj`对象上了,因此`obj`对象也就成了`F`的实例。
|
||||
|
||||
上面代码中,执行的是遍历器对象`f`,但是生成的对象实例是`obj`,有没有办法将这两个对象统一呢?
|
||||
|
||||
@ -1127,9 +1166,9 @@ f.c // 3
|
||||
|
||||
## 含义
|
||||
|
||||
### Generator与状态机
|
||||
### Generator 与状态机
|
||||
|
||||
Generator是实现状态机的最佳结构。比如,下面的clock函数就是一个状态机。
|
||||
Generator 是实现状态机的最佳结构。比如,下面的`clock`函数就是一个状态机。
|
||||
|
||||
```javascript
|
||||
var ticking = true;
|
||||
@ -1142,10 +1181,10 @@ var clock = function() {
|
||||
}
|
||||
```
|
||||
|
||||
上面代码的clock函数一共有两种状态(Tick和Tock),每运行一次,就改变一次状态。这个函数如果用Generator实现,就是下面这样。
|
||||
上面代码的`clock`函数一共有两种状态(`Tick`和`Tock`),每运行一次,就改变一次状态。这个函数如果用 Generator 实现,就是下面这样。
|
||||
|
||||
```javascript
|
||||
var clock = function*() {
|
||||
var clock = function* () {
|
||||
while (true) {
|
||||
console.log('Tick!');
|
||||
yield;
|
||||
@ -1155,9 +1194,9 @@ var clock = function*() {
|
||||
};
|
||||
```
|
||||
|
||||
上面的Generator实现与ES5实现对比,可以看到少了用来保存状态的外部变量`ticking`,这样就更简洁,更安全(状态不会被非法篡改)、更符合函数式编程的思想,在写法上也更优雅。Generator之所以可以不用外部变量保存状态,是因为它本身就包含了一个状态信息,即目前是否处于暂停态。
|
||||
上面的 Generator 实现与 ES5 实现对比,可以看到少了用来保存状态的外部变量`ticking`,这样就更简洁,更安全(状态不会被非法篡改)、更符合函数式编程的思想,在写法上也更优雅。Generator 之所以可以不用外部变量保存状态,是因为它本身就包含了一个状态信息,即目前是否处于暂停态。
|
||||
|
||||
### Generator与协程
|
||||
### Generator 与协程
|
||||
|
||||
协程(coroutine)是一种程序运行的方式,可以理解成“协作的线程”或“协作的函数”。协程既可以用单线程实现,也可以用多线程实现。前者是一种特殊的子例程,后者是一种特殊的线程。
|
||||
|
||||
@ -1171,19 +1210,43 @@ 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`上下文重新加入堆栈,变成当前的上下文,重新恢复执行。
|
||||
|
||||
## 应用
|
||||
|
||||
Generator可以暂停函数执行,返回任意表达式的值。这种特点使得Generator有多种应用场景。
|
||||
Generator 可以暂停函数执行,返回任意表达式的值。这种特点使得 Generator 有多种应用场景。
|
||||
|
||||
### (1)异步操作的同步化表达
|
||||
|
||||
Generator函数的暂停执行的效果,意味着可以把异步操作写在yield语句里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield语句下面,反正要等到调用next方法时再执行。所以,Generator函数的一个重要实际意义就是用来处理异步操作,改写回调函数。
|
||||
Generator 函数的暂停执行的效果,意味着可以把异步操作写在`yield`表达式里面,等到调用`next`方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在`yield`表达式下面,反正要等到调用`next`方法时再执行。所以,Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数。
|
||||
|
||||
```javascript
|
||||
function* loadUI() {
|
||||
@ -1199,9 +1262,9 @@ loader.next()
|
||||
loader.next()
|
||||
```
|
||||
|
||||
上面代码表示,第一次调用loadUI函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用next方法,则会显示Loading界面,并且异步加载数据。等到数据加载完成,再一次使用next方法,则会隐藏Loading界面。可以看到,这种写法的好处是所有Loading界面的逻辑,都被封装在一个函数,按部就班非常清晰。
|
||||
上面代码中,第一次调用`loadUI`函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用`next`方法,则会显示`Loading`界面(`showLoadingScreen`),并且异步加载数据(`loadUIDataAsynchronously`)。等到数据加载完成,再一次使用`next`方法,则会隐藏`Loading`界面。可以看到,这种写法的好处是所有`Loading`界面的逻辑,都被封装在一个函数,按部就班非常清晰。
|
||||
|
||||
Ajax是典型的异步操作,通过Generator函数部署Ajax操作,可以用同步的方式表达。
|
||||
Ajax 是典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。
|
||||
|
||||
```javascript
|
||||
function* main() {
|
||||
@ -1220,9 +1283,9 @@ var it = main();
|
||||
it.next();
|
||||
```
|
||||
|
||||
上面代码的main函数,就是通过Ajax操作获取数据。可以看到,除了多了一个yield,它几乎与同步操作的写法完全一样。注意,makeAjaxCall函数中的next方法,必须加上response参数,因为yield语句构成的表达式,本身是没有值的,总是等于undefined。
|
||||
上面代码的`main`函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个`yield`,它几乎与同步操作的写法完全一样。注意,`makeAjaxCall`函数中的`next`方法,必须加上`response`参数,因为`yield`表达式,本身是没有值的,总是等于`undefined`。
|
||||
|
||||
下面是另一个例子,通过Generator函数逐行读取文本文件。
|
||||
下面是另一个例子,通过 Generator 函数逐行读取文本文件。
|
||||
|
||||
```javascript
|
||||
function* numbers() {
|
||||
@ -1237,7 +1300,7 @@ function* numbers() {
|
||||
}
|
||||
```
|
||||
|
||||
上面代码打开文本文件,使用yield语句可以手动逐行读取文件。
|
||||
上面代码打开文本文件,使用`yield`表达式可以手动逐行读取文件。
|
||||
|
||||
### (2)控制流管理
|
||||
|
||||
@ -1255,7 +1318,7 @@ step1(function (value1) {
|
||||
});
|
||||
```
|
||||
|
||||
采用Promise改写上面的代码。
|
||||
采用 Promise 改写上面的代码。
|
||||
|
||||
```javascript
|
||||
Promise.resolve(step1)
|
||||
@ -1270,7 +1333,7 @@ Promise.resolve(step1)
|
||||
.done();
|
||||
```
|
||||
|
||||
上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量Promise的语法。Generator函数可以进一步改善代码运行流程。
|
||||
上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量 Promise 的语法。Generator 函数可以进一步改善代码运行流程。
|
||||
|
||||
```javascript
|
||||
function* longRunningTask(value1) {
|
||||
@ -1316,22 +1379,22 @@ function *iterateSteps(steps){
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,数组`steps`封装了一个任务的多个步骤,Generator函数`iterateSteps`则是依次为这些步骤加上`yield`命令。
|
||||
上面代码中,数组`steps`封装了一个任务的多个步骤,Generator 函数`iterateSteps`则是依次为这些步骤加上`yield`命令。
|
||||
|
||||
将任务分解成步骤之后,还可以将项目分解成多个依次执行的任务。
|
||||
|
||||
```javascript
|
||||
let jobs = [job1, job2, job3];
|
||||
|
||||
function *iterateJobs(jobs){
|
||||
function* iterateJobs(jobs){
|
||||
for (var i=0; i< jobs.length; i++){
|
||||
var job = jobs[i];
|
||||
yield *iterateSteps(job.steps);
|
||||
yield* iterateSteps(job.steps);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,数组`jobs`封装了一个项目的多个任务,Generator函数`iterateJobs`则是依次为这些任务加上`yield *`命令。
|
||||
上面代码中,数组`jobs`封装了一个项目的多个任务,Generator 函数`iterateJobs`则是依次为这些任务加上`yield*`命令。
|
||||
|
||||
最后,就可以用`for...of`循环一次性依次执行所有任务的所有步骤。
|
||||
|
||||
@ -1356,9 +1419,9 @@ while (!res.done){
|
||||
}
|
||||
```
|
||||
|
||||
### (3)部署Iterator接口
|
||||
### (3)部署 Iterator 接口
|
||||
|
||||
利用Generator函数,可以在任意对象上部署Iterator接口。
|
||||
利用 Generator 函数,可以在任意对象上部署 Iterator 接口。
|
||||
|
||||
```javascript
|
||||
function* iterEntries(obj) {
|
||||
@ -1379,9 +1442,9 @@ for (let [key, value] of iterEntries(myObj)) {
|
||||
// bar 7
|
||||
```
|
||||
|
||||
上述代码中,`myObj`是一个普通对象,通过`iterEntries`函数,就有了Iterator接口。也就是说,可以在任意对象上部署`next`方法。
|
||||
上述代码中,`myObj`是一个普通对象,通过`iterEntries`函数,就有了 Iterator 接口。也就是说,可以在任意对象上部署`next`方法。
|
||||
|
||||
下面是一个对数组部署Iterator接口的例子,尽管数组原生具有这个接口。
|
||||
下面是一个对数组部署 Iterator 接口的例子,尽管数组原生具有这个接口。
|
||||
|
||||
```javascript
|
||||
function* makeSimpleGenerator(array){
|
||||
@ -1401,7 +1464,7 @@ gen.next().done // true
|
||||
|
||||
### (4)作为数据结构
|
||||
|
||||
Generator可以看作是数据结构,更确切地说,可以看作是一个数组结构,因为Generator函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口。
|
||||
Generator 可以看作是数据结构,更确切地说,可以看作是一个数组结构,因为 Generator 函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口。
|
||||
|
||||
```javascript
|
||||
function *doStuff() {
|
||||
@ -1411,7 +1474,7 @@ function *doStuff() {
|
||||
}
|
||||
```
|
||||
|
||||
上面代码就是依次返回三个函数,但是由于使用了Generator函数,导致可以像处理数组那样,处理这三个返回的函数。
|
||||
上面代码就是依次返回三个函数,但是由于使用了 Generator 函数,导致可以像处理数组那样,处理这三个返回的函数。
|
||||
|
||||
```javascript
|
||||
for (task of doStuff()) {
|
||||
@ -1419,7 +1482,7 @@ for (task of doStuff()) {
|
||||
}
|
||||
```
|
||||
|
||||
实际上,如果用ES5表达,完全可以用数组模拟Generator的这种用法。
|
||||
实际上,如果用 ES5 表达,完全可以用数组模拟 Generator 的这种用法。
|
||||
|
||||
```javascript
|
||||
function doStuff() {
|
||||
@ -1431,5 +1494,4 @@ function doStuff() {
|
||||
}
|
||||
```
|
||||
|
||||
上面的函数,可以用一模一样的for...of循环处理!两相一比较,就不难看出Generator使得数据或者操作,具备了类似数组的接口。
|
||||
|
||||
上面的函数,可以用一模一样的`for...of`循环处理!两相一比较,就不难看出 Generator 使得数据或者操作,具备了类似数组的接口。
|
||||
|
239
docs/intro.md
239
docs/intro.md
@ -1,36 +1,38 @@
|
||||
# ECMAScript 6简介
|
||||
# ECMAScript 6 简介
|
||||
|
||||
ECMAScript 6.0(以下简称ES6)是JavaScript语言的下一代标准,已经在2015年6月正式发布了。它的目标,是使得JavaScript语言可以用来编写复杂的大型应用程序,成为企业级开发语言。
|
||||
ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。
|
||||
|
||||
## ECMAScript和JavaScript的关系
|
||||
## ECMAScript 和 JavaScript 的关系
|
||||
|
||||
一个常见的问题是,ECMAScript和JavaScript到底是什么关系?
|
||||
一个常见的问题是,ECMAScript 和 JavaScript 到底是什么关系?
|
||||
|
||||
要讲清楚这个问题,需要回顾历史。1996年11月,JavaScript的创造者Netscape公司,决定将JavaScript提交给国际标准化组织ECMA,希望这种语言能够成为国际标准。次年,ECMA发布262号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为ECMAScript,这个版本就是1.0版。
|
||||
要讲清楚这个问题,需要回顾历史。1996 年 11 月,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给国际标准化组织 ECMA,希望这种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript,这个版本就是 1.0 版。
|
||||
|
||||
该标准从一开始就是针对JavaScript语言制定的,但是之所以不叫JavaScript,有两个原因。一是商标,Java是Sun公司的商标,根据授权协议,只有Netscape公司可以合法地使用JavaScript这个名字,且JavaScript本身也已经被Netscape公司注册为商标。二是想体现这门语言的制定者是ECMA,不是Netscape,这样有利于保证这门语言的开放性和中立性。
|
||||
该标准从一开始就是针对 JavaScript 语言制定的,但是之所以不叫 JavaScript,有两个原因。一是商标,Java 是 Sun 公司的商标,根据授权协议,只有 Netscape 公司可以合法地使用 JavaScript 这个名字,且 JavaScript 本身也已经被 Netscape 公司注册为商标。二是想体现这门语言的制定者是 ECMA,不是 Netscape,这样有利于保证这门语言的开放性和中立性。
|
||||
|
||||
因此,ECMAScript和JavaScript的关系是,前者是后者的规格,后者是前者的一种实现(另外的ECMAScript方言还有Jscript和ActionScript)。日常场合,这两个词是可以互换的。
|
||||
因此,ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 Jscript 和 ActionScript)。日常场合,这两个词是可以互换的。
|
||||
|
||||
## 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 版等等。
|
||||
|
||||
但是,标准的制定者不想这样做。他们想让标准的升级成为常规流程:任何人在任何时候,都可以向标准委员会提交新语法的提案,然后标准委员会每个月开一次会,评估这些提案是否可以接受,需要哪些改进。如果经过多次会议以后,一个提案足够成熟了,就可以正式进入标准了。这就是说,标准的版本升级成为了一个不断滚动的流程,每个月都会有变动。
|
||||
|
||||
标准委员会最终决定,标准在每年的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 1 - Proposal(征求意见阶段)
|
||||
@ -38,90 +40,47 @@ ES6的第一个版本,就这样在2015年6月发布了,正式名称就是《
|
||||
- Stage 3 - Candidate(候选人阶段)
|
||||
- Stage 4 - Finished(定案阶段)
|
||||
|
||||
一个提案只要能进入Stage 2,就差不多等于肯定会包括在以后的正式标准里面。ECMAScript当前的所有提案,可以在TC39的官方网站[Github.com/tc39/ecma262](https://github.com/tc39/ecma262)查看。
|
||||
一个提案只要能进入 Stage 2,就差不多肯定会包括在以后的正式标准里面。ECMAScript 当前的所有提案,可以在 TC39 的官方网站[Github.com/tc39/ecma262](https://github.com/tc39/ecma262)查看。
|
||||
|
||||
本书的写作目标之一,是跟踪ECMAScript语言的最新进展,介绍5.1版本以后所有的新语法。对于那些明确将要列入标准的新语法,尤其是那些Babel转码器(详见后文)已经支持的功能,也将予以介绍。
|
||||
本书的写作目标之一,是跟踪 ECMAScript 语言的最新进展,介绍 5.1 版本以后所有的新语法。对于那些明确或很有希望,将要列入标准的新语法,都将予以介绍。
|
||||
|
||||
## ECMAScript的历史
|
||||
## ECMAScript 的历史
|
||||
|
||||
ES6从开始制定到最后发布,整整用了15年。
|
||||
ES6 从开始制定到最后发布,整整用了 15 年。
|
||||
|
||||
前面提到,ECMAScript 1.0是1997年发布的,接下来的两年,连续发布了ECMAScript 2.0(1998年6月)和ECMAScript 3.0(1999年12月)。3.0版是一个巨大的成功,在业界得到广泛支持,成为通行标准,奠定了JavaScript语言的基本语法,以后的版本完全继承。直到今天,初学者一开始学习JavaScript,其实就是在学3.0版的语法。
|
||||
前面提到,ECMAScript 1.0 是 1997 年发布的,接下来的两年,连续发布了 ECMAScript 2.0(1998 年 6 月)和 ECMAScript 3.0(1999 年 12 月)。3.0 版是一个巨大的成功,在业界得到广泛支持,成为通行标准,奠定了 JavaScript 语言的基本语法,以后的版本完全继承。直到今天,初学者一开始学习 JavaScript,其实就是在学 3.0 版的语法。
|
||||
|
||||
2000年,ECMAScript 4.0开始酝酿。这个版本最后没有通过,但是它的大部分内容被ES6继承了。因此,ES6制定的起点其实是2000年。
|
||||
2000 年,ECMAScript 4.0 开始酝酿。这个版本最后没有通过,但是它的大部分内容被 ES6 继承了。因此,ES6 制定的起点其实是 2000 年。
|
||||
|
||||
为什么ES4没有通过呢?因为这个版本太激进了,对ES3做了彻底升级,导致标准委员会的一些成员不愿意接受。ECMA的第39号技术专家委员会(Technical Committee 39,简称TC39)负责制订ECMAScript标准,成员包括Microsoft、Mozilla、Google等大公司。
|
||||
为什么 ES4 没有通过呢?因为这个版本太激进了,对 ES3 做了彻底升级,导致标准委员会的一些成员不愿意接受。ECMA 的第 39 号技术专家委员会(Technical Committee 39,简称 TC39)负责制订 ECMAScript 标准,成员包括 Microsoft、Mozilla、Google 等大公司。
|
||||
|
||||
2007年10月,ECMAScript 4.0版草案发布,本来预计次年8月发布正式版本。但是,各方对于是否通过这个标准,发生了严重分歧。以Yahoo、Microsoft、Google为首的大公司,反对JavaScript的大幅升级,主张小幅改动;以JavaScript创造者Brendan Eich为首的Mozilla公司,则坚持当前的草案。
|
||||
2007 年 10 月,ECMAScript 4.0 版草案发布,本来预计次年 8 月发布正式版本。但是,各方对于是否通过这个标准,发生了严重分歧。以 Yahoo、Microsoft、Google 为首的大公司,反对 JavaScript 的大幅升级,主张小幅改动;以 JavaScript 创造者 Brendan Eich 为首的 Mozilla 公司,则坚持当前的草案。
|
||||
|
||||
2008年7月,由于对于下一个版本应该包括哪些功能,各方分歧太大,争论过于激烈,ECMA开会决定,中止ECMAScript 4.0的开发,将其中涉及现有功能改善的一小部分,发布为ECMAScript 3.1,而将其他激进的设想扩大范围,放入以后的版本,由于会议的气氛,该版本的项目代号起名为Harmony(和谐)。会后不久,ECMAScript 3.1就改名为ECMAScript 5。
|
||||
2008 年 7 月,由于对于下一个版本应该包括哪些功能,各方分歧太大,争论过于激烈,ECMA 开会决定,中止 ECMAScript 4.0 的开发,将其中涉及现有功能改善的一小部分,发布为 ECMAScript 3.1,而将其他激进的设想扩大范围,放入以后的版本,由于会议的气氛,该版本的项目代号起名为 Harmony(和谐)。会后不久,ECMAScript 3.1 就改名为 ECMAScript 5。
|
||||
|
||||
2009年12月,ECMAScript 5.0版正式发布。Harmony项目则一分为二,一些较为可行的设想定名为JavaScript.next继续开发,后来演变成ECMAScript 6;一些不是很成熟的设想,则被视为JavaScript.next.next,在更远的将来再考虑推出。TC39委员会的总体考虑是,ES5与ES3基本保持兼容,较大的语法修正和新功能加入,将由JavaScript.next完成。当时,JavaScript.next指的是ES6,第六版发布以后,就指ES7。TC39的判断是,ES5会在2013年的年中成为JavaScript开发的主流标准,并在此后五年中一直保持这个位置。
|
||||
2009 年 12 月,ECMAScript 5.0 版正式发布。Harmony 项目则一分为二,一些较为可行的设想定名为 JavaScript.next 继续开发,后来演变成 ECMAScript 6;一些不是很成熟的设想,则被视为 JavaScript.next.next,在更远的将来再考虑推出。TC39 委员会的总体考虑是,ES5 与 ES3 基本保持兼容,较大的语法修正和新功能加入,将由 JavaScript.next 完成。当时,JavaScript.next 指的是 ES6,第六版发布以后,就指 ES7。TC39 的判断是,ES5 会在 2013 年的年中成为 JavaScript 开发的主流标准,并在此后五年中一直保持这个位置。
|
||||
|
||||
2011年6月,ECMAscript 5.1版发布,并且成为ISO国际标准(ISO/IEC 16262:2011)。
|
||||
2011 年 6 月,ECMAscript 5.1 版发布,并且成为 ISO 国际标准(ISO/IEC 16262:2011)。
|
||||
|
||||
2013年3月,ECMAScript 6草案冻结,不再添加新功能。新的功能设想将被放到ECMAScript 7。
|
||||
2013 年 3 月,ECMAScript 6 草案冻结,不再添加新功能。新的功能设想将被放到 ECMAScript 7。
|
||||
|
||||
2013年12月,ECMAScript 6草案发布。然后是12个月的讨论期,听取各方反馈。
|
||||
2013 年 12 月,ECMAScript 6 草案发布。然后是 12 个月的讨论期,听取各方反馈。
|
||||
|
||||
2015年6月,ECMAScript 6正式通过,成为国际标准。从2000年算起,这时已经过去了15年。
|
||||
2015 年 6 月,ECMAScript 6 正式通过,成为国际标准。从 2000 年算起,这时已经过去了 15 年。
|
||||
|
||||
## 部署进度
|
||||
|
||||
各大浏览器的最新版本,对ES6的支持可以查看[kangax.github.io/es5-compat-table/es6/](http://kangax.github.io/es5-compat-table/es6/)。随着时间的推移,支持度已经越来越高了,ES6的大部分特性都实现了。
|
||||
各大浏览器的最新版本,对 ES6 的支持可以查看[kangax.github.io/es5-compat-table/es6/](https://kangax.github.io/es5-compat-table/es6/)。随着时间的推移,支持度已经越来越高了,超过 90%的 ES6 语法特性都实现了。
|
||||
|
||||
Node.js是JavaScript语言的服务器运行环境,对ES6的支持度比浏览器更高。通过Node,可以体验更多ES6的特性。建议使用版本管理工具[nvm](https://github.com/creationix/nvm),来安装Node,因为可以自由切换版本。不过,`nvm`不支持Windows系统,如果你使用Windows系统,下面的操作可以改用[nvmw](https://github.com/hakobera/nvmw)或[nvm-windows](https://github.com/coreybutler/nvm-windows)代替。
|
||||
|
||||
安装nvm需要打开命令行窗口,运行下面的命令。
|
||||
|
||||
```bash
|
||||
$ curl -o- https://raw.githubusercontent.com/creationix/nvm/<version number>/install.sh | bash
|
||||
```
|
||||
|
||||
上面命令的`version number`处,需要用版本号替换。本节写作时的版本号是`v0.29.0`。该命令运行后,`nvm`会默认安装在用户主目录的`.nvm`子目录。
|
||||
|
||||
然后,激活`nvm`。
|
||||
|
||||
```bash
|
||||
$ source ~/.nvm/nvm.sh
|
||||
```
|
||||
|
||||
激活以后,安装Node的最新版。
|
||||
|
||||
```bash
|
||||
$ nvm install node
|
||||
```
|
||||
|
||||
安装完成后,切换到该版本。
|
||||
|
||||
```bash
|
||||
$ nvm use node
|
||||
```
|
||||
|
||||
使用下面的命令,可以查看Node所有已经实现的ES6特性。
|
||||
Node 是 JavaScript 的服务器运行环境(runtime)。它对 ES6 的支持度更高。除了那些默认打开的功能,还有一些语法功能已经实现了,但是默认没有打开。使用下面的命令,可以查看 Node 已经实现的 ES6 特性。
|
||||
|
||||
```bash
|
||||
$ node --v8-options | grep harmony
|
||||
|
||||
--harmony_typeof
|
||||
--harmony_scoping
|
||||
--harmony_modules
|
||||
--harmony_symbols
|
||||
--harmony_proxies
|
||||
--harmony_collections
|
||||
--harmony_observation
|
||||
--harmony_generators
|
||||
--harmony_iteration
|
||||
--harmony_numeric_literals
|
||||
--harmony_strings
|
||||
--harmony_arrays
|
||||
--harmony_maths
|
||||
--harmony
|
||||
```
|
||||
|
||||
上面命令的输出结果,会因为版本的不同而有所不同。
|
||||
|
||||
我写了一个[ES-Checker](https://github.com/ruanyf/es-checker)模块,用来检查各种运行环境对ES6的支持情况。访问[ruanyf.github.io/es-checker](http://ruanyf.github.io/es-checker),可以看到您的浏览器支持ES6的程度。运行下面的命令,可以查看你正在使用的Node环境对ES6的支持程度。
|
||||
我写了一个工具 [ES-Checker](https://github.com/ruanyf/es-checker),用来检查各种运行环境对 ES6 的支持情况。访问[ruanyf.github.io/es-checker](http://ruanyf.github.io/es-checker),可以看到您的浏览器支持 ES6 的程度。运行下面的命令,可以查看你正在使用的 Node 环境对 ES6 的支持程度。
|
||||
|
||||
```bash
|
||||
$ npm install -g es-checker
|
||||
@ -133,9 +92,9 @@ Your runtime supports 57% of ECMAScript 6
|
||||
=========================================
|
||||
```
|
||||
|
||||
## Babel转码器
|
||||
## Babel 转码器
|
||||
|
||||
[Babel](https://babeljs.io/)是一个广泛使用的ES6转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。这意味着,你可以用ES6的方式编写程序,又不用担心现有环境是否支持。下面是一个例子。
|
||||
[Babel](https://babeljs.io/) 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在现有环境执行。这意味着,你可以用 ES6 的方式编写程序,又不用担心现有环境是否支持。下面是一个例子。
|
||||
|
||||
```javascript
|
||||
// 转码前
|
||||
@ -147,11 +106,11 @@ input.map(function (item) {
|
||||
});
|
||||
```
|
||||
|
||||
上面的原始代码用了箭头函数,这个特性还没有得到广泛支持,Babel将其转为普通函数,就能在现有的JavaScript环境执行了。
|
||||
上面的原始代码用了箭头函数,Babel 将其转为普通函数,就能在不支持箭头函数的 JavaScript 环境执行了。
|
||||
|
||||
### 配置文件`.babelrc`
|
||||
|
||||
Babel的配置文件是`.babelrc`,存放在项目的根目录下。使用Babel的第一步,就是配置这个文件。
|
||||
Babel 的配置文件是`.babelrc`,存放在项目的根目录下。使用 Babel 的第一步,就是配置这个文件。
|
||||
|
||||
该文件用来设置转码规则和插件,基本格式如下。
|
||||
|
||||
@ -165,13 +124,13 @@ Babel的配置文件是`.babelrc`,存放在项目的根目录下。使用Babel
|
||||
`presets`字段设定转码规则,官方提供以下的规则集,你可以根据需要安装。
|
||||
|
||||
```bash
|
||||
# ES2015转码规则
|
||||
$ npm install --save-dev babel-preset-es2015
|
||||
# 最新转码规则
|
||||
$ npm install --save-dev babel-preset-latest
|
||||
|
||||
# react转码规则
|
||||
# react 转码规则
|
||||
$ npm install --save-dev babel-preset-react
|
||||
|
||||
# ES7不同阶段语法提案的转码规则(共有4个阶段),选装一个
|
||||
# 不同阶段语法提案的转码规则(共有4个阶段),选装一个
|
||||
$ npm install --save-dev babel-preset-stage-0
|
||||
$ npm install --save-dev babel-preset-stage-1
|
||||
$ npm install --save-dev babel-preset-stage-2
|
||||
@ -183,7 +142,7 @@ $ npm install --save-dev babel-preset-stage-3
|
||||
```javascript
|
||||
{
|
||||
"presets": [
|
||||
"es2015",
|
||||
"latest",
|
||||
"react",
|
||||
"stage-2"
|
||||
],
|
||||
@ -191,11 +150,11 @@ $ npm install --save-dev babel-preset-stage-3
|
||||
}
|
||||
```
|
||||
|
||||
注意,以下所有Babel工具和模块的使用,都必须先写好`.babelrc`。
|
||||
注意,以下所有 Babel 工具和模块的使用,都必须先写好`.babelrc`。
|
||||
|
||||
### 命令行转码`babel-cli`
|
||||
|
||||
Babel提供`babel-cli`工具,用于命令行转码。
|
||||
Babel 提供`babel-cli`工具,用于命令行转码。
|
||||
|
||||
它的安装命令如下。
|
||||
|
||||
@ -225,7 +184,7 @@ $ babel src -d lib
|
||||
$ babel src -d lib -s
|
||||
```
|
||||
|
||||
上面代码是在全局环境下,进行Babel转码。这意味着,如果项目要运行,全局环境必须有Babel,也就是说项目产生了对环境的依赖。另一方面,这样做也无法支持不同项目使用不同版本的Babel。
|
||||
上面代码是在全局环境下,进行 Babel 转码。这意味着,如果项目要运行,全局环境必须有 Babel,也就是说项目产生了对环境的依赖。另一方面,这样做也无法支持不同项目使用不同版本的 Babel。
|
||||
|
||||
一个解决办法是将`babel-cli`安装在项目之中。
|
||||
|
||||
@ -256,9 +215,9 @@ $ npm run build
|
||||
|
||||
### babel-node
|
||||
|
||||
`babel-cli`工具自带一个`babel-node`命令,提供一个支持ES6的REPL环境。它支持Node的REPL环境的所有功能,而且可以直接运行ES6代码。
|
||||
`babel-cli`工具自带一个`babel-node`命令,提供一个支持 ES6 的 REPL 环境。它支持 Node 的 REPL 环境的所有功能,而且可以直接运行 ES6 代码。
|
||||
|
||||
它不用单独安装,而是随`babel-cli`一起安装。然后,执行`babel-node`就进入REPL环境。
|
||||
它不用单独安装,而是随`babel-cli`一起安装。然后,执行`babel-node`就进入 REPL 环境。
|
||||
|
||||
```bash
|
||||
$ babel-node
|
||||
@ -266,7 +225,7 @@ $ babel-node
|
||||
2
|
||||
```
|
||||
|
||||
`babel-node`命令可以直接运行ES6脚本。将上面的代码放入脚本文件`es6.js`,然后直接运行。
|
||||
`babel-node`命令可以直接运行 ES6 脚本。将上面的代码放入脚本文件`es6.js`,然后直接运行。
|
||||
|
||||
```bash
|
||||
$ babel-node es6.js
|
||||
@ -293,7 +252,7 @@ $ npm install --save-dev babel-cli
|
||||
|
||||
### babel-register
|
||||
|
||||
`babel-register`模块改写`require`命令,为它加上一个钩子。此后,每当使用`require`加载`.js`、`.jsx`、`.es`和`.es6`后缀名的文件,就会先用Babel进行转码。
|
||||
`babel-register`模块改写`require`命令,为它加上一个钩子。此后,每当使用`require`加载`.js`、`.jsx`、`.es`和`.es6`后缀名的文件,就会先用 Babel 进行转码。
|
||||
|
||||
```bash
|
||||
$ npm install --save-dev babel-register
|
||||
@ -312,7 +271,7 @@ require("./index.js");
|
||||
|
||||
### babel-core
|
||||
|
||||
如果某些代码需要调用Babel的API进行转码,就要使用`babel-core`模块。
|
||||
如果某些代码需要调用 Babel 的 API 进行转码,就要使用`babel-core`模块。
|
||||
|
||||
安装命令如下。
|
||||
|
||||
@ -351,19 +310,19 @@ babel.transformFromAst(ast, code, options);
|
||||
var es6Code = 'let x = n => n + 1';
|
||||
var es5Code = require('babel-core')
|
||||
.transform(es6Code, {
|
||||
presets: ['es2015']
|
||||
presets: ['latest']
|
||||
})
|
||||
.code;
|
||||
// '"use strict";\n\nvar x = function x(n) {\n return n + 1;\n};'
|
||||
```
|
||||
|
||||
上面代码中,`transform`方法的第一个参数是一个字符串,表示需要被转换的ES6代码,第二个参数是转换的配置对象。
|
||||
上面代码中,`transform`方法的第一个参数是一个字符串,表示需要被转换的 ES6 代码,第二个参数是转换的配置对象。
|
||||
|
||||
### 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`,为当前环境提供一个垫片。
|
||||
|
||||
安装命令如下。
|
||||
|
||||
@ -379,30 +338,11 @@ import 'babel-polyfill';
|
||||
require('babel-polyfill');
|
||||
```
|
||||
|
||||
Babel默认不转码的API非常多,详细清单可以查看`babel-plugin-transform-runtime`模块的[definitions.js](https://github.com/babel/babel/blob/master/packages/babel-plugin-transform-runtime/src/definitions.js)文件。
|
||||
Babel 默认不转码的 API 非常多,详细清单可以查看`babel-plugin-transform-runtime`模块的[definitions.js](https://github.com/babel/babel/blob/master/packages/babel-plugin-transform-runtime/src/definitions.js)文件。
|
||||
|
||||
### 浏览器环境
|
||||
|
||||
Babel也可以用于浏览器环境。但是,从Babel 6.0开始,不再直接提供浏览器版本,而是要用构建工具构建出来。如果你没有或不想使用构建工具,可以通过安装5.x版本的`babel-core`模块获取。
|
||||
|
||||
```bash
|
||||
$ npm install babel-core@5
|
||||
```
|
||||
|
||||
运行上面的命令以后,就可以在当前目录的`node_modules/babel-core/`子目录里面,找到`babel`的浏览器版本`browser.js`(未精简)和`browser.min.js`(已精简)。
|
||||
|
||||
然后,将下面的代码插入网页。
|
||||
|
||||
```html
|
||||
<script src="node_modules/babel-core/browser.js"></script>
|
||||
<script type="text/babel">
|
||||
// Your ES6 code
|
||||
</script>
|
||||
```
|
||||
|
||||
上面代码中,`browser.js`是Babel提供的转换器脚本,可以在浏览器运行。用户的ES6脚本放在`script`标签之中,但是要注明`type="text/babel"`。
|
||||
|
||||
另一种方法是使用[babel-standalone](https://github.com/Daniel15/babel-standalone)模块提供的浏览器版本,将其插入网页。
|
||||
Babel 也可以用于浏览器环境。但是,从 Babel 6.0 开始,不再直接提供浏览器版本,而是要用构建工具构建出来。如果你没有或不想使用构建工具,可以使用[babel-standalone](https://github.com/Daniel15/babel-standalone)模块提供的浏览器版本,将其插入网页。
|
||||
|
||||
```html
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.4.4/babel.min.js"></script>
|
||||
@ -411,42 +351,42 @@ $ npm install babel-core@5
|
||||
</script>
|
||||
```
|
||||
|
||||
注意,网页中实时将ES6代码转为ES5,对性能会有影响。生产环境需要加载已经转码完成的脚本。
|
||||
注意,网页实时将 ES6 代码转为 ES5,对性能会有影响。生产环境需要加载已经转码完成的脚本。
|
||||
|
||||
下面是如何将代码打包成浏览器可以使用的脚本,以`Babel`配合`Browserify`为例。首先,安装`babelify`模块。
|
||||
|
||||
```bash
|
||||
$ npm install --save-dev babelify babel-preset-es2015
|
||||
$ npm install --save-dev babelify babel-preset-latest
|
||||
```
|
||||
|
||||
然后,再用命令行转换ES6脚本。
|
||||
然后,再用命令行转换 ES6 脚本。
|
||||
|
||||
```bash
|
||||
$ browserify script.js -o bundle.js \
|
||||
-t [ babelify --presets [ es2015 ] ]
|
||||
-t [ babelify --presets [ latest ] ]
|
||||
```
|
||||
|
||||
上面代码将ES6脚本`script.js`,转为`bundle.js`,浏览器直接加载后者就可以了。
|
||||
上面代码将 ES6 脚本`script.js`,转为`bundle.js`,浏览器直接加载后者就可以了。
|
||||
|
||||
在`package.json`设置下面的代码,就不用每次命令行都输入参数了。
|
||||
|
||||
```javascript
|
||||
{
|
||||
"browserify": {
|
||||
"transform": [["babelify", { "presets": ["es2015"] }]]
|
||||
"transform": [["babelify", { "presets": ["latest"] }]]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在线转换
|
||||
|
||||
Babel提供一个[REPL在线编译器](https://babeljs.io/repl/),可以在线将ES6代码转为ES5代码。转换后的代码,可以直接作为ES5代码插入网页运行。
|
||||
Babel 提供一个[REPL 在线编译器](https://babeljs.io/repl/),可以在线将 ES6 代码转为 ES5 代码。转换后的代码,可以直接作为 ES5 代码插入网页运行。
|
||||
|
||||
### 与其他工具的配合
|
||||
|
||||
许多工具需要Babel进行前置转码,这里举两个例子:ESLint和Mocha。
|
||||
许多工具需要 Babel 进行前置转码,这里举两个例子:ESLint 和 Mocha。
|
||||
|
||||
ESLint用于静态检查代码的语法和风格,安装命令如下。
|
||||
ESLint 用于静态检查代码的语法和风格,安装命令如下。
|
||||
|
||||
```bash
|
||||
$ npm install --save-dev eslint babel-eslint
|
||||
@ -478,7 +418,7 @@ $ npm install --save-dev eslint babel-eslint
|
||||
}
|
||||
```
|
||||
|
||||
Mocha则是一个测试框架,如果需要执行使用ES6语法的测试脚本,可以修改`package.json`的`scripts.test`。
|
||||
Mocha 则是一个测试框架,如果需要执行使用 ES6 语法的测试脚本,可以修改`package.json`的`scripts.test`。
|
||||
|
||||
```javascript
|
||||
"scripts": {
|
||||
@ -488,13 +428,13 @@ Mocha则是一个测试框架,如果需要执行使用ES6语法的测试脚本
|
||||
|
||||
上面命令中,`--compilers`参数指定脚本的转码器,规定后缀名为`js`的文件,都需要使用`babel-core/register`先转码。
|
||||
|
||||
## Traceur转码器
|
||||
## Traceur 转码器
|
||||
|
||||
Google公司的[Traceur](https://github.com/google/traceur-compiler)转码器,也可以将ES6代码转为ES5代码。
|
||||
Google 公司的[Traceur](https://github.com/google/traceur-compiler)转码器,也可以将 ES6 代码转为 ES5 代码。
|
||||
|
||||
### 直接插入网页
|
||||
|
||||
Traceur允许将ES6代码直接插入网页。首先,必须在网页头部加载Traceur库文件。
|
||||
Traceur 允许将 ES6 代码直接插入网页。首先,必须在网页头部加载 Traceur 库文件。
|
||||
|
||||
```html
|
||||
<script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script>
|
||||
@ -505,19 +445,19 @@ Traceur允许将ES6代码直接插入网页。首先,必须在网页头部加
|
||||
</script>
|
||||
```
|
||||
|
||||
上面代码中,一共有4个`script`标签。第一个是加载Traceur的库文件,第二个和第三个是将这个库文件用于浏览器环境,第四个则是加载用户脚本,这个脚本里面可以使用ES6代码。
|
||||
上面代码中,一共有 4 个`script`标签。第一个是加载 Traceur 的库文件,第二个和第三个是将这个库文件用于浏览器环境,第四个则是加载用户脚本,这个脚本里面可以使用 ES6 代码。
|
||||
|
||||
注意,第四个`script`标签的`type`属性的值是`module`,而不是`text/javascript`。这是Traceur编译器识别ES6代码的标志,编译器会自动将所有`type=module`的代码编译为ES5,然后再交给浏览器执行。
|
||||
注意,第四个`script`标签的`type`属性的值是`module`,而不是`text/javascript`。这是 Traceur 编译器识别 ES6 代码的标志,编译器会自动将所有`type=module`的代码编译为 ES5,然后再交给浏览器执行。
|
||||
|
||||
除了引用外部ES6脚本,也可以直接在网页中放置ES6代码。
|
||||
除了引用外部 ES6 脚本,也可以直接在网页中放置 ES6 代码。
|
||||
|
||||
```javascript
|
||||
<script type="module">
|
||||
class Calc {
|
||||
constructor(){
|
||||
constructor() {
|
||||
console.log('Calc constructor');
|
||||
}
|
||||
add(a, b){
|
||||
add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
}
|
||||
@ -527,9 +467,9 @@ Traceur允许将ES6代码直接插入网页。首先,必须在网页头部加
|
||||
</script>
|
||||
```
|
||||
|
||||
正常情况下,上面代码会在控制台打印出9。
|
||||
正常情况下,上面代码会在控制台打印出`9`。
|
||||
|
||||
如果想对Traceur的行为有精确控制,可以采用下面参数配置的写法。
|
||||
如果想对 Traceur 的行为有精确控制,可以采用下面参数配置的写法。
|
||||
|
||||
```javascript
|
||||
<script>
|
||||
@ -555,13 +495,13 @@ Traceur允许将ES6代码直接插入网页。首先,必须在网页头部加
|
||||
</script>
|
||||
```
|
||||
|
||||
上面代码中,首先生成Traceur的全局对象`window.System`,然后`System.import`方法可以用来加载ES6模块。加载的时候,需要传入一个配置对象`metadata`,该对象的`traceurOptions`属性可以配置支持ES6功能。如果设为`experimental: true`,就表示除了ES6以外,还支持一些实验性的新功能。
|
||||
上面代码中,首先生成 Traceur 的全局对象`window.System`,然后`System.import`方法可以用来加载 ES6。加载的时候,需要传入一个配置对象`metadata`,该对象的`traceurOptions`属性可以配置支持 ES6 功能。如果设为`experimental: true`,就表示除了 ES6 以外,还支持一些实验性的新功能。
|
||||
|
||||
### 在线转换
|
||||
|
||||
Traceur也提供一个[在线编译器](http://google.github.io/traceur-compiler/demo/repl.html),可以在线将ES6代码转为ES5代码。转换后的代码,可以直接作为ES5代码插入网页运行。
|
||||
Traceur 也提供一个[在线编译器](http://google.github.io/traceur-compiler/demo/repl.html),可以在线将 ES6 代码转为 ES5 代码。转换后的代码,可以直接作为 ES5 代码插入网页运行。
|
||||
|
||||
上面的例子转为ES5代码运行,就是下面这个样子。
|
||||
上面的例子转为 ES5 代码运行,就是下面这个样子。
|
||||
|
||||
```javascript
|
||||
<script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script>
|
||||
@ -588,15 +528,15 @@ $traceurRuntime.ModuleStore.getAnonymousModule(function() {
|
||||
|
||||
### 命令行转换
|
||||
|
||||
作为命令行工具使用时,Traceur是一个Node的模块,首先需要用Npm安装。
|
||||
作为命令行工具使用时,Traceur 是一个 Node 的模块,首先需要用 npm 安装。
|
||||
|
||||
```bash
|
||||
$ npm install -g traceur
|
||||
```
|
||||
|
||||
安装成功后,就可以在命令行下使用Traceur了。
|
||||
安装成功后,就可以在命令行下使用 Traceur 了。
|
||||
|
||||
Traceur直接运行es6脚本文件,会在标准输出显示运行结果,以前面的`calc.js`为例。
|
||||
Traceur 直接运行 ES6 脚本文件,会在标准输出显示运行结果,以前面的`calc.js`为例。
|
||||
|
||||
```bash
|
||||
$ traceur calc.js
|
||||
@ -604,7 +544,7 @@ Calc constructor
|
||||
9
|
||||
```
|
||||
|
||||
如果要将ES6脚本转为ES5保存,要采用下面的写法。
|
||||
如果要将 ES6 脚本转为 ES5 保存,要采用下面的写法。
|
||||
|
||||
```bash
|
||||
$ traceur --script calc.es6.js --out calc.es5.js
|
||||
@ -620,15 +560,15 @@ $ traceur --script calc.es6.js --out calc.es5.js --experimental
|
||||
|
||||
命令行下转换生成的文件,就可以直接放到浏览器中运行。
|
||||
|
||||
### Node.js环境的用法
|
||||
### Node 环境的用法
|
||||
|
||||
Traceur的Node.js用法如下(假定已安装traceur模块)。
|
||||
Traceur 的 Node 用法如下(假定已安装`traceur`模块)。
|
||||
|
||||
```javascript
|
||||
var traceur = require('traceur');
|
||||
var fs = require('fs');
|
||||
|
||||
// 将ES6脚本转为字符串
|
||||
// 将 ES6 脚本转为字符串
|
||||
var contents = fs.readFileSync('es6-file.js').toString();
|
||||
|
||||
var result = traceur.compile(contents, {
|
||||
@ -641,9 +581,8 @@ var result = traceur.compile(contents, {
|
||||
if (result.error)
|
||||
throw result.error;
|
||||
|
||||
// result对象的js属性就是转换后的ES5代码
|
||||
// result 对象的 js 属性就是转换后的 ES5 代码
|
||||
fs.writeFileSync('out.js', result.js);
|
||||
// sourceMap属性对应map文件
|
||||
// sourceMap 属性对应 map 文件
|
||||
fs.writeFileSync('out.js.map', result.sourceMap);
|
||||
```
|
||||
|
||||
|
226
docs/iterator.md
226
docs/iterator.md
@ -1,14 +1,14 @@
|
||||
# Iterator和for...of循环
|
||||
# Iterator 和 for...of 循环
|
||||
|
||||
## Iterator(遍历器)的概念
|
||||
|
||||
JavaScript原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6又添加了Map和Set。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是Map,Map的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。
|
||||
JavaScript 原有的表示“集合”的数据结构,主要是数组(`Array`)和对象(`Object`),ES6 又添加了`Map`和`Set`。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是`Map`,`Map`的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。
|
||||
|
||||
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
|
||||
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
|
||||
|
||||
Iterator的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是ES6创造了一种新的遍历命令`for...of`循环,Iterator接口主要供`for...of`消费。
|
||||
Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令`for...of`循环,Iterator 接口主要供`for...of`消费。
|
||||
|
||||
Iterator的遍历过程是这样的。
|
||||
Iterator 的遍历过程是这样的。
|
||||
|
||||
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
|
||||
|
||||
@ -64,14 +64,14 @@ function makeIterator(array) {
|
||||
}
|
||||
```
|
||||
|
||||
由于Iterator只是把接口规格加到数据结构之上,所以,遍历器与它所遍历的那个数据结构,实际上是分开的,完全可以写出没有对应数据结构的遍历器对象,或者说用遍历器对象模拟出数据结构。下面是一个无限运行的遍历器对象的例子。
|
||||
由于 Iterator 只是把接口规格加到数据结构之上,所以,遍历器与它所遍历的那个数据结构,实际上是分开的,完全可以写出没有对应数据结构的遍历器对象,或者说用遍历器对象模拟出数据结构。下面是一个无限运行的遍历器对象的例子。
|
||||
|
||||
```javascript
|
||||
var it = idMaker();
|
||||
|
||||
it.next().value // '0'
|
||||
it.next().value // '1'
|
||||
it.next().value // '2'
|
||||
it.next().value // 0
|
||||
it.next().value // 1
|
||||
it.next().value // 2
|
||||
// ...
|
||||
|
||||
function idMaker() {
|
||||
@ -87,9 +87,7 @@ function idMaker() {
|
||||
|
||||
上面的例子中,遍历器生成函数`idMaker`,返回一个遍历器对象(即指针对象)。但是并没有对应的数据结构,或者说,遍历器对象自己描述了一个数据结构出来。
|
||||
|
||||
在ES6中,有些数据结构原生具备Iterator接口(比如数组),即不用任何处理,就可以被`for...of`循环遍历,有些就不行(比如对象)。原因在于,这些数据结构原生部署了`Symbol.iterator`属性(详见下文),另外一些数据结构没有。凡是部署了`Symbol.iterator`属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。
|
||||
|
||||
如果使用TypeScript的写法,遍历器接口(Iterable)、指针对象(Iterator)和next方法返回值的规格可以描述如下。
|
||||
如果使用 TypeScript 的写法,遍历器接口(Iterable)、指针对象(Iterator)和`next`方法返回值的规格可以描述如下。
|
||||
|
||||
```javascript
|
||||
interface Iterable {
|
||||
@ -106,13 +104,13 @@ interface IterationResult {
|
||||
}
|
||||
```
|
||||
|
||||
## 数据结构的默认Iterator接口
|
||||
## 默认 Iterator 接口
|
||||
|
||||
Iterator接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即`for...of`循环(详见下文)。当使用`for...of`循环遍历某种数据结构时,该循环会自动去寻找Iterator接口。
|
||||
Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即`for...of`循环(详见下文)。当使用`for...of`循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。
|
||||
|
||||
一种数据结构只要部署了Iterator接口,我们就称这种数据结构是”可遍历的“(iterable)。
|
||||
一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。
|
||||
|
||||
ES6规定,默认的Iterator接口部署在数据结构的`Symbol.iterator`属性,或者说,一个数据结构只要具有`Symbol.iterator`属性,就可以认为是“可遍历的”(iterable)。`Symbol.iterator`属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名`Symbol.iterator`,它是一个表达式,返回`Symbol`对象的`iterator`属性,这是一个预定义好的、类型为Symbol的特殊值,所以要放在方括号内。(参见Symbol一章)。
|
||||
ES6 规定,默认的 Iterator 接口部署在数据结构的`Symbol.iterator`属性,或者说,一个数据结构只要具有`Symbol.iterator`属性,就可以认为是“可遍历的”(iterable)。`Symbol.iterator`属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名`Symbol.iterator`,它是一个表达式,返回`Symbol`对象的`iterator`属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内(参见《Symbol》一章)。
|
||||
|
||||
```javascript
|
||||
const obj = {
|
||||
@ -131,7 +129,19 @@ const obj = {
|
||||
|
||||
上面代码中,对象`obj`是可遍历的(iterable),因为具有`Symbol.iterator`属性。执行这个属性,会返回一个遍历器对象。该对象的根本特征就是具有`next`方法。每次调用`next`方法,都会返回一个代表当前成员的信息对象,具有`value`和`done`两个属性。
|
||||
|
||||
在ES6中,有三类数据结构原生具备Iterator接口:数组、某些类似数组的对象、Set和Map结构。
|
||||
ES6 的有些数据结构原生具备 Iterator 接口(比如数组),即不用任何处理,就可以被`for...of`循环遍历。原因在于,这些数据结构原生部署了`Symbol.iterator`属性(详见下文),另外一些数据结构没有(比如对象)。凡是部署了`Symbol.iterator`属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。
|
||||
|
||||
原生具备 Iterator 接口的数据结构如下。
|
||||
|
||||
- Array
|
||||
- Map
|
||||
- Set
|
||||
- String
|
||||
- TypedArray
|
||||
- 函数的 arguments 对象
|
||||
- NodeList 对象
|
||||
|
||||
下面的例子是数组的`Symbol.iterator`属性。
|
||||
|
||||
```javascript
|
||||
let arr = ['a', 'b', 'c'];
|
||||
@ -145,11 +155,11 @@ iter.next() // { value: undefined, done: true }
|
||||
|
||||
上面代码中,变量`arr`是一个数组,原生就具有遍历器接口,部署在`arr`的`Symbol.iterator`属性上面。所以,调用这个属性,就得到遍历器对象。
|
||||
|
||||
上面提到,原生就部署Iterator接口的数据结构有三类,对于这三类数据结构,不用自己写遍历器生成函数,`for...of`循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的Iterator接口,都需要自己在`Symbol.iterator`属性上面部署,这样才会被`for...of`循环遍历。
|
||||
对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,`for...of`循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在`Symbol.iterator`属性上面部署,这样才会被`for...of`循环遍历。
|
||||
|
||||
对象(Object)之所以没有默认部署Iterator接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作Map结构使用,ES5没有Map结构,而ES6原生提供了。
|
||||
对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了。
|
||||
|
||||
一个对象如果要有可被`for...of`循环调用的Iterator接口,就必须在`Symbol.iterator`的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。
|
||||
一个对象如果要具备可被`for...of`循环调用的 Iterator 接口,就必须在`Symbol.iterator`的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。
|
||||
|
||||
```javascript
|
||||
class RangeIterator {
|
||||
@ -165,9 +175,8 @@ class RangeIterator {
|
||||
if (value < this.stop) {
|
||||
this.value++;
|
||||
return {done: false, value: value};
|
||||
} else {
|
||||
return {done: true, value: undefined};
|
||||
}
|
||||
return {done: true, value: undefined};
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,11 +185,11 @@ function range(start, stop) {
|
||||
}
|
||||
|
||||
for (var value of range(0, 3)) {
|
||||
console.log(value);
|
||||
console.log(value); // 0, 1, 2
|
||||
}
|
||||
```
|
||||
|
||||
上面代码是一个类部署Iterator接口的写法。`Symbol.iterator`属性对应一个函数,执行后返回当前对象的遍历器对象。
|
||||
上面代码是一个类部署 Iterator 接口的写法。`Symbol.iterator`属性对应一个函数,执行后返回当前对象的遍历器对象。
|
||||
|
||||
下面是通过遍历器实现指针结构的例子。
|
||||
|
||||
@ -191,9 +200,7 @@ function Obj(value) {
|
||||
}
|
||||
|
||||
Obj.prototype[Symbol.iterator] = function() {
|
||||
var iterator = {
|
||||
next: next
|
||||
};
|
||||
var iterator = { next: next };
|
||||
|
||||
var current = this;
|
||||
|
||||
@ -201,14 +208,9 @@ Obj.prototype[Symbol.iterator] = function() {
|
||||
if (current) {
|
||||
var value = current.value;
|
||||
current = current.next;
|
||||
return {
|
||||
done: false,
|
||||
value: value
|
||||
};
|
||||
return { done: false, value: value };
|
||||
} else {
|
||||
return {
|
||||
done: true
|
||||
};
|
||||
return { done: true };
|
||||
}
|
||||
}
|
||||
return iterator;
|
||||
@ -222,16 +224,13 @@ one.next = two;
|
||||
two.next = three;
|
||||
|
||||
for (var i of one){
|
||||
console.log(i);
|
||||
console.log(i); // 1, 2, 3
|
||||
}
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
```
|
||||
|
||||
上面代码首先在构造函数的原型链上部署`Symbol.iterator`方法,调用该方法会返回遍历器对象`iterator`,调用该对象的`next`方法,在返回一个值的同时,自动将内部指针移到下一个实例。
|
||||
|
||||
下面是另一个为对象添加Iterator接口的例子。
|
||||
下面是另一个为对象添加 Iterator 接口的例子。
|
||||
|
||||
```javascript
|
||||
let obj = {
|
||||
@ -255,7 +254,7 @@ let obj = {
|
||||
};
|
||||
```
|
||||
|
||||
对于类似数组的对象(存在数值键名和length属性),部署Iterator接口,有一个简便方法,就是`Symbol.iterator`方法直接引用数组的Iterator接口。
|
||||
对于类似数组的对象(存在数值键名和`length`属性),部署 Iterator 接口,有一个简便方法,就是`Symbol.iterator`方法直接引用数组的 Iterator 接口。
|
||||
|
||||
```javascript
|
||||
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
|
||||
@ -265,7 +264,9 @@ NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];
|
||||
[...document.querySelectorAll('div')] // 可以执行了
|
||||
```
|
||||
|
||||
下面是类似数组的对象调用数组的`Symbol.iterator`方法的例子。
|
||||
NodeList 对象是类似数组的对象,本来就具有遍历接口,可以直接遍历。上面代码中,我们将它的遍历接口改成数组的`Symbol.iterator`属性,可以看到没有任何影响。
|
||||
|
||||
下面是另一个类似数组的对象调用数组的`Symbol.iterator`方法的例子。
|
||||
|
||||
```javascript
|
||||
let iterable = {
|
||||
@ -305,7 +306,7 @@ obj[Symbol.iterator] = () => 1;
|
||||
[...obj] // TypeError: [] is not a function
|
||||
```
|
||||
|
||||
上面代码中,变量obj的Symbol.iterator方法对应的不是遍历器生成函数,因此报错。
|
||||
上面代码中,变量`obj`的`Symbol.iterator`方法对应的不是遍历器生成函数,因此报错。
|
||||
|
||||
有了遍历器接口,数据结构就可以用`for...of`循环遍历(详见下文),也可以使用`while`循环遍历。
|
||||
|
||||
@ -321,13 +322,13 @@ while (!$result.done) {
|
||||
|
||||
上面代码中,`ITERABLE`代表某种可遍历的数据结构,`$iterator`是它的遍历器对象。遍历器对象每次移动指针(`next`方法),都检查一下返回值的`done`属性,如果遍历还没结束,就移动遍历器对象的指针到下一步(`next`方法),不断循环。
|
||||
|
||||
## 调用Iterator接口的场合
|
||||
## 调用 Iterator 接口的场合
|
||||
|
||||
有一些场合会默认调用Iterator接口(即`Symbol.iterator`方法),除了下文会介绍的`for...of`循环,还有几个别的场合。
|
||||
有一些场合会默认调用 Iterator 接口(即`Symbol.iterator`方法),除了下文会介绍的`for...of`循环,还有几个别的场合。
|
||||
|
||||
**(1)解构赋值**
|
||||
|
||||
对数组和Set结构进行解构赋值时,会默认调用`Symbol.iterator`方法。
|
||||
对数组和 Set 结构进行解构赋值时,会默认调用`Symbol.iterator`方法。
|
||||
|
||||
```javascript
|
||||
let set = new Set().add('a').add('b').add('c');
|
||||
@ -341,7 +342,7 @@ let [first, ...rest] = set;
|
||||
|
||||
**(2)扩展运算符**
|
||||
|
||||
扩展运算符(...)也会调用默认的iterator接口。
|
||||
扩展运算符(...)也会调用默认的 Iterator 接口。
|
||||
|
||||
```javascript
|
||||
// 例一
|
||||
@ -354,17 +355,17 @@ let arr = ['b', 'c'];
|
||||
// ['a', 'b', 'c', 'd']
|
||||
```
|
||||
|
||||
上面代码的扩展运算符内部就调用Iterator接口。
|
||||
上面代码的扩展运算符内部就调用 Iterator 接口。
|
||||
|
||||
实际上,这提供了一种简便机制,可以将任何部署了Iterator接口的数据结构,转为数组。也就是说,只要某个数据结构部署了Iterator接口,就可以对它使用扩展运算符,将其转为数组。
|
||||
实际上,这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。
|
||||
|
||||
```javascript
|
||||
let arr = [...iterable];
|
||||
```
|
||||
|
||||
**(3)yield* **
|
||||
**(3)yield\***
|
||||
|
||||
yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
|
||||
`yield*`后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
|
||||
|
||||
```javascript
|
||||
let generator = function* () {
|
||||
@ -393,9 +394,9 @@ iterator.next() // { value: undefined, done: true }
|
||||
- Promise.all()
|
||||
- Promise.race()
|
||||
|
||||
## 字符串的Iterator接口
|
||||
## 字符串的 Iterator 接口
|
||||
|
||||
字符串是一个类似数组的对象,也原生具有Iterator接口。
|
||||
字符串是一个类似数组的对象,也原生具有 Iterator 接口。
|
||||
|
||||
```javascript
|
||||
var someString = "hi";
|
||||
@ -409,7 +410,7 @@ iterator.next() // { value: "i", done: false }
|
||||
iterator.next() // { value: undefined, done: true }
|
||||
```
|
||||
|
||||
上面代码中,调用`Symbol.iterator`方法返回一个遍历器对象,在这个遍历器上可以调用next方法,实现对于字符串的遍历。
|
||||
上面代码中,调用`Symbol.iterator`方法返回一个遍历器对象,在这个遍历器上可以调用 next 方法,实现对于字符串的遍历。
|
||||
|
||||
可以覆盖原生的`Symbol.iterator`方法,达到修改遍历器行为的目的。
|
||||
|
||||
@ -436,20 +437,20 @@ str[Symbol.iterator] = function() {
|
||||
str // "hi"
|
||||
```
|
||||
|
||||
上面代码中,字符串str的`Symbol.iterator`方法被修改了,所以扩展运算符(`...`)返回的值变成了`bye`,而字符串本身还是`hi`。
|
||||
上面代码中,字符串 str 的`Symbol.iterator`方法被修改了,所以扩展运算符(`...`)返回的值变成了`bye`,而字符串本身还是`hi`。
|
||||
|
||||
## Iterator接口与Generator函数
|
||||
## Iterator 接口与 Generator 函数
|
||||
|
||||
`Symbol.iterator`方法的最简单实现,还是使用下一章要介绍的Generator函数。
|
||||
`Symbol.iterator`方法的最简单实现,还是使用下一章要介绍的 Generator 函数。
|
||||
|
||||
```javascript
|
||||
var myIterable = {};
|
||||
|
||||
myIterable[Symbol.iterator] = function* () {
|
||||
yield 1;
|
||||
yield 2;
|
||||
yield 3;
|
||||
};
|
||||
let myIterable = {
|
||||
[Symbol.iterator]: function* () {
|
||||
yield 1;
|
||||
yield 2;
|
||||
yield 3;
|
||||
}
|
||||
}
|
||||
[...myIterable] // [1, 2, 3]
|
||||
|
||||
// 或者采用下面的简洁写法
|
||||
@ -464,13 +465,13 @@ let obj = {
|
||||
for (let x of obj) {
|
||||
console.log(x);
|
||||
}
|
||||
// hello
|
||||
// world
|
||||
// "hello"
|
||||
// "world"
|
||||
```
|
||||
|
||||
上面代码中,`Symbol.iterator`方法几乎不用部署任何代码,只要用yield命令给出每一步的返回值即可。
|
||||
上面代码中,`Symbol.iterator`方法几乎不用部署任何代码,只要用 yield 命令给出每一步的返回值即可。
|
||||
|
||||
## 遍历器对象的return(),throw()
|
||||
## 遍历器对象的 return(),throw()
|
||||
|
||||
遍历器对象除了具有`next`方法,还可以具有`return`方法和`throw`方法。如果你自己写遍历器对象生成函数,那么`next`方法是必须部署的,`return`方法和`throw`方法是否部署是可选的。
|
||||
|
||||
@ -480,10 +481,7 @@ for (let x of obj) {
|
||||
function readLinesSync(file) {
|
||||
return {
|
||||
next() {
|
||||
if (file.isAtEndOfFile()) {
|
||||
file.close();
|
||||
return { done: true };
|
||||
}
|
||||
return { done: false };
|
||||
},
|
||||
return() {
|
||||
file.close();
|
||||
@ -493,24 +491,39 @@ function readLinesSync(file) {
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,函数`readLinesSync`接受一个文件对象作为参数,返回一个遍历器对象,其中除了`next`方法,还部署了`return`方法。下面,我们让文件的遍历提前返回,这样就会触发执行`return`方法。
|
||||
上面代码中,函数`readLinesSync`接受一个文件对象作为参数,返回一个遍历器对象,其中除了`next`方法,还部署了`return`方法。下面的三种情况,都会触发执行`return`方法。
|
||||
|
||||
```javascript
|
||||
// 情况一
|
||||
for (let line of readLinesSync(fileName)) {
|
||||
console.log(line);
|
||||
break;
|
||||
}
|
||||
|
||||
// 情况二
|
||||
for (let line of readLinesSync(fileName)) {
|
||||
console.log(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 情况三
|
||||
for (let line of readLinesSync(fileName)) {
|
||||
console.log(line);
|
||||
throw new Error();
|
||||
}
|
||||
```
|
||||
|
||||
注意,`return`方法必须返回一个对象,这是Generator规格决定的。
|
||||
上面代码中,情况一输出文件的第一行以后,就会执行`return`方法,关闭这个文件;情况二输出所有行以后,执行`return`方法,关闭该文件;情况三会在执行`return`方法关闭文件之后,再抛出错误。
|
||||
|
||||
`throw`方法主要是配合Generator函数使用,一般的遍历器对象用不到这个方法。请参阅《Generator函数》一章。
|
||||
注意,`return`方法必须返回一个对象,这是 Generator 规格决定的。
|
||||
|
||||
## for...of循环
|
||||
`throw`方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。请参阅《Generator 函数》一章。
|
||||
|
||||
## for...of 循环
|
||||
|
||||
ES6 借鉴 C++、Java、C# 和 Python 语言,引入了`for...of`循环,作为遍历所有数据结构的统一的方法。
|
||||
|
||||
一个数据结构只要部署了`Symbol.iterator`属性,就被视为具有iterator接口,就可以用`for...of`循环遍历它的成员。也就是说,`for...of`循环内部调用的是数据结构的`Symbol.iterator`方法。
|
||||
一个数据结构只要部署了`Symbol.iterator`属性,就被视为具有 iterator 接口,就可以用`for...of`循环遍历它的成员。也就是说,`for...of`循环内部调用的是数据结构的`Symbol.iterator`方法。
|
||||
|
||||
`for...of`循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如`arguments`对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。
|
||||
|
||||
@ -546,7 +559,7 @@ arr.forEach(function (element, index) {
|
||||
});
|
||||
```
|
||||
|
||||
JavaScript原有的`for...in`循环,只能获得对象的键名,不能直接获取键值。ES6提供`for...of`循环,允许遍历获得键值。
|
||||
JavaScript 原有的`for...in`循环,只能获得对象的键名,不能直接获取键值。ES6 提供`for...of`循环,允许遍历获得键值。
|
||||
|
||||
```javascript
|
||||
var arr = ['a', 'b', 'c', 'd'];
|
||||
@ -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`循环也不一样。
|
||||
|
||||
@ -579,9 +592,9 @@ for (let i of arr) {
|
||||
|
||||
上面代码中,`for...of`循环不会返回数组`arr`的`foo`属性。
|
||||
|
||||
### Set和Map结构
|
||||
### Set 和 Map 结构
|
||||
|
||||
Set和Map结构也原生具有Iterator接口,可以直接使用`for...of`循环。
|
||||
Set 和 Map 结构也原生具有 Iterator 接口,可以直接使用`for...of`循环。
|
||||
|
||||
```javascript
|
||||
var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
|
||||
@ -604,7 +617,7 @@ for (var [name, value] of es6) {
|
||||
// standard: ECMA-262
|
||||
```
|
||||
|
||||
上面代码演示了如何遍历Set结构和Map结构。值得注意的地方有两个,首先,遍历的顺序是按照各个成员被添加进数据结构的顺序。其次,Set结构遍历时,返回的是一个值,而Map结构遍历时,返回的是一个数组,该数组的两个成员分别为当前Map成员的键名和键值。
|
||||
上面代码演示了如何遍历 Set 结构和 Map 结构。值得注意的地方有两个,首先,遍历的顺序是按照各个成员被添加进数据结构的顺序。其次,Set 结构遍历时,返回的是一个值,而 Map 结构遍历时,返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值。
|
||||
|
||||
```javascript
|
||||
let map = new Map().set('a', 1).set('b', 2);
|
||||
@ -623,9 +636,9 @@ for (let [key, value] of map) {
|
||||
|
||||
### 计算生成的数据结构
|
||||
|
||||
有些数据结构是在现有数据结构的基础上,计算生成的。比如,ES6的数组、Set、Map都部署了以下三个方法,调用后都返回遍历器对象。
|
||||
有些数据结构是在现有数据结构的基础上,计算生成的。比如,ES6 的数组、Set、Map 都部署了以下三个方法,调用后都返回遍历器对象。
|
||||
|
||||
- `entries()` 返回一个遍历器对象,用来遍历`[键名, 键值]`组成的数组。对于数组,键名就是索引值;对于Set,键名与键值相同。Map结构的iterator接口,默认就是调用entries方法。
|
||||
- `entries()` 返回一个遍历器对象,用来遍历`[键名, 键值]`组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的 Iterator 接口,默认就是调用`entries`方法。
|
||||
- `keys()` 返回一个遍历器对象,用来遍历所有的键名。
|
||||
- `values()` 返回一个遍历器对象,用来遍历所有的键值。
|
||||
|
||||
@ -643,7 +656,7 @@ for (let pair of arr.entries()) {
|
||||
|
||||
### 类似数组的对象
|
||||
|
||||
类似数组的对象包括好几类。下面是`for...of`循环用于字符串、DOM NodeList对象、arguments对象的例子。
|
||||
类似数组的对象包括好几类。下面是`for...of`循环用于字符串、DOM NodeList 对象、`arguments`对象的例子。
|
||||
|
||||
```javascript
|
||||
// 字符串
|
||||
@ -671,7 +684,7 @@ printArgs('a', 'b');
|
||||
// 'b'
|
||||
```
|
||||
|
||||
对于字符串来说,`for...of`循环还有一个特点,就是会正确识别32位UTF-16字符。
|
||||
对于字符串来说,`for...of`循环还有一个特点,就是会正确识别 32 位 UTF-16 字符。
|
||||
|
||||
```javascript
|
||||
for (let x of 'a\uD83D\uDC0A') {
|
||||
@ -681,7 +694,7 @@ for (let x of 'a\uD83D\uDC0A') {
|
||||
// '\uD83D\uDC0A'
|
||||
```
|
||||
|
||||
并不是所有类似数组的对象都具有iterator接口,一个简便的解决方法,就是使用Array.from方法将其转为数组。
|
||||
并不是所有类似数组的对象都具有 Iterator 接口,一个简便的解决方法,就是使用`Array.from`方法将其转为数组。
|
||||
|
||||
```javascript
|
||||
let arrayLike = { length: 2, 0: 'a', 1: 'b' };
|
||||
@ -699,10 +712,10 @@ for (let x of Array.from(arrayLike)) {
|
||||
|
||||
### 对象
|
||||
|
||||
对于普通的对象,`for...of`结构不能直接使用,会报错,必须部署了iterator接口后才能使用。但是,这样情况下,`for...in`循环依然可以用来遍历键名。
|
||||
对于普通的对象,`for...of`结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。但是,这样情况下,`for...in`循环依然可以用来遍历键名。
|
||||
|
||||
```javascript
|
||||
var es6 = {
|
||||
let es6 = {
|
||||
edition: 6,
|
||||
committee: "TC39",
|
||||
standard: "ECMA-262"
|
||||
@ -718,7 +731,7 @@ for (let e in es6) {
|
||||
for (let e of es6) {
|
||||
console.log(e);
|
||||
}
|
||||
// TypeError: es6 is not iterable
|
||||
// TypeError: es6[Symbol.iterator] is not a function
|
||||
```
|
||||
|
||||
上面代码表示,对于普通的对象,`for...in`循环可以遍历键名,`for...of`循环会报错。
|
||||
@ -727,18 +740,11 @@ for (let e of es6) {
|
||||
|
||||
```javascript
|
||||
for (var key of Object.keys(someObject)) {
|
||||
console.log(key + ": " + someObject[key]);
|
||||
console.log(key + ': ' + someObject[key]);
|
||||
}
|
||||
```
|
||||
|
||||
在对象上部署iterator接口的代码,参见本章前面部分。一个方便的方法是将数组的`Symbol.iterator`属性,直接赋值给其他对象的`Symbol.iterator`属性。比如,想要让`for...of`环遍历jQuery对象,只要加上下面这一行就可以了。
|
||||
|
||||
```javascript
|
||||
jQuery.prototype[Symbol.iterator] =
|
||||
Array.prototype[Symbol.iterator];
|
||||
```
|
||||
|
||||
另一个方法是使用Generator函数将对象重新包装一下。
|
||||
另一个方法是使用 Generator 函数将对象重新包装一下。
|
||||
|
||||
```javascript
|
||||
function* entries(obj) {
|
||||
@ -748,7 +754,7 @@ function* entries(obj) {
|
||||
}
|
||||
|
||||
for (let [key, value] of entries(obj)) {
|
||||
console.log(key, "->", value);
|
||||
console.log(key, '->', value);
|
||||
}
|
||||
// a -> 1
|
||||
// b -> 2
|
||||
@ -757,7 +763,7 @@ for (let [key, value] of entries(obj)) {
|
||||
|
||||
### 与其他遍历语法的比较
|
||||
|
||||
以数组为例,JavaScript提供多种遍历语法。最原始的写法就是for循环。
|
||||
以数组为例,JavaScript 提供多种遍历语法。最原始的写法就是`for`循环。
|
||||
|
||||
```javascript
|
||||
for (var index = 0; index < myArray.length; index++) {
|
||||
@ -765,7 +771,7 @@ for (var index = 0; index < myArray.length; index++) {
|
||||
}
|
||||
```
|
||||
|
||||
这种写法比较麻烦,因此数组提供内置的forEach方法。
|
||||
这种写法比较麻烦,因此数组提供内置的`forEach`方法。
|
||||
|
||||
```javascript
|
||||
myArray.forEach(function (value) {
|
||||
@ -773,7 +779,7 @@ myArray.forEach(function (value) {
|
||||
});
|
||||
```
|
||||
|
||||
这种写法的问题在于,无法中途跳出`forEach`循环,break命令或return命令都不能奏效。
|
||||
这种写法的问题在于,无法中途跳出`forEach`循环,`break`命令或`return`命令都不能奏效。
|
||||
|
||||
`for...in`循环可以遍历数组的键名。
|
||||
|
||||
@ -783,11 +789,11 @@ for (var index in myArray) {
|
||||
}
|
||||
```
|
||||
|
||||
for...in循环有几个缺点。
|
||||
`for...in`循环有几个缺点。
|
||||
|
||||
- 数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。
|
||||
- for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
|
||||
- 某些情况下,for...in循环会以任意顺序遍历键名。
|
||||
- 数组的键名是数字,但是`for...in`循环是以字符串作为键名“0”、“1”、“2”等等。
|
||||
- `for...in`循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
|
||||
- 某些情况下,`for...in`循环会以任意顺序遍历键名。
|
||||
|
||||
总之,`for...in`循环主要是为遍历对象而设计的,不适用于遍历数组。
|
||||
|
||||
@ -799,11 +805,11 @@ for (let value of myArray) {
|
||||
}
|
||||
```
|
||||
|
||||
- 有着同for...in一样的简洁语法,但是没有for...in那些缺点。
|
||||
- 不同用于forEach方法,它可以与break、continue和return配合使用。
|
||||
- 有着同`for...in`一样的简洁语法,但是没有`for...in`那些缺点。
|
||||
- 不同于`forEach`方法,它可以与`break`、`continue`和`return`配合使用。
|
||||
- 提供了遍历所有数据结构的统一操作接口。
|
||||
|
||||
下面是一个使用break语句,跳出`for...of`循环的例子。
|
||||
下面是一个使用 break 语句,跳出`for...of`循环的例子。
|
||||
|
||||
```javascript
|
||||
for (var n of fibonacci) {
|
||||
@ -813,4 +819,4 @@ for (var n of fibonacci) {
|
||||
}
|
||||
```
|
||||
|
||||
上面的例子,会输出斐波纳契数列小于等于1000的项。如果当前项大于1000,就会使用break语句跳出`for...of`循环。
|
||||
上面的例子,会输出斐波纳契数列小于等于 1000 的项。如果当前项大于 1000,就会使用`break`语句跳出`for...of`循环。
|
||||
|
201
docs/let.md
201
docs/let.md
@ -1,10 +1,10 @@
|
||||
# let和const命令
|
||||
# let 和 const 命令
|
||||
|
||||
## let命令
|
||||
## let 命令
|
||||
|
||||
### 基本用法
|
||||
|
||||
ES6新增了`let`命令,用来声明变量。它的用法类似于`var`,但是所声明的变量,只在`let`命令所在的代码块内有效。
|
||||
ES6 新增了`let`命令,用来声明变量。它的用法类似于`var`,但是所声明的变量,只在`let`命令所在的代码块内有效。
|
||||
|
||||
```javascript
|
||||
{
|
||||
@ -21,15 +21,17 @@ b // 1
|
||||
`for`循环的计数器,就很合适使用`let`命令。
|
||||
|
||||
```javascript
|
||||
for (let i = 0; i < 10; i++) {}
|
||||
for (let i = 0; i < 10; i++) {
|
||||
// ...
|
||||
}
|
||||
|
||||
console.log(i);
|
||||
//ReferenceError: i is not defined
|
||||
// ReferenceError: i is not defined
|
||||
```
|
||||
|
||||
上面代码中,计数器`i`只在`for`循环体内有效,在循环体外引用就会报错。
|
||||
|
||||
下面的代码如果使用`var`,最后输出的是10。
|
||||
下面的代码如果使用`var`,最后输出的是`10`。
|
||||
|
||||
```javascript
|
||||
var a = [];
|
||||
@ -41,9 +43,9 @@ for (var i = 0; i < 10; i++) {
|
||||
a[6](); // 10
|
||||
```
|
||||
|
||||
上面代码中,变量`i`是`var`声明的,在全局范围内都有效。所以每一次循环,新的`i`值都会覆盖旧值,导致最后输出的是最后一轮的`i`的值。
|
||||
上面代码中,变量`i`是`var`命令声明的,在全局范围内都有效,所以全局只有一个变量`i`。每一次循环,变量`i`的值都会发生改变,而循环内被赋给数组`a`的函数内部的`console.log(i)`,里面的`i`指向的就是全局的`i`。也就是说,所有数组`a`的成员里面的`i`,指向的都是同一个`i`,导致运行时输出的是最后一轮的`i`的值,也就是 10。
|
||||
|
||||
如果使用`let`,声明的变量仅在块级作用域内有效,最后输出的是6。
|
||||
如果使用`let`,声明的变量仅在块级作用域内有效,最后输出的是 6。
|
||||
|
||||
```javascript
|
||||
var a = [];
|
||||
@ -55,17 +57,35 @@ for (let i = 0; i < 10; i++) {
|
||||
a[6](); // 6
|
||||
```
|
||||
|
||||
上面代码中,变量`i`是`let`声明的,当前的`i`只在本轮循环有效,所以每一次循环的`i`其实都是一个新的变量,所以最后输出的是6。
|
||||
上面代码中,变量`i`是`let`声明的,当前的`i`只在本轮循环有效,所以每一次循环的`i`其实都是一个新的变量,所以最后输出的是`6`。你可能会问,如果每一轮循环的变量`i`都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量`i`时,就在上一轮循环的基础上进行计算。
|
||||
|
||||
另外,`for`循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
|
||||
|
||||
```javascript
|
||||
for (let i = 0; i < 3; i++) {
|
||||
let i = 'abc';
|
||||
console.log(i);
|
||||
}
|
||||
// abc
|
||||
// abc
|
||||
// abc
|
||||
```
|
||||
|
||||
上面代码正确运行,输出了 3 次`abc`。这表明函数内部的变量`i`与循环变量`i`不在同一个作用域,有各自单独的作用域。
|
||||
|
||||
### 不存在变量提升
|
||||
|
||||
`let`不像`var`那样会发生“变量提升”现象。所以,变量一定要在声明后使用,否则报错。
|
||||
`var`命令会发生”变量提升“现象,即变量可以在声明之前使用,值为`undefined`。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。
|
||||
|
||||
为了纠正这种现象,`let`命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
|
||||
|
||||
```javascript
|
||||
// var 的情况
|
||||
console.log(foo); // 输出undefined
|
||||
console.log(bar); // 报错ReferenceError
|
||||
|
||||
var foo = 2;
|
||||
|
||||
// let 的情况
|
||||
console.log(bar); // 报错ReferenceError
|
||||
let bar = 2;
|
||||
```
|
||||
|
||||
@ -86,9 +106,9 @@ if (true) {
|
||||
|
||||
上面代码中,存在全局变量`tmp`,但是块级作用域内`let`又声明了一个局部变量`tmp`,导致后者绑定这个块级作用域,所以在`let`声明变量前,对`tmp`赋值会报错。
|
||||
|
||||
ES6明确规定,如果区块中存在`let`和`const`命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
|
||||
ES6 明确规定,如果区块中存在`let`和`const`命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
|
||||
|
||||
总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称TDZ)。
|
||||
总之,在代码块内,使用`let`命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
|
||||
|
||||
```javascript
|
||||
if (true) {
|
||||
@ -142,23 +162,36 @@ function bar(x = 2, y = x) {
|
||||
bar(); // [2, 2]
|
||||
```
|
||||
|
||||
ES6规定暂时性死区和`let`、`const`语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在ES5是很常见的,现在有了这种规定,避免此类错误就很容易了。
|
||||
另外,下面的代码也会报错,与`var`的行为不同。
|
||||
|
||||
```javascript
|
||||
// 不报错
|
||||
var x = x;
|
||||
|
||||
// 报错
|
||||
let x = x;
|
||||
// ReferenceError: x is not defined
|
||||
```
|
||||
|
||||
上面代码报错,也是因为暂时性死区。使用`let`声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量`x`的声明语句还没有执行完成前,就去取`x`的值,导致报错”x 未定义“。
|
||||
|
||||
ES6 规定暂时性死区和`let`、`const`语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。
|
||||
|
||||
总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
|
||||
|
||||
### 不允许重复声明
|
||||
|
||||
let不允许在相同作用域内,重复声明同一个变量。
|
||||
`let`不允许在相同作用域内,重复声明同一个变量。
|
||||
|
||||
```javascript
|
||||
// 报错
|
||||
function () {
|
||||
function func() {
|
||||
let a = 10;
|
||||
var a = 1;
|
||||
}
|
||||
|
||||
// 报错
|
||||
function () {
|
||||
function func() {
|
||||
let a = 10;
|
||||
let a = 1;
|
||||
}
|
||||
@ -182,7 +215,7 @@ function func(arg) {
|
||||
|
||||
### 为什么需要块级作用域?
|
||||
|
||||
ES5只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。
|
||||
ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。
|
||||
|
||||
第一种场景,内层变量可能会覆盖外层变量。
|
||||
|
||||
@ -192,14 +225,14 @@ var tmp = new Date();
|
||||
function f() {
|
||||
console.log(tmp);
|
||||
if (false) {
|
||||
var tmp = "hello world";
|
||||
var tmp = 'hello world';
|
||||
}
|
||||
}
|
||||
|
||||
f(); // undefined
|
||||
```
|
||||
|
||||
上面代码中,函数f执行后,输出结果为`undefined`,原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。
|
||||
上面代码的原意是,`if`代码块的外部使用外层的`tmp`变量,内部使用内层的`tmp`变量。但是,函数`f`执行后,输出结果为`undefined`,原因在于变量提升,导致内层的`tmp`变量覆盖了外层的`tmp`变量。
|
||||
|
||||
第二种场景,用来计数的循环变量泄露为全局变量。
|
||||
|
||||
@ -213,11 +246,11 @@ for (var i = 0; i < s.length; i++) {
|
||||
console.log(i); // 5
|
||||
```
|
||||
|
||||
上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
|
||||
上面代码中,变量`i`只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
|
||||
|
||||
### ES6的块级作用域
|
||||
### ES6 的块级作用域
|
||||
|
||||
`let`实际上为JavaScript新增了块级作用域。
|
||||
`let`实际上为 JavaScript 新增了块级作用域。
|
||||
|
||||
```javascript
|
||||
function f1() {
|
||||
@ -229,9 +262,9 @@ function f1() {
|
||||
}
|
||||
```
|
||||
|
||||
上面的函数有两个代码块,都声明了变量`n`,运行后输出5。这表示外层代码块不受内层代码块的影响。如果使用`var`定义变量`n`,最后输出的值就是10。
|
||||
上面的函数有两个代码块,都声明了变量`n`,运行后输出 5。这表示外层代码块不受内层代码块的影响。如果两次都使用`var`定义变量`n`,最后输出的值才是 10。
|
||||
|
||||
ES6允许块级作用域的任意嵌套。
|
||||
ES6 允许块级作用域的任意嵌套。
|
||||
|
||||
```javascript
|
||||
{{{{{let insane = 'Hello World'}}}}};
|
||||
@ -273,9 +306,9 @@ ES6允许块级作用域的任意嵌套。
|
||||
|
||||
### 块级作用域与函数声明
|
||||
|
||||
函数能不能在块级作用域之中声明,是一个相当令人混淆的问题。
|
||||
函数能不能在块级作用域之中声明?这是一个相当令人混淆的问题。
|
||||
|
||||
ES5规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
|
||||
ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
|
||||
|
||||
```javascript
|
||||
// 情况一
|
||||
@ -287,37 +320,19 @@ if (true) {
|
||||
try {
|
||||
function f() {}
|
||||
} catch(e) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
上面代码的两种函数声明,根据ES5的规定都是非法的。
|
||||
上面两种函数声明,根据 ES5 的规定都是非法的。
|
||||
|
||||
但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。不过,“严格模式”下还是会报错。
|
||||
但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。
|
||||
|
||||
```javascript
|
||||
// ES5严格模式
|
||||
'use strict';
|
||||
if (true) {
|
||||
function f() {}
|
||||
}
|
||||
// 报错
|
||||
```
|
||||
|
||||
ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。
|
||||
|
||||
```javascript
|
||||
// ES6严格模式
|
||||
'use strict';
|
||||
if (true) {
|
||||
function f() {}
|
||||
}
|
||||
// 不报错
|
||||
```
|
||||
|
||||
ES6 规定,块级作用域之中,函数声明语句的行为类似于`let`,在块级作用域之外不可引用。
|
||||
ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于`let`,在块级作用域之外不可引用。
|
||||
|
||||
```javascript
|
||||
function f() { console.log('I am outside!'); }
|
||||
|
||||
(function () {
|
||||
if (false) {
|
||||
// 重复声明一次函数f
|
||||
@ -331,8 +346,9 @@ function f() { console.log('I am outside!'); }
|
||||
上面代码在 ES5 中运行,会得到“I am inside!”,因为在`if`内声明的函数`f`会被提升到函数头部,实际运行的代码如下。
|
||||
|
||||
```javascript
|
||||
// ES5版本
|
||||
// ES5 环境
|
||||
function f() { console.log('I am outside!'); }
|
||||
|
||||
(function () {
|
||||
function f() { console.log('I am inside!'); }
|
||||
if (false) {
|
||||
@ -341,29 +357,22 @@ function f() { console.log('I am outside!'); }
|
||||
}());
|
||||
```
|
||||
|
||||
ES6 的运行结果就完全不一样了,会得到“I am outside!”。因为块级作用域内声明的函数类似于`let`,对作用域之外没有影响,实际运行的代码如下。
|
||||
ES6 就完全不一样了,理论上会得到“I am outside!”。因为块级作用域内声明的函数类似于`let`,对作用域之外没有影响。但是,如果你真的在 ES6 浏览器中运行一下上面的代码,是会报错的,这是为什么呢?
|
||||
|
||||
```javascript
|
||||
// ES6版本
|
||||
function f() { console.log('I am outside!'); }
|
||||
(function () {
|
||||
f();
|
||||
}());
|
||||
```
|
||||
|
||||
很显然,这种行为差异会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6在[附录B](http://www.ecma-international.org/ecma-262/6.0/index.html#sec-block-level-function-declarations-web-legacy-compatibility-semantics)里面规定,浏览器的实现可以不遵守上面的规定,有自己的[行为方式](http://stackoverflow.com/questions/31419897/what-are-the-precise-semantics-of-block-level-functions-in-es6)。
|
||||
原来,如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 在[附录 B](http://www.ecma-international.org/ecma-262/6.0/index.html#sec-block-level-function-declarations-web-legacy-compatibility-semantics)里面规定,浏览器的实现可以不遵守上面的规定,有自己的[行为方式](http://stackoverflow.com/questions/31419897/what-are-the-precise-semantics-of-block-level-functions-in-es6)。
|
||||
|
||||
- 允许在块级作用域内声明函数。
|
||||
- 函数声明类似于`var`,即会提升到全局作用域或函数作用域的头部。
|
||||
- 同时,函数声明还会提升到所在的块级作用域的头部。
|
||||
|
||||
注意,上面三条规则只对ES6的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作`let`处理。
|
||||
注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作`let`处理。
|
||||
|
||||
前面那段代码,在 Chrome 环境下运行会报错。
|
||||
根据这三条规则,在浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于`var`声明的变量。
|
||||
|
||||
```javascript
|
||||
// ES6的浏览器环境
|
||||
// 浏览器的 ES6 环境
|
||||
function f() { console.log('I am outside!'); }
|
||||
|
||||
(function () {
|
||||
if (false) {
|
||||
// 重复声明一次函数f
|
||||
@ -375,10 +384,10 @@ function f() { console.log('I am outside!'); }
|
||||
// Uncaught TypeError: f is not a function
|
||||
```
|
||||
|
||||
上面的代码报错,是因为实际运行的是下面的代码。
|
||||
上面的代码在符合 ES6 的浏览器中,都会报错,因为实际运行的是下面的代码。
|
||||
|
||||
```javascript
|
||||
// ES6的浏览器环境
|
||||
// 浏览器的 ES6 环境
|
||||
function f() { console.log('I am outside!'); }
|
||||
(function () {
|
||||
var f = undefined;
|
||||
@ -411,7 +420,7 @@ function f() { console.log('I am outside!'); }
|
||||
}
|
||||
```
|
||||
|
||||
另外,还有一个需要注意的地方。ES6的块级作用域允许声明函数的规则,只在使用大括号的情况下成立,如果没有使用大括号,就会报错。
|
||||
另外,还有一个需要注意的地方。ES6 的块级作用域允许声明函数的规则,只在使用大括号的情况下成立,如果没有使用大括号,就会报错。
|
||||
|
||||
```javascript
|
||||
// 不报错
|
||||
@ -439,7 +448,7 @@ if (true)
|
||||
|
||||
上面代码中,块级作用域将两个语句封装在一起。但是,在块级作用域以外,没有办法得到`t`的值,因为块级作用域不返回值,除非`t`是全局变量。
|
||||
|
||||
现在有一个[提案](http://wiki.ecmascript.org/doku.php?id=strawman:do_expressions),使得块级作用域可以变为表达式,也就是说可以返回值,办法就是在块级作用域之前加上`do`,使它变为`do`表达式。
|
||||
现在有一个[提案](http://wiki.ecmascript.org/doku.php?id=strawman:do_expressions),使得块级作用域可以变为表达式,也就是说可以返回值,办法就是在块级作用域之前加上`do`,使它变为`do`表达式,然后就会返回内部最后执行的表达式的值。
|
||||
|
||||
```javascript
|
||||
let x = do {
|
||||
@ -448,9 +457,11 @@ let x = do {
|
||||
};
|
||||
```
|
||||
|
||||
上面代码中,变量`x`会得到整个块级作用域的返回值。
|
||||
上面代码中,变量`x`会得到整个块级作用域的返回值(`t * t + 1`)。
|
||||
|
||||
## const命令
|
||||
## const 命令
|
||||
|
||||
### 基本用法
|
||||
|
||||
`const`声明一个只读的常量。一旦声明,常量的值就不能改变。
|
||||
|
||||
@ -464,7 +475,7 @@ PI = 3;
|
||||
|
||||
上面代码表明改变常量的值会报错。
|
||||
|
||||
`const`声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。
|
||||
`const`声明的变量不得改变值,这意味着,`const`一旦声明变量,就必须立即初始化,不能留到以后赋值。
|
||||
|
||||
```javascript
|
||||
const foo;
|
||||
@ -505,15 +516,18 @@ const message = "Goodbye!";
|
||||
const age = 30;
|
||||
```
|
||||
|
||||
对于复合类型的变量,变量名不指向数据,而是指向数据所在的地址。`const`命令只是保证变量名指向的地址不变,并不保证该地址的数据不变,所以将一个对象声明为常量必须非常小心。
|
||||
### 本质
|
||||
|
||||
`const`实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针,`const`只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
|
||||
|
||||
```javascript
|
||||
const foo = {};
|
||||
|
||||
// 为 foo 添加一个属性,可以成功
|
||||
foo.prop = 123;
|
||||
foo.prop // 123
|
||||
|
||||
foo.prop
|
||||
// 123
|
||||
|
||||
// 将 foo 指向另一个对象,就会报错
|
||||
foo = {}; // TypeError: "foo" is read-only
|
||||
```
|
||||
|
||||
@ -521,7 +535,7 @@ foo = {}; // TypeError: "foo" is read-only
|
||||
|
||||
下面是另一个例子。
|
||||
|
||||
```js
|
||||
```javascript
|
||||
const a = [];
|
||||
a.push('Hello'); // 可执行
|
||||
a.length = 0; // 可执行
|
||||
@ -547,7 +561,7 @@ foo.prop = 123;
|
||||
```javascript
|
||||
var constantize = (obj) => {
|
||||
Object.freeze(obj);
|
||||
Object.keys(obj).forEach( (key, value) => {
|
||||
Object.keys(obj).forEach( (key, i) => {
|
||||
if ( typeof obj[key] === 'object' ) {
|
||||
constantize( obj[key] );
|
||||
}
|
||||
@ -555,11 +569,13 @@ var constantize = (obj) => {
|
||||
};
|
||||
```
|
||||
|
||||
ES5只有两种声明变量的方法:`var`命令和`function`命令。ES6除了添加`let`和`const`命令,后面章节还会提到,另外两种声明变量的方法:`import`命令和`class`命令。所以,ES6一共有6种声明变量的方法。
|
||||
### ES6 声明变量的六种方法
|
||||
|
||||
ES5 只有两种声明变量的方法:`var`命令和`function`命令。ES6 除了添加`let`和`const`命令,后面章节还会提到,另外两种声明变量的方法:`import`命令和`class`命令。所以,ES6 一共有 6 种声明变量的方法。
|
||||
|
||||
## 顶层对象的属性
|
||||
|
||||
顶层对象,在浏览器环境指的是`window`对象,在Node指的是`global`对象。ES5之中,顶层对象的属性与全局变量是等价的。
|
||||
顶层对象,在浏览器环境指的是`window`对象,在 Node 指的是`global`对象。ES5 之中,顶层对象的属性与全局变量是等价的。
|
||||
|
||||
```javascript
|
||||
window.a = 1;
|
||||
@ -571,14 +587,14 @@ window.a // 2
|
||||
|
||||
上面代码中,顶层对象的属性赋值与全局变量的赋值,是同一件事。
|
||||
|
||||
顶层对象的属性与全局变量挂钩,被认为是JavaScript语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,`window`对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。
|
||||
顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,`window`对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。
|
||||
|
||||
ES6为了改变这一点,一方面规定,为了保持兼容性,`var`命令和`function`命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,`let`命令、`const`命令、`class`命令声明的全局变量,不属于顶层对象的属性。也就是说,从ES6开始,全局变量将逐步与顶层对象的属性脱钩。
|
||||
ES6 为了改变这一点,一方面规定,为了保持兼容性,`var`命令和`function`命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,`let`命令、`const`命令、`class`命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。
|
||||
|
||||
```javascript
|
||||
var a = 1;
|
||||
// 如果在Node的REPL环境,可以写成global.a
|
||||
// 或者采用通用方法,写成this.a
|
||||
// 如果在 Node 的 REPL 环境,可以写成 global.a
|
||||
// 或者采用通用方法,写成 this.a
|
||||
window.a // 1
|
||||
|
||||
let b = 1;
|
||||
@ -589,17 +605,17 @@ window.b // undefined
|
||||
|
||||
## global 对象
|
||||
|
||||
ES5的顶层对象,本身也是一个问题,因为它在各种实现里面是不统一的。
|
||||
ES5 的顶层对象,本身也是一个问题,因为它在各种实现里面是不统一的。
|
||||
|
||||
- 浏览器里面,顶层对象是`window`,但 Node 和 Web Worker 没有`window`。
|
||||
- 浏览器和 Web Worker 里面,`self`也指向顶层对象,但是Node没有`self`。
|
||||
- 浏览器和 Web Worker 里面,`self`也指向顶层对象,但是 Node 没有`self`。
|
||||
- Node 里面,顶层对象是`global`,但其他环境都不支持。
|
||||
|
||||
同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用`this`变量,但是有局限性。
|
||||
|
||||
- 全局环境中,`this`会返回顶层对象。但是,Node模块和ES6模块中,`this`返回的是当前模块。
|
||||
- 全局环境中,`this`会返回顶层对象。但是,Node 模块和 ES6 模块中,`this`返回的是当前模块。
|
||||
- 函数里面的`this`,如果函数不是作为对象的方法运行,而是单纯作为函数运行,`this`会指向顶层对象。但是,严格模式下,这时`this`会返回`undefined`。
|
||||
- 不管是严格模式,还是普通模式,`new Function('return this')()`,总是会返回全局对象。但是,如果浏览器用了CSP(Content Security Policy,内容安全政策),那么`eval`、`new Function`这些方法都可能无法使用。
|
||||
- 不管是严格模式,还是普通模式,`new Function('return this')()`,总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全政策),那么`eval`、`new Function`这些方法都可能无法使用。
|
||||
|
||||
综上所述,很难找到一种方法,可以在所有情况下,都取到顶层对象。下面是两种勉强可以使用的方法。
|
||||
|
||||
@ -627,23 +643,22 @@ var getGlobal = function () {
|
||||
垫片库[`system.global`](https://github.com/ljharb/System.global)模拟了这个提案,可以在所有环境拿到`global`。
|
||||
|
||||
```javascript
|
||||
// CommonJS的写法
|
||||
// CommonJS 的写法
|
||||
require('system.global/shim')();
|
||||
|
||||
// ES6模块的写法
|
||||
// ES6 模块的写法
|
||||
import shim from 'system.global/shim'; shim();
|
||||
```
|
||||
|
||||
上面代码可以保证各种环境里面,`global`对象都是存在的。
|
||||
|
||||
```javascript
|
||||
// CommonJS的写法
|
||||
// CommonJS 的写法
|
||||
var global = require('system.global')();
|
||||
|
||||
// ES6模块的写法
|
||||
// ES6 模块的写法
|
||||
import getGlobal from 'system.global';
|
||||
const global = getGlobal();
|
||||
```
|
||||
|
||||
上面代码将顶层对象放入变量`global`。
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
# Mixin
|
||||
|
||||
JavaScript语言的设计是单一继承,即子类只能继承一个父类,不允许继承多个父类。这种设计保证了对象继承的层次结构是树状的,而不是复杂的[网状结构](https://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem)。
|
||||
JavaScript 语言的设计是单一继承,即子类只能继承一个父类,不允许继承多个父类。这种设计保证了对象继承的层次结构是树状的,而不是复杂的[网状结构](https://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem)。
|
||||
|
||||
但是,这大大降低了编程的灵活性。因为实际开发中,有时不可避免,子类需要继承多个父类。举例来说,“猫”可以继承“哺乳类动物”,也可以继承“宠物”。
|
||||
|
||||
各种单一继承的编程语言,有不同的多重继承解决方案。比如,Java语言也是子类只能继承一个父类,但是还允许继承多个界面(interface),这样就间接实现了多重继承。Interface与父类一样,也是一个类,只不过它只定义接口(method signature),不定义实现,因此又被称为“抽象类”。凡是继承于Interface的方法,都必须自己定义实现,否则就会报错。这样就避免了多重继承的最大问题:多个父类的同名方法的碰撞(naming collision)。
|
||||
各种单一继承的编程语言,有不同的多重继承解决方案。比如,Java 语言也是子类只能继承一个父类,但是还允许继承多个界面(interface),这样就间接实现了多重继承。Interface 与父类一样,也是一个类,只不过它只定义接口(method signature),不定义实现,因此又被称为“抽象类”。凡是继承于 Interface 的方法,都必须自己定义实现,否则就会报错。这样就避免了多重继承的最大问题:多个父类的同名方法的碰撞(naming collision)。
|
||||
|
||||
JavaScript语言没有采用Interface的方案,而是通过代理(delegation)实现了从其他类引入方法。
|
||||
JavaScript 语言没有采用 Interface 的方案,而是通过代理(delegation)实现了从其他类引入方法。
|
||||
|
||||
```javascript
|
||||
var Enumerable_first = function () {
|
||||
@ -24,15 +24,15 @@ list.first() // "foo"
|
||||
|
||||
## 含义
|
||||
|
||||
Mixin这个名字来自于冰淇淋,在基本口味的冰淇淋上面混入其他口味,这就叫做Mix-in。
|
||||
Mixin 这个名字来自于冰淇淋,在基本口味的冰淇淋上面混入其他口味,这就叫做 Mix-in。
|
||||
|
||||
它允许向一个类里面注入一些代码,使得一个类的功能能够“混入”另一个类。实质上是多重继承的一种解决方案,但是避免了多重继承的复杂性,而且有利于代码复用。
|
||||
|
||||
Mixin就是一个正常的类,不仅定义了接口,还定义了接口的实现。
|
||||
Mixin 就是一个正常的类,不仅定义了接口,还定义了接口的实现。
|
||||
|
||||
子类通过在`this`对象上面绑定方法,达到多重继承的目的。
|
||||
|
||||
很多库提供了Mixin功能。下面以Lodash为例。
|
||||
很多库提供了 Mixin 功能。下面以 Lodash 为例。
|
||||
|
||||
```javascript
|
||||
function vowels(string) {
|
||||
@ -44,9 +44,9 @@ _.mixin(obj, {vowels: vowels})
|
||||
obj.vowels() // true
|
||||
```
|
||||
|
||||
上面代码通过Lodash库的`_.mixin`方法,让`obj`对象继承了`vowels`方法。
|
||||
上面代码通过 Lodash 库的`_.mixin`方法,让`obj`对象继承了`vowels`方法。
|
||||
|
||||
Underscore的类似方法是`_.extend`。
|
||||
Underscore 的类似方法是`_.extend`。
|
||||
|
||||
```javascript
|
||||
var Person = function (fName, lName) {
|
||||
@ -90,7 +90,7 @@ function extend(destination, source) {
|
||||
|
||||
## Trait
|
||||
|
||||
Trait是另外一种多重继承的解决方案。它与Mixin很相似,但是有一些细微的差别。
|
||||
Trait 是另外一种多重继承的解决方案。它与 Mixin 很相似,但是有一些细微的差别。
|
||||
|
||||
- Mixin可以包含状态(state),Trait不包含,即Trait里面的方法都是互不相干,可以线性包含的。比如,`Trait1`包含方法`A`和`B`,`Trait2`继承了`Trait1`,同时还包含一个自己的方法`C`,实际上就等同于直接包含方法`A`、`B`、`C`。
|
||||
- 对于同名方法的碰撞,Mixin包含了解决规则,Trait则是报错。
|
||||
- Mixin 可以包含状态(state),Trait 不包含,即 Trait 里面的方法都是互不相干,可以线性包含的。比如,`Trait1`包含方法`A`和`B`,`Trait2`继承了`Trait1`,同时还包含一个自己的方法`C`,实际上就等同于直接包含方法`A`、`B`、`C`。
|
||||
- 对于同名方法的碰撞,Mixin 包含了解决规则,Trait 则是报错。
|
||||
|
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`方法指定回调函数。
|
730
docs/module.md
730
docs/module.md
@ -1,8 +1,10 @@
|
||||
# Module
|
||||
# Module 的语法
|
||||
|
||||
## 概述
|
||||
|
||||
历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的`require`、Python 的`import`,甚至就连 CSS 都有`@import`,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。
|
||||
|
||||
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代现有的 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
|
||||
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
|
||||
|
||||
ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
|
||||
|
||||
@ -12,10 +14,12 @@ let { stat, exists, readFile } = require('fs');
|
||||
|
||||
// 等同于
|
||||
let _fs = require('fs');
|
||||
let stat = _fs.stat, exists = _fs.exists, readfile = _fs.readfile;
|
||||
let stat = _fs.stat;
|
||||
let exists = _fs.exists;
|
||||
let readfile = _fs.readfile;
|
||||
```
|
||||
|
||||
上面代码的实质是整体加载`fs`模块(即加载`fs`的所有方法),生成一个对象(`_fs`),然后再从这个对象上面读取3个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
|
||||
上面代码的实质是整体加载`fs`模块(即加载`fs`的所有方法),生成一个对象(`_fs`),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
|
||||
|
||||
ES6 模块不是对象,而是通过`export`命令显式指定输出的代码,再通过`import`命令输入。
|
||||
|
||||
@ -24,16 +28,18 @@ ES6 模块不是对象,而是通过`export`命令显式指定输出的代码
|
||||
import { stat, exists, readFile } from 'fs';
|
||||
```
|
||||
|
||||
上面代码的实质是从`fs`模块加载3个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
|
||||
上面代码的实质是从`fs`模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
|
||||
|
||||
由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
|
||||
|
||||
除了静态加载带来的各种好处,ES6 模块还有以下好处。
|
||||
|
||||
- 不再需要`UMD`模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
|
||||
- 将来浏览器的新 API 就能用模块格式提供,不再必要做成全局变量或者`navigator`对象的属性。
|
||||
- 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者`navigator`对象的属性。
|
||||
- 不再需要对象作为命名空间(比如`Math`对象),未来这些功能可以通过模块提供。
|
||||
|
||||
本章介绍 ES6 模块的语法,下一章介绍如何在浏览器和 Node 之中,加载 ES6 模块。
|
||||
|
||||
## 严格模式
|
||||
|
||||
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上`"use strict";`。
|
||||
@ -44,7 +50,7 @@ ES6 的模块自动采用严格模式,不管你有没有在模块头部加上`
|
||||
- 函数的参数不能有同名属性,否则报错
|
||||
- 不能使用`with`语句
|
||||
- 不能对只读属性赋值,否则报错
|
||||
- 不能使用前缀0表示八进制数,否则报错
|
||||
- 不能使用前缀 0 表示八进制数,否则报错
|
||||
- 不能删除不可删除的属性,否则报错
|
||||
- 不能删除变量`delete prop`,会报错,只能删除属性`delete global[prop]`
|
||||
- `eval`不会在它的外层作用域引入变量
|
||||
@ -58,6 +64,8 @@ ES6 的模块自动采用严格模式,不管你有没有在模块头部加上`
|
||||
|
||||
上面这些限制,模块都必须遵守。由于严格模式是 ES5 引入的,不属于 ES6,所以请参阅相关 ES5 书籍,本书不再详细介绍了。
|
||||
|
||||
其中,尤其需要注意`this`的限制。ES6 模块之中,顶层的`this`指向`undefined`,即不应该在顶层代码使用`this`。
|
||||
|
||||
## export 命令
|
||||
|
||||
模块功能主要由两个命令构成:`export`和`import`。`export`命令用于规定模块的对外接口,`import`命令用于输入其他模块提供的功能。
|
||||
@ -71,7 +79,7 @@ export var lastName = 'Jackson';
|
||||
export var year = 1958;
|
||||
```
|
||||
|
||||
上面代码是`profile.js`文件,保存了用户信息。ES6将其视为一个模块,里面用`export`命令对外部输出了三个变量。
|
||||
上面代码是`profile.js`文件,保存了用户信息。ES6 将其视为一个模块,里面用`export`命令对外部输出了三个变量。
|
||||
|
||||
`export`的写法,除了像上面这样,还有另外一种。
|
||||
|
||||
@ -86,7 +94,7 @@ export {firstName, lastName, year};
|
||||
|
||||
上面代码在`export`命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在`var`语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。
|
||||
|
||||
export命令除了输出变量,还可以输出函数或类(class)。
|
||||
`export`命令除了输出变量,还可以输出函数或类(class)。
|
||||
|
||||
```javascript
|
||||
export function multiply(x, y) {
|
||||
@ -122,7 +130,7 @@ var m = 1;
|
||||
export m;
|
||||
```
|
||||
|
||||
上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出1,第二种写法通过变量`m`,还是直接输出1。`1`只是一个值,不是接口。正确的写法是下面这样。
|
||||
上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出 1,第二种写法通过变量`m`,还是直接输出 1。`1`只是一个值,不是接口。正确的写法是下面这样。
|
||||
|
||||
```javascript
|
||||
// 写法一
|
||||
@ -161,11 +169,11 @@ export var foo = 'bar';
|
||||
setTimeout(() => foo = 'baz', 500);
|
||||
```
|
||||
|
||||
上面代码输出变量`foo`,值为`bar`,500毫秒之后变成`baz`。
|
||||
上面代码输出变量`foo`,值为`bar`,500 毫秒之后变成`baz`。
|
||||
|
||||
这一点与CommonJS规范完全不同。CommonJS模块输出的是值的缓存,不存在动态更新,详见下文《ES6模块加载的实质》一节。
|
||||
这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新,详见下文《Module 的加载实现》一节。
|
||||
|
||||
最后,`export`命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的`import`命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了ES6模块的设计初衷。
|
||||
最后,`export`命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的`import`命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
|
||||
|
||||
```javascript
|
||||
function foo() {
|
||||
@ -197,7 +205,7 @@ function setName(element) {
|
||||
import { lastName as surname } from './profile';
|
||||
```
|
||||
|
||||
`import`后面的`from`指定模块文件的位置,可以是相对路径,也可以是绝对路径,`.js`路径可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
|
||||
`import`后面的`from`指定模块文件的位置,可以是相对路径,也可以是绝对路径,`.js`后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
|
||||
|
||||
```javascript
|
||||
import {myMethod} from 'util';
|
||||
@ -262,6 +270,14 @@ import { foo, bar } from 'my_module';
|
||||
|
||||
上面代码中,虽然`foo`和`bar`在两个语句中加载,但是它们对应的是同一个`my_module`实例。也就是说,`import`语句是 Singleton 模式。
|
||||
|
||||
目前阶段,通过 Babel 转码,CommonJS 模块的`require`命令和 ES6 模块的`import`命令,可以写在同一个模块里面,但是最好不要这样做。因为`import`在静态解析阶段执行,所以它是一个模块之中最早执行的。下面的代码可能不会得到预期结果。
|
||||
|
||||
```javascript
|
||||
require('core-js/modules/es6.symbol');
|
||||
require('core-js/modules/es6.promise');
|
||||
import React from 'React';
|
||||
```
|
||||
|
||||
## 模块的整体加载
|
||||
|
||||
除了指定加载某个输出值,还可以使用整体加载,即用星号(`*`)指定一个对象,所有输出值都加载在这个对象上面。
|
||||
@ -300,6 +316,16 @@ console.log('圆面积:' + circle.area(4));
|
||||
console.log('圆周长:' + circle.circumference(14));
|
||||
```
|
||||
|
||||
注意,模块整体加载所在的那个对象(上例是`circle`),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。
|
||||
|
||||
```javascript
|
||||
import * as circle from './circle';
|
||||
|
||||
// 下面两行都是不允许的
|
||||
circle.foo = 'hello';
|
||||
circle.area = function () {};
|
||||
```
|
||||
|
||||
## export default 命令
|
||||
|
||||
从前面的例子可以看出,使用`import`命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。
|
||||
@ -378,9 +404,9 @@ export {add as default};
|
||||
// export default add;
|
||||
|
||||
// app.js
|
||||
import { default as xxx } from 'modules';
|
||||
import { default as foo } from 'modules';
|
||||
// 等同于
|
||||
// import xxx from 'modules';
|
||||
// import foo from 'modules';
|
||||
```
|
||||
|
||||
正是因为`export default`命令其实只是输出一个叫做`default`的变量,所以它后面不能跟变量声明语句。
|
||||
@ -399,16 +425,28 @@ export default var a = 1;
|
||||
|
||||
上面代码中,`export default a`的含义是将变量`a`的值赋给变量`default`。所以,最后一种写法会报错。
|
||||
|
||||
同样地,因为`export default`本质是将该命令后面的值,赋给`default`变量以后再默认,所以直接将一个值写在`export default`之后。
|
||||
|
||||
```javascript
|
||||
// 正确
|
||||
export default 42;
|
||||
|
||||
// 报错
|
||||
export 42;
|
||||
```
|
||||
|
||||
上面代码中,后一句报错是因为没有指定对外的接口,而前一句指定外对接口为`default`。
|
||||
|
||||
有了`export default`命令,输入模块时就非常直观了,以输入 lodash 模块为例。
|
||||
|
||||
```javascript
|
||||
import _ from 'lodash';
|
||||
```
|
||||
|
||||
如果想在一条`import`语句中,同时输入默认方法和其他变量,可以写成下面这样。
|
||||
如果想在一条`import`语句中,同时输入默认方法和其他接口,可以写成下面这样。
|
||||
|
||||
```javascript
|
||||
import _, { each } from 'lodash';
|
||||
import _, { each, each as forEach } from 'lodash';
|
||||
```
|
||||
|
||||
对应上面代码的`export`语句如下。
|
||||
@ -417,20 +455,16 @@ import _, { each } from 'lodash';
|
||||
export default function (obj) {
|
||||
// ···
|
||||
}
|
||||
|
||||
export function each(obj, iterator, context) {
|
||||
// ···
|
||||
}
|
||||
|
||||
export { each as forEach };
|
||||
```
|
||||
|
||||
上面代码的最后一行的意思是,暴露出`forEach`接口,默认指向`each`接口,即`forEach`和`each`指向同一个方法。
|
||||
|
||||
如果要输出默认的值,只需将值跟在`export default`之后即可。
|
||||
|
||||
```javascript
|
||||
export default 42;
|
||||
```
|
||||
|
||||
`export default`也可以用来输出类。
|
||||
|
||||
```javascript
|
||||
@ -451,7 +485,7 @@ export { foo, bar } from 'my_module';
|
||||
|
||||
// 等同于
|
||||
import { foo, bar } from 'my_module';
|
||||
export { foo, boo};
|
||||
export { foo, bar };
|
||||
```
|
||||
|
||||
上面代码中,`export`和`import`语句可以结合在一起,写成一行。
|
||||
@ -488,14 +522,20 @@ export default es6;
|
||||
export { default as es6 } from './someModule';
|
||||
```
|
||||
|
||||
另外,ES7有一个[提案](https://github.com/leebyron/ecmascript-more-export-from),简化先输入后输出的写法,拿掉输出时的大括号。
|
||||
下面三种`import`语句,没有对应的复合写法。
|
||||
|
||||
```javascript
|
||||
// 现行的写法
|
||||
export {v} from 'mod';
|
||||
import * as someIdentifier from "someModule";
|
||||
import someIdentifier from "someModule";
|
||||
import someIdentifier, { namedIdentifier } from "someModule";
|
||||
```
|
||||
|
||||
// 提案的写法
|
||||
export v from 'mod';
|
||||
为了做到形式的对称,现在有[提案](https://github.com/leebyron/ecmascript-export-default-from),提出补上这三种复合写法。
|
||||
|
||||
```javascript
|
||||
export * as someIdentifier from "someModule";
|
||||
export someIdentifier from "someModule";
|
||||
export someIdentifier, { namedIdentifier } from "someModule";
|
||||
```
|
||||
|
||||
## 模块的继承
|
||||
@ -538,477 +578,9 @@ console.log(exp(math.e));
|
||||
|
||||
上面代码中的`import exp`表示,将`circleplus`模块的默认方法加载为`exp`方法。
|
||||
|
||||
## ES6模块加载的实质
|
||||
|
||||
ES6模块加载的机制,与CommonJS模块完全不同。CommonJS模块输出的是一个值的拷贝,而ES6模块输出的是值的引用。
|
||||
|
||||
CommonJS模块输出的是被输出值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件`lib.js`的例子。
|
||||
|
||||
```javascript
|
||||
// lib.js
|
||||
var counter = 3;
|
||||
function incCounter() {
|
||||
counter++;
|
||||
}
|
||||
module.exports = {
|
||||
counter: counter,
|
||||
incCounter: incCounter,
|
||||
};
|
||||
```
|
||||
|
||||
上面代码输出内部变量`counter`和改写这个变量的内部方法`incCounter`。然后,在`main.js`里面加载这个模块。
|
||||
|
||||
```javascript
|
||||
// main.js
|
||||
var mod = require('./lib');
|
||||
|
||||
console.log(mod.counter); // 3
|
||||
mod.incCounter();
|
||||
console.log(mod.counter); // 3
|
||||
```
|
||||
|
||||
上面代码说明,`lib.js`模块加载以后,它的内部变化就影响不到输出的`mod.counter`了。这是因为`mod.counter`是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
|
||||
|
||||
```javascript
|
||||
// lib.js
|
||||
var counter = 3;
|
||||
function incCounter() {
|
||||
counter++;
|
||||
}
|
||||
module.exports = {
|
||||
get counter() {
|
||||
return counter
|
||||
},
|
||||
incCounter: incCounter,
|
||||
};
|
||||
```
|
||||
|
||||
上面代码中,输出的`counter`属性实际上是一个取值器函数。现在再执行`main.js`,就可以正确读取内部变量`counter`的变动了。
|
||||
|
||||
```bash
|
||||
$ node main.js
|
||||
3
|
||||
4
|
||||
```
|
||||
|
||||
ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令`import`时,不会去执行模块,而是只生成一个动态的只读引用。等到真的需要用到时,再到模块里面去取值,换句话说,ES6的输入有点像Unix系统的“符号连接”,原始值变了,`import`输入的值也会跟着变。因此,ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
|
||||
|
||||
还是举上面的例子。
|
||||
|
||||
```javascript
|
||||
// lib.js
|
||||
export let counter = 3;
|
||||
export function incCounter() {
|
||||
counter++;
|
||||
}
|
||||
|
||||
// main.js
|
||||
import { counter, incCounter } from './lib';
|
||||
console.log(counter); // 3
|
||||
incCounter();
|
||||
console.log(counter); // 4
|
||||
```
|
||||
|
||||
上面代码说明,ES6模块输入的变量`counter`是活的,完全反应其所在模块`lib.js`内部的变化。
|
||||
|
||||
再举一个出现在`export`一节中的例子。
|
||||
|
||||
```javascript
|
||||
// m1.js
|
||||
export var foo = 'bar';
|
||||
setTimeout(() => foo = 'baz', 500);
|
||||
|
||||
// m2.js
|
||||
import {foo} from './m1.js';
|
||||
console.log(foo);
|
||||
setTimeout(() => console.log(foo), 500);
|
||||
```
|
||||
|
||||
上面代码中,`m1.js`的变量`foo`,在刚加载时等于`bar`,过了500毫秒,又变为等于`baz`。
|
||||
|
||||
让我们看看,`m2.js`能否正确读取这个变化。
|
||||
|
||||
```bash
|
||||
$ babel-node m2.js
|
||||
|
||||
bar
|
||||
baz
|
||||
```
|
||||
|
||||
上面代码表明,ES6模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。
|
||||
|
||||
由于ES6输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。
|
||||
|
||||
```javascript
|
||||
// lib.js
|
||||
export let obj = {};
|
||||
|
||||
// main.js
|
||||
import { obj } from './lib';
|
||||
|
||||
obj.prop = 123; // OK
|
||||
obj = {}; // TypeError
|
||||
```
|
||||
|
||||
上面代码中,`main.js`从`lib.js`输入变量`obj`,可以对`obj`添加属性,但是重新赋值就会报错。因为变量`obj`指向的地址是只读的,不能重新赋值,这就好比`main.js`创造了一个名为`obj`的const变量。
|
||||
|
||||
最后,`export`通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。
|
||||
|
||||
```javascript
|
||||
// mod.js
|
||||
function C() {
|
||||
this.sum = 0;
|
||||
this.add = function () {
|
||||
this.sum += 1;
|
||||
};
|
||||
this.show = function () {
|
||||
console.log(this.sum);
|
||||
};
|
||||
}
|
||||
|
||||
export let c = new C();
|
||||
```
|
||||
|
||||
上面的脚本`mod.js`,输出的是一个`C`的实例。不同的脚本加载这个模块,得到的都是同一个实例。
|
||||
|
||||
```javascript
|
||||
// x.js
|
||||
import {c} from './mod';
|
||||
c.add();
|
||||
|
||||
// y.js
|
||||
import {c} from './mod';
|
||||
c.show();
|
||||
|
||||
// main.js
|
||||
import './x';
|
||||
import './y';
|
||||
```
|
||||
|
||||
现在执行`main.js`,输出的是1。
|
||||
|
||||
```bash
|
||||
$ babel-node main.js
|
||||
1
|
||||
```
|
||||
|
||||
这就证明了`x.js`和`y.js`加载的都是`C`的同一个实例。
|
||||
|
||||
## 浏览器的模块加载
|
||||
|
||||
浏览器使用 ES6 模块的语法如下。
|
||||
|
||||
```html
|
||||
<script type="module" src="foo.js"></script>
|
||||
```
|
||||
|
||||
上面代码在网页中插入一个模块`foo.js`,由于`type`属性设为`module`,所以浏览器知道这是一个 ES6 模块。
|
||||
|
||||
浏览器对于带有`type="module"`的`<script>`,都是异步加载外部脚本,不会造成堵塞浏览器。
|
||||
|
||||
对于外部的模块脚本(上例是`foo.js`),有几点需要注意。
|
||||
|
||||
- 该脚本自动采用严格模块。
|
||||
- 该脚本内部的顶层变量,都只在该脚本内部有效,外部不可见。
|
||||
- 该脚本内部的顶层的`this`关键字,返回`undefined`,而不是指向`window`。
|
||||
|
||||
## 循环加载
|
||||
|
||||
“循环加载”(circular dependency)指的是,`a`脚本的执行依赖`b`脚本,而`b`脚本的执行又依赖`a`脚本。
|
||||
|
||||
```javascript
|
||||
// a.js
|
||||
var b = require('b');
|
||||
|
||||
// b.js
|
||||
var a = require('a');
|
||||
```
|
||||
|
||||
通常,“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。
|
||||
|
||||
但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目,很容易出现`a`依赖`b`,`b`依赖`c`,`c`又依赖`a`这样的情况。这意味着,模块加载机制必须考虑“循环加载”的情况。
|
||||
|
||||
对于JavaScript语言来说,目前最常见的两种模块格式CommonJS和ES6,处理“循环加载”的方法是不一样的,返回的结果也不一样。
|
||||
|
||||
### CommonJS模块的加载原理
|
||||
|
||||
介绍ES6如何处理"循环加载"之前,先介绍目前最流行的CommonJS模块格式的加载原理。
|
||||
|
||||
CommonJS的一个模块,就是一个脚本文件。`require`命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: '...',
|
||||
exports: { ... },
|
||||
loaded: true,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
上面代码就是Node内部加载模块后生成的一个对象。该对象的`id`属性是模块名,`exports`属性是模块输出的各个接口,`loaded`属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。
|
||||
|
||||
以后需要用到这个模块的时候,就会到`exports`属性上面取值。即使再次执行`require`命令,也不会再次执行该模块,而是到缓存之中取值。也就是说,CommonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
|
||||
|
||||
### CommonJS模块的循环加载
|
||||
|
||||
CommonJS模块的重要特性是加载时执行,即脚本代码在`require`的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
|
||||
|
||||
让我们来看,Node[官方文档](https://nodejs.org/api/modules.html#modules_cycles)里面的例子。脚本文件`a.js`代码如下。
|
||||
|
||||
```javascript
|
||||
exports.done = false;
|
||||
var b = require('./b.js');
|
||||
console.log('在 a.js 之中,b.done = %j', b.done);
|
||||
exports.done = true;
|
||||
console.log('a.js 执行完毕');
|
||||
```
|
||||
|
||||
上面代码之中,`a.js`脚本先输出一个`done`变量,然后加载另一个脚本文件`b.js`。注意,此时`a.js`代码就停在这里,等待`b.js`执行完毕,再往下执行。
|
||||
|
||||
再看`b.js`的代码。
|
||||
|
||||
```javascript
|
||||
exports.done = false;
|
||||
var a = require('./a.js');
|
||||
console.log('在 b.js 之中,a.done = %j', a.done);
|
||||
exports.done = true;
|
||||
console.log('b.js 执行完毕');
|
||||
```
|
||||
|
||||
上面代码之中,`b.js`执行到第二行,就会去加载`a.js`,这时,就发生了“循环加载”。系统会去`a.js`模块对应对象的`exports`属性取值,可是因为`a.js`还没有执行完,从`exports`属性只能取回已经执行的部分,而不是最后的值。
|
||||
|
||||
`a.js`已经执行的部分,只有一行。
|
||||
|
||||
```javascript
|
||||
exports.done = false;
|
||||
```
|
||||
|
||||
因此,对于`b.js`来说,它从`a.js`只输入一个变量`done`,值为`false`。
|
||||
|
||||
然后,`b.js`接着往下执行,等到全部执行完毕,再把执行权交还给`a.js`。于是,`a.js`接着往下执行,直到执行完毕。我们写一个脚本`main.js`,验证这个过程。
|
||||
|
||||
```javascript
|
||||
var a = require('./a.js');
|
||||
var b = require('./b.js');
|
||||
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
|
||||
```
|
||||
|
||||
执行`main.js`,运行结果如下。
|
||||
|
||||
```bash
|
||||
$ node main.js
|
||||
|
||||
在 b.js 之中,a.done = false
|
||||
b.js 执行完毕
|
||||
在 a.js 之中,b.done = true
|
||||
a.js 执行完毕
|
||||
在 main.js 之中, a.done=true, b.done=true
|
||||
```
|
||||
|
||||
上面的代码证明了两件事。一是,在`b.js`之中,`a.js`没有执行完毕,只执行了第一行。二是,`main.js`执行到第二行时,不会再次执行`b.js`,而是输出缓存的`b.js`的执行结果,即它的第四行。
|
||||
|
||||
```javascript
|
||||
exports.done = true;
|
||||
```
|
||||
|
||||
总之,CommonJS输入的是被输出值的拷贝,不是引用。
|
||||
|
||||
另外,由于CommonJS模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。
|
||||
|
||||
```javascript
|
||||
var a = require('a'); // 安全的写法
|
||||
var foo = require('a').foo; // 危险的写法
|
||||
|
||||
exports.good = function (arg) {
|
||||
return a.foo('good', arg); // 使用的是 a.foo 的最新值
|
||||
};
|
||||
|
||||
exports.bad = function (arg) {
|
||||
return foo('bad', arg); // 使用的是一个部分加载时的值
|
||||
};
|
||||
```
|
||||
|
||||
上面代码中,如果发生循环加载,`require('a').foo`的值很可能后面会被改写,改用`require('a')`会更保险一点。
|
||||
|
||||
### ES6模块的循环加载
|
||||
|
||||
ES6处理“循环加载”与CommonJS有本质的不同。ES6模块是动态引用,如果使用`import`从一个模块加载变量(即`import foo from 'foo'`),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
|
||||
|
||||
请看下面这个例子。
|
||||
|
||||
```javascript
|
||||
// a.js如下
|
||||
import {bar} from './b.js';
|
||||
console.log('a.js');
|
||||
console.log(bar);
|
||||
export let foo = 'foo';
|
||||
|
||||
// b.js
|
||||
import {foo} from './a.js';
|
||||
console.log('b.js');
|
||||
console.log(foo);
|
||||
export let bar = 'bar';
|
||||
```
|
||||
|
||||
上面代码中,`a.js`加载`b.js`,`b.js`又加载`a.js`,构成循环加载。执行`a.js`,结果如下。
|
||||
|
||||
```bash
|
||||
$ babel-node a.js
|
||||
b.js
|
||||
undefined
|
||||
a.js
|
||||
bar
|
||||
```
|
||||
|
||||
上面代码中,由于`a.js`的第一行是加载`b.js`,所以先执行的是`b.js`。而`b.js`的第一行又是加载`a.js`,这时由于`a.js`已经开始执行了,所以不会重复执行,而是继续往下执行`b.js`,所以第一行输出的是`b.js`。
|
||||
|
||||
接着,`b.js`要打印变量`foo`,这时`a.js`还没执行完,取不到`foo`的值,导致打印出来是`undefined`。`b.js`执行完,开始执行`a.js`,这时就一切正常了。
|
||||
|
||||
再看一个稍微复杂的例子(摘自 Dr. Axel Rauschmayer 的[《Exploring ES6》](http://exploringjs.com/es6/ch_modules.html))。
|
||||
|
||||
```javascript
|
||||
// a.js
|
||||
import {bar} from './b.js';
|
||||
export function foo() {
|
||||
console.log('foo');
|
||||
bar();
|
||||
console.log('执行完毕');
|
||||
}
|
||||
foo();
|
||||
|
||||
// b.js
|
||||
import {foo} from './a.js';
|
||||
export function bar() {
|
||||
console.log('bar');
|
||||
if (Math.random() > 0.5) {
|
||||
foo();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
按照CommonJS规范,上面的代码是没法执行的。`a`先加载`b`,然后`b`又加载`a`,这时`a`还没有任何执行结果,所以输出结果为`null`,即对于`b.js`来说,变量`foo`的值等于`null`,后面的`foo()`就会报错。
|
||||
|
||||
但是,ES6可以执行上面的代码。
|
||||
|
||||
```bash
|
||||
$ babel-node a.js
|
||||
foo
|
||||
bar
|
||||
执行完毕
|
||||
|
||||
// 执行结果也有可能是
|
||||
foo
|
||||
bar
|
||||
foo
|
||||
bar
|
||||
执行完毕
|
||||
执行完毕
|
||||
```
|
||||
|
||||
上面代码中,`a.js`之所以能够执行,原因就在于ES6加载的变量,都是动态引用其所在的模块。只要引用存在,代码就能执行。
|
||||
|
||||
下面,我们详细分析这段代码的运行过程。
|
||||
|
||||
```javascript
|
||||
// a.js
|
||||
|
||||
// 这一行建立一个引用,
|
||||
// 从`b.js`引用`bar`
|
||||
import {bar} from './b.js';
|
||||
|
||||
export function foo() {
|
||||
// 执行时第一行输出 foo
|
||||
console.log('foo');
|
||||
// 到 b.js 执行 bar
|
||||
bar();
|
||||
console.log('执行完毕');
|
||||
}
|
||||
foo();
|
||||
|
||||
// b.js
|
||||
|
||||
// 建立`a.js`的`foo`引用
|
||||
import {foo} from './a.js';
|
||||
|
||||
export function bar() {
|
||||
// 执行时,第二行输出 bar
|
||||
console.log('bar');
|
||||
// 递归执行 foo,一旦随机数
|
||||
// 小于等于0.5,就停止执行
|
||||
if (Math.random() > 0.5) {
|
||||
foo();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我们再来看ES6模块加载器[SystemJS](https://github.com/ModuleLoader/es6-module-loader/blob/master/docs/circular-references-bindings.md)给出的一个例子。
|
||||
|
||||
```javascript
|
||||
// even.js
|
||||
import { odd } from './odd'
|
||||
export var counter = 0;
|
||||
export function even(n) {
|
||||
counter++;
|
||||
return n == 0 || odd(n - 1);
|
||||
}
|
||||
|
||||
// odd.js
|
||||
import { even } from './even';
|
||||
export function odd(n) {
|
||||
return n != 0 && even(n - 1);
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,`even.js`里面的函数`even`有一个参数`n`,只要不等于0,就会减去1,传入加载的`odd()`。`odd.js`也会做类似操作。
|
||||
|
||||
运行上面这段代码,结果如下。
|
||||
|
||||
```javascript
|
||||
$ babel-node
|
||||
> import * as m from './even.js';
|
||||
> m.even(10);
|
||||
true
|
||||
> m.counter
|
||||
6
|
||||
> m.even(20)
|
||||
true
|
||||
> m.counter
|
||||
17
|
||||
```
|
||||
|
||||
上面代码中,参数`n`从10变为0的过程中,`even()`一共会执行6次,所以变量`counter`等于6。第二次调用`even()`时,参数`n`从20变为0,`even()`一共会执行11次,加上前面的6次,所以变量`counter`等于17。
|
||||
|
||||
这个例子要是改写成CommonJS,就根本无法执行,会报错。
|
||||
|
||||
```javascript
|
||||
// even.js
|
||||
var odd = require('./odd');
|
||||
var counter = 0;
|
||||
exports.counter = counter;
|
||||
exports.even = function(n) {
|
||||
counter++;
|
||||
return n == 0 || odd(n - 1);
|
||||
}
|
||||
|
||||
// odd.js
|
||||
var even = require('./even').even;
|
||||
module.exports = function(n) {
|
||||
return n != 0 && even(n - 1);
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,`even.js`加载`odd.js`,而`odd.js`又去加载`even.js`,形成“循环加载”。这时,执行引擎就会输出`even.js`已经执行的部分(不存在任何结果),所以在`odd.js`之中,变量`even`等于`null`,等到后面调用`even(n-1)`就会报错。
|
||||
|
||||
```bash
|
||||
$ node
|
||||
> var m = require('./even');
|
||||
> m.even(10)
|
||||
TypeError: even is not a function
|
||||
```
|
||||
|
||||
## 跨模块常量
|
||||
|
||||
本书介绍`const`命令的时候说过,`const`声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),可以采用下面的写法。
|
||||
本书介绍`const`命令的时候说过,`const`声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法。
|
||||
|
||||
```javascript
|
||||
// constants.js 模块
|
||||
@ -1053,12 +625,14 @@ export {users} from './users';
|
||||
|
||||
```javascript
|
||||
// script.js
|
||||
import {db, users} from './constants';
|
||||
import {db, users} from './index';
|
||||
```
|
||||
|
||||
## import()
|
||||
|
||||
上面说过了,`import`语句会被JavaScript引擎静态分析,先于模块内的其他模块执行(叫做”连接“更合适)。所以,下面的代码会报错。
|
||||
### 简介
|
||||
|
||||
前面介绍过,`import`命令会被 JavaScript 引擎静态分析,先于模块内的其他模块执行(叫做”连接“更合适)。所以,下面的代码会报错。
|
||||
|
||||
```javascript
|
||||
// 报错
|
||||
@ -1067,9 +641,9 @@ if (x === 2) {
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,引擎处理`import`语句是在执行之前,所以`import`语句放在`if`代码块之中毫无意义,因此会报句法错误,而不是执行时错误。
|
||||
上面代码中,引擎处理`import`语句是在编译时,这时不会去分析或执行`if`语句,所以`import`语句放在`if`代码块之中毫无意义,因此会报句法错误,而不是执行时错误。也就是说,`import`和`export`命令只能在模块的顶层,不能在代码块之中(比如,在`if`代码块之中,或在函数之中)。
|
||||
|
||||
这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。从长远来看,`import`语句会取代 Node 的`require`方法,但是`require`是运行时加载模块,`import`语句显然无法取代这种动态加载功能。
|
||||
这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。在语法上,条件加载就不可能实现。如果`import`命令要取代 Node 的`require`方法,这就形成了一个障碍。因为`require`是运行时加载模块,`import`命令无法取代`require`的动态加载功能。
|
||||
|
||||
```javascript
|
||||
const path = './' + fileName;
|
||||
@ -1084,7 +658,7 @@ const myModual = require(path);
|
||||
import(specifier)
|
||||
```
|
||||
|
||||
上面代码中,`import`函数的参数`specifier`,指定所要加载的模块的位置。`import`语句能够接受什么参数,`import()`函数就能接受什么参数,两者区别主要是后者为动态加载。
|
||||
上面代码中,`import`函数的参数`specifier`,指定所要加载的模块的位置。`import`命令能够接受什么参数,`import()`函数就能接受什么参数,两者区别主要是后者为动态加载。
|
||||
|
||||
`import()`返回一个 Promise 对象。下面是一个例子。
|
||||
|
||||
@ -1104,75 +678,109 @@ import(`./section-modules/${someVariable}.js`)
|
||||
|
||||
`import()`类似于 Node 的`require`方法,区别主要是前者是异步加载,后者是同步加载。
|
||||
|
||||
## ES6模块的转码
|
||||
### 适用场合
|
||||
|
||||
浏览器目前还不支持ES6模块,为了现在就能使用,可以将转为ES5的写法。除了Babel可以用来转码之外,还有以下两个方法,也可以用来转码。
|
||||
下面是`import()`的一些适用场合。
|
||||
|
||||
### ES6 module transpiler
|
||||
(1)按需加载。
|
||||
|
||||
[ES6 module transpiler](https://github.com/esnext/es6-module-transpiler)是 square 公司开源的一个转码器,可以将 ES6 模块转为 CommonJS 模块或 AMD 模块的写法,从而在浏览器中使用。
|
||||
|
||||
首先,安装这个转玛器。
|
||||
|
||||
```bash
|
||||
$ npm install -g es6-module-transpiler
|
||||
```
|
||||
|
||||
然后,使用`compile-modules convert`命令,将 ES6 模块文件转码。
|
||||
|
||||
```bash
|
||||
$ compile-modules convert file1.js file2.js
|
||||
```
|
||||
|
||||
`-o`参数可以指定转码后的文件名。
|
||||
|
||||
```bash
|
||||
$ compile-modules convert -o out.js file1.js
|
||||
```
|
||||
|
||||
### SystemJS
|
||||
|
||||
另一种解决方法是使用 [SystemJS](https://github.com/systemjs/systemjs)。它是一个垫片库(polyfill),可以在浏览器内加载 ES6 模块、AMD 模块和 CommonJS 模块,将其转为 ES5 格式。它在后台调用的是 Google 的 Traceur 转码器。
|
||||
|
||||
使用时,先在网页内载入`system.js`文件。
|
||||
|
||||
```html
|
||||
<script src="system.js"></script>
|
||||
```
|
||||
|
||||
然后,使用`System.import`方法加载模块文件。
|
||||
|
||||
```html
|
||||
<script>
|
||||
System.import('./app.js');
|
||||
</script>
|
||||
```
|
||||
|
||||
上面代码中的`./app`,指的是当前目录下的app.js文件。它可以是ES6模块文件,`System.import`会自动将其转码。
|
||||
|
||||
需要注意的是,`System.import`使用异步加载,返回一个 Promise 对象,可以针对这个对象编程。下面是一个模块文件。
|
||||
`import()`可以在需要的时候,再加载某个模块。
|
||||
|
||||
```javascript
|
||||
// app/es6-file.js:
|
||||
button.addEventListener('click', event => {
|
||||
import('./dialogBox.js')
|
||||
.then(dialogBox => {
|
||||
dialogBox.open();
|
||||
})
|
||||
.catch(error => {
|
||||
/* Error handling */
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
export class q {
|
||||
constructor() {
|
||||
this.es6 = 'hello';
|
||||
}
|
||||
上面代码中,`import()`方法放在`click`事件的监听函数之中,只有用户点击了按钮,才会加载这个模块。
|
||||
|
||||
(2)条件加载
|
||||
|
||||
`import()`可以放在`if`代码块,根据不同的情况,加载不同的模块。
|
||||
|
||||
```javascript
|
||||
if (condition) {
|
||||
import('moduleA').then(...);
|
||||
} else {
|
||||
import('moduleB').then(...);
|
||||
}
|
||||
```
|
||||
|
||||
然后,在网页内加载这个模块文件。
|
||||
上面代码中,如果满足条件,就加载模块 A,否则加载模块 B。
|
||||
|
||||
```html
|
||||
<script>
|
||||
(3)动态的模块路径
|
||||
|
||||
System.import('app/es6-file').then(function(m) {
|
||||
console.log(new m.q().es6); // hello
|
||||
});
|
||||
`import()`允许模块路径动态生成。
|
||||
|
||||
</script>
|
||||
```javascript
|
||||
import(f())
|
||||
.then(...);
|
||||
```
|
||||
|
||||
上面代码中,`System.import`方法返回的是一个 Promise 对象,所以可以用`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();
|
||||
```
|
||||
|
274
docs/number.md
274
docs/number.md
@ -2,14 +2,14 @@
|
||||
|
||||
## 二进制和八进制表示法
|
||||
|
||||
ES6提供了二进制和八进制数值的新的写法,分别用前缀`0b`(或`0B`)和`0o`(或`0O`)表示。
|
||||
ES6 提供了二进制和八进制数值的新的写法,分别用前缀`0b`(或`0B`)和`0o`(或`0O`)表示。
|
||||
|
||||
```javascript
|
||||
0b111110111 === 503 // true
|
||||
0o767 === 503 // true
|
||||
```
|
||||
|
||||
从ES5开始,在严格模式之中,八进制就不再允许使用前缀`0`表示,ES6进一步明确,要使用前缀`0o`表示。
|
||||
从 ES5 开始,在严格模式之中,八进制就不再允许使用前缀`0`表示,ES6 进一步明确,要使用前缀`0o`表示。
|
||||
|
||||
```javascript
|
||||
// 非严格模式
|
||||
@ -33,7 +33,7 @@ Number('0o10') // 8
|
||||
|
||||
## Number.isFinite(), Number.isNaN()
|
||||
|
||||
ES6在Number对象上,新提供了`Number.isFinite()`和`Number.isNaN()`两个方法。
|
||||
ES6 在`Number`对象上,新提供了`Number.isFinite()`和`Number.isNaN()`两个方法。
|
||||
|
||||
`Number.isFinite()`用来检查一个数值是否为有限的(finite)。
|
||||
|
||||
@ -48,7 +48,7 @@ Number.isFinite('15'); // false
|
||||
Number.isFinite(true); // false
|
||||
```
|
||||
|
||||
ES5可以通过下面的代码,部署`Number.isFinite`方法。
|
||||
ES5 可以通过下面的代码,部署`Number.isFinite`方法。
|
||||
|
||||
```javascript
|
||||
(function (global) {
|
||||
@ -77,7 +77,7 @@ Number.isNaN('true'/0) // true
|
||||
Number.isNaN('true'/'true') // true
|
||||
```
|
||||
|
||||
ES5通过下面的代码,部署`Number.isNaN()`。
|
||||
ES5 通过下面的代码,部署`Number.isNaN()`。
|
||||
|
||||
```javascript
|
||||
(function (global) {
|
||||
@ -94,7 +94,7 @@ ES5通过下面的代码,部署`Number.isNaN()`。
|
||||
})(this);
|
||||
```
|
||||
|
||||
它们与传统的全局方法`isFinite()`和`isNaN()`的区别在于,传统方法先调用`Number()`将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,非数值一律返回`false`。
|
||||
它们与传统的全局方法`isFinite()`和`isNaN()`的区别在于,传统方法先调用`Number()`将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,`Number.isFinite()`对于非数值一律返回`false`, `Number.isNaN()`只有对于`NaN`才返回`true`,非`NaN`一律返回`false`。
|
||||
|
||||
```javascript
|
||||
isFinite(25) // true
|
||||
@ -106,11 +106,12 @@ isNaN(NaN) // true
|
||||
isNaN("NaN") // true
|
||||
Number.isNaN(NaN) // true
|
||||
Number.isNaN("NaN") // false
|
||||
Number.isNaN(1) // false
|
||||
```
|
||||
|
||||
## Number.parseInt(), Number.parseFloat()
|
||||
|
||||
ES6将全局方法`parseInt()`和`parseFloat()`,移植到Number对象上面,行为完全保持不变。
|
||||
ES6 将全局方法`parseInt()`和`parseFloat()`,移植到`Number`对象上面,行为完全保持不变。
|
||||
|
||||
```javascript
|
||||
// ES5的写法
|
||||
@ -131,7 +132,7 @@ Number.parseFloat === parseFloat // true
|
||||
|
||||
## Number.isInteger()
|
||||
|
||||
`Number.isInteger()`用来判断一个值是否为整数。需要注意的是,在JavaScript内部,整数和浮点数是同样的储存方法,所以3和3.0被视为同一个值。
|
||||
`Number.isInteger()`用来判断一个值是否为整数。需要注意的是,在 JavaScript 内部,整数和浮点数是同样的储存方法,所以 3 和 3.0 被视为同一个值。
|
||||
|
||||
```javascript
|
||||
Number.isInteger(25) // true
|
||||
@ -141,7 +142,7 @@ Number.isInteger("15") // false
|
||||
Number.isInteger(true) // false
|
||||
```
|
||||
|
||||
ES5可以通过下面的代码,部署`Number.isInteger()`。
|
||||
ES5 可以通过下面的代码,部署`Number.isInteger()`。
|
||||
|
||||
```javascript
|
||||
(function (global) {
|
||||
@ -150,8 +151,8 @@ ES5可以通过下面的代码,部署`Number.isInteger()`。
|
||||
|
||||
Object.defineProperty(Number, 'isInteger', {
|
||||
value: function isInteger(value) {
|
||||
return typeof value === 'number' && isFinite(value) &&
|
||||
value > -9007199254740992 && value < 9007199254740992 &&
|
||||
return typeof value === 'number' &&
|
||||
isFinite(value) &&
|
||||
floor(value) === value;
|
||||
},
|
||||
configurable: true,
|
||||
@ -163,15 +164,21 @@ ES5可以通过下面的代码,部署`Number.isInteger()`。
|
||||
|
||||
## Number.EPSILON
|
||||
|
||||
ES6在Number对象上面,新增一个极小的常量`Number.EPSILON`。
|
||||
ES6 在`Number`对象上面,新增一个极小的常量`Number.EPSILON`。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。
|
||||
|
||||
对于 64 位浮点数来说,大于 1 的最小浮点数相当于二进制的`1.00..001`,小数点后面有连续 51 个零。这个值减去 1 之后,就等于 2 的-52 次方。
|
||||
|
||||
```javascript
|
||||
Number.EPSILON === Math.pow(2, -52)
|
||||
// true
|
||||
Number.EPSILON
|
||||
// 2.220446049250313e-16
|
||||
Number.EPSILON.toFixed(20)
|
||||
// '0.00000000000000022204'
|
||||
// "0.00000000000000022204"
|
||||
```
|
||||
|
||||
`Number.EPSILON`实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。
|
||||
|
||||
引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。
|
||||
|
||||
```javascript
|
||||
@ -185,30 +192,38 @@ Number.EPSILON.toFixed(20)
|
||||
// '0.00000000000000005551'
|
||||
```
|
||||
|
||||
但是如果这个误差能够小于`Number.EPSILON`,我们就可以认为得到了正确结果。
|
||||
上面代码解释了,为什么比较`0.1 + 0.2`与`0.3`得到的结果是`false`。
|
||||
|
||||
```javascript
|
||||
5.551115123125783e-17 < Number.EPSILON
|
||||
0.1 + 0.2 === 0.3 // false
|
||||
```
|
||||
|
||||
`Number.EPSILON`可以用来设置“能够接受的误差范围”。比如,误差范围设为 2 的-50 次方(即`Number.EPSILON * Math.pow(2, 2)`),即如果两个浮点数的差小于这个值,我们就认为这两个浮点数相等。
|
||||
|
||||
```javascript
|
||||
5.551115123125783e-17 < Number.EPSILON * Math.pow(2, 2)
|
||||
// true
|
||||
```
|
||||
|
||||
因此,`Number.EPSILON`的实质是一个可以接受的误差范围。
|
||||
因此,`Number.EPSILON`的实质是一个可以接受的最小误差范围。
|
||||
|
||||
```javascript
|
||||
function withinErrorMargin (left, right) {
|
||||
return Math.abs(left - right) < Number.EPSILON;
|
||||
return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
|
||||
}
|
||||
withinErrorMargin(0.1 + 0.2, 0.3)
|
||||
// true
|
||||
withinErrorMargin(0.2 + 0.2, 0.3)
|
||||
// false
|
||||
|
||||
0.1 + 0.2 === 0.3 // false
|
||||
withinErrorMargin(0.1 + 0.2, 0.3) // true
|
||||
|
||||
1.1 + 1.3 === 2.4 // false
|
||||
withinErrorMargin(1.1 + 1.3, 2.4) // true
|
||||
```
|
||||
|
||||
上面的代码为浮点数运算,部署了一个误差检查函数。
|
||||
|
||||
## 安全整数和Number.isSafeInteger()
|
||||
## 安全整数和 Number.isSafeInteger()
|
||||
|
||||
JavaScript能够准确表示的整数范围在`-2^53`到`2^53`之间(不含两个端点),超过这个范围,无法精确表示这个值。
|
||||
JavaScript 能够准确表示的整数范围在`-2^53`到`2^53`之间(不含两个端点),超过这个范围,无法精确表示这个值。
|
||||
|
||||
```javascript
|
||||
Math.pow(2, 53) // 9007199254740992
|
||||
@ -220,9 +235,9 @@ Math.pow(2, 53) === Math.pow(2, 53) + 1
|
||||
// true
|
||||
```
|
||||
|
||||
上面代码中,超出2的53次方之后,一个数就不精确了。
|
||||
上面代码中,超出 2 的 53 次方之后,一个数就不精确了。
|
||||
|
||||
ES6引入了`Number.MAX_SAFE_INTEGER`和`Number.MIN_SAFE_INTEGER`这两个常量,用来表示这个范围的上下限。
|
||||
ES6 引入了`Number.MAX_SAFE_INTEGER`和`Number.MIN_SAFE_INTEGER`这两个常量,用来表示这个范围的上下限。
|
||||
|
||||
```javascript
|
||||
Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1
|
||||
@ -236,7 +251,7 @@ Number.MIN_SAFE_INTEGER === -9007199254740991
|
||||
// true
|
||||
```
|
||||
|
||||
上面代码中,可以看到JavaScript能够精确表示的极限。
|
||||
上面代码中,可以看到 JavaScript 能够精确表示的极限。
|
||||
|
||||
`Number.isSafeInteger()`则是用来判断一个整数是否落在这个范围之内。
|
||||
|
||||
@ -311,9 +326,9 @@ trusty(1, 2, 3)
|
||||
// 3
|
||||
```
|
||||
|
||||
## Math对象的扩展
|
||||
## Math 对象的扩展
|
||||
|
||||
ES6在Math对象上新增了17个与数学相关的方法。所有这些方法都是静态方法,只能在Math对象上调用。
|
||||
ES6 在 Math 对象上新增了 17 个与数学相关的方法。所有这些方法都是静态方法,只能在 Math 对象上调用。
|
||||
|
||||
### Math.trunc()
|
||||
|
||||
@ -330,16 +345,19 @@ Math.trunc(-0.1234) // -0
|
||||
对于非数值,`Math.trunc`内部使用`Number`方法将其先转为数值。
|
||||
|
||||
```javascript
|
||||
Math.trunc('123.456')
|
||||
// 123
|
||||
Math.trunc('123.456') // 123
|
||||
Math.trunc(true) //1
|
||||
Math.trunc(false) // 0
|
||||
Math.trunc(null) // 0
|
||||
```
|
||||
|
||||
对于空值和无法截取整数的值,返回NaN。
|
||||
对于空值和无法截取整数的值,返回`NaN`。
|
||||
|
||||
```javascript
|
||||
Math.trunc(NaN); // NaN
|
||||
Math.trunc('foo'); // NaN
|
||||
Math.trunc(); // NaN
|
||||
Math.trunc(undefined) // NaN
|
||||
```
|
||||
|
||||
对于没有部署这个方法的环境,可以用下面的代码模拟。
|
||||
@ -352,15 +370,15 @@ Math.trunc = Math.trunc || function(x) {
|
||||
|
||||
### Math.sign()
|
||||
|
||||
`Math.sign`方法用来判断一个数到底是正数、负数、还是零。
|
||||
`Math.sign`方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。
|
||||
|
||||
它会返回五种值。
|
||||
|
||||
- 参数为正数,返回+1;
|
||||
- 参数为负数,返回-1;
|
||||
- 参数为0,返回0;
|
||||
- 参数为-0,返回-0;
|
||||
- 其他值,返回NaN。
|
||||
- 参数为正数,返回`+1`;
|
||||
- 参数为负数,返回`-1`;
|
||||
- 参数为 0,返回`0`;
|
||||
- 参数为-0,返回`-0`;
|
||||
- 其他值,返回`NaN`。
|
||||
|
||||
```javascript
|
||||
Math.sign(-5) // -1
|
||||
@ -368,8 +386,19 @@ Math.sign(5) // +1
|
||||
Math.sign(0) // +0
|
||||
Math.sign(-0) // -0
|
||||
Math.sign(NaN) // NaN
|
||||
Math.sign('foo'); // NaN
|
||||
Math.sign(); // NaN
|
||||
```
|
||||
|
||||
如果参数是非数值,会自动转为数值。对于那些无法转为数值的值,会返回`NaN`。
|
||||
|
||||
```javascript
|
||||
Math.sign('') // 0
|
||||
Math.sign(true) // +1
|
||||
Math.sign(false) // 0
|
||||
Math.sign(null) // 0
|
||||
Math.sign('9') // +1
|
||||
Math.sign('foo') // NaN
|
||||
Math.sign() // NaN
|
||||
Math.sign(undefined) // NaN
|
||||
```
|
||||
|
||||
对于没有部署这个方法的环境,可以用下面的代码模拟。
|
||||
@ -413,7 +442,7 @@ Math.cbrt = Math.cbrt || function(x) {
|
||||
|
||||
### Math.clz32()
|
||||
|
||||
JavaScript的整数使用32位二进制形式表示,`Math.clz32`方法返回一个数的32位无符号整数形式有多少个前导0。
|
||||
JavaScript 的整数使用 32 位二进制形式表示,`Math.clz32`方法返回一个数的 32 位无符号整数形式有多少个前导 0。
|
||||
|
||||
```javascript
|
||||
Math.clz32(0) // 32
|
||||
@ -423,9 +452,9 @@ Math.clz32(0b01000000000000000000000000000000) // 1
|
||||
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`方法直接相关。
|
||||
|
||||
@ -459,7 +488,7 @@ Math.clz32(true) // 31
|
||||
|
||||
### Math.imul()
|
||||
|
||||
`Math.imul`方法返回两个数以32位带符号整数形式相乘的结果,返回的也是一个32位的带符号整数。
|
||||
`Math.imul`方法返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。
|
||||
|
||||
```javascript
|
||||
Math.imul(2, 4) // 8
|
||||
@ -467,13 +496,13 @@ Math.imul(-1, 8) // -8
|
||||
Math.imul(-2, -2) // 4
|
||||
```
|
||||
|
||||
如果只考虑最后32位,大多数情况下,`Math.imul(a, b)`与`a * b`的结果是相同的,即该方法等同于`(a * b)|0`的效果(超过32位的部分溢出)。之所以需要部署这个方法,是因为JavaScript有精度限制,超过2的53次方的值无法精确表示。这就是说,对于那些很大的数的乘法,低位数值往往都是不精确的,`Math.imul`方法可以返回正确的低位数值。
|
||||
如果只考虑最后 32 位,大多数情况下,`Math.imul(a, b)`与`a * b`的结果是相同的,即该方法等同于`(a * b)|0`的效果(超过 32 位的部分溢出)。之所以需要部署这个方法,是因为 JavaScript 有精度限制,超过 2 的 53 次方的值无法精确表示。这就是说,对于那些很大的数的乘法,低位数值往往都是不精确的,`Math.imul`方法可以返回正确的低位数值。
|
||||
|
||||
```javascript
|
||||
(0x7fffffff * 0x7fffffff)|0 // 0
|
||||
```
|
||||
|
||||
上面这个乘法算式,返回结果为0。但是由于这两个二进制数的最低位都是1,所以这个结果肯定是不正确的,因为根据二进制乘法,计算结果的二进制最低位应该也是1。这个错误就是因为它们的乘积超过了2的53次方,JavaScript无法保存额外的精度,就把低位的值都变成了0。`Math.imul`方法可以返回正确的值1。
|
||||
上面这个乘法算式,返回结果为 0。但是由于这两个二进制数的最低位都是 1,所以这个结果肯定是不正确的,因为根据二进制乘法,计算结果的二进制最低位应该也是 1。这个错误就是因为它们的乘积超过了 2 的 53 次方,JavaScript 无法保存额外的精度,就把低位的值都变成了 0。`Math.imul`方法可以返回正确的值 1。
|
||||
|
||||
```javascript
|
||||
Math.imul(0x7fffffff, 0x7fffffff) // 1
|
||||
@ -481,7 +510,7 @@ Math.imul(0x7fffffff, 0x7fffffff) // 1
|
||||
|
||||
### Math.fround()
|
||||
|
||||
Math.fround方法返回一个数的单精度浮点数形式。
|
||||
Math.fround 方法返回一个数的单精度浮点数形式。
|
||||
|
||||
```javascript
|
||||
Math.fround(0) // 0
|
||||
@ -491,7 +520,7 @@ Math.fround(1.5) // 1.5
|
||||
Math.fround(NaN) // NaN
|
||||
```
|
||||
|
||||
对于整数来说,`Math.fround`方法返回结果不会有任何不同,区别主要是那些无法用64个二进制位精确表示的小数。这时,`Math.fround`方法会返回最接近这个小数的单精度浮点数。
|
||||
对于整数来说,`Math.fround`方法返回结果不会有任何不同,区别主要是那些无法用 64 个二进制位精确表示的小数。这时,`Math.fround`方法会返回最接近这个小数的单精度浮点数。
|
||||
|
||||
对于没有部署这个方法的环境,可以用下面的代码模拟。
|
||||
|
||||
@ -515,17 +544,17 @@ Math.hypot(3, 4, '5'); // 7.0710678118654755
|
||||
Math.hypot(-3); // 3
|
||||
```
|
||||
|
||||
上面代码中,3的平方加上4的平方,等于5的平方。
|
||||
上面代码中,3 的平方加上 4 的平方,等于 5 的平方。
|
||||
|
||||
如果参数不是数值,`Math.hypot`方法会将其转为数值。只要有一个参数无法转为数值,就会返回NaN。
|
||||
如果参数不是数值,`Math.hypot`方法会将其转为数值。只要有一个参数无法转为数值,就会返回 NaN。
|
||||
|
||||
### 对数方法
|
||||
|
||||
ES6新增了4个对数相关方法。
|
||||
ES6 新增了 4 个对数相关方法。
|
||||
|
||||
**(1) Math.expm1()**
|
||||
|
||||
`Math.expm1(x)`返回e<sup>x</sup> - 1,即`Math.exp(x) - 1`。
|
||||
`Math.expm1(x)`返回 e<sup>x</sup> - 1,即`Math.exp(x) - 1`。
|
||||
|
||||
```javascript
|
||||
Math.expm1(-1) // -0.6321205588285577
|
||||
@ -562,7 +591,7 @@ Math.log1p = Math.log1p || function(x) {
|
||||
|
||||
**(3)Math.log10()**
|
||||
|
||||
`Math.log10(x)`返回以10为底的`x`的对数。如果`x`小于0,则返回NaN。
|
||||
`Math.log10(x)`返回以 10 为底的`x`的对数。如果`x`小于 0,则返回 NaN。
|
||||
|
||||
```javascript
|
||||
Math.log10(2) // 0.3010299956639812
|
||||
@ -582,7 +611,7 @@ Math.log10 = Math.log10 || function(x) {
|
||||
|
||||
**(4)Math.log2()**
|
||||
|
||||
`Math.log2(x)`返回以2为底的`x`的对数。如果`x`小于0,则返回NaN。
|
||||
`Math.log2(x)`返回以 2 为底的`x`的对数。如果`x`小于 0,则返回 NaN。
|
||||
|
||||
```javascript
|
||||
Math.log2(3) // 1.584962500721156
|
||||
@ -602,9 +631,9 @@ Math.log2 = Math.log2 || function(x) {
|
||||
};
|
||||
```
|
||||
|
||||
### 三角函数方法
|
||||
### 双曲函数方法
|
||||
|
||||
ES6新增了6个三角函数方法。
|
||||
ES6 新增了 6 个双曲函数方法。
|
||||
|
||||
- `Math.sinh(x)` 返回`x`的双曲正弦(hyperbolic sine)
|
||||
- `Math.cosh(x)` 返回`x`的双曲余弦(hyperbolic cosine)
|
||||
@ -613,9 +642,41 @@ ES6新增了6个三角函数方法。
|
||||
- `Math.acosh(x)` 返回`x`的反双曲余弦(inverse hyperbolic cosine)
|
||||
- `Math.atanh(x)` 返回`x`的反双曲正切(inverse hyperbolic tangent)
|
||||
|
||||
## Math.signbit()
|
||||
|
||||
`Math.sign()`用来判断一个值的正负,但是如果参数是`-0`,它会返回`-0`。
|
||||
|
||||
```javascript
|
||||
Math.sign(-0) // -0
|
||||
```
|
||||
|
||||
这导致对于判断符号位的正负,`Math.sign()`不是很有用。JavaScript 内部使用 64 位浮点数(国际标准 IEEE 754)表示数值,IEEE 754 规定第一位是符号位,`0`表示正数,`1`表示负数。所以会有两种零,`+0`是符号位为`0`时的零值,`-0`是符号位为`1`时的零值。实际编程中,判断一个值是`+0`还是`-0`非常麻烦,因为它们是相等的。
|
||||
|
||||
```javascript
|
||||
+0 === -0 // true
|
||||
```
|
||||
|
||||
目前,有一个[提案](http://jfbastien.github.io/papers/Math.signbit.html),引入了`Math.signbit()`方法判断一个数的符号位是否设置了。
|
||||
|
||||
```javascript
|
||||
Math.signbit(2) //false
|
||||
Math.signbit(-2) //true
|
||||
Math.signbit(0) //false
|
||||
Math.signbit(-0) //true
|
||||
```
|
||||
|
||||
可以看到,该方法正确返回了`-0`的符号位是设置了的。
|
||||
|
||||
该方法的算法如下。
|
||||
|
||||
- 如果参数是`NaN`,返回`false`
|
||||
- 如果参数是`-0`,返回`true`
|
||||
- 如果参数是负值,返回`true`
|
||||
- 其他情况返回`false`
|
||||
|
||||
## 指数运算符
|
||||
|
||||
ES7新增了一个指数运算符(`**`),目前Babel转码器已经支持。
|
||||
ES2016 新增了一个指数运算符(`**`)。
|
||||
|
||||
```javascript
|
||||
2 ** 2 // 4
|
||||
@ -625,11 +686,108 @@ ES7新增了一个指数运算符(`**`),目前Babel转码器已经支持
|
||||
指数运算符可以与等号结合,形成一个新的赋值运算符(`**=`)。
|
||||
|
||||
```javascript
|
||||
let a = 2;
|
||||
let a = 1.5;
|
||||
a **= 2;
|
||||
// 等同于 a = a * a;
|
||||
|
||||
let b = 3;
|
||||
let b = 4;
|
||||
b **= 3;
|
||||
// 等同于 b = b * b * b;
|
||||
```
|
||||
|
||||
注意,在 V8 引擎中,指数运算符与`Math.pow`的实现不相同,对于特别大的运算结果,两者会有细微的差异。
|
||||
|
||||
```javascript
|
||||
Math.pow(99, 99)
|
||||
// 3.697296376497263e+197
|
||||
|
||||
99 ** 99
|
||||
// 3.697296376497268e+197
|
||||
```
|
||||
|
||||
上面代码中,两个运算结果的最后一位有效数字是有差异的。
|
||||
|
||||
## Integer 数据类型
|
||||
|
||||
### 简介
|
||||
|
||||
JavaScript 所有数字都保存成 64 位浮点数,这决定了整数的精确程度只能到 53 个二进制位。大于这个范围的整数,JavaScript 是无法精确表示的,这使得 JavaScript 不适合进行科学和金融方面的精确计算。
|
||||
|
||||
现在有一个[提案](https://github.com/tc39/proposal-bigint),引入了新的数据类型 Integer(整数),来解决这个问题。整数类型的数据只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。
|
||||
|
||||
为了与 Number 类型区别,Integer 类型的数据必须使用后缀`n`表示。
|
||||
|
||||
```javascript
|
||||
1n + 2n // 3n
|
||||
```
|
||||
|
||||
二进制、八进制、十六进制的表示法,都要加上后缀`n`。
|
||||
|
||||
```javascript
|
||||
0b1101n // 二进制
|
||||
0o777n // 八进制
|
||||
0xFFn // 十六进制
|
||||
```
|
||||
|
||||
`typeof`运算符对于 Integer 类型的数据返回`integer`。
|
||||
|
||||
```javascript
|
||||
typeof 123n
|
||||
// 'integer'
|
||||
```
|
||||
|
||||
JavaScript 原生提供`Integer`对象,用来生成 Integer 类型的数值。转换规则基本与`Number()`一致。
|
||||
|
||||
```javascript
|
||||
Integer(123) // 123n
|
||||
Integer('123') // 123n
|
||||
Integer(false) // 0n
|
||||
Integer(true) // 1n
|
||||
```
|
||||
|
||||
以下的用法会报错。
|
||||
|
||||
```javascript
|
||||
new Integer() // TypeError
|
||||
Integer(undefined) //TypeError
|
||||
Integer(null) // TypeError
|
||||
Integer('123n') // SyntaxError
|
||||
Integer('abc') // SyntaxError
|
||||
```
|
||||
|
||||
### 运算
|
||||
|
||||
在数学运算方面,Integer 类型的`+`、`-`、`*`和`**`这四个二元运算符,与 Number 类型的行为一致。除法运算`/`会舍去小数部分,返回一个整数。
|
||||
|
||||
```javascript
|
||||
9n / 5n
|
||||
// 1n
|
||||
```
|
||||
|
||||
几乎所有的 Number 运算符都可以用在 Integer,但是有两个除外:不带符号的右移位运算符`>>>`和一元的求正运算符`+`,使用时会报错。前者是因为`>>>`要求最高位补 0,但是 Integer 类型没有最高位,导致这个运算符无意义。后者是因为一元运算符`+`在 asm.js 里面总是返回 Number 类型或者报错。
|
||||
|
||||
Integer 类型不能与 Number 类型进行混合运算。
|
||||
|
||||
```javascript
|
||||
1n + 1
|
||||
// 报错
|
||||
```
|
||||
|
||||
这是因为无论返回的是 Integer 或 Number,都会导致丢失信息。比如`(2n**53n + 1n) + 0.5`这个表达式,如果返回 Integer 类型,`0.5`这个小数部分会丢失;如果返回 Number 类型,会超过 53 位精确数字,精度下降。
|
||||
|
||||
相等运算符(`==`)会改变数据类型,也是不允许混合使用。
|
||||
|
||||
```javascript
|
||||
0n == 0
|
||||
// 报错 TypeError
|
||||
|
||||
0n == false
|
||||
// 报错 TypeError
|
||||
```
|
||||
|
||||
精确相等运算符(`===`)不会改变数据类型,因此可以混合使用。
|
||||
|
||||
```javascript
|
||||
0n === 0
|
||||
// false
|
||||
```
|
||||
|
841
docs/object.md
841
docs/object.md
File diff suppressed because it is too large
Load Diff
378
docs/promise.md
378
docs/promise.md
@ -1,31 +1,33 @@
|
||||
# Promise对象
|
||||
# Promise 对象
|
||||
|
||||
## Promise的含义
|
||||
## Promise 的含义
|
||||
|
||||
Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6将其写进了语言标准,统一了用法,原生提供了`Promise`对象。
|
||||
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了`Promise`对象。
|
||||
|
||||
所谓`Promise`,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。Promise提供统一的API,各种异步操作都可以用同样的方法进行处理。
|
||||
所谓`Promise`,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
|
||||
|
||||
`Promise`对象有以下两个特点。
|
||||
|
||||
(1)对象的状态不受外界影响。`Promise`对象代表一个异步操作,有三种状态:`Pending`(进行中)、`Resolved`(已完成,又称Fulfilled)和`Rejected`(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是`Promise`这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
|
||||
(1)对象的状态不受外界影响。`Promise`对象代表一个异步操作,有三种状态:`pending`(进行中)、`fulfilled`(已成功)和`rejected`(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是`Promise`这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
|
||||
|
||||
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。`Promise`对象的状态改变,只有两种可能:从`Pending`变为`Resolved`和从`Pending`变为`Rejected`。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对`Promise`对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
|
||||
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。`Promise`对象的状态改变,只有两种可能:从`pending`变为`fulfilled`和从`pending`变为`rejected`。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对`Promise`对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
|
||||
|
||||
注意,为了行文方便,本章后面的`resolved`统一只指`fulfilled`状态,不包含`rejected`状态。
|
||||
|
||||
有了`Promise`对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,`Promise`对象提供统一的接口,使得控制异步操作更加容易。
|
||||
|
||||
`Promise`也有一些缺点。首先,无法取消`Promise`,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,`Promise`内部抛出的错误,不会反应到外部。第三,当处于`Pending`状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
|
||||
`Promise`也有一些缺点。首先,无法取消`Promise`,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,`Promise`内部抛出的错误,不会反应到外部。第三,当处于`pending`状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
|
||||
|
||||
如果某些事件不断地反复发生,一般来说,使用stream模式是比部署`Promise`更好的选择。
|
||||
如果某些事件不断地反复发生,一般来说,使用 [Stream](https://nodejs.org/api/stream.html) 模式是比部署`Promise`更好的选择。
|
||||
|
||||
## 基本用法
|
||||
|
||||
ES6规定,Promise对象是一个构造函数,用来生成Promise实例。
|
||||
ES6 规定,`Promise`对象是一个构造函数,用来生成`Promise`实例。
|
||||
|
||||
下面代码创造了一个Promise实例。
|
||||
下面代码创造了一个`Promise`实例。
|
||||
|
||||
```javascript
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
const promise = new Promise(function(resolve, reject) {
|
||||
// ... some code
|
||||
|
||||
if (/* 异步操作成功 */){
|
||||
@ -36,11 +38,11 @@ var promise = new Promise(function(resolve, reject) {
|
||||
});
|
||||
```
|
||||
|
||||
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是`resolve`和`reject`。它们是两个函数,由JavaScript引擎提供,不用自己部署。
|
||||
`Promise`构造函数接受一个函数作为参数,该函数的两个参数分别是`resolve`和`reject`。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
|
||||
|
||||
`resolve`函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从Pending变为Resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;`reject`函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从Pending变为Rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
|
||||
`resolve`函数的作用是,将`Promise`对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;`reject`函数的作用是,将`Promise`对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
|
||||
|
||||
Promise实例生成以后,可以用`then`方法分别指定`Resolved`状态和`Reject`状态的回调函数。
|
||||
`Promise`实例生成以后,可以用`then`方法分别指定`resolved`状态和`rejected`状态的回调函数。
|
||||
|
||||
```javascript
|
||||
promise.then(function(value) {
|
||||
@ -50,9 +52,9 @@ promise.then(function(value) {
|
||||
});
|
||||
```
|
||||
|
||||
`then`方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为Resolved时调用,第二个回调函数是Promise对象的状态变为Reject时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。
|
||||
`then`方法可以接受两个回调函数作为参数。第一个回调函数是`Promise`对象的状态变为`resolved`时调用,第二个回调函数是`Promise`对象的状态变为`rejected`时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受`Promise`对象传出的值作为参数。
|
||||
|
||||
下面是一个Promise对象的简单例子。
|
||||
下面是一个`Promise`对象的简单例子。
|
||||
|
||||
```javascript
|
||||
function timeout(ms) {
|
||||
@ -66,9 +68,9 @@ timeout(100).then((value) => {
|
||||
});
|
||||
```
|
||||
|
||||
上面代码中,`timeout`方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间(`ms`参数)以后,Promise实例的状态变为Resolved,就会触发`then`方法绑定的回调函数。
|
||||
上面代码中,`timeout`方法返回一个`Promise`实例,表示一段时间以后才会发生的结果。过了指定的时间(`ms`参数)以后,`Promise`实例的状态变为`resolved`,就会触发`then`方法绑定的回调函数。
|
||||
|
||||
Promise新建后就会立即执行。
|
||||
Promise 新建后就会立即执行。
|
||||
|
||||
```javascript
|
||||
let promise = new Promise(function(resolve, reject) {
|
||||
@ -77,24 +79,24 @@ let promise = new Promise(function(resolve, reject) {
|
||||
});
|
||||
|
||||
promise.then(function() {
|
||||
console.log('Resolved.');
|
||||
console.log('resolved.');
|
||||
});
|
||||
|
||||
console.log('Hi!');
|
||||
|
||||
// Promise
|
||||
// Hi!
|
||||
// Resolved
|
||||
// resolved
|
||||
```
|
||||
|
||||
上面代码中,Promise新建后立即执行,所以首先输出的是“Promise”。然后,`then`方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以“Resolved”最后输出。
|
||||
上面代码中,Promise 新建后立即执行,所以首先输出的是`Promise`。然后,`then`方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以`resolved`最后输出。
|
||||
|
||||
下面是异步加载图片的例子。
|
||||
|
||||
```javascript
|
||||
function loadImageAsync(url) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var image = new Image();
|
||||
const image = new Image();
|
||||
|
||||
image.onload = function() {
|
||||
resolve(image);
|
||||
@ -109,21 +111,14 @@ function loadImageAsync(url) {
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,使用Promise包装了一个图片加载的异步操作。如果加载成功,就调用`resolve`方法,否则就调用`reject`方法。
|
||||
上面代码中,使用`Promise`包装了一个图片加载的异步操作。如果加载成功,就调用`resolve`方法,否则就调用`reject`方法。
|
||||
|
||||
下面是一个用Promise对象实现的Ajax操作的例子。
|
||||
下面是一个用`Promise`对象实现的 Ajax 操作的例子。
|
||||
|
||||
```javascript
|
||||
var getJSON = function(url) {
|
||||
var promise = new Promise(function(resolve, reject){
|
||||
var client = new XMLHttpRequest();
|
||||
client.open("GET", url);
|
||||
client.onreadystatechange = handler;
|
||||
client.responseType = "json";
|
||||
client.setRequestHeader("Accept", "application/json");
|
||||
client.send();
|
||||
|
||||
function handler() {
|
||||
const getJSON = function(url) {
|
||||
const promise = new Promise(function(resolve, reject){
|
||||
const handler = function() {
|
||||
if (this.readyState !== 4) {
|
||||
return;
|
||||
}
|
||||
@ -133,6 +128,13 @@ var getJSON = function(url) {
|
||||
reject(new Error(this.statusText));
|
||||
}
|
||||
};
|
||||
const client = new XMLHttpRequest();
|
||||
client.open("GET", url);
|
||||
client.onreadystatechange = handler;
|
||||
client.responseType = "json";
|
||||
client.setRequestHeader("Accept", "application/json");
|
||||
client.send();
|
||||
|
||||
});
|
||||
|
||||
return promise;
|
||||
@ -145,31 +147,31 @@ getJSON("/posts.json").then(function(json) {
|
||||
});
|
||||
```
|
||||
|
||||
上面代码中,`getJSON`是对XMLHttpRequest对象的封装,用于发出一个针对JSON数据的HTTP请求,并且返回一个Promise对象。需要注意的是,在`getJSON`内部,`resolve`函数和`reject`函数调用时,都带有参数。
|
||||
上面代码中,`getJSON`是对 XMLHttpRequest 对象的封装,用于发出一个针对 JSON 数据的 HTTP 请求,并且返回一个`Promise`对象。需要注意的是,在`getJSON`内部,`resolve`函数和`reject`函数调用时,都带有参数。
|
||||
|
||||
如果调用`resolve`函数和`reject`函数时带有参数,那么它们的参数会被传递给回调函数。`reject`函数的参数通常是Error对象的实例,表示抛出的错误;`resolve`函数的参数除了正常的值以外,还可能是另一个Promise实例,表示异步操作的结果有可能是一个值,也有可能是另一个异步操作,比如像下面这样。
|
||||
如果调用`resolve`函数和`reject`函数时带有参数,那么它们的参数会被传递给回调函数。`reject`函数的参数通常是`Error`对象的实例,表示抛出的错误;`resolve`函数的参数除了正常的值以外,还可能是另一个 Promise 实例,比如像下面这样。
|
||||
|
||||
```javascript
|
||||
var p1 = new Promise(function (resolve, reject) {
|
||||
const p1 = new Promise(function (resolve, reject) {
|
||||
// ...
|
||||
});
|
||||
|
||||
var p2 = new Promise(function (resolve, reject) {
|
||||
const p2 = new Promise(function (resolve, reject) {
|
||||
// ...
|
||||
resolve(p1);
|
||||
})
|
||||
```
|
||||
|
||||
上面代码中,`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
|
||||
var p1 = new Promise(function (resolve, reject) {
|
||||
const p1 = new Promise(function (resolve, reject) {
|
||||
setTimeout(() => reject(new Error('fail')), 3000)
|
||||
})
|
||||
|
||||
var p2 = new Promise(function (resolve, reject) {
|
||||
const p2 = new Promise(function (resolve, reject) {
|
||||
setTimeout(() => resolve(p1), 1000)
|
||||
})
|
||||
|
||||
@ -179,13 +181,38 @@ p2
|
||||
// Error: fail
|
||||
```
|
||||
|
||||
上面代码中,`p1`是一个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实例具有`then`方法,也就是说,`then`方法是定义在原型对象Promise.prototype上的。它的作用是为Promise实例添加状态改变时的回调函数。前面说过,`then`方法的第一个参数是Resolved状态的回调函数,第二个参数(可选)是Rejected状态的回调函数。
|
||||
Promise 实例具有`then`方法,也就是说,`then`方法是定义在原型对象`Promise.prototype`上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,`then`方法的第一个参数是`resolved`状态的回调函数,第二个参数(可选)是`rejected`状态的回调函数。
|
||||
|
||||
`then`方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即`then`方法后面再调用另一个`then`方法。
|
||||
`then`方法返回的是一个新的`Promise`实例(注意,不是原来那个`Promise`实例)。因此可以采用链式写法,即`then`方法后面再调用另一个`then`方法。
|
||||
|
||||
```javascript
|
||||
getJSON("/posts.json").then(function(json) {
|
||||
@ -197,19 +224,19 @@ getJSON("/posts.json").then(function(json) {
|
||||
|
||||
上面的代码使用`then`方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。
|
||||
|
||||
采用链式的`then`,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。
|
||||
采用链式的`then`,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个`Promise`对象(即有异步操作),这时后一个回调函数,就会等待该`Promise`对象的状态发生变化,才会被调用。
|
||||
|
||||
```javascript
|
||||
getJSON("/post/1.json").then(function(post) {
|
||||
return getJSON(post.commentURL);
|
||||
}).then(function funcA(comments) {
|
||||
console.log("Resolved: ", comments);
|
||||
console.log("resolved: ", comments);
|
||||
}, function funcB(err){
|
||||
console.log("Rejected: ", err);
|
||||
console.log("rejected: ", err);
|
||||
});
|
||||
```
|
||||
|
||||
上面代码中,第一个`then`方法指定的回调函数,返回的是另一个Promise对象。这时,第二个`then`方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为Resolved,就调用`funcA`,如果状态变为Rejected,就调用`funcB`。
|
||||
上面代码中,第一个`then`方法指定的回调函数,返回的是另一个`Promise`对象。这时,第二个`then`方法指定的回调函数,就会等待这个新的`Promise`对象状态发生变化。如果变为`resolved`,就调用`funcA`,如果状态变为`rejected`,就调用`funcB`。
|
||||
|
||||
如果采用箭头函数,上面的代码可以写得更简洁。
|
||||
|
||||
@ -217,8 +244,8 @@ getJSON("/post/1.json").then(function(post) {
|
||||
getJSON("/post/1.json").then(
|
||||
post => getJSON(post.commentURL)
|
||||
).then(
|
||||
comments => console.log("Resolved: ", comments),
|
||||
err => console.log("Rejected: ", err)
|
||||
comments => console.log("resolved: ", comments),
|
||||
err => console.log("rejected: ", err)
|
||||
);
|
||||
```
|
||||
|
||||
@ -227,7 +254,7 @@ getJSON("/post/1.json").then(
|
||||
`Promise.prototype.catch`方法是`.then(null, rejection)`的别名,用于指定发生错误时的回调函数。
|
||||
|
||||
```javascript
|
||||
getJSON("/posts.json").then(function(posts) {
|
||||
getJSON('/posts.json').then(function(posts) {
|
||||
// ...
|
||||
}).catch(function(error) {
|
||||
// 处理 getJSON 和 前一个回调函数运行时发生的错误
|
||||
@ -235,21 +262,21 @@ getJSON("/posts.json").then(function(posts) {
|
||||
});
|
||||
```
|
||||
|
||||
上面代码中,`getJSON`方法返回一个Promise对象,如果该对象状态变为`Resolved`,则会调用`then`方法指定的回调函数;如果异步操作抛出错误,状态就会变为`Rejected`,就会调用`catch`方法指定的回调函数,处理这个错误。另外,`then`方法指定的回调函数,如果运行中抛出错误,也会被`catch`方法捕获。
|
||||
上面代码中,`getJSON`方法返回一个 Promise 对象,如果该对象状态变为`resolved`,则会调用`then`方法指定的回调函数;如果异步操作抛出错误,状态就会变为`rejected`,就会调用`catch`方法指定的回调函数,处理这个错误。另外,`then`方法指定的回调函数,如果运行中抛出错误,也会被`catch`方法捕获。
|
||||
|
||||
```javascript
|
||||
p.then((val) => console.log("fulfilled:", val))
|
||||
.catch((err) => console.log("rejected:", err));
|
||||
p.then((val) => console.log('fulfilled:', val))
|
||||
.catch((err) => console.log('rejected', err));
|
||||
|
||||
// 等同于
|
||||
p.then((val) => console.log("fulfilled:", val))
|
||||
p.then((val) => console.log('fulfilled:', val))
|
||||
.then(null, (err) => console.log("rejected:", err));
|
||||
```
|
||||
|
||||
下面是一个例子。
|
||||
|
||||
```javascript
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
const promise = new Promise(function(resolve, reject) {
|
||||
throw new Error('test');
|
||||
});
|
||||
promise.catch(function(error) {
|
||||
@ -262,7 +289,7 @@ promise.catch(function(error) {
|
||||
|
||||
```javascript
|
||||
// 写法一
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
const promise = new Promise(function(resolve, reject) {
|
||||
try {
|
||||
throw new Error('test');
|
||||
} catch(e) {
|
||||
@ -274,7 +301,7 @@ promise.catch(function(error) {
|
||||
});
|
||||
|
||||
// 写法二
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
const promise = new Promise(function(resolve, reject) {
|
||||
reject(new Error('test'));
|
||||
});
|
||||
promise.catch(function(error) {
|
||||
@ -284,10 +311,10 @@ promise.catch(function(error) {
|
||||
|
||||
比较上面两种写法,可以发现`reject`方法的作用,等同于抛出错误。
|
||||
|
||||
如果Promise状态已经变成`Resolved`,再抛出错误是无效的。
|
||||
如果 Promise 状态已经变成`resolved`,再抛出错误是无效的。
|
||||
|
||||
```javascript
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
const promise = new Promise(function(resolve, reject) {
|
||||
resolve('ok');
|
||||
throw new Error('test');
|
||||
});
|
||||
@ -297,12 +324,12 @@ promise
|
||||
// ok
|
||||
```
|
||||
|
||||
上面代码中,Promise在`resolve`语句后面,再抛出错误,不会被捕获,等于没有抛出。
|
||||
上面代码中,Promise 在`resolve`语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。
|
||||
|
||||
Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个`catch`语句捕获。
|
||||
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个`catch`语句捕获。
|
||||
|
||||
```javascript
|
||||
getJSON("/post/1.json").then(function(post) {
|
||||
getJSON('/post/1.json').then(function(post) {
|
||||
return getJSON(post.commentURL);
|
||||
}).then(function(comments) {
|
||||
// some code
|
||||
@ -311,9 +338,9 @@ getJSON("/post/1.json").then(function(post) {
|
||||
});
|
||||
```
|
||||
|
||||
上面代码中,一共有三个Promise对象:一个由`getJSON`产生,两个由`then`产生。它们之中任何一个抛出的错误,都会被最后一个`catch`捕获。
|
||||
上面代码中,一共有三个 Promise 对象:一个由`getJSON`产生,两个由`then`产生。它们之中任何一个抛出的错误,都会被最后一个`catch`捕获。
|
||||
|
||||
一般来说,不要在`then`方法里面定义Reject状态的回调函数(即`then`的第二个参数),总是使用`catch`方法。
|
||||
一般来说,不要在`then`方法里面定义 Reject 状态的回调函数(即`then`的第二个参数),总是使用`catch`方法。
|
||||
|
||||
```javascript
|
||||
// bad
|
||||
@ -336,10 +363,10 @@ promise
|
||||
|
||||
上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面`then`方法执行中的错误,也更接近同步的写法(`try/catch`)。因此,建议总是使用`catch`方法,而不使用`then`方法的第二个参数。
|
||||
|
||||
跟传统的`try/catch`代码块不同的是,如果没有使用`catch`方法指定错误处理的回调函数,Promise对象抛出的错误不会传递到外层代码,即不会有任何反应。
|
||||
跟传统的`try/catch`代码块不同的是,如果没有使用`catch`方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。
|
||||
|
||||
```javascript
|
||||
var someAsyncThing = function() {
|
||||
const someAsyncThing = function() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
// 下面一行会报错,因为x没有声明
|
||||
resolve(x + 2);
|
||||
@ -349,36 +376,44 @@ var someAsyncThing = function() {
|
||||
someAsyncThing().then(function() {
|
||||
console.log('everything is great');
|
||||
});
|
||||
|
||||
setTimeout(() => { console.log(123) }, 2000);
|
||||
// Uncaught (in promise) ReferenceError: x is not defined
|
||||
// 123
|
||||
```
|
||||
|
||||
上面代码中,`someAsyncThing`函数产生的Promise对象会报错,但是由于没有指定`catch`方法,这个错误不会被捕获,也不会传递到外层代码,导致运行后没有任何输出。注意,Chrome浏览器不遵守这条规定,它会抛出错误“ReferenceError: x is not defined”。
|
||||
上面代码中,`someAsyncThing`函数产生的 Promise 对象,内部有语法错误。浏览器运行到这一行,会打印出错误提示`ReferenceError: x is not defined`,但是不会退出进程、终止脚本执行,2 秒之后还是会输出`123`。这就是说,Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。
|
||||
|
||||
这个脚本放在服务器执行,退出码就是`0`(即表示执行成功)。不过,Node 有一个`unhandledRejection`事件,专门监听未捕获的`reject`错误,上面的脚本会触发这个事件的监听函数,可以在监听函数里面抛出错误。
|
||||
|
||||
```javascript
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
resolve("ok");
|
||||
setTimeout(function() { throw new Error('test') }, 0)
|
||||
process.on('unhandledRejection', function (err, p) {
|
||||
throw err;
|
||||
});
|
||||
promise.then(function(value) { console.log(value) });
|
||||
```
|
||||
|
||||
上面代码中,`unhandledRejection`事件的监听函数有两个参数,第一个是错误对象,第二个是报错的 Promise 实例,它可以用来了解发生错误的环境信息。
|
||||
|
||||
注意,Node 有计划在未来废除`unhandledRejection`事件。如果 Promise 内部有未捕获的错误,会直接终止进程,并且进程的退出码不为 0。
|
||||
|
||||
再看下面的例子。
|
||||
|
||||
```javascript
|
||||
const promise = new Promise(function (resolve, reject) {
|
||||
resolve('ok');
|
||||
setTimeout(function () { throw new Error('test') }, 0)
|
||||
});
|
||||
promise.then(function (value) { console.log(value) });
|
||||
// ok
|
||||
// Uncaught Error: test
|
||||
```
|
||||
|
||||
上面代码中,Promise指定在下一轮“事件循环”再抛出错误,结果由于没有指定使用`try...catch`语句,就冒泡到最外层,成了未捕获的错误。因为此时,Promise的函数体已经运行结束了,所以这个错误是在Promise函数体外抛出的。
|
||||
上面代码中,Promise 指定在下一轮“事件循环”再抛出错误。到了那个时候,Promise 的运行已经结束了,所以这个错误是在 Promise 函数体外抛出的,会冒泡到最外层,成了未捕获的错误。
|
||||
|
||||
Node.js有一个`unhandledRejection`事件,专门监听未捕获的`reject`错误。
|
||||
一般总是建议,Promise 对象后面要跟`catch`方法,这样可以处理 Promise 内部发生的错误。`catch`方法返回的还是一个 Promise 对象,因此后面还可以接着调用`then`方法。
|
||||
|
||||
```javascript
|
||||
process.on('unhandledRejection', function (err, p) {
|
||||
console.error(err.stack)
|
||||
});
|
||||
```
|
||||
|
||||
上面代码中,`unhandledRejection`事件的监听函数有两个参数,第一个是错误对象,第二个是报错的Promise实例,它可以用来了解发生错误的环境信息。。
|
||||
|
||||
需要注意的是,`catch`方法返回的还是一个Promise对象,因此后面还可以接着调用`then`方法。
|
||||
|
||||
```javascript
|
||||
var someAsyncThing = function() {
|
||||
const someAsyncThing = function() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
// 下面一行会报错,因为x没有声明
|
||||
resolve(x + 2);
|
||||
@ -414,7 +449,7 @@ Promise.resolve()
|
||||
`catch`方法之中,还能再抛出错误。
|
||||
|
||||
```javascript
|
||||
var someAsyncThing = function() {
|
||||
const someAsyncThing = function() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
// 下面一行会报错,因为x没有声明
|
||||
resolve(x + 2);
|
||||
@ -425,7 +460,7 @@ someAsyncThing().then(function() {
|
||||
return someOtherAsyncThing();
|
||||
}).catch(function(error) {
|
||||
console.log('oh no', error);
|
||||
// 下面一行会报错,因为y没有声明
|
||||
// 下面一行会报错,因为 y 没有声明
|
||||
y + 2;
|
||||
}).then(function() {
|
||||
console.log('carry on');
|
||||
@ -453,13 +488,13 @@ someAsyncThing().then(function() {
|
||||
|
||||
## Promise.all()
|
||||
|
||||
`Promise.all`方法用于将多个Promise实例,包装成一个新的Promise实例。
|
||||
`Promise.all`方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
|
||||
|
||||
```javascript
|
||||
var p = Promise.all([p1, p2, p3]);
|
||||
const p = Promise.all([p1, p2, p3]);
|
||||
```
|
||||
|
||||
上面代码中,`Promise.all`方法接受一个数组作为参数,`p1`、`p2`、`p3`都是Promise对象的实例,如果不是,就会先调用下面讲到的`Promise.resolve`方法,将参数转为Promise实例,再进一步处理。(`Promise.all`方法的参数可以不是数组,但必须具有Iterator接口,且返回的每个成员都是Promise实例。)
|
||||
上面代码中,`Promise.all`方法接受一个数组作为参数,`p1`、`p2`、`p3`都是 Promise 实例,如果不是,就会先调用下面讲到的`Promise.resolve`方法,将参数转为 Promise 实例,再进一步处理。(`Promise.all`方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。)
|
||||
|
||||
`p`的状态由`p1`、`p2`、`p3`决定,分成两种情况。
|
||||
|
||||
@ -471,8 +506,8 @@ var p = Promise.all([p1, p2, p3]);
|
||||
|
||||
```javascript
|
||||
// 生成一个Promise对象的数组
|
||||
var promises = [2, 3, 5, 7, 11, 13].map(function (id) {
|
||||
return getJSON("/post/" + id + ".json");
|
||||
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
|
||||
return getJSON('/post/' + id + ".json");
|
||||
});
|
||||
|
||||
Promise.all(promises).then(function (posts) {
|
||||
@ -482,14 +517,14 @@ Promise.all(promises).then(function (posts) {
|
||||
});
|
||||
```
|
||||
|
||||
上面代码中,`promises`是包含6个Promise实例的数组,只有这6个实例的状态都变成`fulfilled`,或者其中有一个变为`rejected`,才会调用`Promise.all`方法后面的回调函数。
|
||||
上面代码中,`promises`是包含 6 个 Promise 实例的数组,只有这 6 个实例的状态都变成`fulfilled`,或者其中有一个变为`rejected`,才会调用`Promise.all`方法后面的回调函数。
|
||||
|
||||
下面是另一个例子。
|
||||
|
||||
```javascript
|
||||
const databasePromise = connectDatabase();
|
||||
|
||||
const booksPromise = databaseProimse
|
||||
const booksPromise = databasePromise
|
||||
.then(findAllBooks);
|
||||
|
||||
const userPromise = databasePromise
|
||||
@ -504,42 +539,84 @@ Promise.all([
|
||||
|
||||
上面代码中,`booksPromise`和`userPromise`是两个异步操作,只有等到它们的结果都返回了,才会触发`pickTopRecommentations`这个回调函数。
|
||||
|
||||
## Promise.race()
|
||||
|
||||
`Promise.race`方法同样是将多个Promise实例,包装成一个新的Promise实例。
|
||||
注意,如果作为参数的 Promise 实例,自己定义了`catch`方法,那么它一旦被`rejected`,并不会触发`Promise.all()`的`catch`方法。
|
||||
|
||||
```javascript
|
||||
var p = Promise.race([p1, p2, p3]);
|
||||
const p1 = new Promise((resolve, reject) => {
|
||||
resolve('hello');
|
||||
})
|
||||
.then(result => result)
|
||||
.catch(e => e);
|
||||
|
||||
const p2 = new Promise((resolve, reject) => {
|
||||
throw new Error('报错了');
|
||||
})
|
||||
.then(result => result)
|
||||
.catch(e => e);
|
||||
|
||||
Promise.all([p1, p2])
|
||||
.then(result => console.log(result))
|
||||
.catch(e => console.log(e));
|
||||
// ["hello", Error: 报错了]
|
||||
```
|
||||
|
||||
上面代码中,`p1`会`resolved`,`p2`首先会`rejected`,但是`p2`有自己的`catch`方法,该方法返回的是一个新的 Promise 实例,`p2`指向的实际上是这个实例。该实例执行完`catch`方法后,也会变成`resolved`,导致`Promise.all()`方法参数里面的两个实例都会`resolved`,因此会调用`then`方法指定的回调函数,而不会调用`catch`方法指定的回调函数。
|
||||
|
||||
如果`p2`没有自己的`catch`方法,就会调用`Promise.all()`的`catch`方法。
|
||||
|
||||
```javascript
|
||||
const p1 = new Promise((resolve, reject) => {
|
||||
resolve('hello');
|
||||
})
|
||||
.then(result => result);
|
||||
|
||||
const p2 = new Promise((resolve, reject) => {
|
||||
throw new Error('报错了');
|
||||
})
|
||||
.then(result => result);
|
||||
|
||||
Promise.all([p1, p2])
|
||||
.then(result => console.log(result))
|
||||
.catch(e => console.log(e));
|
||||
// Error: 报错了
|
||||
```
|
||||
|
||||
## Promise.race()
|
||||
|
||||
`Promise.race`方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
|
||||
|
||||
```javascript
|
||||
const p = Promise.race([p1, p2, p3]);
|
||||
```
|
||||
|
||||
上面代码中,只要`p1`、`p2`、`p3`之中有一个实例率先改变状态,`p`的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给`p`的回调函数。
|
||||
|
||||
`Promise.race`方法的参数与`Promise.all`方法一样,如果不是 Promise 实例,就会先调用下面讲到的`Promise.resolve`方法,将参数转为 Promise 实例,再进一步处理。
|
||||
|
||||
下面是一个例子,如果指定时间内没有获得结果,就将Promise的状态变为`reject`,否则变为`resolve`。
|
||||
下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为`reject`,否则变为`resolve`。
|
||||
|
||||
```javascript
|
||||
var p = Promise.race([
|
||||
const p = Promise.race([
|
||||
fetch('/resource-that-may-take-a-while'),
|
||||
new Promise(function (resolve, reject) {
|
||||
setTimeout(() => reject(new Error('request timeout')), 5000)
|
||||
})
|
||||
])
|
||||
p.then(response => console.log(response))
|
||||
p.catch(error => console.log(error))
|
||||
]);
|
||||
p.then(response => console.log(response));
|
||||
p.catch(error => console.log(error));
|
||||
```
|
||||
|
||||
上面代码中,如果5秒之内`fetch`方法无法返回结果,变量`p`的状态就会变为`rejected`,从而触发`catch`方法指定的回调函数。
|
||||
上面代码中,如果 5 秒之内`fetch`方法无法返回结果,变量`p`的状态就会变为`rejected`,从而触发`catch`方法指定的回调函数。
|
||||
|
||||
## Promise.resolve()
|
||||
|
||||
有时需要将现有对象转为Promise对象,`Promise.resolve`方法就起到这个作用。
|
||||
有时需要将现有对象转为 Promise 对象,`Promise.resolve`方法就起到这个作用。
|
||||
|
||||
```javascript
|
||||
var jsPromise = Promise.resolve($.ajax('/whatever.json'));
|
||||
const jsPromise = Promise.resolve($.ajax('/whatever.json'));
|
||||
```
|
||||
|
||||
上面代码将jQuery生成的`deferred`对象,转为一个新的Promise对象。
|
||||
上面代码将 jQuery 生成的`deferred`对象,转为一个新的 Promise 对象。
|
||||
|
||||
`Promise.resolve`等价于下面的写法。
|
||||
|
||||
@ -551,9 +628,9 @@ new Promise(resolve => resolve('foo'))
|
||||
|
||||
`Promise.resolve`方法的参数分成四种情况。
|
||||
|
||||
**(1)参数是一个Promise实例**
|
||||
**(1)参数是一个 Promise 实例**
|
||||
|
||||
如果参数是Promise实例,那么`Promise.resolve`将不做任何修改、原封不动地返回这个实例。
|
||||
如果参数是 Promise 实例,那么`Promise.resolve`将不做任何修改、原封不动地返回这个实例。
|
||||
|
||||
**(2)参数是一个`thenable`对象**
|
||||
|
||||
@ -567,7 +644,7 @@ let thenable = {
|
||||
};
|
||||
```
|
||||
|
||||
`Promise.resolve`方法会将这个对象转为Promise对象,然后就立即执行`thenable`对象的`then`方法。
|
||||
`Promise.resolve`方法会将这个对象转为 Promise 对象,然后就立即执行`thenable`对象的`then`方法。
|
||||
|
||||
```javascript
|
||||
let thenable = {
|
||||
@ -582,14 +659,14 @@ p1.then(function(value) {
|
||||
});
|
||||
```
|
||||
|
||||
上面代码中,`thenable`对象的`then`方法执行后,对象`p1`的状态就变为`resolved`,从而立即执行最后那个`then`方法指定的回调函数,输出42。
|
||||
上面代码中,`thenable`对象的`then`方法执行后,对象`p1`的状态就变为`resolved`,从而立即执行最后那个`then`方法指定的回调函数,输出 42。
|
||||
|
||||
**(3)参数不是具有`then`方法的对象,或根本就不是对象**
|
||||
|
||||
如果参数是一个原始值,或者是一个不具有`then`方法的对象,则`Promise.resolve`方法返回一个新的Promise对象,状态为`Resolved`。
|
||||
如果参数是一个原始值,或者是一个不具有`then`方法的对象,则`Promise.resolve`方法返回一个新的 Promise 对象,状态为`resolved`。
|
||||
|
||||
```javascript
|
||||
var p = Promise.resolve('Hello');
|
||||
const p = Promise.resolve('Hello');
|
||||
|
||||
p.then(function (s){
|
||||
console.log(s)
|
||||
@ -597,25 +674,25 @@ p.then(function (s){
|
||||
// Hello
|
||||
```
|
||||
|
||||
上面代码生成一个新的Promise对象的实例`p`。由于字符串`Hello`不属于异步操作(判断方法是它不是具有then方法的对象),返回Promise实例的状态从一生成就是`Resolved`,所以回调函数会立即执行。`Promise.resolve`方法的参数,会同时传给回调函数。
|
||||
上面代码生成一个新的 Promise 对象的实例`p`。由于字符串`Hello`不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是`resolved`,所以回调函数会立即执行。`Promise.resolve`方法的参数,会同时传给回调函数。
|
||||
|
||||
**(4)不带有任何参数**
|
||||
|
||||
`Promise.resolve`方法允许调用时不带参数,直接返回一个`Resolved`状态的Promise对象。
|
||||
`Promise.resolve`方法允许调用时不带参数,直接返回一个`resolved`状态的 Promise 对象。
|
||||
|
||||
所以,如果希望得到一个Promise对象,比较方便的方法就是直接调用`Promise.resolve`方法。
|
||||
所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用`Promise.resolve`方法。
|
||||
|
||||
```javascript
|
||||
var p = Promise.resolve();
|
||||
const p = Promise.resolve();
|
||||
|
||||
p.then(function () {
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
上面代码的变量`p`就是一个Promise对象。
|
||||
上面代码的变量`p`就是一个 Promise 对象。
|
||||
|
||||
需要注意的是,立即`resolve`的Promise对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。
|
||||
需要注意的是,立即`resolve`的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。
|
||||
|
||||
```javascript
|
||||
setTimeout(function () {
|
||||
@ -633,32 +710,50 @@ console.log('one');
|
||||
// three
|
||||
```
|
||||
|
||||
上面代码中,`setTimeout(fn, 0)`在下一轮“事件循环”开始时执行,`Promise.resolve()`在本轮“事件循环”结束时执行,`console.log(’one‘)`则是立即执行,因此最先输出。
|
||||
上面代码中,`setTimeout(fn, 0)`在下一轮“事件循环”开始时执行,`Promise.resolve()`在本轮“事件循环”结束时执行,`console.log('one')`则是立即执行,因此最先输出。
|
||||
|
||||
## Promise.reject()
|
||||
|
||||
`Promise.reject(reason)`方法也会返回一个新的Promise实例,该实例的状态为`rejected`。它的参数用法与`Promise.resolve`方法完全一致。
|
||||
`Promise.reject(reason)`方法也会返回一个新的 Promise 实例,该实例的状态为`rejected`。
|
||||
|
||||
```javascript
|
||||
var p = Promise.reject('出错了');
|
||||
const p = Promise.reject('出错了');
|
||||
// 等同于
|
||||
var p = new Promise((resolve, reject) => reject('出错了'))
|
||||
const p = new Promise((resolve, reject) => reject('出错了'))
|
||||
|
||||
p.then(null, function (s){
|
||||
p.then(null, function (s) {
|
||||
console.log(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 之中、但很有用的方法。
|
||||
|
||||
### done()
|
||||
|
||||
Promise对象的回调链,不管以`then`方法或`catch`方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为Promise内部的错误不会冒泡到全局)。因此,我们可以提供一个`done`方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。
|
||||
Promise 对象的回调链,不管以`then`方法或`catch`方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为 Promise 内部的错误不会冒泡到全局)。因此,我们可以提供一个`done`方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。
|
||||
|
||||
```javascript
|
||||
asyncFunc()
|
||||
@ -680,13 +775,13 @@ Promise.prototype.done = function (onFulfilled, onRejected) {
|
||||
};
|
||||
```
|
||||
|
||||
从上面代码可见,`done`方法的使用,可以像`then`方法那样用,提供`Fulfilled`和`Rejected`状态的回调函数,也可以不提供任何参数。但不管怎样,`done`都会捕捉到任何可能出现的错误,并向全局抛出。
|
||||
从上面代码可见,`done`方法的使用,可以像`then`方法那样用,提供`fulfilled`和`rejected`状态的回调函数,也可以不提供任何参数。但不管怎样,`done`都会捕捉到任何可能出现的错误,并向全局抛出。
|
||||
|
||||
### finally()
|
||||
|
||||
`finally`方法用于指定不管Promise对象最后状态如何,都会执行的操作。它与`done`方法的最大区别,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。
|
||||
`finally`方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。它与`done`方法的最大区别,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。
|
||||
|
||||
下面是一个例子,服务器使用Promise处理请求,然后使用`finally`方法关掉服务器。
|
||||
下面是一个例子,服务器使用 Promise 处理请求,然后使用`finally`方法关掉服务器。
|
||||
|
||||
```javascript
|
||||
server.listen(0)
|
||||
@ -708,7 +803,7 @@ Promise.prototype.finally = function (callback) {
|
||||
};
|
||||
```
|
||||
|
||||
上面代码中,不管前面的Promise是`fulfilled`还是`rejected`,都会执行回调函数`callback`。
|
||||
上面代码中,不管前面的 Promise 是`fulfilled`还是`rejected`,都会执行回调函数`callback`。
|
||||
|
||||
## 应用
|
||||
|
||||
@ -719,7 +814,7 @@ Promise.prototype.finally = function (callback) {
|
||||
```javascript
|
||||
const preloadImage = function (path) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var image = new Image();
|
||||
const image = new Image();
|
||||
image.onload = resolve;
|
||||
image.onerror = reject;
|
||||
image.src = path;
|
||||
@ -727,9 +822,9 @@ const preloadImage = function (path) {
|
||||
};
|
||||
```
|
||||
|
||||
### Generator函数与Promise的结合
|
||||
### Generator 函数与 Promise 的结合
|
||||
|
||||
使用Generator函数管理流程,遇到异步操作的时候,通常返回一个`Promise`对象。
|
||||
使用 Generator 函数管理流程,遇到异步操作的时候,通常返回一个`Promise`对象。
|
||||
|
||||
```javascript
|
||||
function getFoo () {
|
||||
@ -738,9 +833,9 @@ function getFoo () {
|
||||
});
|
||||
}
|
||||
|
||||
var g = function* () {
|
||||
const g = function* () {
|
||||
try {
|
||||
var foo = yield getFoo();
|
||||
const foo = yield getFoo();
|
||||
console.log(foo);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
@ -748,7 +843,7 @@ var g = function* () {
|
||||
};
|
||||
|
||||
function run (generator) {
|
||||
var it = generator();
|
||||
const it = generator();
|
||||
|
||||
function go(result) {
|
||||
if (result.done) return result.value;
|
||||
@ -766,7 +861,7 @@ function run (generator) {
|
||||
run(g);
|
||||
```
|
||||
|
||||
上面代码的Generator函数`g`之中,有一个异步操作`getFoo`,它返回的就是一个`Promise`对象。函数`run`用来处理这个`Promise`对象,并调用下一个`next`方法。
|
||||
上面代码的 Generator 函数`g`之中,有一个异步操作`getFoo`,它返回的就是一个`Promise`对象。函数`run`用来处理这个`Promise`对象,并调用下一个`next`方法。
|
||||
|
||||
## Promise.try()
|
||||
|
||||
@ -776,7 +871,7 @@ run(g);
|
||||
Promise.resolve().then(f)
|
||||
```
|
||||
|
||||
上面的写法有一个缺点,就是如果`f`是同步函数,那么它会在下一轮事件循环执行。
|
||||
上面的写法有一个缺点,就是如果`f`是同步函数,那么它会在本轮事件循环的末尾执行。
|
||||
|
||||
```javascript
|
||||
const f = () => console.log('now');
|
||||
@ -798,7 +893,7 @@ console.log('next');
|
||||
// next
|
||||
```
|
||||
|
||||
上面代码中,第一行是一个立即执行的匿名函数,会立即执行里面的`async`函数,因此如果`f`是同步的,就会得到同步的结果;如果`f`是异步的,就可以用`then`指定下一步,就像下面的写法。
|
||||
上面代码中,第二行是一个立即执行的匿名函数,会立即执行里面的`async`函数,因此如果`f`是同步的,就会得到同步的结果;如果`f`是异步的,就可以用`then`指定下一步,就像下面的写法。
|
||||
|
||||
```javascript
|
||||
(async () => f())()
|
||||
@ -881,4 +976,3 @@ Promise.try(database.users.get({id: userId}))
|
||||
```
|
||||
|
||||
事实上,`Promise.try`就是模拟`try`代码块,就像`promise.catch`模拟的是`catch`代码块。
|
||||
|
||||
|
535
docs/proxy.md
535
docs/proxy.md
@ -1,6 +1,6 @@
|
||||
# Proxy 和 Reflect
|
||||
# Proxy
|
||||
|
||||
## Proxy 概述
|
||||
## 概述
|
||||
|
||||
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
|
||||
|
||||
@ -68,9 +68,9 @@ proxy.a = 'b';
|
||||
target.a // "b"
|
||||
```
|
||||
|
||||
上面代码中,`handler`是一个空对象,没有任何拦截效果,访问`handler`就等同于访问`target`。
|
||||
上面代码中,`handler`是一个空对象,没有任何拦截效果,访问`proxy`就等同于访问`target`。
|
||||
|
||||
一个技巧是将Proxy对象,设置到`object.proxy`属性,从而可以在`object`对象上调用。
|
||||
一个技巧是将 Proxy 对象,设置到`object.proxy`属性,从而可以在`object`对象上调用。
|
||||
|
||||
```javascript
|
||||
var object = { proxy: new Proxy(target, handler) };
|
||||
@ -116,78 +116,38 @@ var fproxy = new Proxy(function(x, y) {
|
||||
}, handler);
|
||||
|
||||
fproxy(1, 2) // 1
|
||||
new fproxy(1,2) // {value: 2}
|
||||
new fproxy(1, 2) // {value: 2}
|
||||
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`,返回一个布尔值。
|
||||
- **has(target, propKey)**:拦截`propKey in proxy`的操作,返回一个布尔值。
|
||||
- **deleteProperty(target, propKey)**:拦截`delete proxy[propKey]`的操作,返回一个布尔值。
|
||||
- **ownKeys(target)**:拦截`Object.getOwnPropertyNames(proxy)`、`Object.getOwnPropertySymbols(proxy)`、`Object.keys(proxy)`,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而`Object.keys()`的返回结果仅包括目标对象自身的可遍历属性。
|
||||
- **getOwnPropertyDescriptor(target, propKey)**:拦截`Object.getOwnPropertyDescriptor(proxy, propKey)`,返回属性的描述对象。
|
||||
- **defineProperty(target, propKey, propDesc)**:拦截`Object.defineProperty(proxy, propKey, propDesc)`、`Object.defineProperties(proxy, propDescs)`,返回一个布尔值。
|
||||
- **preventExtensions(target)**:拦截`Object.preventExtensions(proxy)`,返回一个布尔值。
|
||||
- **getPrototypeOf(target)**:拦截`Object.getPrototypeOf(proxy)`,返回一个对象。
|
||||
- **isExtensible(target)**:拦截`Object.isExtensible(proxy)`,返回一个布尔值。
|
||||
- **setPrototypeOf(target, proto)**:拦截`Object.setPrototypeOf(proxy, proto)`,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
|
||||
- **apply(target, object, args)**:拦截 Proxy 实例作为函数调用的操作,比如`proxy(...args)`、`proxy.call(object, ...args)`、`proxy.apply(...)`。
|
||||
- **construct(target, args)**:拦截 Proxy 实例作为构造函数调用的操作,比如`new proxy(...args)`。
|
||||
|
||||
最后一个参数`receiver`是一个对象,可选,参见下面`Reflect.get`的部分。
|
||||
|
||||
**(2)set(target, propKey, value, receiver)**
|
||||
|
||||
拦截对象属性的设置,比如`proxy.foo = v`或`proxy['foo'] = v`,返回一个布尔值。
|
||||
|
||||
**(3)has(target, propKey)**
|
||||
|
||||
拦截`propKey in proxy`的操作,以及对象的`hasOwnProperty`方法,返回一个布尔值。
|
||||
|
||||
**(4)deleteProperty(target, propKey)**
|
||||
|
||||
拦截`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 实例的方法
|
||||
|
||||
下面是上面这些拦截方法的详细介绍。
|
||||
|
||||
### get()
|
||||
|
||||
`get`方法用于拦截某个属性的读取操作。上文已经有一个例子,下面是另一个拦截读取操作的例子。
|
||||
`get`方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(即`this`关键字指向的那个对象),其中最后一个参数可选。
|
||||
|
||||
`get`方法的用法,上文已经有一个例子,下面是另一个拦截读取操作的例子。
|
||||
|
||||
```javascript
|
||||
var person = {
|
||||
@ -215,13 +175,13 @@ proxy.age // 抛出一个错误
|
||||
```javascript
|
||||
let proto = new Proxy({}, {
|
||||
get(target, propertyKey, receiver) {
|
||||
console.log('GET '+propertyKey);
|
||||
console.log('GET ' + propertyKey);
|
||||
return target[propertyKey];
|
||||
}
|
||||
});
|
||||
|
||||
let obj = Object.create(proto);
|
||||
obj.xxx // "GET xxx"
|
||||
obj.foo // "GET foo"
|
||||
```
|
||||
|
||||
上面代码中,拦截操作定义在`Prototype`对象上面,所以如果读取`obj`对象继承的属性时,拦截会生效。
|
||||
@ -282,7 +242,7 @@ pipe(3).double.pow.reverseInt.get; // 63
|
||||
|
||||
上面代码设置 Proxy 以后,达到了将函数名链式使用的效果。
|
||||
|
||||
下面的例子则是利用`get`拦截,实现一个生成各种DOM节点的通用函数`dom`。
|
||||
下面的例子则是利用`get`拦截,实现一个生成各种 DOM 节点的通用函数`dom`。
|
||||
|
||||
```javascript
|
||||
const dom = new Proxy({}, {
|
||||
@ -317,11 +277,47 @@ const el = dom.div({},
|
||||
document.body.appendChild(el);
|
||||
```
|
||||
|
||||
下面是一个`get`方法的第三个参数的例子。
|
||||
|
||||
```javascript
|
||||
const proxy = new Proxy({}, {
|
||||
get: function(target, property, receiver) {
|
||||
return receiver;
|
||||
}
|
||||
});
|
||||
proxy.getReceiver === proxy // true
|
||||
```
|
||||
|
||||
上面代码中,`get`方法的第三个参数`receiver`,总是为当前的 Proxy 实例。
|
||||
|
||||
如果一个属性不可配置(configurable)和不可写(writable),则该属性不能被代理,通过 Proxy 对象访问该属性会报错。
|
||||
|
||||
```javascript
|
||||
const target = Object.defineProperties({}, {
|
||||
foo: {
|
||||
value: 123,
|
||||
writable: false,
|
||||
configurable: false
|
||||
},
|
||||
});
|
||||
|
||||
const handler = {
|
||||
get(target, propKey) {
|
||||
return 'abc';
|
||||
}
|
||||
};
|
||||
|
||||
const proxy = new Proxy(target, handler);
|
||||
|
||||
proxy.foo
|
||||
// TypeError: Invariant check failed
|
||||
```
|
||||
|
||||
### set()
|
||||
|
||||
`set`方法用来拦截某个属性的赋值操作。
|
||||
`set`方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。
|
||||
|
||||
假定`Person`对象有一个`age`属性,该属性应该是一个不大于200的整数,那么可以使用`Proxy`保证`age`的属性值符合要求。
|
||||
假定`Person`对象有一个`age`属性,该属性应该是一个不大于 200 的整数,那么可以使用`Proxy`保证`age`的属性值符合要求。
|
||||
|
||||
```javascript
|
||||
let validator = {
|
||||
@ -335,7 +331,7 @@ let validator = {
|
||||
}
|
||||
}
|
||||
|
||||
// 对于age以外的属性,直接保存
|
||||
// 对于满足条件的 age 属性以及其他属性,直接保存
|
||||
obj[prop] = value;
|
||||
}
|
||||
};
|
||||
@ -349,12 +345,12 @@ person.age = 'young' // 报错
|
||||
person.age = 300 // 报错
|
||||
```
|
||||
|
||||
上面代码中,由于设置了存值函数`set`,任何不符合要求的`age`属性赋值,都会抛出一个错误。利用`set`方法,还可以数据绑定,即每当对象发生变化时,会自动更新DOM。
|
||||
上面代码中,由于设置了存值函数`set`,任何不符合要求的`age`属性赋值,都会抛出一个错误,这是数据验证的一种实现方法。利用`set`方法,还可以数据绑定,即每当对象发生变化时,会自动更新 DOM。
|
||||
|
||||
有时,我们会在对象上面设置内部属性,属性名的第一个字符使用下划线开头,表示这些属性不应该被外部使用。结合`get`和`set`方法,就可以做到防止这些内部属性被外部读写。
|
||||
|
||||
```javascript
|
||||
var handler = {
|
||||
const handler = {
|
||||
get (target, key) {
|
||||
invariant(key, 'get');
|
||||
return target[key];
|
||||
@ -370,8 +366,8 @@ function invariant (key, action) {
|
||||
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
|
||||
}
|
||||
}
|
||||
var target = {};
|
||||
var proxy = new Proxy(target, handler);
|
||||
const target = {};
|
||||
const proxy = new Proxy(target, handler);
|
||||
proxy._prop
|
||||
// Error: Invalid attempt to get private "_prop" property
|
||||
proxy._prop = 'c'
|
||||
@ -380,9 +376,28 @@ proxy._prop = 'c'
|
||||
|
||||
上面代码中,只要读写的属性名的第一个字符是下划线,一律抛错,从而达到禁止读写内部属性的目的。
|
||||
|
||||
下面是`set`方法第四个参数的例子。
|
||||
|
||||
```javascript
|
||||
const handler = {
|
||||
set: function(obj, prop, value, receiver) {
|
||||
obj[prop] = receiver;
|
||||
}
|
||||
};
|
||||
const proxy = new Proxy({}, handler);
|
||||
proxy.foo = 'bar';
|
||||
proxy.foo === proxy // true
|
||||
```
|
||||
|
||||
上面代码中,`set`方法的第四个参数`receiver`,总是返回`this`关键字所指向的那个对象,即`proxy`实例本身。
|
||||
|
||||
注意,如果目标对象自身的某个属性,不可写也不可配置,那么`set`不得改变这个属性的值,只能返回同样的值,否则报错。
|
||||
|
||||
### apply()
|
||||
|
||||
`apply`方法拦截函数的调用、call和apply操作。
|
||||
`apply`方法拦截函数的调用、`call`和`apply`操作。
|
||||
|
||||
`apply`方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(`this`)和目标对象的参数数组。
|
||||
|
||||
```javascript
|
||||
var handler = {
|
||||
@ -392,8 +407,6 @@ var handler = {
|
||||
};
|
||||
```
|
||||
|
||||
`apply`方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(`this`)和目标对象的参数数组。
|
||||
|
||||
下面是一个例子。
|
||||
|
||||
```javascript
|
||||
@ -410,7 +423,7 @@ p()
|
||||
// "I am the proxy"
|
||||
```
|
||||
|
||||
上面代码中,变量`p`是Proxy的实例,当它作为函数调用时(`p()`),就会被`apply`方法拦截,返回一个字符串。
|
||||
上面代码中,变量`p`是 Proxy 的实例,当它作为函数调用时(`p()`),就会被`apply`方法拦截,返回一个字符串。
|
||||
|
||||
下面是另外一个例子。
|
||||
|
||||
@ -464,6 +477,7 @@ var proxy = new Proxy(target, handler);
|
||||
```javascript
|
||||
var obj = { a: 10 };
|
||||
Object.preventExtensions(obj);
|
||||
|
||||
var p = new Proxy(obj, {
|
||||
has: function(target, prop) {
|
||||
return false;
|
||||
@ -473,7 +487,7 @@ var p = new Proxy(obj, {
|
||||
'a' in p // TypeError is thrown
|
||||
```
|
||||
|
||||
上面代码中,`obj`对象禁止扩展,结果使用`has`拦截就会报错。
|
||||
上面代码中,`obj`对象禁止扩展,结果使用`has`拦截就会报错。也就是说,如果某个属性不可配置(或者目标对象不可扩展),则`has`方法就不得“隐藏”(即返回`false`)目标对象的该属性。
|
||||
|
||||
值得注意的是,`has`方法拦截的是`HasProperty`操作,而不是`HasOwnProperty`操作,即`has`方法不判断一个属性是对象自身的属性,还是继承的属性。
|
||||
|
||||
@ -516,7 +530,7 @@ for (let b in oproxy2) {
|
||||
// 99
|
||||
```
|
||||
|
||||
上面代码中,`has`拦截只对`in`循环生效,对`for...in`循环不生效,导致不符合要求的属性没有被排除在`for...in`循环之外。
|
||||
上面代码中,`has`拦截只对`in`运算符生效,对`for...in`循环不生效,导致不符合要求的属性没有被排除在`for...in`循环之外。
|
||||
|
||||
### construct()
|
||||
|
||||
@ -538,14 +552,14 @@ var handler = {
|
||||
下面是一个例子。
|
||||
|
||||
```javascript
|
||||
var p = new Proxy(function() {}, {
|
||||
var p = new Proxy(function () {}, {
|
||||
construct: function(target, args) {
|
||||
console.log('called: ' + args.join(', '));
|
||||
return { value: args[0] * 10 };
|
||||
}
|
||||
});
|
||||
|
||||
new p(1).value
|
||||
(new p(1)).value
|
||||
// "called: 1"
|
||||
// 10
|
||||
```
|
||||
@ -587,6 +601,8 @@ delete proxy._prop
|
||||
|
||||
上面代码中,`deleteProperty`方法拦截了`delete`操作符,删除第一个字符为下划线的属性会报错。
|
||||
|
||||
注意,目标对象自身的不可配置(configurable)的属性,不能被`deleteProperty`方法删除,否则报错。
|
||||
|
||||
### defineProperty()
|
||||
|
||||
`defineProperty`方法拦截了`Object.defineProperty`操作。
|
||||
@ -605,9 +621,11 @@ proxy.foo = 'bar'
|
||||
|
||||
上面代码中,`defineProperty`方法返回`false`,导致添加新属性会抛出错误。
|
||||
|
||||
注意,如果目标对象不可扩展(extensible),则`defineProperty`不能增加目标对象上不存在的属性,否则会报错。另外,如果目标对象的某个属性不可写(writable)或不可配置(configurable),则`defineProperty`方法不得改变这两个设置。
|
||||
|
||||
### getOwnPropertyDescriptor()
|
||||
|
||||
`getOwnPropertyDescriptor`方法拦截`Object.getOwnPropertyDescriptor`,返回一个属性描述对象或者`undefined`。
|
||||
`getOwnPropertyDescriptor`方法拦截`Object.getOwnPropertyDescriptor()`,返回一个属性描述对象或者`undefined`。
|
||||
|
||||
```javascript
|
||||
var handler = {
|
||||
@ -632,13 +650,13 @@ Object.getOwnPropertyDescriptor(proxy, 'baz')
|
||||
|
||||
### getPrototypeOf()
|
||||
|
||||
`getPrototypeOf`方法主要用来拦截`Object.getPrototypeOf()`运算符,以及其他一些操作。
|
||||
`getPrototypeOf`方法主要用来拦截获取对象原型。具体来说,拦截下面这些操作。
|
||||
|
||||
- `Object.prototype.__proto__`
|
||||
- `Object.prototype.isPrototypeOf()`
|
||||
- `Object.getPrototypeOf()`
|
||||
- `Reflect.getPrototypeOf()`
|
||||
- `instanceof`运算符
|
||||
- `instanceof`
|
||||
|
||||
下面是一个例子。
|
||||
|
||||
@ -654,6 +672,8 @@ Object.getPrototypeOf(p) === proto // true
|
||||
|
||||
上面代码中,`getPrototypeOf`方法拦截`Object.getPrototypeOf()`,返回`proto`对象。
|
||||
|
||||
注意,`getPrototypeOf`方法的返回值必须是对象或者`null`,否则报错。另外,如果目标对象不可扩展(extensible), `getPrototypeOf`方法必须返回目标对象的原型对象。
|
||||
|
||||
### isExtensible()
|
||||
|
||||
`isExtensible`方法拦截`Object.isExtensible`操作。
|
||||
@ -673,7 +693,9 @@ Object.isExtensible(p)
|
||||
|
||||
上面代码设置了`isExtensible`方法,在调用`Object.isExtensible`时会输出`called`。
|
||||
|
||||
这个方法有一个强限制,如果不能满足下面的条件,就会抛出错误。
|
||||
注意,该方法只能返回布尔值,否则返回值会被自动转为布尔值。
|
||||
|
||||
这个方法有一个强限制,它的返回值必须与目标对象的`isExtensible`属性保持一致,否则就会抛出错误。
|
||||
|
||||
```javascript
|
||||
Object.isExtensible(proxy) === Object.isExtensible(target)
|
||||
@ -693,24 +715,34 @@ Object.isExtensible(p) // 报错
|
||||
|
||||
### ownKeys()
|
||||
|
||||
`ownKeys`方法用来拦截`Object.keys()`操作。
|
||||
`ownKeys`方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作。
|
||||
|
||||
- `Object.getOwnPropertyNames()`
|
||||
- `Object.getOwnPropertySymbols()`
|
||||
- `Object.keys()`
|
||||
|
||||
下面是拦截`Object.keys()`的例子。
|
||||
|
||||
```javascript
|
||||
let target = {};
|
||||
let target = {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3
|
||||
};
|
||||
|
||||
let handler = {
|
||||
ownKeys(target) {
|
||||
return ['hello', 'world'];
|
||||
return ['a'];
|
||||
}
|
||||
};
|
||||
|
||||
let proxy = new Proxy(target, handler);
|
||||
|
||||
Object.keys(proxy)
|
||||
// [ 'hello', 'world' ]
|
||||
// [ 'a' ]
|
||||
```
|
||||
|
||||
上面代码拦截了对于`target`对象的`Object.keys()`操作,返回预先设定的数组。
|
||||
上面代码拦截了对于`target`对象的`Object.keys()`操作,只返回`a`、`b`、`c`三个属性之中的`a`属性。
|
||||
|
||||
下面的例子是拦截第一个字符为下划线的属性名。
|
||||
|
||||
@ -734,11 +766,119 @@ for (let key of Object.keys(proxy)) {
|
||||
// "baz"
|
||||
```
|
||||
|
||||
注意,使用`Object.keys`方法时,有三类属性会被`ownKeys`方法自动过滤,不会返回。
|
||||
|
||||
- 目标对象上不存在的属性
|
||||
- 属性名为 Symbol 值
|
||||
- 不可遍历(`enumerable`)的属性
|
||||
|
||||
```javascript
|
||||
let target = {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3,
|
||||
[Symbol.for('secret')]: '4',
|
||||
};
|
||||
|
||||
Object.defineProperty(target, 'key', {
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: 'static'
|
||||
});
|
||||
|
||||
let handler = {
|
||||
ownKeys(target) {
|
||||
return ['a', 'd', Symbol.for('secret'), 'key'];
|
||||
}
|
||||
};
|
||||
|
||||
let proxy = new Proxy(target, handler);
|
||||
|
||||
Object.keys(proxy)
|
||||
// ['a']
|
||||
```
|
||||
|
||||
上面代码中,`ownKeys`方法之中,显式返回不存在的属性(`d`)、Symbol 值(`Symbol.for('secret')`)、不可遍历的属性(`key`),结果都被自动过滤掉。
|
||||
|
||||
`ownKeys`方法还可以拦截`Object.getOwnPropertyNames()`。
|
||||
|
||||
```javascript
|
||||
var p = new Proxy({}, {
|
||||
ownKeys: function(target) {
|
||||
return ['a', 'b', 'c'];
|
||||
}
|
||||
});
|
||||
|
||||
Object.getOwnPropertyNames(p)
|
||||
// [ 'a', 'b', 'c' ]
|
||||
```
|
||||
|
||||
`ownKeys`方法返回的数组成员,只能是字符串或 Symbol 值。如果有其他类型的值,或者返回的根本不是数组,就会报错。
|
||||
|
||||
```javascript
|
||||
var obj = {};
|
||||
|
||||
var p = new Proxy(obj, {
|
||||
ownKeys: function(target) {
|
||||
return [123, true, undefined, null, {}, []];
|
||||
}
|
||||
});
|
||||
|
||||
Object.getOwnPropertyNames(p)
|
||||
// Uncaught TypeError: 123 is not a valid property name
|
||||
```
|
||||
|
||||
上面代码中,`ownKeys`方法虽然返回一个数组,但是每一个数组成员都不是字符串或 Symbol 值,因此就报错了。
|
||||
|
||||
如果目标对象自身包含不可配置的属性,则该属性必须被`ownKeys`方法返回,否则报错。
|
||||
|
||||
```javascript
|
||||
var obj = {};
|
||||
Object.defineProperty(obj, 'a', {
|
||||
configurable: false,
|
||||
enumerable: true,
|
||||
value: 10 }
|
||||
);
|
||||
|
||||
var p = new Proxy(obj, {
|
||||
ownKeys: function(target) {
|
||||
return ['b'];
|
||||
}
|
||||
});
|
||||
|
||||
Object.getOwnPropertyNames(p)
|
||||
// Uncaught TypeError: 'ownKeys' on proxy: trap result did not include 'a'
|
||||
```
|
||||
|
||||
上面代码中,`obj`对象的`a`属性是不可配置的,这时`ownKeys`方法返回的数组之中,必须包含`a`,否则会报错。
|
||||
|
||||
另外,如果目标对象是不可扩展的(non-extensition),这时`ownKeys`方法返回的数组之中,必须包含原对象的所有属性,且不能包含多余的属性,否则报错。
|
||||
|
||||
```javascript
|
||||
var obj = {
|
||||
a: 1
|
||||
};
|
||||
|
||||
Object.preventExtensions(obj);
|
||||
|
||||
var p = new Proxy(obj, {
|
||||
ownKeys: function(target) {
|
||||
return ['a', 'b'];
|
||||
}
|
||||
});
|
||||
|
||||
Object.getOwnPropertyNames(p)
|
||||
// Uncaught TypeError: 'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensible
|
||||
```
|
||||
|
||||
上面代码中,`obj`对象是不可扩展的,这时`ownKeys`方法返回的数组之中,包含了`obj`对象的多余属性`b`,所以导致了报错。
|
||||
|
||||
### preventExtensions()
|
||||
|
||||
`preventExtensions`方法拦截`Object.preventExtensions()`。该方法必须返回一个布尔值。
|
||||
`preventExtensions`方法拦截`Object.preventExtensions()`。该方法必须返回一个布尔值,否则会被自动转为布尔值。
|
||||
|
||||
这个方法有一个限制,只有当`Object.isExtensible(proxy)`为`false`(即不可扩展)时,`proxy.preventExtensions`才能返回`true`,否则会报错。
|
||||
这个方法有一个限制,只有目标对象不可扩展时(即`Object.isExtensible(proxy)`为`false`),`proxy.preventExtensions`才能返回`true`,否则会报错。
|
||||
|
||||
```javascript
|
||||
var p = new Proxy({}, {
|
||||
@ -757,7 +897,7 @@ Object.preventExtensions(p) // 报错
|
||||
```javascript
|
||||
var p = new Proxy({}, {
|
||||
preventExtensions: function(target) {
|
||||
console.log("called");
|
||||
console.log('called');
|
||||
Object.preventExtensions(target);
|
||||
return true;
|
||||
}
|
||||
@ -783,15 +923,17 @@ var handler = {
|
||||
var proto = {};
|
||||
var target = function () {};
|
||||
var proxy = new Proxy(target, handler);
|
||||
proxy.setPrototypeOf(proxy, proto);
|
||||
Object.setPrototypeOf(proxy, proto);
|
||||
// Error: Changing the prototype is forbidden
|
||||
```
|
||||
|
||||
上面代码中,只要修改`target`的原型对象,就会报错。
|
||||
|
||||
注意,该方法只能返回布尔值,否则会被自动转为布尔值。另外,如果目标对象不可扩展(extensible),`setPrototypeOf`方法不得改变目标对象的原型。
|
||||
|
||||
## Proxy.revocable()
|
||||
|
||||
Proxy.revocable方法返回一个可取消的Proxy实例。
|
||||
`Proxy.revocable`方法返回一个可取消的 Proxy 实例。
|
||||
|
||||
```javascript
|
||||
let target = {};
|
||||
@ -808,6 +950,8 @@ proxy.foo // TypeError: Revoked
|
||||
|
||||
`Proxy.revocable`方法返回一个对象,该对象的`proxy`属性是`Proxy`实例,`revoke`属性是一个函数,可以取消`Proxy`实例。上面代码中,当执行`revoke`函数之后,再访问`Proxy`实例,就会抛出一个错误。
|
||||
|
||||
`Proxy.revocable`的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。
|
||||
|
||||
## this 问题
|
||||
|
||||
虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的`this`关键字会指向 Proxy 代理。
|
||||
@ -879,200 +1023,29 @@ const proxy = new Proxy(target, handler);
|
||||
proxy.getDate() // 1
|
||||
```
|
||||
|
||||
## Reflect概述
|
||||
## 实例:Web 服务的客户端
|
||||
|
||||
`Reflect`对象与`Proxy`对象一样,也是ES6为了操作对象而提供的新API。`Reflect`对象的设计目的有这样几个。
|
||||
|
||||
(1) 将`Object`对象的一些明显属于语言内部的方法(比如`Object.defineProperty`),放到`Reflect`对象上。现阶段,某些方法同时在`Object`和`Reflect`对象上部署,未来的新方法将只部署在`Reflect`对象上。
|
||||
|
||||
(2) 修改某些Object方法的返回结果,让其变得更合理。比如,`Object.defineProperty(obj, name, desc)`在无法定义属性时,会抛出一个错误,而`Reflect.defineProperty(obj, name, desc)`则会返回`false`。
|
||||
Proxy 对象可以拦截目标对象的任意属性,这使得它很合适用来写 Web 服务的客户端。
|
||||
|
||||
```javascript
|
||||
// 老写法
|
||||
try {
|
||||
Object.defineProperty(target, property, attributes);
|
||||
// success
|
||||
} catch (e) {
|
||||
// failure
|
||||
}
|
||||
const service = createWebService('http://example.com/data');
|
||||
|
||||
// 新写法
|
||||
if (Reflect.defineProperty(target, property, attributes)) {
|
||||
// success
|
||||
} else {
|
||||
// failure
|
||||
}
|
||||
service.employees().then(json => {
|
||||
const employees = JSON.parse(json);
|
||||
// ···
|
||||
});
|
||||
```
|
||||
|
||||
(3) 让`Object`操作都变成函数行为。某些`Object`操作是命令式,比如`name in obj`和`delete obj[name]`,而`Reflect.has(obj, name)`和`Reflect.deleteProperty(obj, name)`让它们变成了函数行为。
|
||||
上面代码新建了一个 Web 服务的接口,这个接口返回各种数据。Proxy 可以拦截这个对象的任意属性,所以不用为每一种数据写一个适配方法,只要写一个 Proxy 拦截就可以了。
|
||||
|
||||
```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);
|
||||
function createWebService(baseUrl) {
|
||||
return new Proxy({}, {
|
||||
get(target, propKey, receiver) {
|
||||
return () => httpGet(baseUrl+'/' + propKey);
|
||||
}
|
||||
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对象的方法
|
||||
|
||||
`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 层。
|
||||
|
@ -2,45 +2,46 @@
|
||||
|
||||
## 官方文件
|
||||
|
||||
- [ECMAScript® 2015 Language Specification](http://www.ecma-international.org/ecma-262/6.0/index.html): ECMAScript 2015规格
|
||||
- [ECMAScript® 2016 Language Specification](http://www.ecma-international.org/ecma-262/7.0/): ECMAScript 2016规格
|
||||
- [ECMAScript® 2017 Language Specification](https://tc39.github.io/ecma262/):ECMAScript 2017规格(草案)
|
||||
- [ECMAScript Current Proposals](https://github.com/tc39/ecma262): ECMAScript当前的所有提案
|
||||
- [ECMAScript® 2015 Language Specification](http://www.ecma-international.org/ecma-262/6.0/index.html): ECMAScript 2015 规格
|
||||
- [ECMAScript® 2016 Language Specification](http://www.ecma-international.org/ecma-262/7.0/): ECMAScript 2016 规格
|
||||
- [ECMAScript® 2017 Language Specification](https://tc39.github.io/ecma262/):ECMAScript 2017 规格(草案)
|
||||
- [ECMAScript Current Proposals](https://github.com/tc39/ecma262): ECMAScript 当前的所有提案
|
||||
- [ECMAScript Active Proposals](https://github.com/tc39/proposals): 已经进入正式流程的提案
|
||||
- [ECMAscript proposals](https://github.com/hemanth/es-next):从阶段0到阶段4的所有提案列表
|
||||
- [ECMAscript proposals](https://github.com/hemanth/es-next):从阶段 0 到阶段 4 的所有提案列表
|
||||
- [TC39 meeting agendas](https://github.com/tc39/agendas): TC39 委员会历年的会议记录
|
||||
- [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/): 提案进入正式规格的流程
|
||||
- [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 的含义
|
||||
|
||||
## 综合介绍
|
||||
|
||||
- Axel Rauschmayer, [Exploring ES6: Upgrade to the next version of JavaScript](http://exploringjs.com/es6/): ES6的专著,本书的许多代码实例来自该书
|
||||
- Axel Rauschmayer, [Exploring ES6: Upgrade to the next version of JavaScript](http://exploringjs.com/es6/): ES6 的专著,本书的许多代码实例来自该书
|
||||
- Sayanee Basu, [Use ECMAScript 6 Today](http://net.tutsplus.com/articles/news/ecmascript-6-today/)
|
||||
- Ariya Hidayat, [Toward Modern Web Apps with ECMAScript 6](http://www.sencha.com/blog/toward-modern-web-apps-with-ecmascript-6/)
|
||||
- Dale Schouten, [10 Ecmascript-6 tricks you can perform right now](http://html5hub.com/10-ecmascript-6-tricks-you-can-perform-right-now/)
|
||||
- Colin Toh, [Lightweight ES6 Features That Pack A Punch](http://colintoh.com/blog/lightweight-es6-features): ES6的一些“轻量级”的特性介绍
|
||||
- Colin Toh, [Lightweight ES6 Features That Pack A Punch](http://colintoh.com/blog/lightweight-es6-features): ES6 的一些“轻量级”的特性介绍
|
||||
- Domenic Denicola, [ES6: The Awesome Parts](http://www.slideshare.net/domenicdenicola/es6-the-awesome-parts)
|
||||
- Nicholas C. Zakas, [Understanding ECMAScript 6](https://github.com/nzakas/understandinges6)
|
||||
- Justin Drake, [ECMAScript 6 in Node.JS](https://github.com/JustinDrake/node-es6-examples)
|
||||
- Ryan Dao, [Summary of ECMAScript 6 major features](http://ryandao.net/portal/content/summary-ecmascript-6-major-features)
|
||||
- Luke Hoban, [ES6 features](https://github.com/lukehoban/es6features): ES6新语法点的罗列
|
||||
- Traceur-compiler, [Language Features](https://github.com/google/traceur-compiler/wiki/LanguageFeatures): Traceur文档列出的一些ES6例子
|
||||
- Axel Rauschmayer, [ECMAScript 6: what’s next for JavaScript?](https://speakerdeck.com/rauschma/ecmascript-6-whats-next-for-javascript-august-2014): 关于ES6新增语法的综合介绍,有很多例子
|
||||
- Axel Rauschmayer, [Getting started with ECMAScript 6](http://www.2ality.com/2015/08/getting-started-es6.html): ES6语法点的综合介绍
|
||||
- Luke Hoban, [ES6 features](https://github.com/lukehoban/es6features): ES6 新语法点的罗列
|
||||
- Traceur-compiler, [Language Features](https://github.com/google/traceur-compiler/wiki/LanguageFeatures): Traceur 文档列出的一些 ES6 例子
|
||||
- Axel Rauschmayer, [ECMAScript 6: what’s next for JavaScript?](https://speakerdeck.com/rauschma/ecmascript-6-whats-next-for-javascript-august-2014): 关于 ES6 新增语法的综合介绍,有很多例子
|
||||
- Axel Rauschmayer, [Getting started with ECMAScript 6](http://www.2ality.com/2015/08/getting-started-es6.html): ES6 语法点的综合介绍
|
||||
- Toby Ho, [ES6 in io.js](http://davidwalsh.name/es6-io)
|
||||
- Guillermo Rauch, [ECMAScript 6](http://rauchg.com/2015/ecmascript-6/)
|
||||
- Charles King, [The power of ECMAScript 6](http://charlesbking.com/power_of_es6/#/)
|
||||
- Benjamin De Cock, [Frontend Guidelines](https://github.com/bendc/frontend-guidelines): ES6最佳实践
|
||||
- Benjamin De Cock, [Frontend Guidelines](https://github.com/bendc/frontend-guidelines): ES6 最佳实践
|
||||
- Jani Hartikainen, [ES6: What are the benefits of the new features in practice?](http://codeutopia.net/blog/2015/01/06/es6-what-are-the-benefits-of-the-new-features-in-practice/)
|
||||
- kangax, [Javascript quiz. ES6 edition](http://perfectionkills.com/javascript-quiz-es6/): ES6小测试
|
||||
- Jeremy Fairbank, [HTML5DevConf ES7 and Beyond!](https://speakerdeck.com/jfairbank/html5devconf-es7-and-beyond): ES7新增语法点介绍
|
||||
- kangax, [Javascript quiz. ES6 edition](http://perfectionkills.com/javascript-quiz-es6/): ES6 小测试
|
||||
- Jeremy Fairbank, [HTML5DevConf ES7 and Beyond!](https://speakerdeck.com/jfairbank/html5devconf-es7-and-beyond): ES7 新增语法点介绍
|
||||
|
||||
## let和const
|
||||
## let 和 const
|
||||
|
||||
- Kyle Simpson, [For and against let](http://davidwalsh.name/for-and-against-let): 讨论let命令的作用域
|
||||
- kangax, [Why typeof is no longer “safe”](http://es-discourse.com/t/why-typeof-is-no-longer-safe/15): 讨论在块级作用域内,let命令的变量声明和赋值的行为
|
||||
- Axel Rauschmayer, [Variables and scoping in ECMAScript 6](http://www.2ality.com/2015/02/es6-scoping.html): 讨论块级作用域与let和const的行为
|
||||
- Kyle Simpson, [For and against let](http://davidwalsh.name/for-and-against-let): 讨论 let 命令的作用域
|
||||
- kangax, [Why typeof is no longer “safe”](http://es-discourse.com/t/why-typeof-is-no-longer-safe/15): 讨论在块级作用域内,let 命令的变量声明和赋值的行为
|
||||
- Axel Rauschmayer, [Variables and scoping in ECMAScript 6](http://www.2ality.com/2015/02/es6-scoping.html): 讨论块级作用域与 let 和 const 的行为
|
||||
- Nicolas Bevacqua, [ES6 Let, Const and the “Temporal Dead Zone” (TDZ) in Depth](http://ponyfoo.com/articles/es6-let-const-and-temporal-dead-zone-in-depth)
|
||||
- acorn, [Function statements in strict mode](https://github.com/ternjs/acorn/issues/118): 块级作用域对严格模式的函数声明的影响
|
||||
- Axel Rauschmayer, [ES proposal: global](http://www.2ality.com/2016/09/global.html): 顶层对象`global`
|
||||
@ -61,18 +62,20 @@
|
||||
|
||||
## 正则
|
||||
|
||||
- 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正则特性的详细介绍
|
||||
- Mathias Bynens, [Unicode-aware regular expressions in ES6](https://mathiasbynens.be/notes/es6-unicode-regex): 详细介绍正则表达式的 u 修饰符
|
||||
- Axel Rauschmayer, [New regular expression features in ECMAScript 6](http://www.2ality.com/2015/07/regexp-es6.html):ES6 正则特性的详细介绍
|
||||
- Yang Guo, [RegExp lookbehind assertions](http://v8project.blogspot.jp/2016/02/regexp-lookbehind-assertions.html):介绍后行断言
|
||||
- Axel Rauschmayer, [ES proposal: RegExp named capture groups](http://2ality.com/2017/05/regexp-named-capture-groups.html): 具名组匹配的介绍
|
||||
|
||||
## 数值
|
||||
|
||||
- Nicolas Bevacqua, [ES6 Number Improvements in Depth](http://ponyfoo.com/articles/es6-number-improvements-in-depth)
|
||||
- Axel Rauschmayer, [ES proposal: arbitrary precision integers](http://2ality.com/2017/03/es-integer.html)
|
||||
|
||||
## 数组
|
||||
|
||||
- Axel Rauschmayer, [ECMAScript 6’s new array methods](http://www.2ality.com/2014/05/es6-array-methods.html): 对ES6新增的数组方法的全面介绍
|
||||
- TC39, [Array.prototype.includes](https://github.com/tc39/Array.prototype.includes/): 数组的includes方法的规格
|
||||
- Axel Rauschmayer, [ECMAScript 6’s new array methods](http://www.2ality.com/2014/05/es6-array-methods.html): 对 ES6 新增的数组方法的全面介绍
|
||||
- TC39, [Array.prototype.includes](https://github.com/tc39/Array.prototype.includes/): 数组的 includes 方法的规格
|
||||
- Axel Rauschmayer, [ECMAScript 6: holes in Arrays](http://www.2ality.com/2015/09/holes-arrays-es6.html): 数组的空位问题
|
||||
|
||||
## 函数
|
||||
@ -81,17 +84,18 @@
|
||||
- Jack Franklin, [Real Life ES6 - Arrow Functions](http://javascriptplayground.com/blog/2014/04/real-life-es6-arrow-fn/)
|
||||
- Axel Rauschmayer, [Handling required parameters in ECMAScript 6](http://www.2ality.com/2014/04/required-parameters-es6.html)
|
||||
- Dmitry Soshnikov, [ES6 Notes: Default values of parameters](http://dmitrysoshnikov.com/ecmascript/es6-notes-default-values-of-parameters/): 介绍参数的默认值
|
||||
- Ragan Wald, [Destructuring and Recursion in ES6](http://raganwald.com/2015/02/02/destructuring.html): rest参数和扩展运算符的详细介绍
|
||||
- Axel Rauschmayer, [The names of functions in ES6](http://www.2ality.com/2015/09/function-names-es6.html): 函数的name属性的详细介绍
|
||||
- Kyle Simpson, [Arrow This](http://blog.getify.com/arrow-this/): 箭头函数并没有自己的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指向,必须非常小心
|
||||
- Ragan Wald, [Destructuring and Recursion in ES6](http://raganwald.com/2015/02/02/destructuring.html): rest 参数和扩展运算符的详细介绍
|
||||
- Axel Rauschmayer, [The names of functions in ES6](http://www.2ality.com/2015/09/function-names-es6.html): 函数的 name 属性的详细介绍
|
||||
- Kyle Simpson, [Arrow This](http://blog.getify.com/arrow-this/): 箭头函数并没有自己的 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): 如何自己实现尾递归优化
|
||||
- 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)
|
||||
|
||||
## 对象
|
||||
|
||||
- Addy Osmani, [Data-binding Revolutions with Object.observe()](http://www.html5rocks.com/en/tutorials/es7/observe/): 介绍Object.observe()的概念
|
||||
- Sella Rafaeli, [Native JavaScript Data-Binding](http://www.sellarafaeli.com/blog/native_javascript_data_binding): 如何使用Object.observe方法,实现数据对象与DOM对象的双向绑定
|
||||
- Addy Osmani, [Data-binding Revolutions with Object.observe()](http://www.html5rocks.com/en/tutorials/es7/observe/): 介绍 Object.observe()的概念
|
||||
- Sella Rafaeli, [Native JavaScript Data-Binding](http://www.sellarafaeli.com/blog/native_javascript_data_binding): 如何使用 Object.observe 方法,实现数据对象与 DOM 对象的双向绑定
|
||||
- Axel Rauschmayer, [`__proto__` in ECMAScript 6](http://www.2ality.com/2015/09/proto-es6.html)
|
||||
- Axel Rauschmayer, [Enumerability in ECMAScript 6](http://www.2ality.com/2015/10/enumerability-es6.html)
|
||||
- Axel Rauschmayer, [ES proposal: Object.getOwnPropertyDescriptors()](http://www.2ality.com/2016/02/object-getownpropertydescriptors.html)
|
||||
@ -99,29 +103,29 @@
|
||||
|
||||
## Symbol
|
||||
|
||||
- Axel Rauschmayer, [Symbols in ECMAScript 6](http://www.2ality.com/2014/12/es6-symbols.html): Symbol简介
|
||||
- MDN, [Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol): Symbol类型的详细介绍
|
||||
- Axel Rauschmayer, [Symbols in ECMAScript 6](http://www.2ality.com/2014/12/es6-symbols.html): Symbol 简介
|
||||
- MDN, [Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol): Symbol 类型的详细介绍
|
||||
- Jason Orendorff, [ES6 In Depth: Symbols](https://hacks.mozilla.org/2015/06/es6-in-depth-symbols/)
|
||||
- Keith Cirkel, [Metaprogramming in ES6: Symbols and why they're awesome](http://blog.keithcirkel.co.uk/metaprogramming-in-es6-symbols/): Symbol的深入介绍
|
||||
- Keith Cirkel, [Metaprogramming in ES6: Symbols and why they're awesome](http://blog.keithcirkel.co.uk/metaprogramming-in-es6-symbols/): Symbol 的深入介绍
|
||||
- Axel Rauschmayer, [Customizing ES6 via well-known symbols](http://www.2ality.com/2015/09/well-known-symbols-es6.html)
|
||||
- Derick Bailey, [Creating A True Singleton In Node.js, With ES6 Symbols](https://derickbailey.com/2016/03/09/creating-a-true-singleton-in-node-js-with-es6-symbols/)
|
||||
- Das Surma, [How to read web specs Part IIa – Or: ECMAScript Symbols](https://dassur.ma/things/reading-specs-2/): 介绍 Symbol 的规格
|
||||
|
||||
## Set和Map
|
||||
## Set 和 Map
|
||||
|
||||
- Mozilla Developer Network, [WeakSet](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet):介绍WeakSet数据结构
|
||||
- Dwayne Charrington, [What Are Weakmaps In ES6?](http://ilikekillnerds.com/2015/02/what-are-weakmaps-in-es6/): WeakMap数据结构介绍
|
||||
- Axel Rauschmayer, [ECMAScript 6: maps and sets](http://www.2ality.com/2015/01/es6-maps-sets.html): Set和Map结构的详细介绍
|
||||
- Jason Orendorff, [ES6 In Depth: Collections](https://hacks.mozilla.org/2015/06/es6-in-depth-collections/):Set和Map结构的设计思想
|
||||
- Axel Rauschmayer, [Converting ES6 Maps to and from JSON](http://www.2ality.com/2015/08/es6-map-json.html): 如何将Map与其他数据结构互相转换
|
||||
- Mozilla Developer Network, [WeakSet](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet):介绍 WeakSet 数据结构
|
||||
- Dwayne Charrington, [What Are Weakmaps In ES6?](http://ilikekillnerds.com/2015/02/what-are-weakmaps-in-es6/): WeakMap 数据结构介绍
|
||||
- Axel Rauschmayer, [ECMAScript 6: maps and sets](http://www.2ality.com/2015/01/es6-maps-sets.html): Set 和 Map 结构的详细介绍
|
||||
- Jason Orendorff, [ES6 In Depth: Collections](https://hacks.mozilla.org/2015/06/es6-in-depth-collections/):Set 和 Map 结构的设计思想
|
||||
- Axel Rauschmayer, [Converting ES6 Maps to and from JSON](http://www.2ality.com/2015/08/es6-map-json.html): 如何将 Map 与其他数据结构互相转换
|
||||
|
||||
## Proxy和Reflect
|
||||
## Proxy 和 Reflect
|
||||
|
||||
- Nicholas C. Zakas, [Creating defensive objects with ES6 proxies](http://www.nczonline.net/blog/2014/04/22/creating-defensive-objects-with-es6-proxies/)
|
||||
- Axel Rauschmayer, [Meta programming with ECMAScript 6 proxies](http://www.2ality.com/2014/12/es6-proxies.html): Proxy详解
|
||||
- Daniel Zautner, [Meta-programming JavaScript Using Proxies](http://dzautner.com/meta-programming-javascript-using-proxies/): 使用Proxy实现元编程
|
||||
- Tom Van Cutsem, [Harmony-reflect](https://github.com/tvcutsem/harmony-reflect/wiki): Reflect对象的设计目的
|
||||
- Tom Van Cutsem, [Proxy Traps](https://github.com/tvcutsem/harmony-reflect/blob/master/doc/traps.md): Proxy拦截操作一览
|
||||
- Axel Rauschmayer, [Meta programming with ECMAScript 6 proxies](http://www.2ality.com/2014/12/es6-proxies.html): Proxy 详解
|
||||
- Daniel Zautner, [Meta-programming JavaScript Using Proxies](http://dzautner.com/meta-programming-javascript-using-proxies/): 使用 Proxy 实现元编程
|
||||
- Tom Van Cutsem, [Harmony-reflect](https://github.com/tvcutsem/harmony-reflect/wiki): Reflect 对象的设计目的
|
||||
- Tom Van Cutsem, [Proxy Traps](https://github.com/tvcutsem/harmony-reflect/blob/master/doc/traps.md): Proxy 拦截操作一览
|
||||
- Tom Van Cutsem, [Reflect API](https://github.com/tvcutsem/harmony-reflect/blob/master/doc/api.md)
|
||||
- Tom Van Cutsem, [Proxy Handler API](https://github.com/tvcutsem/harmony-reflect/blob/master/doc/handler_api.md)
|
||||
- Nicolas Bevacqua, [ES6 Proxies in Depth](http://ponyfoo.com/articles/es6-proxies-in-depth)
|
||||
@ -129,14 +133,15 @@
|
||||
- Nicolas Bevacqua, [More ES6 Proxy Traps in Depth](http://ponyfoo.com/articles/more-es6-proxy-traps-in-depth)
|
||||
- Axel Rauschmayer, [Pitfall: not all objects can be wrapped transparently by proxies](http://www.2ality.com/2016/11/proxying-builtins.html)
|
||||
- Bertalan Miklos, [Writing a JavaScript Framework - Data Binding with ES6 Proxies](https://blog.risingstack.com/writing-a-javascript-framework-data-binding-es6-proxy/): 使用 Proxy 实现观察者模式
|
||||
- Keith Cirkel, [Metaprogramming in ES6: Part 2 - Reflect](https://www.keithcirkel.co.uk/metaprogramming-in-es6-part-2-reflect/): Reflect API 的详细介绍
|
||||
|
||||
## Promise对象
|
||||
## Promise 对象
|
||||
|
||||
- Jake Archibald, [JavaScript Promises: There and back again](http://www.html5rocks.com/en/tutorials/es6/promises/)
|
||||
- Tilde, [rsvp.js](https://github.com/tildeio/rsvp.js)
|
||||
- Sandeep Panda, [An Overview of JavaScript Promises](http://www.sitepoint.com/overview-javascript-promises/): ES6 Promise入门介绍
|
||||
- Dave Atchley, [ES6 Promises](http://www.datchley.name/es6-promises/): Promise的语法介绍
|
||||
- Axel Rauschmayer, [ECMAScript 6 promises (2/2): the API](http://www.2ality.com/2014/10/es6-promises-api.html): 对ES6 Promise规格和用法的详细介绍
|
||||
- Sandeep Panda, [An Overview of JavaScript Promises](http://www.sitepoint.com/overview-javascript-promises/): ES6 Promise 入门介绍
|
||||
- Dave Atchley, [ES6 Promises](http://www.datchley.name/es6-promises/): Promise 的语法介绍
|
||||
- Axel Rauschmayer, [ECMAScript 6 promises (2/2): the API](http://www.2ality.com/2014/10/es6-promises-api.html): 对 ES6 Promise 规格和用法的详细介绍
|
||||
- Jack Franklin, [Embracing Promises in JavaScript](http://javascriptplayground.com/blog/2015/02/promises/): catch 方法的例子
|
||||
- Ronald Chen, [How to escape Promise Hell](https://medium.com/@pyrolistical/how-to-get-out-of-promise-hell-8c20e0ab0513#.2an1he6vf): 如何使用`Promise.all`方法的一些很好的例子
|
||||
- Jordan Harband, [proposal-promise-try](https://github.com/ljharb/proposal-promise-try): Promise.try() 方法的提案
|
||||
@ -147,9 +152,9 @@
|
||||
|
||||
- Mozilla Developer Network, [Iterators and generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)
|
||||
- Mozilla Developer Network, [The Iterator protocol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/The_Iterator_protocol)
|
||||
- Jason Orendorff, [ES6 In Depth: Iterators and the for-of loop](https://hacks.mozilla.org/2015/04/es6-in-depth-iterators-and-the-for-of-loop/): 遍历器与for...of循环的介绍
|
||||
- Axel Rauschmayer, [Iterators and generators in ECMAScript 6](http://www.2ality.com/2013/06/iterators-generators.html): 探讨Iterator和Generator的设计目的
|
||||
- Axel Rauschmayer, [Iterables and iterators in ECMAScript 6](http://www.2ality.com/2015/02/es6-iteration.html): Iterator的详细介绍
|
||||
- Jason Orendorff, [ES6 In Depth: Iterators and the for-of loop](https://hacks.mozilla.org/2015/04/es6-in-depth-iterators-and-the-for-of-loop/): 遍历器与 for...of 循环的介绍
|
||||
- Axel Rauschmayer, [Iterators and generators in ECMAScript 6](http://www.2ality.com/2013/06/iterators-generators.html): 探讨 Iterator 和 Generator 的设计目的
|
||||
- Axel Rauschmayer, [Iterables and iterators in ECMAScript 6](http://www.2ality.com/2015/02/es6-iteration.html): Iterator 的详细介绍
|
||||
- Kyle Simpson, [Iterating ES6 Numbers](http://blog.getify.com/iterating-es6-numbers/): 在数值对象上部署遍历器
|
||||
|
||||
## Generator
|
||||
@ -157,63 +162,71 @@
|
||||
- Matt Baker, [Replacing callbacks with ES6 Generators](http://flippinawesome.org/2014/02/10/replacing-callbacks-with-es6-generators/)
|
||||
- Steven Sanderson, [Experiments with Koa and JavaScript Generators](http://blog.stevensanderson.com/2013/12/21/experiments-with-koa-and-javascript-generators/)
|
||||
- jmar777, [What's the Big Deal with Generators?](http://devsmash.com/blog/whats-the-big-deal-with-generators)
|
||||
- Marc Harter, [Generators in Node.js: Common Misconceptions and Three Good Use Cases](http://strongloop.com/strongblog/how-to-generators-node-js-yield-use-cases/): 讨论Generator函数的作用
|
||||
- StackOverflow, [ES6 yield : what happens to the arguments of the first call next()?](http://stackoverflow.com/questions/20977379/es6-yield-what-happens-to-the-arguments-of-the-first-call-next): 第一次使用next方法时不能带有参数
|
||||
- Kyle Simpson, [ES6 Generators: Complete Series](http://davidwalsh.name/es6-generators): 由浅入深探讨Generator的系列文章,共四篇
|
||||
- Gajus Kuizinas, [The Definitive Guide to the JavaScript Generators](http://gajus.com/blog/2/the-definetive-guide-to-the-javascript-generators): 对Generator的综合介绍
|
||||
- Jan Krems, [Generators Are Like Arrays](https://gist.github.com/jkrems/04a2b34fb9893e4c2b5c): 讨论Generator可以被当作数据结构看待
|
||||
- Harold Cooper, [Coroutine Event Loops in Javascript](http://syzygy.st/javascript-coroutines/): Generator用于实现状态机
|
||||
- Ruslan Ismagilov, [learn-generators](https://github.com/isRuslan/learn-generators): 编程练习,共6道题
|
||||
- Steven Sanderson, [Experiments with Koa and JavaScript Generators](http://blog.stevensanderson.com/2013/12/21/experiments-with-koa-and-javascript-generators/): Generator入门介绍,以Koa框架为例
|
||||
- Mahdi Dibaiee, [ES7 Array and Generator comprehensions](http://dibaiee.ir/es7-array-generator-comprehensions/):ES7的Generator推导
|
||||
- Marc Harter, [Generators in Node.js: Common Misconceptions and Three Good Use Cases](http://strongloop.com/strongblog/how-to-generators-node-js-yield-use-cases/): 讨论 Generator 函数的作用
|
||||
- StackOverflow, [ES6 yield : what happens to the arguments of the first call next()?](http://stackoverflow.com/questions/20977379/es6-yield-what-happens-to-the-arguments-of-the-first-call-next): 第一次使用 next 方法时不能带有参数
|
||||
- Kyle Simpson, [ES6 Generators: Complete Series](http://davidwalsh.name/es6-generators): 由浅入深探讨 Generator 的系列文章,共四篇
|
||||
- Gajus Kuizinas, [The Definitive Guide to the JavaScript Generators](http://gajus.com/blog/2/the-definetive-guide-to-the-javascript-generators): 对 Generator 的综合介绍
|
||||
- Jan Krems, [Generators Are Like Arrays](https://gist.github.com/jkrems/04a2b34fb9893e4c2b5c): 讨论 Generator 可以被当作数据结构看待
|
||||
- Harold Cooper, [Coroutine Event Loops in Javascript](http://syzygy.st/javascript-coroutines/): Generator 用于实现状态机
|
||||
- Ruslan Ismagilov, [learn-generators](https://github.com/isRuslan/learn-generators): 编程练习,共 6 道题
|
||||
- Steven Sanderson, [Experiments with Koa and JavaScript Generators](http://blog.stevensanderson.com/2013/12/21/experiments-with-koa-and-javascript-generators/): Generator 入门介绍,以 Koa 框架为例
|
||||
- Mahdi Dibaiee, [ES7 Array and Generator comprehensions](http://dibaiee.ir/es7-array-generator-comprehensions/):ES7 的 Generator 推导
|
||||
- Nicolas Bevacqua, [ES6 Generators in Depth](http://ponyfoo.com/articles/es6-generators-in-depth)
|
||||
- Axel Rauschmayer, [ES6 generators in depth](http://www.2ality.com/2015/03/es6-generators.html): Generator规格的详尽讲解
|
||||
- Axel Rauschmayer, [ES6 generators in depth](http://www.2ality.com/2015/03/es6-generators.html): Generator 规格的详尽讲解
|
||||
- Derick Bailey, [Using ES6 Generators To Short-Circuit Hierarchical Data Iteration](https://derickbailey.com/2015/10/05/using-es6-generators-to-short-circuit-hierarchical-data-iteration/):使用 for...of 循环完成预定的操作步骤
|
||||
|
||||
## 异步操作和Async函数
|
||||
## 异步操作和 Async 函数
|
||||
|
||||
- Luke Hoban, [Async Functions for ECMAScript](https://github.com/lukehoban/ecmascript-asyncawait): Async函数的设计思想,与Promise、Gernerator函数的关系
|
||||
- Jafar Husain, [Asynchronous Generators for ES7](https://github.com/jhusain/asyncgenerator): Async函数的深入讨论
|
||||
- Nolan Lawson, [Taming the asynchronous beast with ES7](http://pouchdb.com/2015/03/05/taming-the-async-beast-with-es7.html): async函数通俗的实例讲解
|
||||
- Jafar Husain, [Async Generators](https://docs.google.com/file/d/0B4PVbLpUIdzoMDR5dWstRllXblU/view?sle=true): 对async与Generator混合使用的一些讨论
|
||||
- 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的关系
|
||||
- Luke Hoban, [Async Functions for ECMAScript](https://github.com/lukehoban/ecmascript-asyncawait): Async 函数的设计思想,与 Promise、Gernerator 函数的关系
|
||||
- Jafar Husain, [Asynchronous Generators for ES7](https://github.com/jhusain/asyncgenerator): Async 函数的深入讨论
|
||||
- Nolan Lawson, [Taming the asynchronous beast with ES7](http://pouchdb.com/2015/03/05/taming-the-async-beast-with-es7.html): async 函数通俗的实例讲解
|
||||
- Jafar Husain, [Async Generators](https://docs.google.com/file/d/0B4PVbLpUIdzoMDR5dWstRllXblU/view?sle=true): 对 async 与 Generator 混合使用的一些讨论
|
||||
- Daniel Brain, [Understand promises before you start using async/await](https://medium.com/@bluepnume/learn-about-promises-before-you-start-using-async-await-eb148164a9c8): 讨论 async/await 与 Promise 的关系
|
||||
- Jake Archibald, [Async functions - making promises friendly](https://developers.google.com/web/fundamentals/getting-started/primers/async-functions)
|
||||
- Axel Rauschmayer, [ES proposal: asynchronous iteration](http://www.2ality.com/2016/10/asynchronous-iteration.html): 异步遍历器的详细介绍
|
||||
- Dima Grossman, [How to write async await without try-catch blocks in Javascript](http://blog.grossman.io/how-to-write-async-await-without-try-catch-blocks-in-javascript/): 除了 try/catch 以外的 async 函数内部捕捉错误的方法
|
||||
|
||||
## Class
|
||||
|
||||
- Sebastian Porto, [ES6 classes and JavaScript prototypes](https://reinteractive.net/posts/235-es6-classes-and-javascript-prototypes): ES6 Class的写法与ES5 Prototype的写法对比
|
||||
- Jack Franklin, [An introduction to ES6 classes](http://javascriptplayground.com/blog/2014/07/introduction-to-es6-classes-tutorial/): ES6 class的入门介绍
|
||||
- Sebastian Porto, [ES6 classes and JavaScript prototypes](https://reinteractive.net/posts/235-es6-classes-and-javascript-prototypes): ES6 Class 的写法与 ES5 Prototype 的写法对比
|
||||
- Jack Franklin, [An introduction to ES6 classes](http://javascriptplayground.com/blog/2014/07/introduction-to-es6-classes-tutorial/): ES6 class 的入门介绍
|
||||
- Axel Rauschmayer, [ECMAScript 6: new OOP features besides classes](http://www.2ality.com/2014/12/es6-oop.html)
|
||||
- Axel Rauschmayer, [Classes in ECMAScript 6 (final semantics)](http://www.2ality.com/2015/02/es6-classes-final.html): Class语法的详细介绍和设计思想分析
|
||||
- Eric Faust, [ES6 In Depth: Subclassing](https://hacks.mozilla.org/2015/08/es6-in-depth-subclassing/): Class语法的深入介绍
|
||||
- Nicolás Bevacqua, [Binding Methods to Class Instance Objects](https://ponyfoo.com/articles/binding-methods-to-class-instance-objects): 如何绑定类的实例中的this
|
||||
- Axel Rauschmayer, [Classes in ECMAScript 6 (final semantics)](http://www.2ality.com/2015/02/es6-classes-final.html): Class 语法的详细介绍和设计思想分析
|
||||
- Eric Faust, [ES6 In Depth: Subclassing](https://hacks.mozilla.org/2015/08/es6-in-depth-subclassing/): Class 语法的深入介绍
|
||||
- Nicolás Bevacqua, [Binding Methods to Class Instance Objects](https://ponyfoo.com/articles/binding-methods-to-class-instance-objects): 如何绑定类的实例中的 this
|
||||
|
||||
## Decorator
|
||||
|
||||
- Maximiliano Fierro, [Declarative vs Imperative](http://elmasse.github.io/js/decorators-bindings-es7.html): Decorators和Mixin介绍
|
||||
- Justin Fagnani, ["Real" Mixins with JavaScript Classes](http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/): 使用类的继承实现Mixin
|
||||
- Addy Osmani, [Exploring ES2016 Decorators](https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841): Decorator的深入介绍
|
||||
- Maximiliano Fierro, [Declarative vs Imperative](http://elmasse.github.io/js/decorators-bindings-es7.html): Decorators 和 Mixin 介绍
|
||||
- Justin Fagnani, ["Real" Mixins with JavaScript Classes](http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/): 使用类的继承实现 Mixin
|
||||
- Addy Osmani, [Exploring ES2016 Decorators](https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841): Decorator 的深入介绍
|
||||
- Sebastian McKenzie, [Allow decorators for functions as well](https://github.com/wycats/javascript-decorators/issues/4): 为什么修饰器不能用于函数
|
||||
- Maximiliano Fierro, [Traits with ES7 Decorators](http://cocktailjs.github.io/blog/traits-with-es7-decorators.html): Trait的用法介绍
|
||||
- Maximiliano Fierro, [Traits with ES7 Decorators](http://cocktailjs.github.io/blog/traits-with-es7-decorators.html): Trait 的用法介绍
|
||||
- Jonathan Creamer: [Using ES2016 Decorators to Publish on an Event Bus](http://jonathancreamer.com/using-es2016-decorators-to-publish-on-an-event-bus/): 使用修饰器实现自动发布事件
|
||||
|
||||
## Module
|
||||
|
||||
- Jack Franklin, [JavaScript Modules the ES6 Way](http://24ways.org/2014/javascript-modules-the-es6-way/): ES6模块入门
|
||||
- Axel Rauschmayer, [ECMAScript 6 modules: the final syntax](http://www.2ality.com/2014/09/es6-modules-final.html): ES6模块的介绍,以及与CommonJS规格的详细比较
|
||||
- Dave Herman, [Static module resolution](http://calculist.org/blog/2012/06/29/static-module-resolution/): 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模块的设计思想
|
||||
- Jack Franklin, [JavaScript Modules the ES6 Way](http://24ways.org/2014/javascript-modules-the-es6-way/): ES6 模块入门
|
||||
- Axel Rauschmayer, [ECMAScript 6 modules: the final syntax](http://www.2ality.com/2014/09/es6-modules-final.html): ES6 模块的介绍,以及与 CommonJS 规格的详细比较
|
||||
- Dave Herman, [Static module resolution](http://calculist.org/blog/2012/06/29/static-module-resolution/): 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 模块的设计思想
|
||||
- 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 模块的处理规格
|
||||
|
||||
## 二进制数组
|
||||
|
||||
- Ilmari Heikkinen, [Typed Arrays: Binary Data in the Browser](http://www.html5rocks.com/en/tutorials/webgl/typed_arrays/)
|
||||
- Khronos, [Typed Array Specification](http://www.khronos.org/registry/typedarray/specs/latest/)
|
||||
- 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)
|
||||
- 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
|
||||
|
||||
@ -224,16 +237,15 @@
|
||||
|
||||
## 工具
|
||||
|
||||
- Babel, [Babel Handbook](https://github.com/thejameskyle/babel-handbook/tree/master/translations/en): Babel的用法介绍
|
||||
- Google, [traceur-compiler](https://github.com/google/traceur-compiler): Traceur编译器
|
||||
- Babel, [Babel Handbook](https://github.com/thejameskyle/babel-handbook/tree/master/translations/en): Babel 的用法介绍
|
||||
- Google, [traceur-compiler](https://github.com/google/traceur-compiler): Traceur 编译器
|
||||
- Casper Beyer, [ECMAScript 6 Features and Tools](http://caspervonb.github.io/2014/03/05/ecmascript6-features-and-tools.html)
|
||||
- Stoyan Stefanov, [Writing ES6 today with jstransform](http://www.phpied.com/writing-es6-today-with-jstransform/)
|
||||
- ES6 Module Loader, [ES6 Module Loader Polyfill](https://github.com/ModuleLoader/es6-module-loader): 在浏览器和node.js加载ES6模块的一个库,文档里对ES6模块有详细解释
|
||||
- Paul Miller, [es6-shim](https://github.com/paulmillr/es6-shim): 一个针对老式浏览器,模拟ES6部分功能的垫片库(shim)
|
||||
- army8735, [Javascript Downcast](https://github.com/army8735/jsdc): 国产的ES6到ES5的转码器
|
||||
- esnext, [ES6 Module Transpiler](https://github.com/esnext/es6-module-transpiler):基于node.js的将ES6模块转为ES5代码的命令行工具
|
||||
- Sebastian McKenzie, [BabelJS](http://babeljs.io/): 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垫片库清单
|
||||
- Facebook, [regenerator](https://github.com/facebook/regenerator): 将Generator函数转为ES5的转码器
|
||||
|
||||
- ES6 Module Loader, [ES6 Module Loader Polyfill](https://github.com/ModuleLoader/es6-module-loader): 在浏览器和 node.js 加载 ES6 模块的一个库,文档里对 ES6 模块有详细解释
|
||||
- Paul Miller, [es6-shim](https://github.com/paulmillr/es6-shim): 一个针对老式浏览器,模拟 ES6 部分功能的垫片库(shim)
|
||||
- army8735, [Javascript Downcast](https://github.com/army8735/jsdc): 国产的 ES6 到 ES5 的转码器
|
||||
- esnext, [ES6 Module Transpiler](https://github.com/esnext/es6-module-transpiler):基于 node.js 的将 ES6 模块转为 ES5 代码的命令行工具
|
||||
- Sebastian McKenzie, [BabelJS](http://babeljs.io/): 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 垫片库清单
|
||||
- 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`之中,会自动执行所有观察者。
|
294
docs/regex.md
294
docs/regex.md
@ -1,8 +1,8 @@
|
||||
# 正则的扩展
|
||||
|
||||
## RegExp构造函数
|
||||
## RegExp 构造函数
|
||||
|
||||
在ES5中,RegExp构造函数的参数有两种情况。
|
||||
在 ES5 中,`RegExp`构造函数的参数有两种情况。
|
||||
|
||||
第一种情况是,参数是字符串,这时第二个参数表示正则表达式的修饰符(flag)。
|
||||
|
||||
@ -20,14 +20,14 @@ var regex = new RegExp(/xyz/i);
|
||||
var regex = /xyz/i;
|
||||
```
|
||||
|
||||
但是,ES5不允许此时使用第二个参数,添加修饰符,否则会报错。
|
||||
但是,ES5 不允许此时使用第二个参数添加修饰符,否则会报错。
|
||||
|
||||
```javascript
|
||||
var regex = new RegExp(/xyz/, 'i');
|
||||
// Uncaught TypeError: Cannot supply flags when constructing one RegExp from another
|
||||
```
|
||||
|
||||
ES6改变了这种行为。如果RegExp构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。
|
||||
ES6 改变了这种行为。如果`RegExp`构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。
|
||||
|
||||
```javascript
|
||||
new RegExp(/abc/ig, 'i').flags
|
||||
@ -38,33 +38,31 @@ 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.replace` 调用 `RegExp.prototype[Symbol.replace]`
|
||||
- `String.prototype.search` 调用 `RegExp.prototype[Symbol.search]`
|
||||
- `String.prototype.split` 调用 `RegExp.prototype[Symbol.split]`
|
||||
|
||||
## u修饰符
|
||||
## u 修饰符
|
||||
|
||||
ES6对正则表达式添加了`u`修饰符,含义为“Unicode模式”,用来正确处理大于`\uFFFF`的Unicode字符。也就是说,会正确处理四个字节的UTF-16编码。
|
||||
ES6 对正则表达式添加了`u`修饰符,含义为“Unicode 模式”,用来正确处理大于`\uFFFF`的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码。
|
||||
|
||||
```javascript
|
||||
/^\uD83D/u.test('\uD83D\uDC2A')
|
||||
// false
|
||||
/^\uD83D/.test('\uD83D\uDC2A')
|
||||
// true
|
||||
/^\uD83D/u.test('\uD83D\uDC2A') // false
|
||||
/^\uD83D/.test('\uD83D\uDC2A') // true
|
||||
```
|
||||
|
||||
上面代码中,`\uD83D\uDC2A`是一个四个字节的UTF-16编码,代表一个字符。但是,ES5不支持四个字节的UTF-16编码,会将其识别为两个字符,导致第二行代码结果为`true`。加了`u`修饰符以后,ES6就会识别其为一个字符,所以第一行代码结果为`false`。
|
||||
上面代码中,`\uD83D\uDC2A`是一个四个字节的 UTF-16 编码,代表一个字符。但是,ES5 不支持四个字节的 UTF-16 编码,会将其识别为两个字符,导致第二行代码结果为`true`。加了`u`修饰符以后,ES6 就会识别其为一个字符,所以第一行代码结果为`false`。
|
||||
|
||||
一旦加上`u`修饰符号,就会修改下面这些正则表达式的行为。
|
||||
|
||||
**(1)点字符**
|
||||
|
||||
点(`.`)字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码点大于`0xFFFF`的Unicode字符,点字符不能识别,必须加上`u`修饰符。
|
||||
点(`.`)字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码点大于`0xFFFF`的 Unicode 字符,点字符不能识别,必须加上`u`修饰符。
|
||||
|
||||
```javascript
|
||||
var s = '𠮷';
|
||||
@ -75,9 +73,9 @@ var s = '𠮷';
|
||||
|
||||
上面代码表示,如果不添加`u`修饰符,正则表达式就会认为字符串为两个字符,从而匹配失败。
|
||||
|
||||
**(2)Unicode字符表示法**
|
||||
**(2)Unicode 字符表示法**
|
||||
|
||||
ES6新增了使用大括号表示Unicode字符,这种表示法在正则表达式中必须加上`u`修饰符,才能识别。
|
||||
ES6 新增了使用大括号表示 Unicode 字符,这种表示法在正则表达式中必须加上`u`修饰符,才能识别当中的大括号,否则会被解读为量词。
|
||||
|
||||
```javascript
|
||||
/\u{61}/.test('a') // false
|
||||
@ -85,11 +83,11 @@ ES6新增了使用大括号表示Unicode字符,这种表示法在正则表达
|
||||
/\u{20BB7}/u.test('𠮷') // true
|
||||
```
|
||||
|
||||
上面代码表示,如果不加`u`修饰符,正则表达式无法识别`\u{61}`这种表示法,只会认为这匹配61个连续的`u`。
|
||||
上面代码表示,如果不加`u`修饰符,正则表达式无法识别`\u{61}`这种表示法,只会认为这匹配 61 个连续的`u`。
|
||||
|
||||
**(3)量词**
|
||||
|
||||
使用`u`修饰符后,所有量词都会正确识别码点大于`0xFFFF`的Unicode字符。
|
||||
使用`u`修饰符后,所有量词都会正确识别码点大于`0xFFFF`的 Unicode 字符。
|
||||
|
||||
```javascript
|
||||
/a{2}/.test('aa') // true
|
||||
@ -98,24 +96,16 @@ ES6新增了使用大括号表示Unicode字符,这种表示法在正则表达
|
||||
/𠮷{2}/u.test('𠮷𠮷') // true
|
||||
```
|
||||
|
||||
另外,只有在使用`u`修饰符的情况下,Unicode表达式当中的大括号才会被正确解读,否则会被解读为量词。
|
||||
|
||||
```javascript
|
||||
/^\u{3}$/.test('uuu') // true
|
||||
```
|
||||
|
||||
上面代码中,由于正则表达式没有`u`修饰符,所以大括号被解读为量词。加上`u`修饰符,就会被解读为Unicode表达式。
|
||||
|
||||
**(4)预定义模式**
|
||||
|
||||
`u`修饰符也影响到预定义模式,能否正确识别码点大于`0xFFFF`的Unicode字符。
|
||||
`u`修饰符也影响到预定义模式,能否正确识别码点大于`0xFFFF`的 Unicode 字符。
|
||||
|
||||
```javascript
|
||||
/^\S$/.test('𠮷') // false
|
||||
/^\S$/u.test('𠮷') // true
|
||||
```
|
||||
|
||||
上面代码的`\S`是预定义模式,匹配所有不是空格的字符。只有加了`u`修饰符,它才能正确匹配码点大于`0xFFFF`的Unicode字符。
|
||||
上面代码的`\S`是预定义模式,匹配所有不是空格的字符。只有加了`u`修饰符,它才能正确匹配码点大于`0xFFFF`的 Unicode 字符。
|
||||
|
||||
利用这一点,可以写出一个正确返回字符串长度的函数。
|
||||
|
||||
@ -131,20 +121,20 @@ s.length // 4
|
||||
codePointLength(s) // 2
|
||||
```
|
||||
|
||||
**(5)i修饰符**
|
||||
**(5)i 修饰符**
|
||||
|
||||
有些Unicode字符的编码不同,但是字型很相近,比如,`\u004B`与`\u212A`都是大写的`K`。
|
||||
有些 Unicode 字符的编码不同,但是字型很相近,比如,`\u004B`与`\u212A`都是大写的`K`。
|
||||
|
||||
```javascript
|
||||
/[a-z]/i.test('\u212A') // false
|
||||
/[a-z]/iu.test('\u212A') // true
|
||||
```
|
||||
|
||||
上面代码中,不加`u`修饰符,就无法识别非规范的K字符。
|
||||
上面代码中,不加`u`修饰符,就无法识别非规范的`K`字符。
|
||||
|
||||
## y 修饰符
|
||||
|
||||
除了`u`修饰符,ES6还为正则表达式添加了`y`修饰符,叫做“粘连”(sticky)修饰符。
|
||||
除了`u`修饰符,ES6 还为正则表达式添加了`y`修饰符,叫做“粘连”(sticky)修饰符。
|
||||
|
||||
`y`修饰符的作用与`g`修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,`g`修饰符只要剩余位置中存在匹配就可,而`y`修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。
|
||||
|
||||
@ -217,7 +207,7 @@ match.index // 3
|
||||
REGEX.lastIndex // 4
|
||||
```
|
||||
|
||||
进一步说,`y`修饰符号隐含了头部匹配的标志`^`。
|
||||
实际上,`y`修饰符号隐含了头部匹配的标志`^`。
|
||||
|
||||
```javascript
|
||||
/b/y.exec('aba')
|
||||
@ -226,28 +216,6 @@ REGEX.lastIndex // 4
|
||||
|
||||
上面代码由于不能保证头部匹配,所以返回`null`。`y`修饰符的设计本意,就是让头部匹配的标志`^`在全局匹配中都有效。
|
||||
|
||||
在`split`方法中使用`y`修饰符,原字符串必须以分隔符开头。这也意味着,只要匹配成功,数组的第一个成员肯定是空字符串。
|
||||
|
||||
```javascript
|
||||
// 没有找到匹配
|
||||
'x##'.split(/#/y)
|
||||
// [ 'x##' ]
|
||||
|
||||
// 找到两个匹配
|
||||
'##x'.split(/#/y)
|
||||
// [ '', '', 'x' ]
|
||||
```
|
||||
|
||||
后续的分隔符只有紧跟前面的分隔符,才会被识别。
|
||||
|
||||
```javascript
|
||||
'#x#'.split(/#/y)
|
||||
// [ '', 'x#' ]
|
||||
|
||||
'##'.split(/#/y)
|
||||
// [ '', '', '' ]
|
||||
```
|
||||
|
||||
下面是字符串对象的`replace`方法的例子。
|
||||
|
||||
```javascript
|
||||
@ -255,7 +223,7 @@ const REGEX = /a/gy;
|
||||
'aaxa'.replace(REGEX, '-') // '--xa'
|
||||
```
|
||||
|
||||
上面代码中,最后一个`a`因为不是出现下一次匹配的头部,所以不会被替换。
|
||||
上面代码中,最后一个`a`因为不是出现在下一次匹配的头部,所以不会被替换。
|
||||
|
||||
单单一个`y`修饰符对`match`方法,只能返回第一个匹配,必须与`g`修饰符联用,才能返回所有匹配。
|
||||
|
||||
@ -264,7 +232,7 @@ const REGEX = /a/gy;
|
||||
'a1a2a3'.match(/a\d/gy) // ["a1", "a2", "a3"]
|
||||
```
|
||||
|
||||
`y`修饰符的一个应用,是从字符串提取token(词元),`y`修饰符确保了匹配之间不会有漏掉的字符。
|
||||
`y`修饰符的一个应用,是从字符串提取 token(词元),`y`修饰符确保了匹配之间不会有漏掉的字符。
|
||||
|
||||
```javascript
|
||||
const TOKEN_Y = /\s*(\+|[0-9]+)\s*/y;
|
||||
@ -296,76 +264,31 @@ tokenize(TOKEN_G, '3x + 4')
|
||||
|
||||
上面代码中,`g`修饰符会忽略非法字符,而`y`修饰符不会,这样就很容易发现错误。
|
||||
|
||||
## sticky属性
|
||||
## sticky 属性
|
||||
|
||||
与`y`修饰符相匹配,ES6的正则对象多了`sticky`属性,表示是否设置了`y`修饰符。
|
||||
与`y`修饰符相匹配,ES6 的正则对象多了`sticky`属性,表示是否设置了`y`修饰符。
|
||||
|
||||
```javascript
|
||||
var r = /hello\d/y;
|
||||
r.sticky // true
|
||||
```
|
||||
|
||||
## flags属性
|
||||
## flags 属性
|
||||
|
||||
ES6为正则表达式新增了`flags`属性,会返回正则表达式的修饰符。
|
||||
ES6 为正则表达式新增了`flags`属性,会返回正则表达式的修饰符。
|
||||
|
||||
```javascript
|
||||
// ES5的source属性
|
||||
// ES5 的 source 属性
|
||||
// 返回正则表达式的正文
|
||||
/abc/ig.source
|
||||
// "abc"
|
||||
|
||||
// ES6的flags属性
|
||||
// ES6 的 flags 属性
|
||||
// 返回正则表达式的修饰符
|
||||
/abc/ig.flags
|
||||
// 'gi'
|
||||
```
|
||||
|
||||
## RegExp.escape()
|
||||
|
||||
字符串必须转义,才能作为正则模式。
|
||||
|
||||
```javascript
|
||||
function escapeRegExp(str) {
|
||||
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
|
||||
}
|
||||
|
||||
let str = '/path/to/resource.html?search=query';
|
||||
escapeRegExp(str)
|
||||
// "\/path\/to\/resource\.html\?search=query"
|
||||
```
|
||||
|
||||
上面代码中,`str`是一个正常字符串,必须使用反斜杠对其中的特殊字符转义,才能用来作为一个正则匹配的模式。
|
||||
|
||||
已经有[提议](https://esdiscuss.org/topic/regexp-escape)将这个需求标准化,作为RegExp对象的静态方法[RegExp.escape()](https://github.com/benjamingr/RexExp.escape),放入ES7。2015年7月31日,TC39认为,这个方法有安全风险,又不愿这个方法变得过于复杂,没有同意将其列入ES7,但这不失为一个真实的需求。
|
||||
|
||||
```javascript
|
||||
RegExp.escape('The Quick Brown Fox');
|
||||
// "The Quick Brown Fox"
|
||||
|
||||
RegExp.escape('Buy it. use it. break it. fix it.');
|
||||
// "Buy it\. use it\. break it\. fix it\."
|
||||
|
||||
RegExp.escape('(*.*)');
|
||||
// "\(\*\.\*\)"
|
||||
```
|
||||
|
||||
字符串转义以后,可以使用RegExp构造函数生成正则模式。
|
||||
|
||||
```javascript
|
||||
var str = 'hello. how are you?';
|
||||
var regex = new RegExp(RegExp.escape(str), 'g');
|
||||
assert.equal(String(regex), '/hello\. how are you\?/g');
|
||||
```
|
||||
|
||||
目前,该方法可以用上文的`escapeRegExp`函数或者垫片模块[regexp.escape](https://github.com/ljharb/regexp.escape)实现。
|
||||
|
||||
```javascript
|
||||
var escape = require('regexp.escape');
|
||||
escape('hi. how are you?');
|
||||
// "hi\\. how are you\\?"
|
||||
```
|
||||
|
||||
## s 修饰符:dotAll 模式
|
||||
|
||||
正则表达式中,点(`.`)是一个特殊字符,代表任意的单个字符,但是行终止符(line terminator character)除外。
|
||||
@ -413,9 +336,7 @@ re.flags // 's'
|
||||
|
||||
## 后行断言
|
||||
|
||||
JavaScript语言的正则表达式,只支持先行断言(lookahead)和先行否定断言(negative lookahead),不支持后行断言(lookbehind)和后行否定断言(negative lookbehind)。
|
||||
|
||||
目前,有一个[提案](https://github.com/goyakin/es-regexp-lookbehind),在ES7加入后行断言。V8引擎4.9版已经支持,Chrome浏览器49版打开”experimental JavaScript features“开关(地址栏键入`about:flags`),就可以使用这项功能。
|
||||
JavaScript 语言的正则表达式,只支持先行断言(lookahead)和先行否定断言(negative lookahead),不支持后行断言(lookbehind)和后行否定断言(negative lookbehind)。目前,有一个[提案](https://github.com/goyakin/es-regexp-lookbehind),引入后行断言,V8 引擎 4.9 版已经支持。
|
||||
|
||||
”先行断言“指的是,`x`只有在`y`前面才匹配,必须写成`/x(?=y)/`。比如,只匹配百分号之前的数字,要写成`/\d+(?=%)/`。”先行否定断言“指的是,`x`只有不在`y`前面才匹配,必须写成`/x(?!y)/`。比如,只匹配不在百分号之前的数字,要写成`/\d+(?!%)/`。
|
||||
|
||||
@ -424,18 +345,28 @@ JavaScript语言的正则表达式,只支持先行断言(lookahead)和先
|
||||
/\d+(?!%)/.exec('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
|
||||
/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill') // ["100"]
|
||||
/(?<!\$)\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,20 +384,20 @@ JavaScript语言的正则表达式,只支持先行断言(lookahead)和先
|
||||
/(?<=\1d(o))r/.exec('hodor') // ["r", "o"]
|
||||
```
|
||||
|
||||
上面代码中,如果后行断言的反斜杠引用(`\1`)放在括号的后面,就不会得到匹配结果,必须放在前面才可以。
|
||||
上面代码中,如果后行断言的反斜杠引用(`\1`)放在括号的后面,就不会得到匹配结果,必须放在前面才可以。因为后行断言是先从左到右扫描,发现匹配以后再回过头,从右到左完成反斜杠引用。
|
||||
|
||||
## Unicode属性类
|
||||
## Unicode 属性类
|
||||
|
||||
目前,有一个[提案](https://github.com/mathiasbynens/es-regexp-unicode-property-escapes),引入了一种新的类的写法`\p{...}`和`\P{...}`,允许正则表达式匹配符合Unicode某种属性的所有字符。
|
||||
目前,有一个[提案](https://github.com/mathiasbynens/es-regexp-unicode-property-escapes),引入了一种新的类的写法`\p{...}`和`\P{...}`,允许正则表达式匹配符合 Unicode 某种属性的所有字符。
|
||||
|
||||
```javascript
|
||||
const regexGreekSymbol = /\p{Script=Greek}/u;
|
||||
regexGreekSymbol.test('π') // u
|
||||
regexGreekSymbol.test('π') // true
|
||||
```
|
||||
|
||||
上面代码中,`\p{Script=Greek}`指定匹配一个希腊文字母,所以匹配`π`成功。
|
||||
|
||||
Unicode属性类要指定属性名和属性值。
|
||||
Unicode 属性类要指定属性名和属性值。
|
||||
|
||||
```javascript
|
||||
\p{UnicodePropertyName=UnicodePropertyValue}
|
||||
@ -480,9 +411,9 @@ Unicode属性类要指定属性名和属性值。
|
||||
|
||||
`\P{…}`是`\p{…}`的反向匹配,即匹配不满足条件的字符。
|
||||
|
||||
注意,这两种类只对Unicode有效,所以使用的时候一定要加上`u`修饰符。如果不加`u`修饰符,正则表达式使用`\p`和`\P`会报错,ECMAScript预留了这两个类。
|
||||
注意,这两种类只对 Unicode 有效,所以使用的时候一定要加上`u`修饰符。如果不加`u`修饰符,正则表达式使用`\p`和`\P`会报错,ECMAScript 预留了这两个类。
|
||||
|
||||
由于Unicode的各种属性非常多,所以这种新的类的表达能力非常强。
|
||||
由于 Unicode 的各种属性非常多,所以这种新的类的表达能力非常强。
|
||||
|
||||
```javascript
|
||||
const regex = /^\p{Decimal_Number}+$/u;
|
||||
@ -504,14 +435,127 @@ regex.test('ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ') // true
|
||||
下面是其他一些例子。
|
||||
|
||||
```javascript
|
||||
// 匹配各种文字的所有字母,等同于Unicode版的\w
|
||||
// 匹配各种文字的所有字母,等同于 Unicode 版的 \w
|
||||
[\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]
|
||||
|
||||
// 匹配各种文字的所有非字母的字符,等同于Unicode版的\W
|
||||
[\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]
|
||||
// 匹配各种文字的所有非字母的字符,等同于 Unicode 版的 \W
|
||||
[^\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]
|
||||
|
||||
// 匹配所有的箭头字符
|
||||
const regexArrows = /^\p{Block=Arrows}+$/u;
|
||||
regexArrows.test('←↑→↓↔↕↖↗↘↙⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇧⇩') // true
|
||||
```
|
||||
|
||||
## 具名组匹配
|
||||
|
||||
### 简介
|
||||
|
||||
正则表达式使用圆括号进行组匹配。
|
||||
|
||||
```javascript
|
||||
const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;
|
||||
```
|
||||
|
||||
上面代码中,正则表达式里面有三组圆括号。使用`exec`方法,就可以将这三组匹配结果提取出来。
|
||||
|
||||
```javascript
|
||||
const matchObj = RE_DATE.exec('1999-12-31');
|
||||
const year = matchObj[1]; // 1999
|
||||
const month = matchObj[2]; // 12
|
||||
const day = matchObj[3]; // 31
|
||||
```
|
||||
|
||||
组匹配的一个问题是,每一组的匹配含义不容易看出来,而且只能用数字序号引用,要是组的顺序变了,引用的时候就必须修改序号。
|
||||
|
||||
现在有一个“具名组匹配”(Named Capture Groups)的[提案](https://github.com/tc39/proposal-regexp-named-groups),允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用。
|
||||
|
||||
```javascript
|
||||
const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
|
||||
|
||||
const matchObj = RE_DATE.exec('1999-12-31');
|
||||
const year = matchObj.groups.year; // 1999
|
||||
const month = matchObj.groups.month; // 12
|
||||
const day = matchObj.groups.day; // 31
|
||||
```
|
||||
|
||||
上面代码中,“具名组匹配”在圆括号内部,模式的头部添加“问号 + 尖括号 + 组名”(`?<year>`),然后就可以在`exec`方法返回结果的`groups`属性上引用该组名。同时,数字序号(`matchObj[1]`)依然有效。
|
||||
|
||||
具名组匹配等于为每一组匹配加上了 ID,便于描述匹配的目的。如果组的顺序变了,也不用改变匹配后的处理代码。
|
||||
|
||||
如果具名组没有匹配,那么对应的`groups`对象属性会是`undefined`。
|
||||
|
||||
```javascript
|
||||
const RE_OPT_A = /^(?<as>a+)?$/;
|
||||
const matchObj = RE_OPT_A.exec('');
|
||||
|
||||
matchObj.groups.as // undefined
|
||||
'as' in matchObj.groups // true
|
||||
```
|
||||
|
||||
上面代码中,具名组`as`没有找到匹配,那么`matchObj.groups.as`属性值就是`undefined`,并且`as`这个键名在`groups`是始终存在的。
|
||||
|
||||
### 解构赋值和替换
|
||||
|
||||
有了具名组匹配以后,可以使用解构赋值直接从匹配结果上为变量赋值。
|
||||
|
||||
```javascript
|
||||
let {groups: {one, two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar');
|
||||
one // foo
|
||||
two // bar
|
||||
```
|
||||
|
||||
字符串替换时,使用`$<组名>`引用具名组。
|
||||
|
||||
```javascript
|
||||
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
|
||||
|
||||
'2015-01-02'.replace(re, '$<day>/$<month>/$<year>')
|
||||
// '02/01/2015'
|
||||
```
|
||||
|
||||
上面代码中,`replace`方法的第二个参数是一个字符串,而不是正则表达式。
|
||||
|
||||
`replace`方法的第二个参数也可以是函数,该函数的参数序列如下。
|
||||
|
||||
```javascript
|
||||
'2015-01-02'.replace(re, (
|
||||
matched, // 整个匹配结果 2015-01-02
|
||||
capture1, // 第一个组匹配 2015
|
||||
capture2, // 第二个组匹配 01
|
||||
capture3, // 第三个组匹配 02
|
||||
position, // 匹配开始的位置 0
|
||||
S, // 原字符串 2015-01-02
|
||||
groups // 具名组构成的一个对象 {year, month, day}
|
||||
) => {
|
||||
let {day, month, year} = args[args.length - 1];
|
||||
return `${day}/${month}/${year}`;
|
||||
});
|
||||
```
|
||||
|
||||
具名组匹配在原来的基础上,新增了最后一个函数参数:具名组构成的一个对象。函数内部可以直接对这个对象进行解构赋值。
|
||||
|
||||
### 引用
|
||||
|
||||
如果要在正则表达式内部引用某个“具名组匹配”,可以使用`\k<组名>`的写法。
|
||||
|
||||
```javascript
|
||||
const RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/;
|
||||
RE_TWICE.test('abc!abc') // true
|
||||
RE_TWICE.test('abc!ab') // false
|
||||
```
|
||||
|
||||
数字引用(`\1`)依然有效。
|
||||
|
||||
```javascript
|
||||
const RE_TWICE = /^(?<word>[a-z]+)!\1$/;
|
||||
RE_TWICE.test('abc!abc') // true
|
||||
RE_TWICE.test('abc!ab') // false
|
||||
```
|
||||
|
||||
这两种引用语法还可以同时使用。
|
||||
|
||||
```javascript
|
||||
const RE_TWICE = /^(?<word>[a-z]+)!\k<word>!\1$/;
|
||||
RE_TWICE.test('abc!abc!abc') // true
|
||||
RE_TWICE.test('abc!abc!ab') // false
|
||||
```
|
||||
|
557
docs/set-map.md
557
docs/set-map.md
File diff suppressed because it is too large
Load Diff
151
docs/simd.md
151
docs/simd.md
@ -2,11 +2,11 @@
|
||||
|
||||
## 概述
|
||||
|
||||
SIMD(发音`/sim-dee/`)是“Single Instruction/Multiple Data”的缩写,意为“单指令,多数据”。它是JavaScript操作CPU对应指令的接口,你可以看做这是一种不同的运算执行模式。与它相对的是SISD(“Single Instruction/Single Data”),即“单指令,单数据”。
|
||||
SIMD(发音`/sim-dee/`)是“Single Instruction/Multiple Data”的缩写,意为“单指令,多数据”。它是 JavaScript 操作 CPU 对应指令的接口,你可以看做这是一种不同的运算执行模式。与它相对的是 SISD(“Single Instruction/Single Data”),即“单指令,单数据”。
|
||||
|
||||
SIMD的含义是使用一个指令,完成多个数据的运算;SISD的含义是使用一个指令,完成单个数据的运算,这是JavaScript的默认运算模式。显而易见,SIMD的执行效率要高于SISD,所以被广泛用于3D图形运算、物理模拟等运算量超大的项目之中。
|
||||
SIMD 的含义是使用一个指令,完成多个数据的运算;SISD 的含义是使用一个指令,完成单个数据的运算,这是 JavaScript 的默认运算模式。显而易见,SIMD 的执行效率要高于 SISD,所以被广泛用于 3D 图形运算、物理模拟等运算量超大的项目之中。
|
||||
|
||||
为了理解SIMD,请看下面的例子。
|
||||
为了理解 SIMD,请看下面的例子。
|
||||
|
||||
```javascript
|
||||
var a = [1, 2, 3, 4];
|
||||
@ -20,9 +20,9 @@ c[3] = a[3] + b[3];
|
||||
c // Array[6, 8, 10, 12]
|
||||
```
|
||||
|
||||
上面代码中,数组`a`和`b`的对应成员相加,结果放入数组`c`。它的运算模式是依次处理每个数组成员,一共有四个数组成员,所以需要运算4次。
|
||||
上面代码中,数组`a`和`b`的对应成员相加,结果放入数组`c`。它的运算模式是依次处理每个数组成员,一共有四个数组成员,所以需要运算 4 次。
|
||||
|
||||
如果采用SIMD模式,只要运算一次就够了。
|
||||
如果采用 SIMD 模式,只要运算一次就够了。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Float32x4(1, 2, 3, 4);
|
||||
@ -30,54 +30,54 @@ var b = SIMD.Float32x4(5, 6, 7, 8);
|
||||
var c = SIMD.Float32x4.add(a, b); // Float32x4[6, 8, 10, 12]
|
||||
```
|
||||
|
||||
上面代码之中,数组`a`和`b`的四个成员的各自相加,只用一条指令就完成了。因此,速度比上一种写法提高了4倍。
|
||||
上面代码之中,数组`a`和`b`的四个成员的各自相加,只用一条指令就完成了。因此,速度比上一种写法提高了 4 倍。
|
||||
|
||||
一次SIMD运算,可以处理多个数据,这些数据被称为“通道”(lane)。上面代码中,一次运算了四个数据,因此就是四个通道。
|
||||
一次 SIMD 运算,可以处理多个数据,这些数据被称为“通道”(lane)。上面代码中,一次运算了四个数据,因此就是四个通道。
|
||||
|
||||
SIMD通常用于矢量运算。
|
||||
SIMD 通常用于矢量运算。
|
||||
|
||||
```javascript
|
||||
v + w = 〈v1, …, vn〉+ 〈w1, …, wn〉
|
||||
= 〈v1+w1, …, vn+wn〉
|
||||
```
|
||||
|
||||
上面代码中,`v`和`w`是两个多元矢量。它们的加运算,在SIMD下是一个指令、而不是n个指令完成的,这就大大提高了效率。这对于3D动画、图像处理、信号处理、数值处理、加密等运算是非常重要的。比如,Canvas的`getImageData()`会将图像文件读成一个二进制数组,SIMD就很适合对于这种数组的处理。
|
||||
上面代码中,`v`和`w`是两个多元矢量。它们的加运算,在 SIMD 下是一个指令、而不是 n 个指令完成的,这就大大提高了效率。这对于 3D 动画、图像处理、信号处理、数值处理、加密等运算是非常重要的。比如,Canvas 的`getImageData()`会将图像文件读成一个二进制数组,SIMD 就很适合对于这种数组的处理。
|
||||
|
||||
总得来说,SIMD是数据并行处理(parallelism)的一种手段,可以加速一些运算密集型操作的速度。
|
||||
总的来说,SIMD 是数据并行处理(parallelism)的一种手段,可以加速一些运算密集型操作的速度。将来与 WebAssembly 结合以后,可以让 JavaScript 达到二进制代码的运行速度。
|
||||
|
||||
## 数据类型
|
||||
|
||||
SIMD提供12种数据类型,总长度都是128个二进制位。
|
||||
SIMD 提供 12 种数据类型,总长度都是 128 个二进制位。
|
||||
|
||||
- Float32x4:四个32位浮点数
|
||||
- Float64x2:两个64位浮点数
|
||||
- Int32x4:四个32位整数
|
||||
- Int16x8:八个16位整数
|
||||
- Int8x16:十六个8位整数
|
||||
- Uint32x4:四个无符号的32位整数
|
||||
- Uint16x8:八个无符号的16位整数
|
||||
- Uint8x16:十六个无符号的8位整数
|
||||
- Bool32x4:四个32位布尔值
|
||||
- Bool16x8:八个16位布尔值
|
||||
- Bool8x16:十六个8位布尔值
|
||||
- Bool64x2:两个64位布尔值
|
||||
- Float32x4:四个 32 位浮点数
|
||||
- Float64x2:两个 64 位浮点数
|
||||
- Int32x4:四个 32 位整数
|
||||
- Int16x8:八个 16 位整数
|
||||
- Int8x16:十六个 8 位整数
|
||||
- Uint32x4:四个无符号的 32 位整数
|
||||
- Uint16x8:八个无符号的 16 位整数
|
||||
- Uint8x16:十六个无符号的 8 位整数
|
||||
- Bool32x4:四个 32 位布尔值
|
||||
- Bool16x8:八个 16 位布尔值
|
||||
- Bool8x16:十六个 8 位布尔值
|
||||
- Bool64x2:两个 64 位布尔值
|
||||
|
||||
每种数据类型被`x`符号分隔成两部分,后面的部分表示通道数,前面的部分表示每个通道的宽度和类型。比如,`Float32x4`就表示这个值有4个通道,每个通道是一个32位浮点数。
|
||||
每种数据类型被`x`符号分隔成两部分,后面的部分表示通道数,前面的部分表示每个通道的宽度和类型。比如,`Float32x4`就表示这个值有 4 个通道,每个通道是一个 32 位浮点数。
|
||||
|
||||
每个通道之中,可以放置四种数据。
|
||||
|
||||
- 浮点数(float,比如1.0)
|
||||
- 浮点数(float,比如 1.0)
|
||||
- 带符号的整数(Int,比如-1)
|
||||
- 无符号的整数(Uint,比如1)
|
||||
- 无符号的整数(Uint,比如 1)
|
||||
- 布尔值(Bool,包含`true`和`false`两种值)
|
||||
|
||||
每种SIMD的数据类型都是一个函数方法,可以传入参数,生成对应的值。
|
||||
每种 SIMD 的数据类型都是一个函数方法,可以传入参数,生成对应的值。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Float32x4(1.0, 2.0, 3.0, 4.0);
|
||||
```
|
||||
|
||||
上面代码中,变量`a`就是一个128位、包含四个32位浮点数(即四个通道)的值。
|
||||
上面代码中,变量`a`就是一个 128 位、包含四个 32 位浮点数(即四个通道)的值。
|
||||
|
||||
注意,这些数据类型方法都不是构造函数,前面不能加`new`,否则会报错。
|
||||
|
||||
@ -92,7 +92,7 @@ var v = new SIMD.Float32x4(0, 1, 2, 3);
|
||||
|
||||
### SIMD.%type%.abs(),SIMD.%type%.neg()
|
||||
|
||||
`abs`方法接受一个SIMD值作为参数,将它的每个通道都转成绝对值,作为一个新的SIMD值返回。
|
||||
`abs`方法接受一个 SIMD 值作为参数,将它的每个通道都转成绝对值,作为一个新的 SIMD 值返回。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Float32x4(-1, -2, 0, NaN);
|
||||
@ -100,7 +100,7 @@ SIMD.Float32x4.abs(a)
|
||||
// Float32x4[1, 2, 0, NaN]
|
||||
```
|
||||
|
||||
`neg`方法接受一个SIMD值作为参数,将它的每个通道都转成负值,作为一个新的SIMD值返回。
|
||||
`neg`方法接受一个 SIMD 值作为参数,将它的每个通道都转成负值,作为一个新的 SIMD 值返回。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Float32x4(-1, -2, 3, 0);
|
||||
@ -114,7 +114,7 @@ SIMD.Float64x2.neg(b)
|
||||
|
||||
### SIMD.%type%.add(),SIMD.%type%.addSaturate()
|
||||
|
||||
`add`方法接受两个SIMD值作为参数,将它们的每个通道相加,作为一个新的SIMD值返回。
|
||||
`add`方法接受两个 SIMD 值作为参数,将它们的每个通道相加,作为一个新的 SIMD 值返回。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Float32x4(1.0, 2.0, 3.0, 4.0);
|
||||
@ -122,7 +122,7 @@ var b = SIMD.Float32x4(5.0, 10.0, 15.0, 20.0);
|
||||
var c = SIMD.Float32x4.add(a, b);
|
||||
```
|
||||
|
||||
上面代码中,经过加法运算,新的SIMD值为`(6.0, 12.0, 18.0. 24.0)`。
|
||||
上面代码中,经过加法运算,新的 SIMD 值为`(6.0, 12.0, 18.0. 24.0)`。
|
||||
|
||||
`addSaturate`方法跟`add`方法的作用相同,都是两个通道相加,但是溢出的处理不一致。对于`add`方法,如果两个值相加发生溢出,溢出的二进制位会被丢弃; `addSaturate`方法则是返回该数据类型的最大值。
|
||||
|
||||
@ -138,13 +138,13 @@ SIMD.Int16x8.addSaturate(c, d);
|
||||
// Int16x8[32766, 32767, 32767, 32767, 2, 2, 2, 2]
|
||||
```
|
||||
|
||||
上面代码中,`Uint16`的最大值是65535,`Int16`的最大值是32767。一旦发生溢出,就返回这两个值。
|
||||
上面代码中,`Uint16`的最大值是 65535,`Int16`的最大值是 32767。一旦发生溢出,就返回这两个值。
|
||||
|
||||
注意,`Uint32x4`和`Int32x4`这两种数据类型没有`addSaturate`方法。
|
||||
|
||||
### SIMD.%type%.sub(),SIMD.%type%.subSaturate()
|
||||
|
||||
`sub`方法接受两个SIMD值作为参数,将它们的每个通道相减,作为一个新的SIMD值返回。
|
||||
`sub`方法接受两个 SIMD 值作为参数,将它们的每个通道相减,作为一个新的 SIMD 值返回。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Float32x4(-1, -2, 3, 4);
|
||||
@ -167,11 +167,11 @@ SIMD.Int16x8.subSaturate(c, d)
|
||||
// Int16x8[-32768, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
```
|
||||
|
||||
上面代码中,`Uint16`的最小值是`0`,`subSaturate`的最小值是`-32678`。一旦运算发生溢出,就返回最小值。
|
||||
上面代码中,`Uint16`的最小值是`0`,`Int16`的最小值是`-32678`。一旦运算发生溢出,就返回最小值。
|
||||
|
||||
### SIMD.%type%.mul(),SIMD.%type%.div(),SIMD.%type%.sqrt()
|
||||
|
||||
`mul`方法接受两个SIMD值作为参数,将它们的每个通道相乘,作为一个新的SIMD值返回。
|
||||
`mul`方法接受两个 SIMD 值作为参数,将它们的每个通道相乘,作为一个新的 SIMD 值返回。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Float32x4(-1, -2, 3, 4);
|
||||
@ -180,7 +180,7 @@ SIMD.Float32x4.mul(a, b)
|
||||
// Float32x4[-3, -6, 9, 12]
|
||||
```
|
||||
|
||||
`div`方法接受两个SIMD值作为参数,将它们的每个通道相除,作为一个新的SIMD值返回。
|
||||
`div`方法接受两个 SIMD 值作为参数,将它们的每个通道相除,作为一个新的 SIMD 值返回。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Float32x4(2, 2, 2, 2);
|
||||
@ -189,7 +189,7 @@ SIMD.Float32x4.div(a, b)
|
||||
// Float32x4[0.5, 0.5, 0.5, 0.5]
|
||||
```
|
||||
|
||||
`sqrt`方法接受一个SIMD值作为参数,求出每个通道的平方根,作为一个新的SIMD值返回。
|
||||
`sqrt`方法接受一个 SIMD 值作为参数,求出每个通道的平方根,作为一个新的 SIMD 值返回。
|
||||
|
||||
```javascript
|
||||
var b = SIMD.Float64x2(4, 8);
|
||||
@ -199,7 +199,7 @@ SIMD.Float64x2.sqrt(b)
|
||||
|
||||
### SIMD.%FloatType%.reciprocalApproximation(),SIMD.%type%.reciprocalSqrtApproximation()
|
||||
|
||||
`reciprocalApproximation`方法接受一个SIMD值作为参数,求出每个通道的倒数(`1 / x`),作为一个新的SIMD值返回。
|
||||
`reciprocalApproximation`方法接受一个 SIMD 值作为参数,求出每个通道的倒数(`1 / x`),作为一个新的 SIMD 值返回。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Float32x4(1, 2, 3, 4);
|
||||
@ -207,7 +207,7 @@ SIMD.Float32x4.reciprocalApproximation(a);
|
||||
// Float32x4[1, 0.5, 0.3333333432674408, 0.25]
|
||||
```
|
||||
|
||||
`reciprocalSqrtApproximation`方法接受一个SIMD值作为参数,求出每个通道的平方根的倒数(`1 / (x^0.5)`),作为一个新的SIMD值返回。
|
||||
`reciprocalSqrtApproximation`方法接受一个 SIMD 值作为参数,求出每个通道的平方根的倒数(`1 / (x^0.5)`),作为一个新的 SIMD 值返回。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Float32x4(1, 2, 3, 4);
|
||||
@ -219,7 +219,7 @@ SIMD.Float32x4.reciprocalSqrtApproximation(a)
|
||||
|
||||
### SIMD.%IntegerType%.shiftLeftByScalar()
|
||||
|
||||
`shiftLeftByScalar`方法接受一个SIMD值作为参数,然后将每个通道的值左移指定的位数,作为一个新的SIMD值返回。
|
||||
`shiftLeftByScalar`方法接受一个 SIMD 值作为参数,然后将每个通道的值左移指定的位数,作为一个新的 SIMD 值返回。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Int32x4(1, 2, 4, 8);
|
||||
@ -239,7 +239,7 @@ var jx4 = SIMD.Int32x4.shiftLeftByScalar(ix4, 32);
|
||||
|
||||
### SIMD.%IntegerType%.shiftRightByScalar()
|
||||
|
||||
`shiftRightByScalar`方法接受一个SIMD值作为参数,然后将每个通道的值右移指定的位数,返回一个新的SIMD值。
|
||||
`shiftRightByScalar`方法接受一个 SIMD 值作为参数,然后将每个通道的值右移指定的位数,返回一个新的 SIMD 值。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Int32x4(1, 2, 4, -8);
|
||||
@ -255,7 +255,7 @@ SIMD.Uint32x4.shiftRightByScalar(a, 1);
|
||||
// Uint32x4[0, 1, 2, 2147483644]
|
||||
```
|
||||
|
||||
上面代码中,`-8`右移一位变成了`2147483644`,是因为对于32位无符号整数来说,`-8`的二进制形式是`11111111111111111111111111111000`,右移一位就变成了`01111111111111111111111111111100`,相当于`2147483644`。
|
||||
上面代码中,`-8`右移一位变成了`2147483644`,是因为对于 32 位无符号整数来说,`-8`的二进制形式是`11111111111111111111111111111000`,右移一位就变成了`01111111111111111111111111111100`,相当于`2147483644`。
|
||||
|
||||
注意,只有整数的数据类型才有这个方法。
|
||||
|
||||
@ -263,7 +263,7 @@ SIMD.Uint32x4.shiftRightByScalar(a, 1);
|
||||
|
||||
### SIMD.%type%.check()
|
||||
|
||||
`check`方法用于检查一个值是否为当前类型的SIMD值。如果是的,就返回这个值,否则就报错。
|
||||
`check`方法用于检查一个值是否为当前类型的 SIMD 值。如果是的,就返回这个值,否则就报错。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Float32x4(1, 2, 3, 9);
|
||||
@ -278,14 +278,14 @@ SIMD.Int32x4.check('hello world') // 报错
|
||||
|
||||
### SIMD.%type%.extractLane(),SIMD.%type%.replaceLane()
|
||||
|
||||
`extractLane`方法用于返回给定通道的值。它接受两个参数,分别是SIMD值和通道编号。
|
||||
`extractLane`方法用于返回给定通道的值。它接受两个参数,分别是 SIMD 值和通道编号。
|
||||
|
||||
```javascript
|
||||
var t = SIMD.Float32x4(1, 2, 3, 4);
|
||||
SIMD.Float32x4.extractLane(t, 2) // 3
|
||||
```
|
||||
|
||||
`replaceLane`方法用于替换指定通道的值,并返回一个新的SIMD值。它接受三个参数,分别是原来的SIMD值、通道编号和新的通道值。
|
||||
`replaceLane`方法用于替换指定通道的值,并返回一个新的 SIMD 值。它接受三个参数,分别是原来的 SIMD 值、通道编号和新的通道值。
|
||||
|
||||
```javascript
|
||||
var t = SIMD.Float32x4(1, 2, 3, 4);
|
||||
@ -295,7 +295,7 @@ SIMD.Float32x4.replaceLane(t, 2, 42)
|
||||
|
||||
### SIMD.%type%.load()
|
||||
|
||||
`load`方法用于从二进制数组读入数据,生成一个新的SIMD值。
|
||||
`load`方法用于从二进制数组读入数据,生成一个新的 SIMD 值。
|
||||
|
||||
```javascript
|
||||
var a = new Int32Array([1,2,3,4,5,6,7,8]);
|
||||
@ -307,7 +307,7 @@ SIMD.Int32x4.load(a, 2);
|
||||
// Int32x4[3, 4, 5, 6]
|
||||
```
|
||||
|
||||
`load`方法接受两个参数:一个二进制数组和开始读取的位置(从0开始)。如果位置不合法(比如`-1`或者超出二进制数组的大小),就会抛出一个错误。
|
||||
`load`方法接受两个参数:一个二进制数组和开始读取的位置(从 0 开始)。如果位置不合法(比如`-1`或者超出二进制数组的大小),就会抛出一个错误。
|
||||
|
||||
这个方法还有三个变种`load1()`、`load2()`、`load3()`,表示从指定位置开始,只加载一个通道、二个通道、三个通道的值。
|
||||
|
||||
@ -330,7 +330,7 @@ SIMD.Int32x4.load3(a, 0);
|
||||
|
||||
### SIMD.%type%.store()
|
||||
|
||||
`store`方法用于将一个SIMD值,写入一个二进制数组。它接受三个参数,分别是二进制数组、开始写入的数组位置、SIMD值。它返回写入值以后的二进制数组。
|
||||
`store`方法用于将一个 SIMD 值,写入一个二进制数组。它接受三个参数,分别是二进制数组、开始写入的数组位置、SIMD 值。它返回写入值以后的二进制数组。
|
||||
|
||||
```javascript
|
||||
var t1 = new Int32Array(8);
|
||||
@ -344,7 +344,7 @@ SIMD.Int32x4.store(t2, 2, v2)
|
||||
// Int32Array[0, 0, 1, 2, 3, 4, 0, 0]
|
||||
```
|
||||
|
||||
上面代码中,`t1`是一个二进制数组,`v1`是一个SIMD值,只有四个通道。所以写入`t1`以后,只有前四个位置有值,后四个位置都是0。而`t2`是从2号位置开始写入,所以前两个位置和后两个位置都是0。
|
||||
上面代码中,`t1`是一个二进制数组,`v1`是一个 SIMD 值,只有四个通道。所以写入`t1`以后,只有前四个位置有值,后四个位置都是 0。而`t2`是从 2 号位置开始写入,所以前两个位置和后两个位置都是 0。
|
||||
|
||||
这个方法还有三个变种`store1()`、`store2()`和`store3()`,表示只写入一个通道、二个通道和三个通道的值。
|
||||
|
||||
@ -357,7 +357,7 @@ SIMD.Int32x4.store1(tarray, 0, value);
|
||||
|
||||
### SIMD.%type%.splat()
|
||||
|
||||
`splat`方法返回一个新的SIMD值,该值的所有通道都会设成同一个预先给定的值。
|
||||
`splat`方法返回一个新的 SIMD 值,该值的所有通道都会设成同一个预先给定的值。
|
||||
|
||||
```javascript
|
||||
SIMD.Float32x4.splat(3);
|
||||
@ -366,11 +366,11 @@ SIMD.Float64x2.splat(3);
|
||||
// Float64x2[3, 3]
|
||||
```
|
||||
|
||||
如果省略参数,所有整数型的SIMD值都会设定`0`,浮点型的SIMD值都会设成`NaN`。
|
||||
如果省略参数,所有整数型的 SIMD 值都会设定`0`,浮点型的 SIMD 值都会设成`NaN`。
|
||||
|
||||
### SIMD.%type%.swizzle()
|
||||
|
||||
`swizzle`方法返回一个新的SIMD值,重新排列原有的SIMD值的通道顺序。
|
||||
`swizzle`方法返回一个新的 SIMD 值,重新排列原有的 SIMD 值的通道顺序。
|
||||
|
||||
```javascript
|
||||
var t = SIMD.Float32x4(1, 2, 3, 4);
|
||||
@ -378,7 +378,7 @@ SIMD.Float32x4.swizzle(t, 1, 2, 0, 3);
|
||||
// Float32x4[2,3,1,4]
|
||||
```
|
||||
|
||||
上面代码中,`swizzle`方法的第一个参数是原有的SIMD值,后面的参数对应将要返回的SIMD值的四个通道。它的意思是新的SIMD的四个通道,依次是原来SIMD值的1号通道、2号通道、0号通道、3号通道。由于SIMD值最多可以有16个通道,所以`swizzle`方法除了第一个参数以外,最多还可以接受16个参数。
|
||||
上面代码中,`swizzle`方法的第一个参数是原有的 SIMD 值,后面的参数对应将要返回的 SIMD 值的四个通道。它的意思是新的 SIMD 的四个通道,依次是原来 SIMD 值的 1 号通道、2 号通道、0 号通道、3 号通道。由于 SIMD 值最多可以有 16 个通道,所以`swizzle`方法除了第一个参数以外,最多还可以接受 16 个参数。
|
||||
|
||||
下面是另一个例子。
|
||||
|
||||
@ -398,7 +398,7 @@ var d = SIMD.Float32x4.swizzle(a, 3, 2, 1, 0);
|
||||
|
||||
### SIMD.%type%.shuffle()
|
||||
|
||||
`shuffle`方法从两个SIMD值之中取出指定通道,返回一个新的SIMD值。
|
||||
`shuffle`方法从两个 SIMD 值之中取出指定通道,返回一个新的 SIMD 值。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Float32x4(1, 2, 3, 4);
|
||||
@ -408,13 +408,13 @@ SIMD.Float32x4.shuffle(a, b, 1, 5, 7, 2);
|
||||
// Float32x4[2, 6, 8, 3]
|
||||
```
|
||||
|
||||
上面代码中,`a`和`b`一共有8个通道,依次编号为0到7。`shuffle`根据编号,取出相应的通道,返回一个新的SIMD值。
|
||||
上面代码中,`a`和`b`一共有 8 个通道,依次编号为 0 到 7。`shuffle`根据编号,取出相应的通道,返回一个新的 SIMD 值。
|
||||
|
||||
## 静态方法:比较运算
|
||||
|
||||
### SIMD.%type%.equal(),SIMD.%type%.notEqual()
|
||||
|
||||
`equal`方法用来比较两个SIMD值`a`和`b`的每一个通道,根据两者是否精确相等(`a === b`),得到一个布尔值。最后,所有通道的比较结果,组成一个新的SIMD值,作为掩码返回。`notEqual`方法则是比较两个通道是否不相等(`a !== b`)。
|
||||
`equal`方法用来比较两个 SIMD 值`a`和`b`的每一个通道,根据两者是否精确相等(`a === b`),得到一个布尔值。最后,所有通道的比较结果,组成一个新的 SIMD 值,作为掩码返回。`notEqual`方法则是比较两个通道是否不相等(`a !== b`)。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Float32x4(1, 2, 3, 9);
|
||||
@ -429,7 +429,7 @@ SIMD.Float32x4.notEqual(a,b);
|
||||
|
||||
### SIMD.%type%.greaterThan(),SIMD.%type%.greaterThanOrEqual()
|
||||
|
||||
`greatThan`方法用来比较两个SIMD值`a`和`b`的每一个通道,如果在该通道中,`a`较大就得到`true`,否则得到`false`。最后,所有通道的比较结果,组成一个新的SIMD值,作为掩码返回。`greaterThanOrEqual`则是比较`a`是否大于等于`b`。
|
||||
`greatThan`方法用来比较两个 SIMD 值`a`和`b`的每一个通道,如果在该通道中,`a`较大就得到`true`,否则得到`false`。最后,所有通道的比较结果,组成一个新的 SIMD 值,作为掩码返回。`greaterThanOrEqual`则是比较`a`是否大于等于`b`。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Float32x4(1, 6, 3, 11);
|
||||
@ -444,7 +444,7 @@ SIMD.Float32x4.greaterThanOrEqual(a, b)
|
||||
|
||||
### SIMD.%type%.lessThan(),SIMD.%type%.lessThanOrEqual()
|
||||
|
||||
`lessThan`方法用来比较两个SIMD值`a`和`b`的每一个通道,如果在该通道中,`a`较小就得到`true`,否则得到`false`。最后,所有通道的比较结果,会组成一个新的SIMD值,作为掩码返回。`lessThanOrEqual`方法则是比较`a`是否等于`b`。
|
||||
`lessThan`方法用来比较两个 SIMD 值`a`和`b`的每一个通道,如果在该通道中,`a`较小就得到`true`,否则得到`false`。最后,所有通道的比较结果,会组成一个新的 SIMD 值,作为掩码返回。`lessThanOrEqual`方法则是比较`a`是否等于`b`。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Float32x4(1, 2, 3, 11);
|
||||
@ -459,7 +459,7 @@ SIMD.Float32x4.lessThanOrEqual(a, b)
|
||||
|
||||
### SIMD.%type%.select()
|
||||
|
||||
`select`方法通过掩码生成一个新的SIMD值。它接受三个参数,分别是掩码和两个SIMD值。
|
||||
`select`方法通过掩码生成一个新的 SIMD 值。它接受三个参数,分别是掩码和两个 SIMD 值。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Float32x4(1, 2, 3, 4);
|
||||
@ -471,7 +471,7 @@ SIMD.Float32x4.select(mask, a, b);
|
||||
// Float32x4[1, 6, 7, 4]
|
||||
```
|
||||
|
||||
上面代码中,`select`方法接受掩码和两个SIMD值作为参数。当某个通道对应的掩码为`true`时,会选择第一个SIMD值的对应通道,否则选择第二个SIMD值的对应通道。
|
||||
上面代码中,`select`方法接受掩码和两个 SIMD 值作为参数。当某个通道对应的掩码为`true`时,会选择第一个 SIMD 值的对应通道,否则选择第二个 SIMD 值的对应通道。
|
||||
|
||||
这个方法通常与比较运算符结合使用。
|
||||
|
||||
@ -486,11 +486,11 @@ var result = SIMD.Float32x4.select(mask, a, b);
|
||||
// Float32x4[0, 6, 3, 4]
|
||||
```
|
||||
|
||||
上面代码中,先通过`lessThan`方法生成一个掩码,然后通过`select`方法生成一个由每个通道的较小值组成的新的SIMD值。
|
||||
上面代码中,先通过`lessThan`方法生成一个掩码,然后通过`select`方法生成一个由每个通道的较小值组成的新的 SIMD 值。
|
||||
|
||||
### SIMD.%BooleanType%.allTrue(),SIMD.%BooleanType%.anyTrue()
|
||||
|
||||
`allTrue`方法接受一个SIMD值作为参数,然后返回一个布尔值,表示该SIMD值的所有通道是否都为`true`。
|
||||
`allTrue`方法接受一个 SIMD 值作为参数,然后返回一个布尔值,表示该 SIMD 值的所有通道是否都为`true`。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Bool32x4(true, true, true, true);
|
||||
@ -524,7 +524,7 @@ var b2 = SIMD.Int32x4.anyTrue(ix4); // true
|
||||
|
||||
### SIMD.%type%.min(),SIMD.%type%.minNum()
|
||||
|
||||
`min`方法接受两个SIMD值作为参数,将两者的对应通道的较小值,组成一个新的SIMD值返回。
|
||||
`min`方法接受两个 SIMD 值作为参数,将两者的对应通道的较小值,组成一个新的 SIMD 值返回。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Float32x4(-1, -2, 3, 5.2);
|
||||
@ -555,7 +555,7 @@ var dx4 = SIMD.Float32x4.minNum(ax4, bx4);
|
||||
|
||||
### SIMD.%type%.max(),SIMD.%type%.maxNum()
|
||||
|
||||
`max`方法接受两个SIMD值作为参数,将两者的对应通道的较大值,组成一个新的SIMD值返回。
|
||||
`max`方法接受两个 SIMD 值作为参数,将两者的对应通道的较大值,组成一个新的 SIMD 值返回。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Float32x4(-1, -2, 3, 5.2);
|
||||
@ -586,7 +586,7 @@ SIMD.Float64x2.maxNum(c, d)
|
||||
|
||||
### SIMD.%type%.and(),SIMD.%type%.or(),SIMD.%type%.xor(),SIMD.%type%.not()
|
||||
|
||||
`and`方法接受两个SIMD值作为参数,返回两者对应的通道进行二进制`AND`运算(`&`)后得到的新的SIMD值。
|
||||
`and`方法接受两个 SIMD 值作为参数,返回两者对应的通道进行二进制`AND`运算(`&`)后得到的新的 SIMD 值。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Int32x4(1, 2, 4, 8);
|
||||
@ -597,7 +597,7 @@ SIMD.Int32x4.and(a, b)
|
||||
|
||||
上面代码中,以通道`0`为例,`1`的二进制形式是`0001`,`5`的二进制形式是`01001`,所以进行`AND`运算以后,得到`0001`。
|
||||
|
||||
`or`方法接受两个SIMD值作为参数,返回两者对应的通道进行二进制`OR`运算(`|`)后得到的新的SIMD值。
|
||||
`or`方法接受两个 SIMD 值作为参数,返回两者对应的通道进行二进制`OR`运算(`|`)后得到的新的 SIMD 值。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Int32x4(1, 2, 4, 8);
|
||||
@ -606,7 +606,7 @@ SIMD.Int32x4.or(a, b)
|
||||
// Int32x4[5, 7, 5, 13]
|
||||
```
|
||||
|
||||
`xor`方法接受两个SIMD值作为参数,返回两者对应的通道进行二进制”异或“运算(`^`)后得到的新的SIMD值。
|
||||
`xor`方法接受两个 SIMD 值作为参数,返回两者对应的通道进行二进制”异或“运算(`^`)后得到的新的 SIMD 值。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Int32x4(1, 2, 4, 8);
|
||||
@ -615,7 +615,7 @@ SIMD.Int32x4.xor(a, b)
|
||||
// Int32x4[4, 7, 1, 13]
|
||||
```
|
||||
|
||||
`not`方法接受一个SIMD值作为参数,返回每个通道进行二进制”否“运算(`~`)后得到的新的SIMD值。
|
||||
`not`方法接受一个 SIMD 值作为参数,返回每个通道进行二进制”否“运算(`~`)后得到的新的 SIMD 值。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Int32x4(1, 2, 4, 8);
|
||||
@ -623,11 +623,11 @@ SIMD.Int32x4.not(a)
|
||||
// Int32x4[-2, -3, -5, -9]
|
||||
```
|
||||
|
||||
上面代码中,`1`的否运算之所以得到`-2`,是因为在计算机内部,负数采用”2的补码“这种形式进行表示。也就是说,整数`n`的负数形式`-n`,是对每一个二进制位取反以后,再加上1。因此,直接取反就相当于负数形式再减去1,比如`1`的负数形式是`-1`,再减去1,就得到了`-2`。
|
||||
上面代码中,`1`的否运算之所以得到`-2`,是因为在计算机内部,负数采用”2 的补码“这种形式进行表示。也就是说,整数`n`的负数形式`-n`,是对每一个二进制位取反以后,再加上 1。因此,直接取反就相当于负数形式再减去 1,比如`1`的负数形式是`-1`,再减去 1,就得到了`-2`。
|
||||
|
||||
## 静态方法:数据类型转换
|
||||
|
||||
SIMD提供以下方法,用来将一种数据类型转为另一种数据类型。
|
||||
SIMD 提供以下方法,用来将一种数据类型转为另一种数据类型。
|
||||
|
||||
- `SIMD.%type%.fromFloat32x4()`
|
||||
- `SIMD.%type%.fromFloat32x4Bits()`
|
||||
@ -662,7 +662,7 @@ SIMD.Int16x8.fromFloat32x4Bits(t);
|
||||
// Int16x8[0, 16256, 0, 16384, 0, 16448, 0, 16512]
|
||||
```
|
||||
|
||||
上面代码中,原始SIMD值`t`是4通道的,而目标值是8通道的。
|
||||
上面代码中,原始 SIMD 值`t`是 4 通道的,而目标值是 8 通道的。
|
||||
|
||||
如果数据转换时,原通道的数据大小,超过了目标通道的最大宽度,就会报错。
|
||||
|
||||
@ -670,7 +670,7 @@ SIMD.Int16x8.fromFloat32x4Bits(t);
|
||||
|
||||
### SIMD.%type%.prototype.toString()
|
||||
|
||||
`toString`方法返回一个SIMD值的字符串形式。
|
||||
`toString`方法返回一个 SIMD 值的字符串形式。
|
||||
|
||||
```javascript
|
||||
var a = SIMD.Float32x4(11, 22, 33, 44);
|
||||
@ -692,7 +692,7 @@ function average(list) {
|
||||
}
|
||||
```
|
||||
|
||||
使用SIMD,可以将计算次数减少到`n`次的四分之一。
|
||||
使用 SIMD,可以将计算次数减少到`n`次的四分之一。
|
||||
|
||||
```javascript
|
||||
function average(list) {
|
||||
@ -712,5 +712,4 @@ function average(list) {
|
||||
}
|
||||
```
|
||||
|
||||
上面代码先是每隔四位,将所有的值读入一个SIMD,然后立刻累加。然后,得到累加值四个通道的总和,再除以`n`就可以了。
|
||||
|
||||
上面代码先是每隔四位,将所有的值读入一个 SIMD,然后立刻累加。然后,得到累加值四个通道的总和,再除以`n`就可以了。
|
||||
|
110
docs/spec.md
110
docs/spec.md
@ -6,17 +6,17 @@
|
||||
|
||||
一般来说,没有必要阅读规格,除非你要写编译器。因为规格写得非常抽象和精炼,又缺乏实例,不容易理解,而且对于解决实际的应用问题,帮助不大。但是,如果你遇到疑难的语法问题,实在找不到答案,这时可以去查看规格文件,了解语言标准是怎么说的。规格是解决问题的“最后一招”。
|
||||
|
||||
这对JavaScript语言很有必要。因为它的使用场景复杂,语法规则不统一,例外很多,各种运行环境的行为不一致,导致奇怪的语法问题层出不穷,任何语法书都不可能囊括所有情况。查看规格,不失为一种解决语法问题的最可靠、最权威的终极方法。
|
||||
这对 JavaScript 语言很有必要。因为它的使用场景复杂,语法规则不统一,例外很多,各种运行环境的行为不一致,导致奇怪的语法问题层出不穷,任何语法书都不可能囊括所有情况。查看规格,不失为一种解决语法问题的最可靠、最权威的终极方法。
|
||||
|
||||
本章介绍如何读懂ECMAScript 6的规格文件。
|
||||
本章介绍如何读懂 ECMAScript 6 的规格文件。
|
||||
|
||||
ECMAScript 6的规格,可以在ECMA国际标准组织的官方网站([www.ecma-international.org/ecma-262/6.0/](http://www.ecma-international.org/ecma-262/6.0/))免费下载和在线阅读。
|
||||
ECMAScript 6 的规格,可以在 ECMA 国际标准组织的官方网站([www.ecma-international.org/ecma-262/6.0/](http://www.ecma-international.org/ecma-262/6.0/))免费下载和在线阅读。
|
||||
|
||||
这个规格文件相当庞大,一共有26章,A4打印的话,足足有545页。它的特点就是规定得非常细致,每一个语法行为、每一个函数的实现都做了详尽的清晰的描述。基本上,编译器作者只要把每一步翻译成代码就可以了。这很大程度上,保证了所有ES6实现都有一致的行为。
|
||||
这个规格文件相当庞大,一共有 26 章,A4 打印的话,足足有 545 页。它的特点就是规定得非常细致,每一个语法行为、每一个函数的实现都做了详尽的清晰的描述。基本上,编译器作者只要把每一步翻译成代码就可以了。这很大程度上,保证了所有 ES6 实现都有一致的行为。
|
||||
|
||||
ECMAScript 6规格的26章之中,第1章到第3章是对文件本身的介绍,与语言关系不大。第4章是对这门语言总体设计的描述,有兴趣的读者可以读一下。第5章到第8章是语言宏观层面的描述。第5章是规格的名词解释和写法的介绍,第6章介绍数据类型,第7章介绍语言内部用到的抽象操作,第8章介绍代码如何运行。第9章到第26章介绍具体的语法。
|
||||
ECMAScript 6 规格的 26 章之中,第 1 章到第 3 章是对文件本身的介绍,与语言关系不大。第 4 章是对这门语言总体设计的描述,有兴趣的读者可以读一下。第 5 章到第 8 章是语言宏观层面的描述。第 5 章是规格的名词解释和写法的介绍,第 6 章介绍数据类型,第 7 章介绍语言内部用到的抽象操作,第 8 章介绍代码如何运行。第 9 章到第 26 章介绍具体的语法。
|
||||
|
||||
对于一般用户来说,除了第4章,其他章节都涉及某一方面的细节,不用通读,只要在用到的时候,查阅相关章节即可。下面通过一些例子,介绍如何使用这份规格。
|
||||
对于一般用户来说,除了第 4 章,其他章节都涉及某一方面的细节,不用通读,只要在用到的时候,查阅相关章节即可。下面通过一些例子,介绍如何使用这份规格。
|
||||
|
||||
## 相等运算符
|
||||
|
||||
@ -28,7 +28,7 @@ ECMAScript 6规格的26章之中,第1章到第3章是对文件本身的介绍
|
||||
0 == null
|
||||
```
|
||||
|
||||
如果你不确定答案,或者想知道语言内部怎么处理,就可以去查看规格,[7.2.12小节](http://www.ecma-international.org/ecma-262/6.0/#sec-7.2.12)是对相等运算符(`==`)的描述。
|
||||
如果你不确定答案,或者想知道语言内部怎么处理,就可以去查看规格,[7.2.12 小节](http://www.ecma-international.org/ecma-262/6.0/#sec-7.2.12)是对相等运算符(`==`)的描述。
|
||||
|
||||
规格对每一种语法行为的描述,都分成两部分:先是总体的行为描述,然后是实现的算法细节。相等运算符的总体描述,只有一句话。
|
||||
|
||||
@ -40,23 +40,23 @@ ECMAScript 6规格的26章之中,第1章到第3章是对文件本身的介绍
|
||||
|
||||
> 1. ReturnIfAbrupt(x).
|
||||
> 1. ReturnIfAbrupt(y).
|
||||
> 1. If `Type(x)` is the same as `Type(y)`, then
|
||||
> Return the result of performing Strict Equality Comparison `x === y`.
|
||||
> 1. If `Type(x)` is the same as `Type(y)`, then\
|
||||
> Return the result of performing Strict Equality Comparison `x === y`.
|
||||
> 1. If `x` is `null` and `y` is `undefined`, return `true`.
|
||||
> 1. If `x` is `undefined` and `y` is `null`, return `true`.
|
||||
> 1. If `Type(x)` is Number and `Type(y)` is String,
|
||||
> return the result of the comparison `x == ToNumber(y)`.
|
||||
> 1. If `Type(x)` is String and `Type(y)` is Number,
|
||||
> return the result of the comparison `ToNumber(x) == y`.
|
||||
> 1. If `Type(x)` is Number and `Type(y)` is String,\
|
||||
> return the result of the comparison `x == ToNumber(y)`.
|
||||
> 1. If `Type(x)` is String and `Type(y)` is Number,\
|
||||
> return the result of the comparison `ToNumber(x) == y`.
|
||||
> 1. If `Type(x)` is Boolean, return the result of the comparison `ToNumber(x) == y`.
|
||||
> 1. If `Type(y)` is Boolean, return the result of the comparison `x == ToNumber(y)`.
|
||||
> 1. If `Type(x)` is either String, Number, or Symbol and `Type(y)` is Object, then
|
||||
> 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
|
||||
> return the result of the comparison `ToPrimitive(x) == y`.
|
||||
> 1. If `Type(x)` is either String, Number, or Symbol and `Type(y)` is Object, then\
|
||||
> return the result of the comparison `x == ToPrimitive(y)`.
|
||||
> 1. If `Type(x)` is Object and `Type(y)` is either String, Number, or Symbol, then\
|
||||
> return the result of the comparison `ToPrimitive(x) == y`.
|
||||
> 1. Return `false`.
|
||||
|
||||
上面这段算法,一共有12步,翻译如下。
|
||||
上面这段算法,一共有 12 步,翻译如下。
|
||||
|
||||
> 1. 如果`x`不是正常值(比如抛出一个错误),中断执行。
|
||||
> 1. 如果`y`不是正常值,中断执行。
|
||||
@ -71,7 +71,7 @@ ECMAScript 6规格的26章之中,第1章到第3章是对文件本身的介绍
|
||||
> 1. 如果`Type(x)`是对象,`Type(y)`是字符串或数值或`Symbol`值,返回`ToPrimitive(x) == y`的结果。
|
||||
> 1. 返回`false`。
|
||||
|
||||
由于`0`的类型是数值,`null`的类型是Null(这是规格[4.3.13小节](http://www.ecma-international.org/ecma-262/6.0/#sec-4.3.13)的规定,是内部Type运算的结果,跟`typeof`运算符无关)。因此上面的前11步都得不到结果,要到第12步才能得到`false`。
|
||||
由于`0`的类型是数值,`null`的类型是 Null(这是规格[4.3.13 小节](http://www.ecma-international.org/ecma-262/6.0/#sec-4.3.13)的规定,是内部 Type 运算的结果,跟`typeof`运算符无关)。因此上面的前 11 步都得不到结果,要到第 12 步才能得到`false`。
|
||||
|
||||
```javascript
|
||||
0 == null // false
|
||||
@ -94,7 +94,7 @@ a2[0] // undefined
|
||||
a1[0] === a2[0] // true
|
||||
```
|
||||
|
||||
上面代码中,数组`a1`的成员是三个`undefined`,数组`a2`的成员是三个空位。这两个数组很相似,长度都是3,每个位置的成员读取出来都是`undefined`。
|
||||
上面代码中,数组`a1`的成员是三个`undefined`,数组`a2`的成员是三个空位。这两个数组很相似,长度都是 3,每个位置的成员读取出来都是`undefined`。
|
||||
|
||||
但是,它们实际上存在重大差异。
|
||||
|
||||
@ -116,23 +116,23 @@ a2.map(n => 1) // [, , ,]
|
||||
|
||||
为什么`a1`与`a2`成员的行为不一致?数组的成员是`undefined`或空位,到底有什么不同?
|
||||
|
||||
规格的[12.2.5小节《数组的初始化》](http://www.ecma-international.org/ecma-262/6.0/#sec-12.2.5)给出了答案。
|
||||
规格的[12.2.5 小节《数组的初始化》](http://www.ecma-international.org/ecma-262/6.0/#sec-12.2.5)给出了答案。
|
||||
|
||||
> “Array elements may be elided at the beginning, middle or end of the element list. Whenever a comma in the element list is not preceded by an AssignmentExpression (i.e., a comma at the beginning or after another comma), the missing array element contributes to the length of the Array and increases the index of subsequent elements. Elided array elements are not defined. If an element is elided at the end of an array, that element does not contribute to the length of the Array.”
|
||||
|
||||
翻译如下。
|
||||
|
||||
> "数组成员可以省略。只要逗号前面没有任何表达式,数组的`length`属性就会加1,并且相应增加其后成员的位置索引。被省略的成员不会被定义。如果被省略的成员是数组最后一个成员,则不会导致数组`length`属性增加。”
|
||||
> "数组成员可以省略。只要逗号前面没有任何表达式,数组的`length`属性就会加 1,并且相应增加其后成员的位置索引。被省略的成员不会被定义。如果被省略的成员是数组最后一个成员,则不会导致数组`length`属性增加。”
|
||||
|
||||
上面的规格说得很清楚,数组的空位会反映在`length`属性,也就是说空位有自己的位置,但是这个位置的值是未定义,即这个值是不存在的。如果一定要读取,结果就是`undefined`(因为`undefined`在JavaScript语言中表示不存在)。
|
||||
上面的规格说得很清楚,数组的空位会反映在`length`属性,也就是说空位有自己的位置,但是这个位置的值是未定义,即这个值是不存在的。如果一定要读取,结果就是`undefined`(因为`undefined`在 JavaScript 语言中表示不存在)。
|
||||
|
||||
这就解释了为什么`in`运算符、数组的`hasOwnProperty`方法、`Object.keys`方法,都取不到空位的属性名。因为这个属性名根本就不存在,规格里面没说要为空位分配属性名(位置索引),只说要为下一个元素的位置索引加1。
|
||||
这就解释了为什么`in`运算符、数组的`hasOwnProperty`方法、`Object.keys`方法,都取不到空位的属性名。因为这个属性名根本就不存在,规格里面没说要为空位分配属性名(位置索引),只说要为下一个元素的位置索引加 1。
|
||||
|
||||
至于为什么数组的`map`方法会跳过空位,请看下一节。
|
||||
|
||||
## 数组的map方法
|
||||
## 数组的 map 方法
|
||||
|
||||
规格的[22.1.3.15小节](http://www.ecma-international.org/ecma-262/6.0/#sec-22.1.3.15)定义了数组的`map`方法。该小节先是总体描述`map`方法的行为,里面没有提到数组空位。
|
||||
规格的[22.1.3.15 小节](http://www.ecma-international.org/ecma-262/6.0/#sec-22.1.3.15)定义了数组的`map`方法。该小节先是总体描述`map`方法的行为,里面没有提到数组空位。
|
||||
|
||||
后面的算法描述是这样的。
|
||||
|
||||
@ -145,18 +145,18 @@ a2.map(n => 1) // [, , ,]
|
||||
> 1. Let `A` be `ArraySpeciesCreate(O, len)`.
|
||||
> 1. `ReturnIfAbrupt(A)`.
|
||||
> 1. Let `k` be 0.
|
||||
> 1. Repeat, while `k` < `len`
|
||||
> a. Let `Pk` be `ToString(k)`.
|
||||
> b. Let `kPresent` be `HasProperty(O, Pk)`.
|
||||
> c. `ReturnIfAbrupt(kPresent)`.
|
||||
> d. If `kPresent` is `true`, then
|
||||
> d-1. Let `kValue` be `Get(O, Pk)`.
|
||||
> d-2. `ReturnIfAbrupt(kValue)`.
|
||||
> d-3. Let `mappedValue` be `Call(callbackfn, T, «kValue, k, O»)`.
|
||||
> d-4. `ReturnIfAbrupt(mappedValue)`.
|
||||
> d-5. Let `status` be `CreateDataPropertyOrThrow (A, Pk, mappedValue)`.
|
||||
> d-6. `ReturnIfAbrupt(status)`.
|
||||
> e. Increase `k` by 1.
|
||||
> 1. Repeat, while `k` < `len`\
|
||||
> a. Let `Pk` be `ToString(k)`.\
|
||||
> b. Let `kPresent` be `HasProperty(O, Pk)`.\
|
||||
> c. `ReturnIfAbrupt(kPresent)`.\
|
||||
> d. If `kPresent` is `true`, then\
|
||||
> d-1. Let `kValue` be `Get(O, Pk)`.\
|
||||
> d-2. `ReturnIfAbrupt(kValue)`.\
|
||||
> d-3. Let `mappedValue` be `Call(callbackfn, T, «kValue, k, O»)`.\
|
||||
> d-4. `ReturnIfAbrupt(mappedValue)`.\
|
||||
> d-5. Let `status` be `CreateDataPropertyOrThrow (A, Pk, mappedValue)`.\
|
||||
> d-6. `ReturnIfAbrupt(status)`.\
|
||||
> e. Increase `k` by 1.
|
||||
> 1. Return `A`.
|
||||
|
||||
翻译如下。
|
||||
@ -165,26 +165,26 @@ a2.map(n => 1) // [, , ,]
|
||||
> 1. 如果报错就返回
|
||||
> 1. 求出当前数组的`length`属性
|
||||
> 1. 如果报错就返回
|
||||
> 1. 如果map方法的参数`callbackfn`不可执行,就报错
|
||||
> 1. 如果map方法的参数之中,指定了`this`,就让`T`等于该参数,否则`T`为`undefined`
|
||||
> 1. 如果 map 方法的参数`callbackfn`不可执行,就报错
|
||||
> 1. 如果 map 方法的参数之中,指定了`this`,就让`T`等于该参数,否则`T`为`undefined`
|
||||
> 1. 生成一个新的数组`A`,跟当前数组的`length`属性保持一致
|
||||
> 1. 如果报错就返回
|
||||
> 1. 设定`k`等于0
|
||||
> 1. 只要`k`小于当前数组的`length`属性,就重复下面步骤
|
||||
> a. 设定`Pk`等于`ToString(k)`,即将`K`转为字符串
|
||||
> b. 设定`kPresent`等于`HasProperty(O, Pk)`,即求当前数组有没有指定属性
|
||||
> c. 如果报错就返回
|
||||
> d. 如果`kPresent`等于`true`,则进行下面步骤
|
||||
> d-1. 设定`kValue`等于`Get(O, Pk)`,取出当前数组的指定属性
|
||||
> d-2. 如果报错就返回
|
||||
> d-3. 设定`mappedValue`等于`Call(callbackfn, T, «kValue, k, O»)`,即执行回调函数
|
||||
> d-4. 如果报错就返回
|
||||
> d-5. 设定`status`等于`CreateDataPropertyOrThrow (A, Pk, mappedValue)`,即将回调函数的值放入`A`数组的指定位置
|
||||
> d-6. 如果报错就返回
|
||||
> e. `k`增加1
|
||||
> 1. 设定`k`等于 0
|
||||
> 1. 只要`k`小于当前数组的`length`属性,就重复下面步骤\
|
||||
> a. 设定`Pk`等于`ToString(k)`,即将`K`转为字符串\
|
||||
> b. 设定`kPresent`等于`HasProperty(O, Pk)`,即求当前数组有没有指定属性\
|
||||
> c. 如果报错就返回\
|
||||
> d. 如果`kPresent`等于`true`,则进行下面步骤\
|
||||
> d-1. 设定`kValue`等于`Get(O, Pk)`,取出当前数组的指定属性\
|
||||
> d-2. 如果报错就返回\
|
||||
> d-3. 设定`mappedValue`等于`Call(callbackfn, T, «kValue, k, O»)`,即执行回调函数\
|
||||
> d-4. 如果报错就返回\
|
||||
> d-5. 设定`status`等于`CreateDataPropertyOrThrow (A, Pk, mappedValue)`,即将回调函数的值放入`A`数组的指定位置\
|
||||
> d-6. 如果报错就返回\
|
||||
> e. `k`增加 1
|
||||
> 1. 返回`A`
|
||||
|
||||
仔细查看上面的算法,可以发现,当处理一个全是空位的数组时,前面步骤都没有问题。进入第10步的b时,`kpresent`会报错,因为空位对应的属性名,对于数组来说是不存在的,因此就会返回,不会进行后面的步骤。
|
||||
仔细查看上面的算法,可以发现,当处理一个全是空位的数组时,前面步骤都没有问题。进入第 10 步的 b 时,`kpresent`会报错,因为空位对应的属性名,对于数组来说是不存在的,因此就会返回,不会进行后面的步骤。
|
||||
|
||||
```javascript
|
||||
const arr = [, , ,];
|
||||
@ -196,7 +196,7 @@ arr.map(n => {
|
||||
|
||||
上面代码中,`arr`是一个全是空位的数组,`map`方法遍历成员时,发现是空位,就直接跳过,不会进入回调函数。因此,回调函数里面的`console.log`语句根本不会执行,整个`map`方法返回一个全是空位的新数组。
|
||||
|
||||
V8引擎对`map`方法的[实现](https://github.com/v8/v8/blob/44c44521ae11859478b42004f57ea93df52526ee/src/js/array.js#L1347)如下,可以看到跟规格的算法描述完全一致。
|
||||
V8 引擎对`map`方法的[实现](https://github.com/v8/v8/blob/44c44521ae11859478b42004f57ea93df52526ee/src/js/array.js#L1347)如下,可以看到跟规格的算法描述完全一致。
|
||||
|
||||
```javascript
|
||||
function ArrayMap(f, receiver) {
|
||||
|
197
docs/string.md
197
docs/string.md
@ -1,17 +1,17 @@
|
||||
# 字符串的扩展
|
||||
|
||||
ES6加强了对Unicode的支持,并且扩展了字符串对象。
|
||||
ES6 加强了对 Unicode 的支持,并且扩展了字符串对象。
|
||||
|
||||
## 字符的Unicode表示法
|
||||
## 字符的 Unicode 表示法
|
||||
|
||||
JavaScript允许采用`\uxxxx`形式表示一个字符,其中“xxxx”表示字符的码点。
|
||||
JavaScript 允许采用`\uxxxx`形式表示一个字符,其中`xxxx`表示字符的 Unicode 码点。
|
||||
|
||||
```javascript
|
||||
"\u0061"
|
||||
// "a"
|
||||
```
|
||||
|
||||
但是,这种表示法只限于`\u0000`——`\uFFFF`之间的字符。超出这个范围的字符,必须用两个双字节的形式表达。
|
||||
但是,这种表示法只限于码点在`\u0000`~`\uFFFF`之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。
|
||||
|
||||
```javascript
|
||||
"\uD842\uDFB7"
|
||||
@ -21,7 +21,7 @@ JavaScript允许采用`\uxxxx`形式表示一个字符,其中“xxxx”表示
|
||||
// " 7"
|
||||
```
|
||||
|
||||
上面代码表示,如果直接在`\u`后面跟上超过`0xFFFF`的数值(比如`\u20BB7`),JavaScript会理解成`\u20BB+7`。由于`\u20BB`是一个不可打印字符,所以只会显示一个空格,后面跟着一个`7`。
|
||||
上面代码表示,如果直接在`\u`后面跟上超过`0xFFFF`的数值(比如`\u20BB7`),JavaScript 会理解成`\u20BB+7`。由于`\u20BB`是一个不可打印字符,所以只会显示一个空格,后面跟着一个`7`。
|
||||
|
||||
ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符。
|
||||
|
||||
@ -39,9 +39,9 @@ hell\u{6F} // 123
|
||||
// true
|
||||
```
|
||||
|
||||
上面代码中,最后一个例子表明,大括号表示法与四字节的UTF-16编码是等价的。
|
||||
上面代码中,最后一个例子表明,大括号表示法与四字节的 UTF-16 编码是等价的。
|
||||
|
||||
有了这种表示法之后,JavaScript共有6种方法可以表示一个字符。
|
||||
有了这种表示法之后,JavaScript 共有 6 种方法可以表示一个字符。
|
||||
|
||||
```javascript
|
||||
'\z' === 'z' // true
|
||||
@ -53,7 +53,7 @@ hell\u{6F} // 123
|
||||
|
||||
## codePointAt()
|
||||
|
||||
JavaScript内部,字符以UTF-16的格式储存,每个字符固定为`2`个字节。对于那些需要`4`个字节储存的字符(Unicode码点大于`0xFFFF`的字符),JavaScript会认为它们是两个字符。
|
||||
JavaScript 内部,字符以 UTF-16 的格式储存,每个字符固定为`2`个字节。对于那些需要`4`个字节储存的字符(Unicode 码点大于`0xFFFF`的字符),JavaScript 会认为它们是两个字符。
|
||||
|
||||
```javascript
|
||||
var s = "𠮷";
|
||||
@ -65,12 +65,12 @@ s.charCodeAt(0) // 55362
|
||||
s.charCodeAt(1) // 57271
|
||||
```
|
||||
|
||||
上面代码中,汉字“𠮷”(注意,这个字不是”吉祥“的”吉“)的码点是`0x20BB7`,UTF-16编码为`0xD842 0xDFB7`(十进制为`55362 57271`),需要`4`个字节储存。对于这种`4`个字节的字符,JavaScript不能正确处理,字符串长度会误判为`2`,而且`charAt`方法无法读取整个字符,`charCodeAt`方法只能分别返回前两个字节和后两个字节的值。
|
||||
上面代码中,汉字“𠮷”(注意,这个字不是“吉祥”的“吉”)的码点是`0x20BB7`,UTF-16 编码为`0xD842 0xDFB7`(十进制为`55362 57271`),需要`4`个字节储存。对于这种`4`个字节的字符,JavaScript 不能正确处理,字符串长度会误判为`2`,而且`charAt`方法无法读取整个字符,`charCodeAt`方法只能分别返回前两个字节和后两个字节的值。
|
||||
|
||||
ES6提供了`codePointAt`方法,能够正确处理4个字节储存的字符,返回一个字符的码点。
|
||||
ES6 提供了`codePointAt`方法,能够正确处理 4 个字节储存的字符,返回一个字符的码点。
|
||||
|
||||
```javascript
|
||||
var s = '𠮷a';
|
||||
let s = '𠮷a';
|
||||
|
||||
s.codePointAt(0) // 134071
|
||||
s.codePointAt(1) // 57271
|
||||
@ -78,23 +78,23 @@ s.codePointAt(1) // 57271
|
||||
s.codePointAt(2) // 97
|
||||
```
|
||||
|
||||
`codePointAt`方法的参数,是字符在字符串中的位置(从0开始)。上面代码中,JavaScript将“𠮷a”视为三个字符,codePointAt方法在第一个字符上,正确地识别了“𠮷”,返回了它的十进制码点134071(即十六进制的`20BB7`)。在第二个字符(即“𠮷”的后两个字节)和第三个字符“a”上,`codePointAt`方法的结果与`charCodeAt`方法相同。
|
||||
`codePointAt`方法的参数,是字符在字符串中的位置(从 0 开始)。上面代码中,JavaScript 将“𠮷a”视为三个字符,codePointAt 方法在第一个字符上,正确地识别了“𠮷”,返回了它的十进制码点 134071(即十六进制的`20BB7`)。在第二个字符(即“𠮷”的后两个字节)和第三个字符“a”上,`codePointAt`方法的结果与`charCodeAt`方法相同。
|
||||
|
||||
总之,`codePointAt`方法会正确返回32位的UTF-16字符的码点。对于那些两个字节储存的常规字符,它的返回结果与`charCodeAt`方法相同。
|
||||
总之,`codePointAt`方法会正确返回 32 位的 UTF-16 字符的码点。对于那些两个字节储存的常规字符,它的返回结果与`charCodeAt`方法相同。
|
||||
|
||||
`codePointAt`方法返回的是码点的十进制值,如果想要十六进制的值,可以使用`toString`方法转换一下。
|
||||
|
||||
```javascript
|
||||
var s = '𠮷a';
|
||||
let s = '𠮷a';
|
||||
|
||||
s.codePointAt(0).toString(16) // "20bb7"
|
||||
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
|
||||
var s = '𠮷a';
|
||||
let s = '𠮷a';
|
||||
for (let ch of s) {
|
||||
console.log(ch.codePointAt(0).toString(16));
|
||||
}
|
||||
@ -115,7 +115,7 @@ is32Bit("a") // false
|
||||
|
||||
## String.fromCodePoint()
|
||||
|
||||
ES5提供`String.fromCharCode`方法,用于从码点返回对应字符,但是这个方法不能识别32位的UTF-16字符(Unicode编号大于`0xFFFF`)。
|
||||
ES5 提供`String.fromCharCode`方法,用于从码点返回对应字符,但是这个方法不能识别 32 位的 UTF-16 字符(Unicode 编号大于`0xFFFF`)。
|
||||
|
||||
```javascript
|
||||
String.fromCharCode(0x20BB7)
|
||||
@ -124,7 +124,7 @@ String.fromCharCode(0x20BB7)
|
||||
|
||||
上面代码中,`String.fromCharCode`不能识别大于`0xFFFF`的码点,所以`0x20BB7`就发生了溢出,最高位`2`被舍弃了,最后返回码点`U+0BB7`对应的字符,而不是码点`U+20BB7`对应的字符。
|
||||
|
||||
ES6提供了`String.fromCodePoint`方法,可以识别`0xFFFF`的字符,弥补了`String.fromCharCode`方法的不足。在作用上,正好与`codePointAt`方法相反。
|
||||
ES6 提供了`String.fromCodePoint`方法,可以识别大于`0xFFFF`的字符,弥补了`String.fromCharCode`方法的不足。在作用上,正好与`codePointAt`方法相反。
|
||||
|
||||
```javascript
|
||||
String.fromCodePoint(0x20BB7)
|
||||
@ -139,7 +139,7 @@ String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y'
|
||||
|
||||
## 字符串的遍历器接口
|
||||
|
||||
ES6为字符串添加了遍历器接口(详见《Iterator》一章),使得字符串可以被`for...of`循环遍历。
|
||||
ES6 为字符串添加了遍历器接口(详见《Iterator》一章),使得字符串可以被`for...of`循环遍历。
|
||||
|
||||
```javascript
|
||||
for (let codePoint of 'foo') {
|
||||
@ -153,7 +153,7 @@ for (let codePoint of 'foo') {
|
||||
除了遍历字符串,这个遍历器最大的优点是可以识别大于`0xFFFF`的码点,传统的`for`循环无法识别这样的码点。
|
||||
|
||||
```javascript
|
||||
var text = String.fromCodePoint(0x20BB7);
|
||||
let text = String.fromCodePoint(0x20BB7);
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
console.log(text[i]);
|
||||
@ -171,16 +171,16 @@ for (let i of text) {
|
||||
|
||||
## at()
|
||||
|
||||
ES5对字符串对象提供`charAt`方法,返回字符串给定位置的字符。该方法不能识别码点大于`0xFFFF`的字符。
|
||||
ES5 对字符串对象提供`charAt`方法,返回字符串给定位置的字符。该方法不能识别码点大于`0xFFFF`的字符。
|
||||
|
||||
```javascript
|
||||
'abc'.charAt(0) // "a"
|
||||
'𠮷'.charAt(0) // "\uD842"
|
||||
```
|
||||
|
||||
上面代码中,`charAt`方法返回的是UTF-16编码的第一个字节,实际上是无法显示的。
|
||||
上面代码中,`charAt`方法返回的是 UTF-16 编码的第一个字节,实际上是无法显示的。
|
||||
|
||||
目前,有一个提案,提出字符串实例的`at`方法,可以识别Unicode编号大于`0xFFFF`的字符,返回正确的字符。
|
||||
目前,有一个提案,提出字符串实例的`at`方法,可以识别 Unicode 编号大于`0xFFFF`的字符,返回正确的字符。
|
||||
|
||||
```javascript
|
||||
'abc'.at(0) // "a"
|
||||
@ -191,9 +191,9 @@ ES5对字符串对象提供`charAt`方法,返回字符串给定位置的字符
|
||||
|
||||
## normalize()
|
||||
|
||||
许多欧洲语言有语调符号和重音符号。为了表示它们,Unicode提供了两种方法。一种是直接提供带重音符号的字符,比如`Ǒ`(\u01D1)。另一种是提供合成符号(combining character),即原字符与重音符号的合成,两个字符合成一个字符,比如`O`(\u004F)和`ˇ`(\u030C)合成`Ǒ`(\u004F\u030C)。
|
||||
许多欧洲语言有语调符号和重音符号。为了表示它们,Unicode 提供了两种方法。一种是直接提供带重音符号的字符,比如`Ǒ`(\u01D1)。另一种是提供合成符号(combining character),即原字符与重音符号的合成,两个字符合成一个字符,比如`O`(\u004F)和`ˇ`(\u030C)合成`Ǒ`(\u004F\u030C)。
|
||||
|
||||
这两种表示方法,在视觉和语义上都等价,但是JavaScript不能识别。
|
||||
这两种表示方法,在视觉和语义上都等价,但是 JavaScript 不能识别。
|
||||
|
||||
```javascript
|
||||
'\u01D1'==='\u004F\u030C' //false
|
||||
@ -202,9 +202,9 @@ ES5对字符串对象提供`charAt`方法,返回字符串给定位置的字符
|
||||
'\u004F\u030C'.length // 2
|
||||
```
|
||||
|
||||
上面代码表示,JavaScript将合成字符视为两个字符,导致两种表示方法不相等。
|
||||
上面代码表示,JavaScript 将合成字符视为两个字符,导致两种表示方法不相等。
|
||||
|
||||
ES6提供字符串实例的`normalize()`方法,用来将字符的不同表示方法统一为同样的形式,这称为Unicode正规化。
|
||||
ES6 提供字符串实例的`normalize()`方法,用来将字符的不同表示方法统一为同样的形式,这称为 Unicode 正规化。
|
||||
|
||||
```javascript
|
||||
'\u01D1'.normalize() === '\u004F\u030C'.normalize()
|
||||
@ -225,18 +225,18 @@ ES6提供字符串实例的`normalize()`方法,用来将字符的不同表示
|
||||
|
||||
上面代码表示,`NFC`参数返回字符的合成形式,`NFD`参数返回字符的分解形式。
|
||||
|
||||
不过,`normalize`方法目前不能识别三个或三个以上字符的合成。这种情况下,还是只能使用正则表达式,通过Unicode编号区间判断。
|
||||
不过,`normalize`方法目前不能识别三个或三个以上字符的合成。这种情况下,还是只能使用正则表达式,通过 Unicode 编号区间判断。
|
||||
|
||||
## includes(), startsWith(), endsWith()
|
||||
|
||||
传统上,JavaScript只有`indexOf`方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6又提供了三种新方法。
|
||||
传统上,JavaScript 只有`indexOf`方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。
|
||||
|
||||
- **includes()**:返回布尔值,表示是否找到了参数字符串。
|
||||
- **startsWith()**:返回布尔值,表示参数字符串是否在源字符串的头部。
|
||||
- **endsWith()**:返回布尔值,表示参数字符串是否在源字符串的尾部。
|
||||
- **startsWith()**:返回布尔值,表示参数字符串是否在原字符串的头部。
|
||||
- **endsWith()**:返回布尔值,表示参数字符串是否在原字符串的尾部。
|
||||
|
||||
```javascript
|
||||
var s = 'Hello world!';
|
||||
let s = 'Hello world!';
|
||||
|
||||
s.startsWith('Hello') // true
|
||||
s.endsWith('!') // true
|
||||
@ -246,7 +246,7 @@ s.includes('o') // true
|
||||
这三个方法都支持第二个参数,表示开始搜索的位置。
|
||||
|
||||
```javascript
|
||||
var s = 'Hello world!';
|
||||
let s = 'Hello world!';
|
||||
|
||||
s.startsWith('world', 6) // true
|
||||
s.endsWith('Hello', 5) // true
|
||||
@ -280,13 +280,13 @@ s.includes('Hello', 6) // false
|
||||
// RangeError
|
||||
```
|
||||
|
||||
但是,如果参数是0到-1之间的小数,则等同于0,这是因为会先进行取整运算。0到-1之间的小数,取整以后等于`-0`,`repeat`视同为0。
|
||||
但是,如果参数是 0 到-1 之间的小数,则等同于 0,这是因为会先进行取整运算。0 到-1 之间的小数,取整以后等于`-0`,`repeat`视同为 0。
|
||||
|
||||
```javascript
|
||||
'na'.repeat(-0.9) // ""
|
||||
```
|
||||
|
||||
参数`NaN`等同于0。
|
||||
参数`NaN`等同于 0。
|
||||
|
||||
```javascript
|
||||
'na'.repeat(NaN) // ""
|
||||
@ -301,7 +301,7 @@ s.includes('Hello', 6) // false
|
||||
|
||||
## padStart(),padEnd()
|
||||
|
||||
ES7推出了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。`padStart`用于头部补全,`padEnd`用于尾部补全。
|
||||
ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。`padStart()`用于头部补全,`padEnd()`用于尾部补全。
|
||||
|
||||
```javascript
|
||||
'x'.padStart(5, 'ab') // 'ababx'
|
||||
@ -327,14 +327,14 @@ ES7推出了字符串补全长度的功能。如果某个字符串不够指定
|
||||
// '0123456abc'
|
||||
```
|
||||
|
||||
如果省略第二个参数,则会用空格补全长度。
|
||||
如果省略第二个参数,默认使用空格补全长度。
|
||||
|
||||
```javascript
|
||||
'x'.padStart(4) // ' x'
|
||||
'x'.padEnd(4) // 'x '
|
||||
```
|
||||
|
||||
`padStart`的常见用途是为数值补全指定位数。下面代码生成10位的数值字符串。
|
||||
`padStart`的常见用途是为数值补全指定位数。下面代码生成 10 位的数值字符串。
|
||||
|
||||
```javascript
|
||||
'1'.padStart(10, '0') // "0000000001"
|
||||
@ -351,7 +351,7 @@ ES7推出了字符串补全长度的功能。如果某个字符串不够指定
|
||||
|
||||
## 模板字符串
|
||||
|
||||
传统的JavaScript语言,输出模板通常是这样写的。
|
||||
传统的 JavaScript 语言,输出模板通常是这样写的。
|
||||
|
||||
```javascript
|
||||
$('#result').append(
|
||||
@ -362,7 +362,7 @@ $('#result').append(
|
||||
);
|
||||
```
|
||||
|
||||
上面这种写法相当繁琐不方便,ES6引入了模板字符串解决这个问题。
|
||||
上面这种写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题。
|
||||
|
||||
```javascript
|
||||
$('#result').append(`
|
||||
@ -386,14 +386,14 @@ console.log(`string text line 1
|
||||
string text line 2`);
|
||||
|
||||
// 字符串中嵌入变量
|
||||
var name = "Bob", time = "today";
|
||||
let name = "Bob", time = "today";
|
||||
`Hello ${name}, how are you ${time}?`
|
||||
```
|
||||
|
||||
上面代码中的模板字符串,都是用反引号表示。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。
|
||||
|
||||
```javascript
|
||||
var greeting = `\`Yo\` World!`;
|
||||
let greeting = `\`Yo\` World!`;
|
||||
```
|
||||
|
||||
如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。
|
||||
@ -409,7 +409,6 @@ $('#list').html(`
|
||||
|
||||
上面代码中,所有模板字符串的空格和换行,都是被保留的,比如`<ul>`标签前面会有一个换行。如果你不想要这个换行,可以使用`trim`方法消除它。
|
||||
|
||||
|
||||
```javascript
|
||||
$('#list').html(`
|
||||
<ul>
|
||||
@ -436,11 +435,11 @@ function authorize(user, action) {
|
||||
}
|
||||
```
|
||||
|
||||
大括号内部可以放入任意的JavaScript表达式,可以进行运算,以及引用对象属性。
|
||||
大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。
|
||||
|
||||
```javascript
|
||||
var x = 1;
|
||||
var y = 2;
|
||||
let x = 1;
|
||||
let y = 2;
|
||||
|
||||
`${x} + ${y} = ${x + y}`
|
||||
// "1 + 2 = 3"
|
||||
@ -448,9 +447,9 @@ var y = 2;
|
||||
`${x} + ${y * 2} = ${x + y * 2}`
|
||||
// "1 + 4 = 5"
|
||||
|
||||
var obj = {x: 1, y: 2};
|
||||
let obj = {x: 1, y: 2};
|
||||
`${obj.x + obj.y}`
|
||||
// 3
|
||||
// "3"
|
||||
```
|
||||
|
||||
模板字符串之中还能调用函数。
|
||||
@ -470,11 +469,11 @@ function fn() {
|
||||
|
||||
```javascript
|
||||
// 变量place没有声明
|
||||
var msg = `Hello, ${place}`;
|
||||
let msg = `Hello, ${place}`;
|
||||
// 报错
|
||||
```
|
||||
|
||||
由于模板字符串的大括号内部,就是执行JavaScript代码,因此如果大括号内部是一个字符串,将会原样输出。
|
||||
由于模板字符串的大括号内部,就是执行 JavaScript 代码,因此如果大括号内部是一个字符串,将会原样输出。
|
||||
|
||||
```javascript
|
||||
`Hello ${'World'}`
|
||||
@ -533,24 +532,24 @@ func('Jack') // "Hello Jack!"
|
||||
下面,我们来看一个通过模板字符串,生成正式模板的实例。
|
||||
|
||||
```javascript
|
||||
var template = `
|
||||
let template = `
|
||||
<ul>
|
||||
<% for(var i=0; i < data.supplies.length; i++) { %>
|
||||
<% for(let i=0; i < data.supplies.length; i++) { %>
|
||||
<li><%= data.supplies[i] %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
`;
|
||||
```
|
||||
|
||||
上面代码在模板字符串之中,放置了一个常规模板。该模板使用`<%...%>`放置JavaScript代码,使用`<%= ... %>`输出JavaScript表达式。
|
||||
上面代码在模板字符串之中,放置了一个常规模板。该模板使用`<%...%>`放置 JavaScript 代码,使用`<%= ... %>`输出 JavaScript 表达式。
|
||||
|
||||
怎么编译这个模板字符串呢?
|
||||
|
||||
一种思路是将其转换为JavaScript表达式字符串。
|
||||
一种思路是将其转换为 JavaScript 表达式字符串。
|
||||
|
||||
```javascript
|
||||
echo('<ul>');
|
||||
for(var i=0; i < data.supplies.length; i++) {
|
||||
for(let i=0; i < data.supplies.length; i++) {
|
||||
echo('<li>');
|
||||
echo(data.supplies[i]);
|
||||
echo('</li>');
|
||||
@ -561,8 +560,8 @@ echo('</ul>');
|
||||
这个转换使用正则表达式就行了。
|
||||
|
||||
```javascript
|
||||
var evalExpr = /<%=(.+?)%>/g;
|
||||
var expr = /<%([\s\S]+?)%>/g;
|
||||
let evalExpr = /<%=(.+?)%>/g;
|
||||
let expr = /<%([\s\S]+?)%>/g;
|
||||
|
||||
template = template
|
||||
.replace(evalExpr, '`); \n echo( $1 ); \n echo(`')
|
||||
@ -574,9 +573,9 @@ template = 'echo(`' + template + '`);';
|
||||
然后,将`template`封装在一个函数里面返回,就可以了。
|
||||
|
||||
```javascript
|
||||
var script =
|
||||
let script =
|
||||
`(function parse(data){
|
||||
var output = "";
|
||||
let output = "";
|
||||
|
||||
function echo(html){
|
||||
output += html;
|
||||
@ -594,8 +593,8 @@ return script;
|
||||
|
||||
```javascript
|
||||
function compile(template){
|
||||
var evalExpr = /<%=(.+?)%>/g;
|
||||
var expr = /<%([\s\S]+?)%>/g;
|
||||
const evalExpr = /<%=(.+?)%>/g;
|
||||
const expr = /<%([\s\S]+?)%>/g;
|
||||
|
||||
template = template
|
||||
.replace(evalExpr, '`); \n echo( $1 ); \n echo(`')
|
||||
@ -603,9 +602,9 @@ function compile(template){
|
||||
|
||||
template = 'echo(`' + template + '`);';
|
||||
|
||||
var script =
|
||||
let script =
|
||||
`(function parse(data){
|
||||
var output = "";
|
||||
let output = "";
|
||||
|
||||
function echo(html){
|
||||
output += html;
|
||||
@ -623,7 +622,7 @@ function compile(template){
|
||||
`compile`函数的用法如下。
|
||||
|
||||
```javascript
|
||||
var parse = eval(compile(template));
|
||||
let parse = eval(compile(template));
|
||||
div.innerHTML = parse({ supplies: [ "broom", "mop", "cleaner" ] });
|
||||
// <ul>
|
||||
// <li>broom</li>
|
||||
@ -647,8 +646,8 @@ alert(123)
|
||||
但是,如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。
|
||||
|
||||
```javascript
|
||||
var a = 5;
|
||||
var b = 10;
|
||||
let a = 5;
|
||||
let b = 10;
|
||||
|
||||
tag`Hello ${ a + b } world ${ a * b }`;
|
||||
// 等同于
|
||||
@ -690,8 +689,8 @@ tag(['Hello ', ' world ', ''], 15, 50)
|
||||
我们可以按照需要编写`tag`函数的代码。下面是`tag`函数的一种写法,以及运行结果。
|
||||
|
||||
```javascript
|
||||
var a = 5;
|
||||
var b = 10;
|
||||
let a = 5;
|
||||
let b = 10;
|
||||
|
||||
function tag(s, v1, v2) {
|
||||
console.log(s[0]);
|
||||
@ -715,12 +714,12 @@ tag`Hello ${ a + b } world ${ a * b}`;
|
||||
下面是一个更复杂的例子。
|
||||
|
||||
```javascript
|
||||
var total = 30;
|
||||
var msg = passthru`The total is ${total} (${total*1.05} with tax)`;
|
||||
let total = 30;
|
||||
let msg = passthru`The total is ${total} (${total*1.05} with tax)`;
|
||||
|
||||
function passthru(literals) {
|
||||
var result = '';
|
||||
var i = 0;
|
||||
let result = '';
|
||||
let i = 0;
|
||||
|
||||
while (i < literals.length) {
|
||||
result += literals[i++];
|
||||
@ -737,12 +736,13 @@ msg // "The total is 30 (31.5 with tax)"
|
||||
|
||||
上面这个例子展示了,如何将各个参数按照原来的位置拼合回去。
|
||||
|
||||
`passthru`函数采用rest参数的写法如下。
|
||||
`passthru`函数采用 rest 参数的写法如下。
|
||||
|
||||
```javascript
|
||||
function passthru(literals, ...values) {
|
||||
var output = "";
|
||||
for (var index = 0; index < values.length; index++) {
|
||||
let output = "";
|
||||
let index;
|
||||
for (index = 0; index < values.length; index++) {
|
||||
output += literals[index] + values[index];
|
||||
}
|
||||
|
||||
@ -751,16 +751,16 @@ function passthru(literals, ...values) {
|
||||
}
|
||||
```
|
||||
|
||||
“标签模板”的一个重要应用,就是过滤HTML字符串,防止用户输入恶意内容。
|
||||
“标签模板”的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容。
|
||||
|
||||
```javascript
|
||||
var message =
|
||||
let message =
|
||||
SaferHTML`<p>${sender} has sent you a message.</p>`;
|
||||
|
||||
function SaferHTML(templateData) {
|
||||
var s = templateData[0];
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
var arg = String(arguments[i]);
|
||||
let s = templateData[0];
|
||||
for (let i = 1; i < arguments.length; i++) {
|
||||
let arg = String(arguments[i]);
|
||||
|
||||
// Escape special characters in the substitution.
|
||||
s += arg.replace(/&/g, "&")
|
||||
@ -777,14 +777,13 @@ function SaferHTML(templateData) {
|
||||
上面代码中,`sender`变量往往是用户提供的,经过`SaferHTML`函数处理,里面的特殊字符都会被转义。
|
||||
|
||||
```javascript
|
||||
var sender = '<script>alert("abc")</script>'; // 恶意代码
|
||||
var message = SaferHTML`<p>${sender} has sent you a message.</p>`;
|
||||
let sender = '<script>alert("abc")</script>'; // 恶意代码
|
||||
let message = SaferHTML`<p>${sender} has sent you a message.</p>`;
|
||||
|
||||
message
|
||||
// <p><script>alert("abc")</script> has sent you a message.</p>
|
||||
```
|
||||
|
||||
|
||||
标签模板的另一个应用,就是多语言转换(国际化处理)。
|
||||
|
||||
```javascript
|
||||
@ -792,12 +791,12 @@ i18n`Welcome to ${siteName}, you are visitor number ${visitorNumber}!`
|
||||
// "欢迎访问xxx,您是第xxxx位访问者!"
|
||||
```
|
||||
|
||||
模板字符串本身并不能取代Mustache之类的模板库,因为没有条件判断和循环处理功能,但是通过标签函数,你可以自己添加这些功能。
|
||||
模板字符串本身并不能取代 Mustache 之类的模板库,因为没有条件判断和循环处理功能,但是通过标签函数,你可以自己添加这些功能。
|
||||
|
||||
```javascript
|
||||
// 下面的hashTemplate函数
|
||||
// 是一个自定义的模板处理函数
|
||||
var libraryHtml = hashTemplate`
|
||||
let libraryHtml = hashTemplate`
|
||||
<ul>
|
||||
#for book in ${myBooks}
|
||||
<li><i>#{book.title}</i> by #{book.author}</li>
|
||||
@ -806,7 +805,7 @@ var libraryHtml = hashTemplate`
|
||||
`;
|
||||
```
|
||||
|
||||
除此之外,你甚至可以使用标签模板,在JavaScript语言之中嵌入其他语言。
|
||||
除此之外,你甚至可以使用标签模板,在 JavaScript 语言之中嵌入其他语言。
|
||||
|
||||
```javascript
|
||||
jsx`
|
||||
@ -820,9 +819,9 @@ jsx`
|
||||
`
|
||||
```
|
||||
|
||||
上面的代码通过`jsx`函数,将一个DOM字符串转为React对象。你可以在Github找到`jsx`函数的[具体实现](https://gist.github.com/lygaret/a68220defa69174bdec5)。
|
||||
上面的代码通过`jsx`函数,将一个 DOM 字符串转为 React 对象。你可以在 Github 找到`jsx`函数的[具体实现](https://gist.github.com/lygaret/a68220defa69174bdec5)。
|
||||
|
||||
下面则是一个假想的例子,通过`java`函数,在JavaScript代码之中运行Java代码。
|
||||
下面则是一个假想的例子,通过`java`函数,在 JavaScript 代码之中运行 Java 代码。
|
||||
|
||||
```javascript
|
||||
java`
|
||||
@ -851,15 +850,16 @@ tag`First line\nSecond line`
|
||||
|
||||
function tag(strings) {
|
||||
console.log(strings.raw[0]);
|
||||
// "First line\\nSecond line"
|
||||
// strings.raw[0] 为 "First line\\nSecond line"
|
||||
// 打印输出 "First line\nSecond line"
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,`tag`函数的第一个参数`strings`,有一个`raw`属性,也指向一个数组。该数组的成员与`strings`数组完全一致。比如,`strings`数组是`["First line\nSecond line"]`,那么`strings.raw`数组就是`["First line\\nSecond line"]`。两者唯一的区别,就是字符串里面的斜杠都被转义了。比如,strings.raw数组会将`\n`视为`\\`和`n`两个字符,而不是换行符。这是为了方便取得转义之前的原始模板而设计的。
|
||||
上面代码中,`tag`函数的第一个参数`strings`,有一个`raw`属性,也指向一个数组。该数组的成员与`strings`数组完全一致。比如,`strings`数组是`["First line\nSecond line"]`,那么`strings.raw`数组就是`["First line\\nSecond line"]`。两者唯一的区别,就是字符串里面的斜杠都被转义了。比如,strings.raw 数组会将`\n`视为`\\`和`n`两个字符,而不是换行符。这是为了方便取得转义之前的原始模板而设计的。
|
||||
|
||||
## String.raw()
|
||||
|
||||
ES6还为原生的String对象,提供了一个`raw`方法。
|
||||
ES6 还为原生的 String 对象,提供了一个`raw`方法。
|
||||
|
||||
`String.raw`方法,往往用来充当模板字符串的处理函数,返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,对应于替换变量后的模板字符串。
|
||||
|
||||
@ -882,8 +882,9 @@ String.raw`Hi\\n`
|
||||
|
||||
```javascript
|
||||
String.raw = function (strings, ...values) {
|
||||
var output = "";
|
||||
for (var index = 0; index < values.length; index++) {
|
||||
let output = "";
|
||||
let index;
|
||||
for (index = 0; index < values.length; index++) {
|
||||
output += strings.raw[index] + values[index];
|
||||
}
|
||||
|
||||
@ -906,9 +907,9 @@ String.raw({ raw: ['t','e','s','t'] }, 0, 1, 2);
|
||||
|
||||
## 模板字符串的限制
|
||||
|
||||
前面提到标签模板里面,可以内嵌其他语言。但是,模板字符串默认会将字符串转义,因此导致了无法嵌入其他语言。
|
||||
前面提到标签模板里面,可以内嵌其他语言。但是,模板字符串默认会将字符串转义,导致无法嵌入其他语言。
|
||||
|
||||
举例来说,在标签模板里面可以嵌入Latex语言。
|
||||
举例来说,标签模板里面可以嵌入 LaTEX 语言。
|
||||
|
||||
```javascript
|
||||
function latex(strings) {
|
||||
@ -924,9 +925,9 @@ Breve over the h goes \u{h}ere // 报错
|
||||
`
|
||||
```
|
||||
|
||||
上面代码中,变量`document`内嵌的模板字符串,对于Latex语言来说完全是合法的,但是JavaScript引擎会报错。原因就在于字符串的转义。
|
||||
上面代码中,变量`document`内嵌的模板字符串,对于 LaTEX 语言来说完全是合法的,但是 JavaScript 引擎会报错。原因就在于字符串的转义。
|
||||
|
||||
模板字符串会将`\u00FF`和`\u{42}`当作Unicode字符进行转义,所以`\unicode`解析时报错;而`\x56`会被当作十六进制字符串转义,所以`\xerxes`会报错。
|
||||
模板字符串会将`\u00FF`和`\u{42}`当作 Unicode 字符进行转义,所以`\unicode`解析时报错;而`\x56`会被当作十六进制字符串转义,所以`\xerxes`会报错。也就是说,`\u`和`\x`在 LaTEX 里面有特殊含义,但是 JavaScript 将它们转义了。
|
||||
|
||||
为了解决这个问题,现在有一个[提案](https://tc39.github.io/proposal-template-literal-revision/),放松对标签模板里面的字符串转义的限制。如果遇到不合法的字符串转义,就返回`undefined`,而不是报错,并且从`raw`属性上面可以得到原始字符串。
|
||||
|
||||
@ -938,7 +939,7 @@ function tag(strs) {
|
||||
tag`\unicode and \u{55}`
|
||||
```
|
||||
|
||||
上面代码中,模板字符串原本是应该报错的,但是由于放松了对字符串转义的限制,所以不报错了,JavaScript引擎将第一个字符设置为`undefined`,但是`raw`属性依然可以得到原始字符串,因此`tag`函数还是可以对原字符串进行处理。
|
||||
上面代码中,模板字符串原本是应该报错的,但是由于放松了对字符串转义的限制,所以不报错了,JavaScript 引擎将第一个字符设置为`undefined`,但是`raw`属性依然可以得到原始字符串,因此`tag`函数还是可以对原字符串进行处理。
|
||||
|
||||
注意,这种对字符串转义的放松,只在标签模板解析字符串时生效,不是标签模板的场合,依然会报错。
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
# 编程风格
|
||||
|
||||
本章探讨如何将ES6的新语法,运用到编码实践之中,与传统的JavaScript语法结合在一起,写出合理的、易于阅读和维护的代码。
|
||||
本章探讨如何将 ES6 的新语法,运用到编码实践之中,与传统的 JavaScript 语法结合在一起,写出合理的、易于阅读和维护的代码。
|
||||
|
||||
多家公司和组织已经公开了它们的风格规范,具体可参阅[jscs.info](http://jscs.info/),下面的内容主要参考了[Airbnb](https://github.com/airbnb/javascript)的JavaScript风格规范。
|
||||
多家公司和组织已经公开了它们的风格规范,下面的内容主要参考了 [Airbnb](https://github.com/airbnb/javascript) 公司的 JavaScript 风格规范。
|
||||
|
||||
## 块级作用域
|
||||
|
||||
**(1)let 取代 var**
|
||||
|
||||
ES6提出了两个新的声明变量的命令:`let`和`const`。其中,`let`完全可以取代`var`,因为两者语义相同,而且`let`没有副作用。
|
||||
ES6 提出了两个新的声明变量的命令:`let`和`const`。其中,`let`完全可以取代`var`,因为两者语义相同,而且`let`没有副作用。
|
||||
|
||||
```javascript
|
||||
'use strict';
|
||||
@ -29,7 +29,7 @@ for (let i = 0; i < 10; i++) {
|
||||
```javascript
|
||||
'use strict';
|
||||
|
||||
if(true) {
|
||||
if (true) {
|
||||
console.log(x); // ReferenceError
|
||||
let x = 'hello';
|
||||
}
|
||||
@ -43,7 +43,7 @@ if(true) {
|
||||
|
||||
在`let`和`const`之间,建议优先使用`const`,尤其是在全局环境,不应该设置变量,只应设置常量。
|
||||
|
||||
`const`优于`let`有几个原因。一个是`const`可以提醒阅读程序的人,这个变量不应该改变;另一个是`const`比较符合函数式编程思想,运算不改变值,只是新建值,而且这样也有利于将来的分布式运算;最后一个原因是 JavaScript 编译器会对`const`进行优化,所以多使用`const`,有利于提供程序的运行效率,也就是说`let`和`const`的本质区别,其实是编译器内部的处理不同。
|
||||
`const`优于`let`有几个原因。一个是`const`可以提醒阅读程序的人,这个变量不应该改变;另一个是`const`比较符合函数式编程思想,运算不改变值,只是新建值,而且这样也有利于将来的分布式运算;最后一个原因是 JavaScript 编译器会对`const`进行优化,所以多使用`const`,有利于提高程序的运行效率,也就是说`let`和`const`的本质区别,其实是编译器内部的处理不同。
|
||||
|
||||
```javascript
|
||||
// bad
|
||||
@ -62,7 +62,7 @@ const [a, b, c] = [1, 2, 3];
|
||||
|
||||
所有的函数都应该设置为常量。
|
||||
|
||||
长远来看,JavaScript可能会有多线程的实现(比如Intel的River Trail那一类的项目),这时`let`表示的变量,只应出现在单线程运行的代码中,不能是多线程共享的,这样有利于保证线程安全。
|
||||
长远来看,JavaScript 可能会有多线程的实现(比如 Intel 公司的 River Trail 那一类的项目),这时`let`表示的变量,只应出现在单线程运行的代码中,不能是多线程共享的,这样有利于保证线程安全。
|
||||
|
||||
## 字符串
|
||||
|
||||
@ -234,7 +234,7 @@ for (i = 0; i < len; i++) {
|
||||
const itemsCopy = [...items];
|
||||
```
|
||||
|
||||
使用Array.from方法,将类似数组的对象转为数组。
|
||||
使用 Array.from 方法,将类似数组的对象转为数组。
|
||||
|
||||
```javascript
|
||||
const foo = document.querySelectorAll('.foo');
|
||||
@ -251,7 +251,7 @@ const nodes = Array.from(foo);
|
||||
})();
|
||||
```
|
||||
|
||||
那些需要使用函数表达式的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了this。
|
||||
那些需要使用函数表达式的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了 this。
|
||||
|
||||
```javascript
|
||||
// bad
|
||||
@ -268,7 +268,7 @@ const nodes = Array.from(foo);
|
||||
[1, 2, 3].map(x => x * x);
|
||||
```
|
||||
|
||||
箭头函数取代`Function.prototype.bind`,不应再用self/\_this/that绑定 this。
|
||||
箭头函数取代`Function.prototype.bind`,不应再用 self/\_this/that 绑定 this。
|
||||
|
||||
```javascript
|
||||
// bad
|
||||
@ -298,7 +298,7 @@ function divide(a, b, { option = false } = {}) {
|
||||
}
|
||||
```
|
||||
|
||||
不要在函数体内使用arguments变量,使用rest运算符(...)代替。因为rest运算符显式表明你想要获取参数,而且arguments是一个类似数组的对象,而rest运算符可以提供一个真正的数组。
|
||||
不要在函数体内使用 arguments 变量,使用 rest 运算符(...)代替。因为 rest 运算符显式表明你想要获取参数,而且 arguments 是一个类似数组的对象,而 rest 运算符可以提供一个真正的数组。
|
||||
|
||||
```javascript
|
||||
// bad
|
||||
@ -327,9 +327,9 @@ function handleThings(opts = {}) {
|
||||
}
|
||||
```
|
||||
|
||||
## Map结构
|
||||
## Map 结构
|
||||
|
||||
注意区分Object和Map,只有模拟现实世界的实体对象时,才使用Object。如果只是需要`key: value`的数据结构,使用Map结构。因为Map有内建的遍历机制。
|
||||
注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要`key: value`的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。
|
||||
|
||||
```javascript
|
||||
let map = new Map(arr);
|
||||
@ -349,7 +349,7 @@ for (let item of map.entries()) {
|
||||
|
||||
## Class
|
||||
|
||||
总是用Class,取代需要prototype的操作。因为Class的写法更简洁,更易于理解。
|
||||
总是用 Class,取代需要 prototype 的操作。因为 Class 的写法更简洁,更易于理解。
|
||||
|
||||
```javascript
|
||||
// bad
|
||||
@ -398,7 +398,7 @@ class PeekableQueue extends Queue {
|
||||
|
||||
## 模块
|
||||
|
||||
首先,Module语法是JavaScript模块的标准写法,坚持使用这种写法。使用`import`取代`require`。
|
||||
首先,Module 语法是 JavaScript 模块的标准写法,坚持使用这种写法。使用`import`取代`require`。
|
||||
|
||||
```javascript
|
||||
// bad
|
||||
@ -427,22 +427,22 @@ module.exports = Breadcrumbs;
|
||||
// ES6的写法
|
||||
import React from 'react';
|
||||
|
||||
const Breadcrumbs = React.createClass({
|
||||
class Breadcrumbs extends React.Component {
|
||||
render() {
|
||||
return <nav />;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default Breadcrumbs
|
||||
export default Breadcrumbs;
|
||||
```
|
||||
|
||||
如果模块只有一个输出值,就使用`export default`,如果模块有多个输出值,就不使用`export default`,不要`export default`与普通的`export`同时使用。
|
||||
如果模块只有一个输出值,就使用`export default`,如果模块有多个输出值,就不使用`export default`,`export default`与普通的`export`不要同时使用。
|
||||
|
||||
不要在模块输入中使用通配符。因为这样可以确保你的模块之中,有一个默认输出(export default)。
|
||||
|
||||
```javascript
|
||||
// bad
|
||||
import * as myObject './importModule';
|
||||
import * as myObject from './importModule';
|
||||
|
||||
// good
|
||||
import myObject from './importModule';
|
||||
@ -468,23 +468,24 @@ const StyleGuide = {
|
||||
export default StyleGuide;
|
||||
```
|
||||
|
||||
## ESLint的使用
|
||||
## ESLint 的使用
|
||||
|
||||
ESLint是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。
|
||||
ESLint 是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。
|
||||
|
||||
首先,安装ESLint。
|
||||
首先,安装 ESLint。
|
||||
|
||||
```bash
|
||||
$ npm i -g eslint
|
||||
```
|
||||
|
||||
然后,安装Airbnb语法规则。
|
||||
然后,安装 Airbnb 语法规则,以及 import、a11y、react 插件。
|
||||
|
||||
```bash
|
||||
$ npm i -g eslint-config-airbnb
|
||||
$ npm i -g eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react
|
||||
```
|
||||
|
||||
最后,在项目的根目录下新建一个`.eslintrc`文件,配置ESLint。
|
||||
最后,在项目的根目录下新建一个`.eslintrc`文件,配置 ESLint。
|
||||
|
||||
```javascript
|
||||
{
|
||||
@ -507,16 +508,18 @@ function greet() {
|
||||
greet();
|
||||
```
|
||||
|
||||
使用ESLint检查这个文件。
|
||||
使用 ESLint 检查这个文件,就会报出错误。
|
||||
|
||||
```bash
|
||||
$ eslint index.js
|
||||
index.js
|
||||
1:1 error Unexpected var, use let or const instead no-var
|
||||
1:5 error unusued is defined but never used no-unused-vars
|
||||
4:5 error Expected indentation of 2 characters but found 4 indent
|
||||
4:5 error Unexpected var, use let or const instead no-var
|
||||
5:5 error Expected indentation of 2 characters but found 4 indent
|
||||
|
||||
✖ 3 problems (3 errors, 0 warnings)
|
||||
✖ 5 problems (5 errors, 0 warnings)
|
||||
```
|
||||
|
||||
上面代码说明,原文件有三个错误,一个是定义了变量,却没有使用,另外两个是行首缩进为4个空格,而不是规定的2个空格。
|
||||
上面代码说明,原文件有五个错误,其中两个是不应该使用`var`命令,而要使用`let`或`const`;一个是定义了变量,却没有使用;另外两个是行首缩进为 4 个空格,而不是规定的 2 个空格。
|
||||
|
272
docs/symbol.md
272
docs/symbol.md
@ -2,11 +2,11 @@
|
||||
|
||||
## 概述
|
||||
|
||||
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 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
|
||||
|
||||
```javascript
|
||||
let s = Symbol();
|
||||
@ -15,15 +15,15 @@ typeof s
|
||||
// "symbol"
|
||||
```
|
||||
|
||||
上面代码中,变量`s`就是一个独一无二的值。`typeof`运算符的结果,表明变量`s`是Symbol数据类型,而不是字符串之类的其他类型。
|
||||
上面代码中,变量`s`就是一个独一无二的值。`typeof`运算符的结果,表明变量`s`是 Symbol 数据类型,而不是字符串之类的其他类型。
|
||||
|
||||
注意,`Symbol`函数前不能使用`new`命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,不是对象。也就是说,由于Symbol值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。
|
||||
注意,`Symbol`函数前不能使用`new`命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。
|
||||
|
||||
`Symbol`函数可以接受一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
|
||||
`Symbol`函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
|
||||
|
||||
```javascript
|
||||
var s1 = Symbol('foo');
|
||||
var s2 = Symbol('bar');
|
||||
let s1 = Symbol('foo');
|
||||
let s2 = Symbol('bar');
|
||||
|
||||
s1 // Symbol(foo)
|
||||
s2 // Symbol(bar)
|
||||
@ -32,7 +32,7 @@ s1.toString() // "Symbol(foo)"
|
||||
s2.toString() // "Symbol(bar)"
|
||||
```
|
||||
|
||||
上面代码中,`s1`和`s2`是两个Symbol值。如果不加参数,它们在控制台的输出都是`Symbol()`,不利于区分。有了参数以后,就等于为它们加上了描述,输出的时候就能够分清,到底是哪一个值。
|
||||
上面代码中,`s1`和`s2`是两个 Symbol 值。如果不加参数,它们在控制台的输出都是`Symbol()`,不利于区分。有了参数以后,就等于为它们加上了描述,输出的时候就能够分清,到底是哪一个值。
|
||||
|
||||
如果 Symbol 的参数是一个对象,就会调用该对象的`toString`方法,将其转为字符串,然后才生成一个 Symbol 值。
|
||||
|
||||
@ -50,24 +50,24 @@ sym // Symbol(abc)
|
||||
|
||||
```javascript
|
||||
// 没有参数的情况
|
||||
var s1 = Symbol();
|
||||
var s2 = Symbol();
|
||||
let s1 = Symbol();
|
||||
let s2 = Symbol();
|
||||
|
||||
s1 === s2 // false
|
||||
|
||||
// 有参数的情况
|
||||
var s1 = Symbol('foo');
|
||||
var s2 = Symbol('foo');
|
||||
let s1 = Symbol('foo');
|
||||
let s2 = Symbol('foo');
|
||||
|
||||
s1 === s2 // false
|
||||
```
|
||||
|
||||
上面代码中,`s1`和`s2`都是`Symbol`函数的返回值,而且参数相同,但是它们是不相等的。
|
||||
|
||||
Symbol值不能与其他类型的值进行运算,会报错。
|
||||
Symbol 值不能与其他类型的值进行运算,会报错。
|
||||
|
||||
```javascript
|
||||
var sym = Symbol('My symbol');
|
||||
let sym = Symbol('My symbol');
|
||||
|
||||
"your symbol is " + sym
|
||||
// TypeError: can't convert symbol to string
|
||||
@ -75,19 +75,19 @@ var sym = Symbol('My symbol');
|
||||
// TypeError: can't convert symbol to string
|
||||
```
|
||||
|
||||
但是,Symbol值可以显式转为字符串。
|
||||
但是,Symbol 值可以显式转为字符串。
|
||||
|
||||
```javascript
|
||||
var sym = Symbol('My symbol');
|
||||
let sym = Symbol('My symbol');
|
||||
|
||||
String(sym) // 'Symbol(My symbol)'
|
||||
sym.toString() // 'Symbol(My symbol)'
|
||||
```
|
||||
|
||||
另外,Symbol值也可以转为布尔值,但是不能转为数值。
|
||||
另外,Symbol 值也可以转为布尔值,但是不能转为数值。
|
||||
|
||||
```javascript
|
||||
var sym = Symbol();
|
||||
let sym = Symbol();
|
||||
Boolean(sym) // true
|
||||
!sym // false
|
||||
|
||||
@ -99,46 +99,46 @@ Number(sym) // TypeError
|
||||
sym + 2 // TypeError
|
||||
```
|
||||
|
||||
## 作为属性名的Symbol
|
||||
## 作为属性名的 Symbol
|
||||
|
||||
由于每一个Symbol值都是不相等的,这意味着Symbol值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。
|
||||
由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。
|
||||
|
||||
```javascript
|
||||
var mySymbol = Symbol();
|
||||
let mySymbol = Symbol();
|
||||
|
||||
// 第一种写法
|
||||
var a = {};
|
||||
let a = {};
|
||||
a[mySymbol] = 'Hello!';
|
||||
|
||||
// 第二种写法
|
||||
var a = {
|
||||
let a = {
|
||||
[mySymbol]: 'Hello!'
|
||||
};
|
||||
|
||||
// 第三种写法
|
||||
var a = {};
|
||||
let a = {};
|
||||
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
|
||||
|
||||
// 以上写法都得到同样结果
|
||||
a[mySymbol] // "Hello!"
|
||||
```
|
||||
|
||||
上面代码通过方括号结构和`Object.defineProperty`,将对象的属性名指定为一个Symbol值。
|
||||
上面代码通过方括号结构和`Object.defineProperty`,将对象的属性名指定为一个 Symbol 值。
|
||||
|
||||
注意,Symbol值作为对象属性名时,不能用点运算符。
|
||||
注意,Symbol 值作为对象属性名时,不能用点运算符。
|
||||
|
||||
```javascript
|
||||
var mySymbol = Symbol();
|
||||
var a = {};
|
||||
const mySymbol = Symbol();
|
||||
const a = {};
|
||||
|
||||
a.mySymbol = 'Hello!';
|
||||
a[mySymbol] // undefined
|
||||
a['mySymbol'] // "Hello!"
|
||||
```
|
||||
|
||||
上面代码中,因为点运算符后面总是字符串,所以不会读取`mySymbol`作为标识名所指代的那个值,导致`a`的属性名实际上是一个字符串,而不是一个Symbol值。
|
||||
上面代码中,因为点运算符后面总是字符串,所以不会读取`mySymbol`作为标识名所指代的那个值,导致`a`的属性名实际上是一个字符串,而不是一个 Symbol 值。
|
||||
|
||||
同理,在对象的内部,使用Symbol值定义属性时,Symbol值必须放在方括号之中。
|
||||
同理,在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。
|
||||
|
||||
```javascript
|
||||
let s = Symbol();
|
||||
@ -150,7 +150,7 @@ let obj = {
|
||||
obj[s](123);
|
||||
```
|
||||
|
||||
上面代码中,如果`s`不放在方括号中,该属性的键名就是字符串`s`,而不是`s`所代表的那个Symbol值。
|
||||
上面代码中,如果`s`不放在方括号中,该属性的键名就是字符串`s`,而不是`s`所代表的那个 Symbol 值。
|
||||
|
||||
采用增强的对象写法,上面代码的`obj`对象可以写得更简洁一些。
|
||||
|
||||
@ -160,7 +160,7 @@ let obj = {
|
||||
};
|
||||
```
|
||||
|
||||
Symbol类型还可以用于定义一组常量,保证这组常量的值都是不相等的。
|
||||
Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的。
|
||||
|
||||
```javascript
|
||||
log.levels = {
|
||||
@ -190,17 +190,17 @@ function getComplement(color) {
|
||||
}
|
||||
```
|
||||
|
||||
常量使用Symbol值最大的好处,就是其他任何值都不可能有相同的值了,因此可以保证上面的`switch`语句会按设计的方式工作。
|
||||
常量使用 Symbol 值最大的好处,就是其他任何值都不可能有相同的值了,因此可以保证上面的`switch`语句会按设计的方式工作。
|
||||
|
||||
还有一点需要注意,Symbol值作为属性名时,该属性还是公开属性,不是私有属性。
|
||||
还有一点需要注意,Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。
|
||||
|
||||
## 实例:消除魔术字符串
|
||||
|
||||
魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,该由含义清晰的变量代替。
|
||||
魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。
|
||||
|
||||
```javascript
|
||||
function getArea(shape, options) {
|
||||
var area = 0;
|
||||
let area = 0;
|
||||
|
||||
switch (shape) {
|
||||
case 'Triangle': // 魔术字符串
|
||||
@ -215,17 +215,17 @@ function getArea(shape, options) {
|
||||
getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串
|
||||
```
|
||||
|
||||
上面代码中,字符串“Triangle”就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。
|
||||
上面代码中,字符串`Triangle`就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。
|
||||
|
||||
常用的消除魔术字符串的方法,就是把它写成一个变量。
|
||||
|
||||
```javascript
|
||||
var shapeType = {
|
||||
const shapeType = {
|
||||
triangle: 'Triangle'
|
||||
};
|
||||
|
||||
function getArea(shape, options) {
|
||||
var area = 0;
|
||||
let area = 0;
|
||||
switch (shape) {
|
||||
case shapeType.triangle:
|
||||
area = .5 * options.width * options.height;
|
||||
@ -237,9 +237,9 @@ function getArea(shape, options) {
|
||||
getArea(shapeType.triangle, { width: 100, height: 100 });
|
||||
```
|
||||
|
||||
上面代码中,我们把“Triangle”写成`shapeType`对象的`triangle`属性,这样就消除了强耦合。
|
||||
上面代码中,我们把`Triangle`写成`shapeType`对象的`triangle`属性,这样就消除了强耦合。
|
||||
|
||||
如果仔细分析,可以发现`shapeType.triangle`等于哪个值并不重要,只要确保不会跟其他`shapeType`属性的值冲突即可。因此,这里就很适合改用Symbol值。
|
||||
如果仔细分析,可以发现`shapeType.triangle`等于哪个值并不重要,只要确保不会跟其他`shapeType`属性的值冲突即可。因此,这里就很适合改用 Symbol 值。
|
||||
|
||||
```javascript
|
||||
const shapeType = {
|
||||
@ -247,7 +247,7 @@ const shapeType = {
|
||||
};
|
||||
```
|
||||
|
||||
上面代码中,除了将`shapeType.triangle`的值设为一个Symbol,其他地方都不用修改。
|
||||
上面代码中,除了将`shapeType.triangle`的值设为一个 Symbol,其他地方都不用修改。
|
||||
|
||||
## 属性名的遍历
|
||||
|
||||
@ -256,14 +256,14 @@ Symbol 作为属性名,该属性不会出现在`for...in`、`for...of`循环
|
||||
`Object.getOwnPropertySymbols`方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
|
||||
|
||||
```javascript
|
||||
var obj = {};
|
||||
var a = Symbol('a');
|
||||
var b = Symbol('b');
|
||||
const obj = {};
|
||||
let a = Symbol('a');
|
||||
let b = Symbol('b');
|
||||
|
||||
obj[a] = 'Hello';
|
||||
obj[b] = 'World';
|
||||
|
||||
var objectSymbols = Object.getOwnPropertySymbols(obj);
|
||||
const objectSymbols = Object.getOwnPropertySymbols(obj);
|
||||
|
||||
objectSymbols
|
||||
// [Symbol(a), Symbol(b)]
|
||||
@ -272,15 +272,15 @@ objectSymbols
|
||||
下面是另一个例子,`Object.getOwnPropertySymbols`方法与`for...in`循环、`Object.getOwnPropertyNames`方法进行对比的例子。
|
||||
|
||||
```javascript
|
||||
var obj = {};
|
||||
const obj = {};
|
||||
|
||||
var foo = Symbol("foo");
|
||||
let foo = Symbol("foo");
|
||||
|
||||
Object.defineProperty(obj, foo, {
|
||||
value: "foobar",
|
||||
});
|
||||
|
||||
for (var i in obj) {
|
||||
for (let i in obj) {
|
||||
console.log(i); // 无输出
|
||||
}
|
||||
|
||||
@ -293,7 +293,7 @@ Object.getOwnPropertySymbols(obj)
|
||||
|
||||
上面代码中,使用`Object.getOwnPropertyNames`方法得不到`Symbol`属性名,需要使用`Object.getOwnPropertySymbols`方法。
|
||||
|
||||
另一个新的API,`Reflect.ownKeys`方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
|
||||
另一个新的 API,`Reflect.ownKeys`方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
|
||||
|
||||
```javascript
|
||||
let obj = {
|
||||
@ -303,13 +303,13 @@ let obj = {
|
||||
};
|
||||
|
||||
Reflect.ownKeys(obj)
|
||||
// [Symbol(my_key), 'enum', 'nonEnum']
|
||||
// ["enum", "nonEnum", Symbol(my_key)]
|
||||
```
|
||||
|
||||
由于以 Symbol 值作为名称的属性,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。
|
||||
|
||||
```javascript
|
||||
var size = Symbol('size');
|
||||
let size = Symbol('size');
|
||||
|
||||
class Collection {
|
||||
constructor() {
|
||||
@ -326,7 +326,7 @@ class Collection {
|
||||
}
|
||||
}
|
||||
|
||||
var x = new Collection();
|
||||
let x = new Collection();
|
||||
Collection.sizeOf(x) // 0
|
||||
|
||||
x.add('foo');
|
||||
@ -341,18 +341,18 @@ Object.getOwnPropertySymbols(x) // [Symbol(size)]
|
||||
|
||||
## Symbol.for(),Symbol.keyFor()
|
||||
|
||||
有时,我们希望重新使用同一个Symbol值,`Symbol.for`方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值。
|
||||
有时,我们希望重新使用同一个 Symbol 值,`Symbol.for`方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。
|
||||
|
||||
```javascript
|
||||
var s1 = Symbol.for('foo');
|
||||
var s2 = Symbol.for('foo');
|
||||
let s1 = Symbol.for('foo');
|
||||
let s2 = Symbol.for('foo');
|
||||
|
||||
s1 === s2 // true
|
||||
```
|
||||
|
||||
上面代码中,`s1`和`s2`都是 Symbol 值,但是它们都是同样参数的`Symbol.for`方法生成的,所以实际上是同一个值。
|
||||
|
||||
`Symbol.for()`与`Symbol()`这两种写法,都会生成新的Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。`Symbol.for()`不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的`key`是否已经存在,如果不存在才会新建一个值。比如,如果你调用`Symbol.for("cat")`30次,每次都会返回同一个 Symbol 值,但是调用`Symbol("cat")`30次,会返回30个不同的Symbol值。
|
||||
`Symbol.for()`与`Symbol()`这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。`Symbol.for()`不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的`key`是否已经存在,如果不存在才会新建一个值。比如,如果你调用`Symbol.for("cat")`30 次,每次都会返回同一个 Symbol 值,但是调用`Symbol("cat")`30 次,会返回 30 个不同的 Symbol 值。
|
||||
|
||||
```javascript
|
||||
Symbol.for("bar") === Symbol.for("bar")
|
||||
@ -367,16 +367,16 @@ Symbol("bar") === Symbol("bar")
|
||||
`Symbol.keyFor`方法返回一个已登记的 Symbol 类型值的`key`。
|
||||
|
||||
```javascript
|
||||
var s1 = Symbol.for("foo");
|
||||
let s1 = Symbol.for("foo");
|
||||
Symbol.keyFor(s1) // "foo"
|
||||
|
||||
var s2 = Symbol("foo");
|
||||
let s2 = Symbol("foo");
|
||||
Symbol.keyFor(s2) // undefined
|
||||
```
|
||||
|
||||
上面代码中,变量`s2`属于未登记的Symbol值,所以返回`undefined`。
|
||||
上面代码中,变量`s2`属于未登记的 Symbol 值,所以返回`undefined`。
|
||||
|
||||
需要注意的是,`Symbol.for`为Symbol值登记的名字,是全局环境的,可以在不同的 iframe 或 service worker 中取到同一个值。
|
||||
需要注意的是,`Symbol.for`为 Symbol 值登记的名字,是全局环境的,可以在不同的 iframe 或 service worker 中取到同一个值。
|
||||
|
||||
```javascript
|
||||
iframe = document.createElement('iframe');
|
||||
@ -391,9 +391,9 @@ iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo')
|
||||
|
||||
## 实例:模块的 Singleton 模式
|
||||
|
||||
Singleton模式指的是调用一个类,任何时候返回的都是同一个实例。
|
||||
Singleton 模式指的是调用一个类,任何时候返回的都是同一个实例。
|
||||
|
||||
对于Node来说,模块文件可以看成是一个类。怎么保证每次执行这个模块文件,返回的都是同一个实例呢?
|
||||
对于 Node 来说,模块文件可以看成是一个类。怎么保证每次执行这个模块文件,返回的都是同一个实例呢?
|
||||
|
||||
很容易想到,可以把实例放到顶层对象`global`。
|
||||
|
||||
@ -413,7 +413,7 @@ module.exports = global._foo;
|
||||
然后,加载上面的`mod.js`。
|
||||
|
||||
```javascript
|
||||
var a = require('./mod.js');
|
||||
const a = require('./mod.js');
|
||||
console.log(a.foo);
|
||||
```
|
||||
|
||||
@ -422,13 +422,13 @@ console.log(a.foo);
|
||||
但是,这里有一个问题,全局变量`global._foo`是可写的,任何文件都可以修改。
|
||||
|
||||
```javascript
|
||||
var a = require('./mod.js');
|
||||
const a = require('./mod.js');
|
||||
global._foo = 123;
|
||||
```
|
||||
|
||||
上面的代码,会使得别的脚本加载`mod.js`都失真。
|
||||
|
||||
为了防止这种情况出现,我们就可以使用Symbol。
|
||||
为了防止这种情况出现,我们就可以使用 Symbol。
|
||||
|
||||
```javascript
|
||||
// mod.js
|
||||
@ -448,7 +448,7 @@ module.exports = global[FOO_KEY];
|
||||
上面代码中,可以保证`global[FOO_KEY]`不会被无意间覆盖,但还是可以被改写。
|
||||
|
||||
```javascript
|
||||
var a = require('./mod.js');
|
||||
const a = require('./mod.js');
|
||||
global[Symbol.for('foo')] = 123;
|
||||
```
|
||||
|
||||
@ -461,11 +461,11 @@ const FOO_KEY = Symbol('foo');
|
||||
// 后面代码相同 ……
|
||||
```
|
||||
|
||||
上面代码将导致其他脚本都无法引用`FOO_KEY`。但这样也有一个问题,就是如果多次执行这个脚本,每次得到的`FOO_KEY`都是不一样的。虽然Node会将脚本的执行结果缓存,一般情况下,不会多次执行同一个脚本,但是用户可以手动清除缓存,所以也不是完全可靠。
|
||||
上面代码将导致其他脚本都无法引用`FOO_KEY`。但这样也有一个问题,就是如果多次执行这个脚本,每次得到的`FOO_KEY`都是不一样的。虽然 Node 会将脚本的执行结果缓存,一般情况下,不会多次执行同一个脚本,但是用户可以手动清除缓存,所以也不是完全可靠。
|
||||
|
||||
## 内置的Symbol值
|
||||
## 内置的 Symbol 值
|
||||
|
||||
除了定义自己使用的Symbol值以外,ES6还提供了11个内置的Symbol值,指向语言内部使用的方法。
|
||||
除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。
|
||||
|
||||
### Symbol.hasInstance
|
||||
|
||||
@ -492,6 +492,13 @@ class Even {
|
||||
}
|
||||
}
|
||||
|
||||
// 等同于
|
||||
const Even = {
|
||||
[Symbol.hasInstance](obj) {
|
||||
return Number(obj) % 2 === 0;
|
||||
}
|
||||
};
|
||||
|
||||
1 instanceof Even // false
|
||||
2 instanceof Even // true
|
||||
12345 instanceof Even // false
|
||||
@ -499,7 +506,7 @@ class Even {
|
||||
|
||||
### Symbol.isConcatSpreadable
|
||||
|
||||
对象的`Symbol.isConcatSpreadable`属性等于一个布尔值,表示该对象使用`Array.prototype.concat()`时,是否可以展开。
|
||||
对象的`Symbol.isConcatSpreadable`属性等于一个布尔值,表示该对象用于`Array.prototype.concat()`时,是否可以展开。
|
||||
|
||||
```javascript
|
||||
let arr1 = ['c', 'd'];
|
||||
@ -511,9 +518,9 @@ arr2[Symbol.isConcatSpreadable] = false;
|
||||
['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']
|
||||
```
|
||||
|
||||
上面代码说明,数组的默认行为是可以展开。`Symbol.isConcatSpreadable`属性等于`true`或`undefined`,都有这个效果。
|
||||
上面代码说明,数组的默认行为是可以展开,`Symbol.isConcatSpreadable`默认等于`undefined`。该属性等于`true`时,也有展开的效果。
|
||||
|
||||
类似数组的对象也可以展开,但它的`Symbol.isConcatSpreadable`属性默认为`false`,必须手动打开。
|
||||
类似数组的对象正好相反,默认不展开。它的`Symbol.isConcatSpreadable`属性设为`true`,才可以展开。
|
||||
|
||||
```javascript
|
||||
let obj = {length: 2, 0: 'c', 1: 'd'};
|
||||
@ -523,7 +530,7 @@ obj[Symbol.isConcatSpreadable] = true;
|
||||
['a', 'b'].concat(obj, 'e') // ['a', 'b', 'c', 'd', 'e']
|
||||
```
|
||||
|
||||
对于一个类来说,`Symbol.isConcatSpreadable`属性必须写成实例的属性。
|
||||
`Symbol.isConcatSpreadable`属性也可以定义在类里面。
|
||||
|
||||
```javascript
|
||||
class A1 extends Array {
|
||||
@ -535,7 +542,9 @@ class A1 extends Array {
|
||||
class A2 extends Array {
|
||||
constructor(args) {
|
||||
super(args);
|
||||
this[Symbol.isConcatSpreadable] = false;
|
||||
}
|
||||
get [Symbol.isConcatSpreadable] () {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let a1 = new A1();
|
||||
@ -550,11 +559,31 @@ a2[1] = 6;
|
||||
|
||||
上面代码中,类`A1`是可展开的,类`A2`是不可展开的,所以使用`concat`时有不一样的结果。
|
||||
|
||||
注意,`Symbol.isConcatSpreadable`的位置差异,`A1`是定义在实例上,`A2`是定义在类本身,效果相同。
|
||||
|
||||
### Symbol.species
|
||||
|
||||
对象的`Symbol.species`属性,指向一个方法。该对象作为构造函数创造实例时,会调用这个方法。即如果`this.constructor[Symbol.species]`存在,就会使用这个属性作为构造函数,来创造新的实例对象。
|
||||
对象的`Symbol.species`属性,指向一个构造函数。创建造衍生对象时,会使用该属性。
|
||||
|
||||
`Symbol.species`属性默认的读取器如下。
|
||||
```javascript
|
||||
class MyArray extends Array {
|
||||
}
|
||||
|
||||
const a = new MyArray();
|
||||
a.map(x => x) instanceof MyArray // true
|
||||
```
|
||||
|
||||
上面代码中,子类`MyArray`继承了父类`Array`。`a.map(x => x)`会创建一个`MyArray`的衍生对象,该衍生对象还是`MyArray`的实例。
|
||||
|
||||
现在,`MyArray`设置`Symbol.species`属性。
|
||||
|
||||
```javascript
|
||||
class MyArray extends Array {
|
||||
static get [Symbol.species]() { return Array; }
|
||||
}
|
||||
```
|
||||
|
||||
上面代码中,由于定义了`Symbol.species`属性,创建衍生对象时就会使用这个属性返回的的函数,作为构造函数。这个例子也说明,定义`Symbol.species`属性要采用`get`读取器。默认的`Symbol.species`属性等同于下面的写法。
|
||||
|
||||
```javascript
|
||||
static get [Symbol.species]() {
|
||||
@ -562,6 +591,40 @@ static get [Symbol.species]() {
|
||||
}
|
||||
```
|
||||
|
||||
现在,再来看前面的例子。
|
||||
|
||||
```javascript
|
||||
class MyArray extends Array {
|
||||
static get [Symbol.species]() { return Array; }
|
||||
}
|
||||
|
||||
const a = new MyArray();
|
||||
a.map(x => x) instanceof MyArray // false
|
||||
a.map(x => x) instanceof Array // true
|
||||
```
|
||||
|
||||
上面代码中,`a.map(x => x)`创建的衍生对象,就不是`MyArray`的实例,而直接就是`Array`的实例。
|
||||
|
||||
再看一个例子。
|
||||
|
||||
```javascript
|
||||
class T1 extends Promise {
|
||||
}
|
||||
|
||||
class T2 extends Promise {
|
||||
static get [Symbol.species]() {
|
||||
return Promise;
|
||||
}
|
||||
}
|
||||
|
||||
new T1(r => r()).then(v => v) instanceof T1 // true
|
||||
new T2(r => r()).then(v => v) instanceof T2 // false
|
||||
```
|
||||
|
||||
上面代码中,`T2`定义了`Symbol.species`属性,`T1`没有。结果就导致了创建衍生对象时(`then`方法),`T1`调用的是自身的构造方法,而`T2`调用的是`Promise`的构造方法。
|
||||
|
||||
总之,`Symbol.species`的作用在于,实例对象在运行过程中,需要再次调用自身的构造函数时,会调用该属性指定的构造函数。它主要的用途是,有些类库是在基类的基础上修改的,那么子类使用继承的方法时,作者可能希望返回基类的实例,而不是子类的实例。
|
||||
|
||||
### Symbol.match
|
||||
|
||||
对象的`Symbol.match`属性,指向一个函数。当执行`str.match(myObject)`时,如果该属性存在,会调用它,返回该方法的返回值。
|
||||
@ -631,12 +694,43 @@ String.prototype.split(separator, limit)
|
||||
separator[Symbol.split](this, limit)
|
||||
```
|
||||
|
||||
下面是一个例子。
|
||||
|
||||
```javascript
|
||||
class MySplitter {
|
||||
constructor(value) {
|
||||
this.value = value;
|
||||
}
|
||||
[Symbol.split](string) {
|
||||
let index = string.indexOf(this.value);
|
||||
if (index === -1) {
|
||||
return string;
|
||||
}
|
||||
return [
|
||||
string.substr(0, index),
|
||||
string.substr(index + this.value.length)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
'foobar'.split(new MySplitter('foo'))
|
||||
// ['', 'bar']
|
||||
|
||||
'foobar'.split(new MySplitter('bar'))
|
||||
// ['foo', '']
|
||||
|
||||
'foobar'.split(new MySplitter('baz'))
|
||||
// 'foobar'
|
||||
```
|
||||
|
||||
上面方法使用`Symbol.split`方法,重新定义了字符串对象的`split`方法的行为,
|
||||
|
||||
### Symbol.iterator
|
||||
|
||||
对象的`Symbol.iterator`属性,指向该对象的默认遍历器方法。
|
||||
|
||||
```javascript
|
||||
var myIterable = {};
|
||||
const myIterable = {};
|
||||
myIterable[Symbol.iterator] = function* () {
|
||||
yield 1;
|
||||
yield 2;
|
||||
@ -646,7 +740,7 @@ myIterable[Symbol.iterator] = function* () {
|
||||
[...myIterable] // [1, 2, 3]
|
||||
```
|
||||
|
||||
对象进行`for...of`循环时,会调用`Symbol.iterator`方法,返回该对象的默认遍历器,详细介绍参见《Iterator和for...of循环》一章。
|
||||
对象进行`for...of`循环时,会调用`Symbol.iterator`方法,返回该对象的默认遍历器,详细介绍参见《Iterator 和 for...of 循环》一章。
|
||||
|
||||
```javascript
|
||||
class Collection {
|
||||
@ -717,15 +811,15 @@ class Collection {
|
||||
return 'xxx';
|
||||
}
|
||||
}
|
||||
var x = new Collection();
|
||||
let x = new Collection();
|
||||
Object.prototype.toString.call(x) // "[object xxx]"
|
||||
```
|
||||
|
||||
ES6新增内置对象的`Symbol.toStringTag`属性值如下。
|
||||
ES6 新增内置对象的`Symbol.toStringTag`属性值如下。
|
||||
|
||||
- `JSON[Symbol.toStringTag]`:'JSON'
|
||||
- `Math[Symbol.toStringTag]`:'Math'
|
||||
- Module对象`M[Symbol.toStringTag]`:'Module'
|
||||
- Module 对象`M[Symbol.toStringTag]`:'Module'
|
||||
- `ArrayBuffer.prototype[Symbol.toStringTag]`:'ArrayBuffer'
|
||||
- `DataView.prototype[Symbol.toStringTag]`:'DataView'
|
||||
- `Map.prototype[Symbol.toStringTag]`:'Map'
|
||||
@ -752,18 +846,19 @@ Array.prototype[Symbol.unscopables]
|
||||
// entries: true,
|
||||
// fill: true,
|
||||
// find: true,
|
||||
// findIndex: true,
|
||||
// findIndex: true,
|
||||
// includes: true,
|
||||
// keys: true
|
||||
// }
|
||||
|
||||
Object.keys(Array.prototype[Symbol.unscopables])
|
||||
// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'keys']
|
||||
// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'includes', 'keys']
|
||||
```
|
||||
|
||||
上面代码说明,数组有6个属性,会被with命令排除。
|
||||
上面代码说明,数组有 7 个属性,会被`with`命令排除。
|
||||
|
||||
```javascript
|
||||
// 没有unscopables时
|
||||
// 没有 unscopables 时
|
||||
class MyClass {
|
||||
foo() { return 1; }
|
||||
}
|
||||
@ -774,7 +869,7 @@ with (MyClass.prototype) {
|
||||
foo(); // 1
|
||||
}
|
||||
|
||||
// 有unscopables时
|
||||
// 有 unscopables 时
|
||||
class MyClass {
|
||||
foo() { return 1; }
|
||||
get [Symbol.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>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="app/bower_components/normalize-css/normalize.css">
|
||||
<link rel="stylesheet" href="css/app.css">
|
||||
@ -23,7 +23,7 @@
|
||||
<div id="back_to_top">back to top</div>
|
||||
<div id="edit">edit</div>
|
||||
<div id="loading">Loading ...</div>
|
||||
<div id="error">Opps! ... File not found!</div>
|
||||
<div id="error">Oops! ... File not found!</div>
|
||||
<div id="flip"><div id="pageup">上一章</div><div id="pagedown">下一章</div></div>
|
||||
<div class="progress-indicator-2"></div>
|
||||
|
||||
|
@ -139,7 +139,7 @@ function init_back_to_top_button() {
|
||||
|
||||
function goTop(e) {
|
||||
if(e) e.preventDefault();
|
||||
$('html body').animate({
|
||||
$('html, body').animate({
|
||||
scrollTop: 0
|
||||
}, 200);
|
||||
history.pushState(null, null, '#' + location.hash.split('#')[1]);
|
||||
|
27
sidebar.md
27
sidebar.md
@ -7,28 +7,31 @@
|
||||
## 目录
|
||||
1. [前言](#README)
|
||||
1. [ECMAScript 6简介](#docs/intro)
|
||||
1. [let和const命令](#docs/let)
|
||||
1. [let 和 const 命令](#docs/let)
|
||||
1. [变量的解构赋值](#docs/destructuring)
|
||||
1. [字符串的扩展](#docs/string)
|
||||
1. [正则的扩展](#docs/regex)
|
||||
1. [数值的扩展](#docs/number)
|
||||
1. [数组的扩展](#docs/array)
|
||||
1. [函数的扩展](#docs/function)
|
||||
1. [数组的扩展](#docs/array)
|
||||
1. [对象的扩展](#docs/object)
|
||||
1. [Symbol](#docs/symbol)
|
||||
1. [Set和Map数据结构](#docs/set-map)
|
||||
1. [Proxy和Reflect](#docs/proxy)
|
||||
1. [Iterator和for...of循环](#docs/iterator)
|
||||
1. [Generator函数](#docs/generator)
|
||||
1. [Promise对象](#docs/promise)
|
||||
1. [异步操作和Async函数](#docs/async)
|
||||
1. [Class](#docs/class)
|
||||
1. [Set 和 Map 数据结构](#docs/set-map)
|
||||
1. [Proxy](#docs/proxy)
|
||||
1. [Reflect](#docs/reflect)
|
||||
1. [Promise 对象](#docs/promise)
|
||||
1. [Iterator 和 for...of 循环](#docs/iterator)
|
||||
1. [Generator 函数的语法](#docs/generator)
|
||||
1. [Generator 函数的异步应用](#docs/generator-async)
|
||||
1. [async 函数](#docs/async)
|
||||
1. [Class 的基本语法](#docs/class)
|
||||
1. [Class 的继承](#docs/class-extends)
|
||||
1. [Decorator](#docs/decorator)
|
||||
1. [Module](#docs/module)
|
||||
1. [Module 的语法](#docs/module)
|
||||
1. [Module 的加载实现](#docs/module-loader)
|
||||
1. [编程风格](#docs/style)
|
||||
1. [读懂规格](#docs/spec)
|
||||
1. [二进制数组](#docs/arraybuffer)
|
||||
1. [SIMD](#docs/simd)
|
||||
1. [ArrayBuffer](#docs/arraybuffer)
|
||||
1. [参考链接](#docs/reference)
|
||||
|
||||
## 其他
|
||||
|
Loading…
x
Reference in New Issue
Block a user