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

Merge pull request #3 from ruanyf/gh-pages

asdf
This commit is contained in:
bigbird231 2019-06-27 10:20:54 +08:00 committed by GitHub
commit c63caa345d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 681 additions and 439 deletions

View File

@ -546,7 +546,7 @@ function ArrayOf(){
## 数组实例的 copyWithin()
数组实例的`copyWithin`方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
数组实例的`copyWithin()`方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
```javascript
Array.prototype.copyWithin(target, start = 0, end = this.length)
@ -555,8 +555,8 @@ Array.prototype.copyWithin(target, start = 0, end = this.length)
它接受三个参数。
- target必需从该位置开始替换数据。如果为负值表示倒数。
- start可选从该位置开始读取数据默认为 0。如果为负值表示倒数
- end可选到该位置前停止读取数据默认等于数组长度。如果为负值表示倒数
- start可选从该位置开始读取数据默认为 0。如果为负值表示从末尾开始计算
- end可选到该位置前停止读取数据默认等于数组长度。如果为负值表示从末尾开始计算
这三个参数都应该是数值,如果不是,会自动转为数值。

View File

@ -34,11 +34,11 @@
很多浏览器操作的 API用到了二进制数组操作二进制数据下面是其中的几个。
- File API
- XMLHttpRequest
- Fetch API
- Canvas
- WebSockets
- [Canvas](#canvas)
- [Fetch API](#fetch-api)
- [File API](#file-api)
- [WebSockets](#websocket)
- [XMLHttpRequest](#ajax)
## ArrayBuffer 对象
@ -448,37 +448,42 @@ Float64Array.BYTES_PER_ELEMENT // 8
### ArrayBuffer 与字符串的互相转换
`ArrayBuffer`转为字符串,或者字符串转为`ArrayBuffer`,有一个前提,即字符串的编码方法是确定的。假定字符串采用 UTF-16 编码JavaScript 的内部编码方式),可以自己编写转换函数
`ArrayBuffer` 和字符串的相互转换,使用原生 `TextEncoder``TextDecoder` 方法。为了便于说明用法,下面的代码都按照 TypeScript 的用法,给出了类型签名
```javascript
// ArrayBuffer 转为字符串,参数为 ArrayBuffer 对象
function ab2str(buf) {
// 注意,如果是大型二进制数组,为了避免溢出,
// 必须一个一个字符地转
if (buf && buf.byteLength < 1024) {
return String.fromCharCode.apply(null, new Uint16Array(buf));
}
const bufView = new Uint16Array(buf);
const len = bufView.length;
const bstr = new Array(len);
for (let i = 0; i < len; i++) {
bstr[i] = String.fromCharCode.call(null, bufView[i]);
}
return bstr.join('');
/**
* Convert ArrayBuffer/TypedArray to String via TextDecoder
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder
*/
function ab2str(
input: ArrayBuffer | Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array,
outputEncoding: string = 'utf8',
): string {
const decoder = new TextDecoder(outputEncoding)
return decoder.decode(input)
}
// 字符串转为 ArrayBuffer 对象,参数为字符串
function str2ab(str) {
const buf = new ArrayBuffer(str.length * 2); // 每个字符占用2个字节
const bufView = new Uint16Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
/**
* Convert String to ArrayBuffer via TextEncoder
*
* @see https://developer.mozilla.org/zh-CN/docs/Web/API/TextEncoder
*/
function str2ab(input: string): ArrayBuffer {
const view = str2Uint8Array(input)
return view.buffer
}
/** Convert String to Uint8Array */
function str2Uint8Array(input: string): Uint8Array {
const encoder = new TextEncoder()
const view = encoder.encode(input)
return view
}
```
上面代码中,`ab2str()`的第二个参数`outputEncoding`给出了输出编码的编码,一般保持默认值(`utf-8`),其他可选值参见[官方文档](https://encoding.spec.whatwg.org)或 [Node.js 文档](https://nodejs.org/api/util.html#util_whatwg_supported_encodings)。
### 溢出
不同的视图类型所能容纳的数值范围是确定的。超出这个范围就会出现溢出。比如8 位视图只能容纳一个 8 位的二进制值,如果放入一个 9 位的值,就会溢出。
@ -574,7 +579,7 @@ v3.byteOffset // 2
### TypedArray.prototype.length
`length`属性表示 TypedArray 数组含有多少个成员。注意将`byteLength`属性和`length`属性区分,前者是字节长度,后者是成员长度。
`length`属性表示 `TypedArray` 数组含有多少个成员。注意将 `length` 属性和 `byteLength` 属性区分,前者是成员长度,后者是字节长度。
```javascript
const a = new Int16Array(8);

475
docs/async-iterator.md Normal file
View File

@ -0,0 +1,475 @@
# 异步遍历器
## 同步遍历器的问题
《遍历器》一章说过Iterator 接口是一种数据遍历的协议,只要调用遍历器对象的`next`方法,就会得到一个对象,表示当前遍历指针所在的那个位置的信息。`next`方法返回的对象的结构是`{value, done}`,其中`value`表示当前的数据的值,`done`是一个布尔值,表示遍历是否结束。
```javascript
function idMaker() {
let index = 0;
return {
next: function() {
return { value: index++, done: false };
}
};
}
const it = idMaker();
it.next().value // 0
it.next().value // 1
it.next().value // 2
// ...
```
上面代码中,变量`it`是一个遍历器iterator。每次调用`it.next()`方法,就返回一个对象,表示当前遍历位置的信息。
这里隐含着一个规定,`it.next()`方法必须是同步的,只要调用就必须立刻返回值。也就是说,一旦执行`it.next()`方法,就必须同步地得到`value``done`这两个属性。如果遍历指针正好指向同步操作,当然没有问题,但对于异步操作,就不太合适了。
```javascript
function idMaker() {
let index = 0;
return {
next: function() {
return new Promise(function (resolve, reject) {
setTimeout(() => {
resolve({ value: index++, done: false });
}, 1000);
});
}
};
}
```
上面代码中,`next()`方法返回的是一个 Promise 对象,这样就不行,不符合 Iterator 协议只要代码里面包含异步操作都不行。也就是说Iterator 协议里面`next()`方法只能包含同步操作。
目前的解决方法是,将异步操作包装成 Thunk 函数或者 Promise 对象,即`next()`方法返回值的`value`属性是一个 Thunk 函数或者 Promise 对象,等待以后返回真正的值,而`done`属性则还是同步产生的。
```javascript
function idMaker() {
let index = 0;
return {
next: function() {
return {
value: new Promise(resolve => setTimeout(() => resolve(index++), 1000)),
done: false
};
}
};
}
const it = idMaker();
it.next().value.then(o => console.log(o)) // 1
it.next().value.then(o => console.log(o)) // 2
it.next().value.then(o => console.log(o)) // 3
// ...
```
上面代码中,`value`属性的返回值是一个 Promise 对象,用来放置异步操作。但是这样写很麻烦,不太符合直觉,语义也比较绕。
ES2018 [引入](https://github.com/tc39/proposal-async-iteration)了“异步遍历器”Async Iterator为异步操作提供原生的遍历器接口`value``done`这两个属性都是异步产生。
## 异步遍历的接口
异步遍历器的最大的语法特点,就是调用遍历器的`next`方法,返回的是一个 Promise 对象。
```javascript
asyncIterator
.next()
.then(
({ value, done }) => /* ... */
);
```
上面代码中,`asyncIterator`是一个异步遍历器,调用`next`方法以后,返回一个 Promise 对象。因此,可以使用`then`方法指定,这个 Promise 对象的状态变为`resolve`以后的回调函数。回调函数的参数,则是一个具有`value``done`两个属性的对象,这个跟同步遍历器是一样的。
我们知道,一个对象的同步遍历器的接口,部署在`Symbol.iterator`属性上面。同样地,对象的异步遍历器接口,部署在`Symbol.asyncIterator`属性上面。不管是什么样的对象,只要它的`Symbol.asyncIterator`属性有值,就表示应该对它进行异步遍历。
下面是一个异步遍历器的例子。
```javascript
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
asyncIterator
.next()
.then(iterResult1 => {
console.log(iterResult1); // { value: 'a', done: false }
return asyncIterator.next();
})
.then(iterResult2 => {
console.log(iterResult2); // { value: 'b', done: false }
return asyncIterator.next();
})
.then(iterResult3 => {
console.log(iterResult3); // { value: undefined, done: true }
});
```
上面代码中,异步遍历器其实返回了两次值。第一次调用的时候,返回一个 Promise 对象;等到 Promise 对象`resolve`了,再返回一个表示当前数据成员信息的对象。这就是说,异步遍历器与同步遍历器最终行为是一致的,只是会先返回 Promise 对象,作为中介。
由于异步遍历器的`next`方法,返回的是一个 Promise 对象。因此,可以把它放在`await`命令后面。
```javascript
async function f() {
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
console.log(await asyncIterator.next());
// { value: 'a', done: false }
console.log(await asyncIterator.next());
// { value: 'b', done: false }
console.log(await asyncIterator.next());
// { value: undefined, done: true }
}
```
上面代码中,`next`方法用`await`处理以后,就不必使用`then`方法了。整个流程已经很接近同步处理了。
注意,异步遍历器的`next`方法是可以连续调用的,不必等到上一步产生的 Promise 对象`resolve`以后再调用。这种情况下,`next`方法会累积起来,自动按照每一步的顺序运行下去。下面是一个例子,把所有的`next`方法放在`Promise.all`方法里面。
```javascript
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
const [{value: v1}, {value: v2}] = await Promise.all([
asyncIterator.next(), asyncIterator.next()
]);
console.log(v1, v2); // a b
```
另一种用法是一次性调用所有的`next`方法,然后`await`最后一步操作。
```javascript
async function runner() {
const writer = openFile('someFile.txt');
writer.next('hello');
writer.next('world');
await writer.return();
}
runner();
```
## for await...of
前面介绍过,`for...of`循环用于遍历同步的 Iterator 接口。新引入的`for await...of`循环,则是用于遍历异步的 Iterator 接口。
```javascript
async function f() {
for await (const x of createAsyncIterable(['a', 'b'])) {
console.log(x);
}
}
// a
// b
```
上面代码中,`createAsyncIterable()`返回一个拥有异步遍历器接口的对象,`for...of`循环自动调用这个对象的异步遍历器的`next`方法,会得到一个 Promise 对象。`await`用来处理这个 Promise 对象,一旦`resolve`,就把得到的值(`x`)传入`for...of`的循环体。
`for await...of`循环的一个用途,是部署了 asyncIterable 操作的异步接口,可以直接放入这个循环。
```javascript
let body = '';
async function f() {
for await(const data of req) body += data;
const parsed = JSON.parse(body);
console.log('got', parsed);
}
```
上面代码中,`req`是一个 asyncIterable 对象,用来异步读取数据。可以看到,使用`for await...of`循环以后,代码会非常简洁。
如果`next`方法返回的 Promise 对象被`reject``for await...of`就会报错,要用`try...catch`捕捉。
```javascript
async function () {
try {
for await (const x of createRejectingIterable()) {
console.log(x);
}
} catch (e) {
console.error(e);
}
}
```
注意,`for await...of`循环也可以用于同步遍历器。
```javascript
(async function () {
for await (const x of ['a', 'b']) {
console.log(x);
}
})();
// a
// b
```
Node v10 支持异步遍历器Stream 就部署了这个接口。下面是读取文件的传统写法与异步遍历器写法的差异。
```javascript
// 传统写法
function main(inputFilePath) {
const readStream = fs.createReadStream(
inputFilePath,
{ encoding: 'utf8', highWaterMark: 1024 }
);
readStream.on('data', (chunk) => {
console.log('>>> '+chunk);
});
readStream.on('end', () => {
console.log('### DONE ###');
});
}
// 异步遍历器写法
async function main(inputFilePath) {
const readStream = fs.createReadStream(
inputFilePath,
{ encoding: 'utf8', highWaterMark: 1024 }
);
for await (const chunk of readStream) {
console.log('>>> '+chunk);
}
console.log('### DONE ###');
}
```
## 异步 Generator 函数
就像 Generator 函数返回一个同步遍历器对象一样,异步 Generator 函数的作用,是返回一个异步遍历器对象。
在语法上,异步 Generator 函数就是`async`函数与 Generator 函数的结合。
```javascript
async function* gen() {
yield 'hello';
}
const genObj = gen();
genObj.next().then(x => console.log(x));
// { value: 'hello', done: false }
```
上面代码中,`gen`是一个异步 Generator 函数,执行后返回一个异步 Iterator 对象。对该对象调用`next`方法,返回一个 Promise 对象。
异步遍历器的设计目的之一,就是 Generator 函数处理同步操作和异步操作时,能够使用同一套接口。
```javascript
// 同步 Generator 函数
function* map(iterable, func) {
const iter = iterable[Symbol.iterator]();
while (true) {
const {value, done} = iter.next();
if (done) break;
yield func(value);
}
}
// 异步 Generator 函数
async function* map(iterable, func) {
const iter = iterable[Symbol.asyncIterator]();
while (true) {
const {value, done} = await iter.next();
if (done) break;
yield func(value);
}
}
```
上面代码中,`map`是一个 Generator 函数,第一个参数是可遍历对象`iterable`,第二个参数是一个回调函数`func``map`的作用是将`iterable`每一步返回的值,使用`func`进行处理。上面有两个版本的`map`,前一个处理同步遍历器,后一个处理异步遍历器,可以看到两个版本的写法基本上是一致的。
下面是另一个异步 Generator 函数的例子。
```javascript
async function* readLines(path) {
let file = await fileOpen(path);
try {
while (!file.EOF) {
yield await file.readLine();
}
} finally {
await file.close();
}
}
```
上面代码中,异步操作前面使用`await`关键字标明,即`await`后面的操作,应该返回 Promise 对象。凡是使用`yield`关键字的地方,就是`next`方法停下来的地方,它后面的表达式的值(即`await file.readLine()`的值),会作为`next()`返回对象的`value`属性,这一点是与同步 Generator 函数一致的。
异步 Generator 函数内部,能够同时使用`await``yield`命令。可以这样理解,`await`命令用于将外部操作产生的值输入函数内部,`yield`命令用于将函数内部的值输出。
上面代码定义的异步 Generator 函数的用法如下。
```javascript
(async function () {
for await (const line of readLines(filePath)) {
console.log(line);
}
})()
```
异步 Generator 函数可以与`for await...of`循环结合起来使用。
```javascript
async function* prefixLines(asyncIterable) {
for await (const line of asyncIterable) {
yield '> ' + line;
}
}
```
异步 Generator 函数的返回值是一个异步 Iterator即每次调用它的`next`方法,会返回一个 Promise 对象,也就是说,跟在`yield`命令后面的,应该是一个 Promise 对象。如果像上面那个例子那样,`yield`命令后面是一个字符串,会被自动包装成一个 Promise 对象。
```javascript
function fetchRandom() {
const url = 'https://www.random.org/decimal-fractions/'
+ '?num=1&dec=10&col=1&format=plain&rnd=new';
return fetch(url);
}
async function* asyncGenerator() {
console.log('Start');
const result = await fetchRandom(); // (A)
yield 'Result: ' + await result.text(); // (B)
console.log('Done');
}
const ag = asyncGenerator();
ag.next().then(({value, done}) => {
console.log(value);
})
```
上面代码中,`ag``asyncGenerator`函数返回的异步遍历器对象。调用`ag.next()`以后,上面代码的执行顺序如下。
1. `ag.next()`立刻返回一个 Promise 对象。
1. `asyncGenerator`函数开始执行,打印出`Start`
1. `await`命令返回一个 Promise 对象,`asyncGenerator`函数停在这里。
1. A 处变成 fulfilled 状态,产生的值放入`result`变量,`asyncGenerator`函数继续往下执行。
1. 函数在 B 处的`yield`暂停执行,一旦`yield`命令取到值,`ag.next()`返回的那个 Promise 对象变成 fulfilled 状态。
1. `ag.next()`后面的`then`方法指定的回调函数开始执行。该回调函数的参数是一个对象`{value, done}`,其中`value`的值是`yield`命令后面的那个表达式的值,`done`的值是`false`
A 和 B 两行的作用类似于下面的代码。
```javascript
return new Promise((resolve, reject) => {
fetchRandom()
.then(result => result.text())
.then(result => {
resolve({
value: 'Result: ' + result,
done: false,
});
});
});
```
如果异步 Generator 函数抛出错误,会导致 Promise 对象的状态变为`reject`,然后抛出的错误被`catch`方法捕获。
```javascript
async function* asyncGenerator() {
throw new Error('Problem!');
}
asyncGenerator()
.next()
.catch(err => console.log(err)); // Error: Problem!
```
注意,普通的 async 函数返回的是一个 Promise 对象,而异步 Generator 函数返回的是一个异步 Iterator 对象。可以这样理解async 函数和异步 Generator 函数,是封装异步操作的两种方法,都用来达到同一种目的。区别在于,前者自带执行器,后者通过`for await...of`执行,或者自己编写执行器。下面就是一个异步 Generator 函数的执行器。
```javascript
async function takeAsync(asyncIterable, count = Infinity) {
const result = [];
const iterator = asyncIterable[Symbol.asyncIterator]();
while (result.length < count) {
const {value, done} = await iterator.next();
if (done) break;
result.push(value);
}
return result;
}
```
上面代码中,异步 Generator 函数产生的异步遍历器,会通过`while`循环自动执行,每当`await iterator.next()`完成,就会进入下一轮循环。一旦`done`属性变为`true`,就会跳出循环,异步遍历器执行结束。
下面是这个自动执行器的一个使用实例。
```javascript
async function f() {
async function* gen() {
yield 'a';
yield 'b';
yield 'c';
}
return await takeAsync(gen());
}
f().then(function (result) {
console.log(result); // ['a', 'b', 'c']
})
```
异步 Generator 函数出现以后JavaScript 就有了四种函数形式普通函数、async 函数、Generator 函数和异步 Generator 函数。请注意区分每种函数的不同之处。基本上,如果是一系列按照顺序执行的异步操作(比如读取文件,然后写入新内容,再存入硬盘),可以使用 async 函数;如果是一系列产生相同数据结构的异步操作(比如一行一行读取文件),可以使用异步 Generator 函数。
异步 Generator 函数也可以通过`next`方法的参数,接收外部传入的数据。
```javascript
const writer = openFile('someFile.txt');
writer.next('hello'); // 立即执行
writer.next('world'); // 立即执行
await writer.return(); // 等待写入结束
```
上面代码中,`openFile`是一个异步 Generator 函数。`next`方法的参数,向该函数内部的操作传入数据。每次`next`方法都是同步执行的,最后的`await`命令用于等待整个写入操作结束。
最后,同步的数据结构,也可以使用异步 Generator 函数。
```javascript
async function* createAsyncIterable(syncIterable) {
for (const elem of syncIterable) {
yield elem;
}
}
```
上面代码中,由于没有异步操作,所以也就没有使用`await`关键字。
## yield\* 语句
`yield*`语句也可以跟一个异步遍历器。
```javascript
async function* gen1() {
yield 'a';
yield 'b';
return 2;
}
async function* gen2() {
// result 最终会等于 2
const result = yield* gen1();
}
```
上面代码中,`gen2`函数里面的`result`变量,最后的值是`2`
与同步 Generator 函数一样,`for await...of`循环会展开`yield*`
```javascript
(async function () {
for await (const x of gen2()) {
console.log(x);
}
})();
// a
// b
```

View File

@ -242,13 +242,34 @@ class Sleep {
}
(async () => {
const actualTime = await new Sleep(1000);
console.log(actualTime);
const sleepTime = await new Sleep(1000);
console.log(sleepTime);
})();
// 1000
```
上面代码中,`await`命令后面是一个`Sleep`对象的实例。这个实例不是 Promise 对象,但是因为定义了`then`方法,`await`会将其视为`Promise`处理。
这个例子还演示了如何实现休眠效果。JavaScript 一直没有休眠的语法,但是借助`await`命令就可以让程序停顿指定的时间。下面给出了一个简化的`sleep`实现。
```javascript
function sleep(interval) {
return new Promise(resolve => {
setTimeout(resolve, interval);
})
}
// 用法
async function one2FiveInAsync() {
for(let i = 1; i <= 5; i++) {
console.log(i);
await sleep(1000);
}
}
one2FiveInAsync();
```
`await`命令后面的 Promise 对象如果变为`reject`状态,则`reject`的参数会被`catch`方法的回调函数接收到。
```javascript
@ -486,24 +507,6 @@ async function dbFuc(db) {
}
```
目前,[`esm`](https://www.npmjs.com/package/esm)模块加载器支持顶层`await`,即`await`命令可以不放在 async 函数里面,直接使用。
```javascript
// async 函数的写法
const start = async () => {
const res = await fetch('google.com');
return res.text();
};
start().then(console.log);
// 顶层 await 的写法
const res = await fetch('google.com');
console.log(await res.text());
```
上面代码中,第二种写法的脚本必须使用`esm`加载器,才会生效。
第四点async 函数可以保留运行堆栈。
```javascript
@ -702,410 +705,155 @@ async function logInOrder(urls) {
上面代码中,虽然`map`方法的参数是`async`函数,但它是并发执行的,因为只有`async`函数内部是继发执行,外部不受影响。后面的`for..of`循环内部使用了`await`,因此实现了按顺序输出。
## 异步遍历器
## 顶层 await
《遍历器》一章说过Iterator 接口是一种数据遍历的协议,只要调用遍历器对象的`next`方法,就会得到一个对象,表示当前遍历指针所在的那个位置的信息。`next`方法返回的对象的结构是`{value, done}`,其中`value`表示当前的数据的值,`done`是一个布尔值,表示遍历是否结束。
这里隐含着一个规定,`next`方法必须是同步的,只要调用就必须立刻返回值。也就是说,一旦执行`next`方法,就必须同步地得到`value``done`这两个属性。如果遍历指针正好指向同步操作当然没有问题但对于异步操作就不太合适了。目前的解决方法是Generator 函数里面的异步操作,返回一个 Thunk 函数或者 Promise 对象,即`value`属性是一个 Thunk 函数或者 Promise 对象,等待以后返回真正的值,而`done`属性则还是同步产生的。
ES2018 [引入](https://github.com/tc39/proposal-async-iteration)了“异步遍历器”Async Iterator为异步操作提供原生的遍历器接口`value``done`这两个属性都是异步产生。
### 异步遍历的接口
异步遍历器的最大的语法特点,就是调用遍历器的`next`方法,返回的是一个 Promise 对象。
根据语法规格,`await`命令只能出现在 async 函数内部,否则都会报错。
```javascript
asyncIterator
.next()
.then(
({ value, done }) => /* ... */
);
// 报错
const data = await fetch('https://api.example.com');
```
上面代码中,`asyncIterator`是一个异步遍历器,调用`next`方法以后,返回一个 Promise 对象。因此,可以使用`then`方法指定,这个 Promise 对象的状态变为`resolve`以后的回调函数。回调函数的参数,则是一个具有`value``done`两个属性的对象,这个跟同步遍历器是一样的
上面代码中,`await`命令独立使用,没有放在 async 函数里面,就会报错。
我们知道,一个对象的同步遍历器的接口,部署在`Symbol.iterator`属性上面。同样地,对象的异步遍历器接口,部署在`Symbol.asyncIterator`属性上面。不管是什么样的对象,只要它的`Symbol.asyncIterator`属性有值,就表示应该对它进行异步遍历。
下面是一个异步遍历器的例子。
目前,有一个[语法提案](https://github.com/tc39/proposal-top-level-await),允许在模块的顶层独立使用`await`命令。这个提案的目的,是借用`await`解决模块异步加载的问题。
```javascript
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
// awaiting.js
let output;
async function main() {
const dynamic = await import(someMission);
const data = await fetch(url);
output = someProcess(dynamic.default, data);
}
main();
export { output };
```
asyncIterator
.next()
.then(iterResult1 => {
console.log(iterResult1); // { value: 'a', done: false }
return asyncIterator.next();
})
.then(iterResult2 => {
console.log(iterResult2); // { value: 'b', done: false }
return asyncIterator.next();
})
.then(iterResult3 => {
console.log(iterResult3); // { value: undefined, done: true }
上面代码中,模块`awaiting.js`的输出值`output`,取决于异步操作。我们把异步操作包装在一个 async 函数里面,然后调用这个函数,只有等里面的异步操作都执行,变量`output`才会有值,否则就返回`undefined`
上面的代码也可以写成立即执行函数的形式。
```javascript
// awaiting.js
let output;
(async function main() {
const dynamic = await import(someMission);
const data = await fetch(url);
output = someProcess(dynamic.default, data);
})();
export { output };
```
下面是加载这个模块的写法。
```javascript
// usage.js
import { output } from "./awaiting.js";
function outputPlusValue(value) { return output + value }
console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000);
```
上面代码中,`outputPlusValue()`的执行结果,完全取决于执行的时间。如果`awaiting.js`里面的异步操作没执行完,加载进来的`output`的值就是`undefined`
目前的解决方法,就是让原始模块输出一个 Promise 对象,从这个 Promise 对象判断异步操作有没有结束。
```javascript
// awaiting.js
let output;
export default (async function main() {
const dynamic = await import(someMission);
const data = await fetch(url);
output = someProcess(dynamic.default, data);
})();
export { output };
```
上面代码中,`awaiting.js`除了输出`output`,还默认输出一个 Promise 对象async 函数立即执行后,返回一个 Promise 对象),从这个对象判断异步操作是否结束。
下面是加载这个模块的新的写法。
```javascript
// usage.js
import promise, { output } from "./awaiting.js";
function outputPlusValue(value) { return output + value }
promise.then(() => {
console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000);
});
```
上面代码中,异步遍历器其实返回了两次值。第一次调用的时候,返回一个 Promise 对象;等到 Promise 对象`resolve`了,再返回一个表示当前数据成员信息的对象。这就是说,异步遍历器与同步遍历器最终行为是一致的,只是会先返回 Promise 对象,作为中介。
上面代码中,`awaiting.js`对象的输出,放在`promise.then()`里面,这样就能保证异步操作完成以后,才去读取`output`
由于异步遍历器的`next`方法,返回的是一个 Promise 对象。因此,可以把它放在`await`命令后面。
这种写法比较麻烦,等于要求模块的使用者遵守一个额外的使用协议,按照特殊的方法使用这个模块。一旦你忘了要用 Promise 加载,只使用正常的加载方法,依赖这个模块的代码就可能出错。而且,如果上面的`usage.js`又有对外的输出,等于这个依赖链的所有模块都要使用 Promise 加载。
顶层的`await`命令,就是为了解决这个问题。它保证只有异步操作完成,模块才会输出值。
```javascript
async function f() {
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
console.log(await asyncIterator.next());
// { value: 'a', done: false }
console.log(await asyncIterator.next());
// { value: 'b', done: false }
console.log(await asyncIterator.next());
// { value: undefined, done: true }
// awaiting.js
const dynamic = import(someMission);
const data = fetch(url);
export const output = someProcess((await dynamic).default, await data);
```
上面代码中,两个异步操作在输出的时候,都加上了`await`命令。只有等到异步操作完成,这个模块才会输出值。
加载这个模块的写法如下。
```javascript
// usage.js
import { output } from "./awaiting.js";
function outputPlusValue(value) { return output + value }
console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000);
```
上面代码的写法,与普通的模块加载完全一样。也就是说,模块的使用者完全不用关心,依赖模块的内部有没有异步操作,正常加载即可。
这时,模块的加载会等待依赖模块(上例是`awaiting.js`)的异步操作完成,才执行后面的代码,有点像暂停在那里。所以,它总是会得到正确的`output`,不会因为加载时机的不同,而得到不一样的值。
下面是顶层`await`的一些使用场景。
```javascript
// import() 方法加载
const strings = await import(`/i18n/${navigator.language}`);
// 数据库操作
const connection = await dbConnector();
// 依赖回滚
let jQuery;
try {
jQuery = await import('https://cdn-a.com/jQuery');
} catch {
jQuery = await import('https://cdn-b.com/jQuery');
}
```
上面代码中,`next`方法用`await`处理以后,就不必使用`then`方法了。整个流程已经很接近同步处理了。
注意,异步遍历器的`next`方法是可以连续调用的,不必等到上一步产生的 Promise 对象`resolve`以后再调用。这种情况下,`next`方法会累积起来,自动按照每一步的顺序运行下去。下面是一个例子,把所有的`next`方法放在`Promise.all`方法里面。
注意,如果加载多个包含顶层`await`命令的模块,加载命令是同步执行的。
```javascript
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
const [{value: v1}, {value: v2}] = await Promise.all([
asyncIterator.next(), asyncIterator.next()
]);
// x.js
console.log("X1");
await new Promise(r => setTimeout(r, 1000));
console.log("X2");
console.log(v1, v2); // a b
// y.js
console.log("Y");
// z.js
import "./x.js";
import "./y.js";
console.log("Z");
```
另一种用法是一次性调用所有的`next`方法,然后`await`最后一步操作。
上面代码有三个模块,最后的`z.js`加载`x.js``y.js`,打印结果是`X1``Y``X2``Z`。这说明,`z.js`并没有等待`x.js`加载完成,再去加载`y.js`
```javascript
async function runner() {
const writer = openFile('someFile.txt');
writer.next('hello');
writer.next('world');
await writer.return();
}
顶层的`await`命令有点像,交出代码的执行权给其他的模块加载,等异步操作完成后,再拿回执行权,继续向下执行。
runner();
```
### for await...of
前面介绍过,`for...of`循环用于遍历同步的 Iterator 接口。新引入的`for await...of`循环,则是用于遍历异步的 Iterator 接口。
```javascript
async function f() {
for await (const x of createAsyncIterable(['a', 'b'])) {
console.log(x);
}
}
// a
// b
```
上面代码中,`createAsyncIterable()`返回一个拥有异步遍历器接口的对象,`for...of`循环自动调用这个对象的异步遍历器的`next`方法,会得到一个 Promise 对象。`await`用来处理这个 Promise 对象,一旦`resolve`,就把得到的值(`x`)传入`for...of`的循环体。
`for await...of`循环的一个用途,是部署了 asyncIterable 操作的异步接口,可以直接放入这个循环。
```javascript
let body = '';
async function f() {
for await(const data of req) body += data;
const parsed = JSON.parse(body);
console.log('got', parsed);
}
```
上面代码中,`req`是一个 asyncIterable 对象,用来异步读取数据。可以看到,使用`for await...of`循环以后,代码会非常简洁。
如果`next`方法返回的 Promise 对象被`reject``for await...of`就会报错,要用`try...catch`捕捉。
```javascript
async function () {
try {
for await (const x of createRejectingIterable()) {
console.log(x);
}
} catch (e) {
console.error(e);
}
}
```
注意,`for await...of`循环也可以用于同步遍历器。
```javascript
(async function () {
for await (const x of ['a', 'b']) {
console.log(x);
}
})();
// a
// b
```
Node v10 支持异步遍历器Stream 就部署了这个接口。下面是读取文件的传统写法与异步遍历器写法的差异。
```javascript
// 传统写法
function main(inputFilePath) {
const readStream = fs.createReadStream(
inputFilePath,
{ encoding: 'utf8', highWaterMark: 1024 }
);
readStream.on('data', (chunk) => {
console.log('>>> '+chunk);
});
readStream.on('end', () => {
console.log('### DONE ###');
});
}
// 异步遍历器写法
async function main(inputFilePath) {
const readStream = fs.createReadStream(
inputFilePath,
{ encoding: 'utf8', highWaterMark: 1024 }
);
for await (const chunk of readStream) {
console.log('>>> '+chunk);
}
console.log('### DONE ###');
}
```
### 异步 Generator 函数
就像 Generator 函数返回一个同步遍历器对象一样,异步 Generator 函数的作用,是返回一个异步遍历器对象。
在语法上,异步 Generator 函数就是`async`函数与 Generator 函数的结合。
```javascript
async function* gen() {
yield 'hello';
}
const genObj = gen();
genObj.next().then(x => console.log(x));
// { value: 'hello', done: false }
```
上面代码中,`gen`是一个异步 Generator 函数,执行后返回一个异步 Iterator 对象。对该对象调用`next`方法,返回一个 Promise 对象。
异步遍历器的设计目的之一,就是 Generator 函数处理同步操作和异步操作时,能够使用同一套接口。
```javascript
// 同步 Generator 函数
function* map(iterable, func) {
const iter = iterable[Symbol.iterator]();
while (true) {
const {value, done} = iter.next();
if (done) break;
yield func(value);
}
}
// 异步 Generator 函数
async function* map(iterable, func) {
const iter = iterable[Symbol.asyncIterator]();
while (true) {
const {value, done} = await iter.next();
if (done) break;
yield func(value);
}
}
```
上面代码中,`map`是一个 Generator 函数,第一个参数是可遍历对象`iterable`,第二个参数是一个回调函数`func``map`的作用是将`iterable`每一步返回的值,使用`func`进行处理。上面有两个版本的`map`,前一个处理同步遍历器,后一个处理异步遍历器,可以看到两个版本的写法基本上是一致的。
下面是另一个异步 Generator 函数的例子。
```javascript
async function* readLines(path) {
let file = await fileOpen(path);
try {
while (!file.EOF) {
yield await file.readLine();
}
} finally {
await file.close();
}
}
```
上面代码中,异步操作前面使用`await`关键字标明,即`await`后面的操作,应该返回 Promise 对象。凡是使用`yield`关键字的地方,就是`next`方法停下来的地方,它后面的表达式的值(即`await file.readLine()`的值),会作为`next()`返回对象的`value`属性,这一点是与同步 Generator 函数一致的。
异步 Generator 函数内部,能够同时使用`await``yield`命令。可以这样理解,`await`命令用于将外部操作产生的值输入函数内部,`yield`命令用于将函数内部的值输出。
上面代码定义的异步 Generator 函数的用法如下。
```javascript
(async function () {
for await (const line of readLines(filePath)) {
console.log(line);
}
})()
```
异步 Generator 函数可以与`for await...of`循环结合起来使用。
```javascript
async function* prefixLines(asyncIterable) {
for await (const line of asyncIterable) {
yield '> ' + line;
}
}
```
异步 Generator 函数的返回值是一个异步 Iterator即每次调用它的`next`方法,会返回一个 Promise 对象,也就是说,跟在`yield`命令后面的,应该是一个 Promise 对象。如果像上面那个例子那样,`yield`命令后面是一个字符串,会被自动包装成一个 Promise 对象。
```javascript
function fetchRandom() {
const url = 'https://www.random.org/decimal-fractions/'
+ '?num=1&dec=10&col=1&format=plain&rnd=new';
return fetch(url);
}
async function* asyncGenerator() {
console.log('Start');
const result = await fetchRandom(); // (A)
yield 'Result: ' + await result.text(); // (B)
console.log('Done');
}
const ag = asyncGenerator();
ag.next().then(({value, done}) => {
console.log(value);
})
```
上面代码中,`ag``asyncGenerator`函数返回的异步遍历器对象。调用`ag.next()`以后,上面代码的执行顺序如下。
1. `ag.next()`立刻返回一个 Promise 对象。
1. `asyncGenerator`函数开始执行,打印出`Start`
1. `await`命令返回一个 Promise 对象,`asyncGenerator`函数停在这里。
1. A 处变成 fulfilled 状态,产生的值放入`result`变量,`asyncGenerator`函数继续往下执行。
1. 函数在 B 处的`yield`暂停执行,一旦`yield`命令取到值,`ag.next()`返回的那个 Promise 对象变成 fulfilled 状态。
1. `ag.next()`后面的`then`方法指定的回调函数开始执行。该回调函数的参数是一个对象`{value, done}`,其中`value`的值是`yield`命令后面的那个表达式的值,`done`的值是`false`
A 和 B 两行的作用类似于下面的代码。
```javascript
return new Promise((resolve, reject) => {
fetchRandom()
.then(result => result.text())
.then(result => {
resolve({
value: 'Result: ' + result,
done: false,
});
});
});
```
如果异步 Generator 函数抛出错误,会导致 Promise 对象的状态变为`reject`,然后抛出的错误被`catch`方法捕获。
```javascript
async function* asyncGenerator() {
throw new Error('Problem!');
}
asyncGenerator()
.next()
.catch(err => console.log(err)); // Error: Problem!
```
注意,普通的 async 函数返回的是一个 Promise 对象,而异步 Generator 函数返回的是一个异步 Iterator 对象。可以这样理解async 函数和异步 Generator 函数,是封装异步操作的两种方法,都用来达到同一种目的。区别在于,前者自带执行器,后者通过`for await...of`执行,或者自己编写执行器。下面就是一个异步 Generator 函数的执行器。
```javascript
async function takeAsync(asyncIterable, count = Infinity) {
const result = [];
const iterator = asyncIterable[Symbol.asyncIterator]();
while (result.length < count) {
const {value, done} = await iterator.next();
if (done) break;
result.push(value);
}
return result;
}
```
上面代码中,异步 Generator 函数产生的异步遍历器,会通过`while`循环自动执行,每当`await iterator.next()`完成,就会进入下一轮循环。一旦`done`属性变为`true`,就会跳出循环,异步遍历器执行结束。
下面是这个自动执行器的一个使用实例。
```javascript
async function f() {
async function* gen() {
yield 'a';
yield 'b';
yield 'c';
}
return await takeAsync(gen());
}
f().then(function (result) {
console.log(result); // ['a', 'b', 'c']
})
```
异步 Generator 函数出现以后JavaScript 就有了四种函数形式普通函数、async 函数、Generator 函数和异步 Generator 函数。请注意区分每种函数的不同之处。基本上,如果是一系列按照顺序执行的异步操作(比如读取文件,然后写入新内容,再存入硬盘),可以使用 async 函数;如果是一系列产生相同数据结构的异步操作(比如一行一行读取文件),可以使用异步 Generator 函数。
异步 Generator 函数也可以通过`next`方法的参数,接收外部传入的数据。
```javascript
const writer = openFile('someFile.txt');
writer.next('hello'); // 立即执行
writer.next('world'); // 立即执行
await writer.return(); // 等待写入结束
```
上面代码中,`openFile`是一个异步 Generator 函数。`next`方法的参数,向该函数内部的操作传入数据。每次`next`方法都是同步执行的,最后的`await`命令用于等待整个写入操作结束。
最后,同步的数据结构,也可以使用异步 Generator 函数。
```javascript
async function* createAsyncIterable(syncIterable) {
for (const elem of syncIterable) {
yield elem;
}
}
```
上面代码中,由于没有异步操作,所以也就没有使用`await`关键字。
### yield\* 语句
`yield*`语句也可以跟一个异步遍历器。
```javascript
async function* gen1() {
yield 'a';
yield 'b';
return 2;
}
async function* gen2() {
// result 最终会等于 2
const result = yield* gen1();
}
```
上面代码中,`gen2`函数里面的`result`变量,最后的值是`2`
与同步 Generator 函数一样,`for await...of`循环会展开`yield*`
```javascript
(async function () {
for await (const x of gen2()) {
console.log(x);
}
})();
// a
// b
```

View File

@ -89,7 +89,7 @@ var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};
export { firstName, lastName, year };
```
上面代码在`export`命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在`var`语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。
@ -190,7 +190,7 @@ foo()
```javascript
// main.js
import {firstName, lastName, year} from './profile.js';
import { firstName, lastName, year } from './profile.js';
function setName(element) {
element.textContent = firstName + ' ' + lastName;

View File

@ -424,7 +424,7 @@ Math.cbrt = Math.cbrt || function(x) {
### Math.clz32()
`Math.clz32()`方法将参数转为 32 位无符号整数的形式,然后这个 32 位值里面有多少个前导 0。
`Math.clz32()`方法将参数转为 32 位无符号整数的形式,然后返回这个 32 位值里面有多少个前导 0。
```javascript
Math.clz32(0) // 32

View File

@ -40,7 +40,7 @@ Reflect.has(Object, 'assign') // true
```javascript
Proxy(target, {
set: function(target, name, value, receiver) {
var success = Reflect.set(target,name, value, receiver);
var success = Reflect.set(target, name, value, receiver);
if (success) {
console.log('property ' + name + ' on ' + target + ' set to ' + value);
}

View File

@ -43,6 +43,8 @@ String.raw`Hi\u000A!`;
```javascript
String.raw`Hi\\n`
// 返回 "Hi\\\\n"
String.raw`Hi\\n` === "Hi\\\\n" // true
```
`String.raw()`方法可以作为处理模板字符串的基本方法,它会将所有变量替换,而且对斜杠进行转义,方便下一步作为字符串来使用。
@ -123,6 +125,17 @@ for (let ch of s) {
// 61
```
另一种方法也可以,使用扩展运算符(`...`)进行展开运算。
```javascript
let arr = [...'𠮷a']; // arr.length === 2
arr.forEach(
ch => console.log(ch.codePointAt(0).toString(16))
);
// 20bb7
// 61
```
`codePointAt()`方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。
```javascript

View File

@ -32,6 +32,7 @@
1. [Module 的加载实现](#docs/module-loader)
1. [编程风格](#docs/style)
1. [读懂规格](#docs/spec)
1. [异步遍历器](#docs/async-iterator)
1. [ArrayBuffer](#docs/arraybuffer)
1. [最新提案](#docs/proposals)
1. [Decorator](#docs/decorator)