ES6读书笔记(三)

前言

前段时间整理了ES6的读书笔记:《ES6读书笔记(一)》《ES6读书笔记(二)》,现在为第三篇,本篇内容包括:

  • 一、Promise
  • 二、Iterator和for of循环
  • 三、Generator
  • 四、async

本文笔记也主要是根据阮一峰老师的《ECMAScript 6 入门》和平时的理解进行整理的,希望对你有所帮助,喜欢的就点个赞吧!

一、Promise

1. 执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});

promise.then(function() {
console.log('resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// resolved

2.异步加载图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
const image = new Image();

image.onload = function() {
resolve(image);
};

image.onerror = function() {
reject(new Error('Could not load image at ' + url));
};

image.src = url;
});
}

3.用Promise对象实现Ajax:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
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;
};

getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});

4.then方法返回的是一个新的Promise实例(不是原来那个Promise实例)

5.如果 Promise 状态已经变成resolved,再抛出错误是无效的:

1
2
3
4
5
6
7
8
const promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok

上面代码中,Promise 在resolve语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了

6.跟传统的try/catch代码块不同的是,如果没有使用catch方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应,通俗的说法就是“Promise 会吃掉错误”:

1
2
3
4
5
6
7
8
9
10
11
12
13
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
resolve(x + 2); // 会报错,因为x没有声明
});
};

someAsyncThing().then(function() {
console.log('everything is great');
});

setTimeout(() => { console.log(123) }, 2000); // 虽然以上有错误,但没有阻塞后面的代码
// Uncaught (in promise) ReferenceError: x is not defined
// 123

7.

1
2
3
4
5
6
7
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 指定在下一轮“事件循环”再抛出错误。到了那个时候,Promise 的运行已经结束了,所以这个错误是在 Promise 函数体外抛出的,会冒泡到最外层,成了未捕获的错误,相当于是js引擎去执行了这个回调,而不是在promise内部执行。

一般总是建议,Promise 对象后面要跟catch方法,这样可以处理 Promise 内部发生的错误。catch方法返回的还是一个 Promise 对象,因此后面还可以接着调用then方法:

  • ①如果有错误,但没有去catch,则会阻塞promise内部的代码,但不会阻塞外部的代码;
  • ②如果有catch,但是没有错误,则会跳过catch,继续执行后面的代码;
  • ③如果有catch,然后被catch捕获了错误,那依旧可以继续执行后面的代码;
  • ④如果有catch,catch捕获到了前面的错误,但catch内部又有错误的话,则会阻塞后面的代码,除非后面再链式调用catch捕获该错误。

以上总结就是只要promise内部有错误没有被捕获,就会阻塞内部代码,但不会阻塞外部代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};

someAsyncThing()
.catch(function(error) {
console.log('oh no', error);
})
.then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on
//--------------------------------------------
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};

someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
y + 2; // y 没有声明会报错,且这个错误未被捕获,会阻塞后面的代码
}).then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]

8.Promise.prototype.finally()

  • ①finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作:
    1
    2
    3
    4
    promise
    .then(result => {···})
    .catch(error => {···})
    .finally(() => {···});

上面代码中,不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。

  • ②finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

  • ③finally本质上是then方法的特例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    promise
    .finally(() => {
    // 语句
    });

    // 等同于
    promise
    .then(
    result => {
    // 语句
    return result;
    },
    error => {
    // 语句
    throw error;
    }
    );
  • ④finally方法总是会返回原来的值:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // resolve 的值是 undefined
    Promise.resolve(2).then(() => {}, () => {})

    // resolve 的值是 2
    Promise.resolve(2).finally(() => {})

    // reject 的值是 undefined
    Promise.reject(3).then(() => {}, () => {})

    // reject 的值是 3
    Promise.reject(3).finally(() => {})

9.Promise.all(数组或具有 Iterator 接口,且返回的每个成员都是 Promise 实例)

  • ①如果参数全为fulfilled,则返回对应的数组结果(是等全部得到结果了再一起返回),但如果有一个是rejected,则返回第一个rejected的返回值,状态就为rejected。

  • ②catch后会变为resolved:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const p1 = new Promise((resolve, reject) => {
    resolve('hello');
    })
    .then(result => result)
    .catch(e => e);

    const p2 = new Promise((resolve, reject) => {
    throw new Error('报错了');
    // reject(“world”);
    })
    .then(result => result)
    .catch(e => e); // catch后会变为resolved

    Promise.all([p1, p2])
    .then(result => console.log(result))
    .catch(e => console.log(e)); // 传入的p2有自己的catch,所以不会触发这里的catch,所以没有捕获到错误,所以就相当于都是执行正确的,所以会有结果
    // ["hello", Error: 报错了]

10.Promise.race

参数中谁率先改变了状态,就返回谁的状态,这意味着只返回一个结果

11.Promise.resolve()

  • ①Promise.resolve方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。
    所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve方法:

    1
    2
    3
    4
    5
    const p = Promise.resolve();

    p.then(function () {
    // ...
    });
  • ②立即resolve的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    setTimeout(function () {
    console.log('three');
    }, 0);

    Promise.resolve().then(function () {
    console.log('two');
    });

    console.log('one');

    // one
    // two
    // three

12.Promise.reject()

Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致:

1
2
3
4
5
6
7
8
9
10
const thenable = {
then(resolve, reject) {
reject('出错了');
}
};
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true

上面代码中,Promise.reject方法的参数是一个thenable对象,执行以后,后面catch方法的参数不是reject抛出的“出错了”这个字符串,而是thenable对象。

13.如果对于一个函数,不管是同步或异步,都想使用then方法指定下一流程,可使用以下方式,让它是同步时就按同步执行,是异步时就按异步执行:

不要直接使用promise.resolve(),因为如果是同步函数,会在本轮事件循环末尾才会执行:

1
2
3
4
5
const f = () => console.log('now');
Promise.resolve().then(f); // then才是微任务,resolve时还是同步的
console.log('next');
// next
// now

  • ①使用async:

    1
    2
    3
    4
    5
    6
    7
    8
    const f = () => console.log('now');
    (async () => f())()
    .then(...)
    .catch(...);

    console.log('next');
    // now
    // next
  • ②使用new Promise():

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const f = () => console.log('now');
    (
    () => new Promise(
    resolve => resolve(f())
    )
    )();
    console.log('next');
    // now
    // next
  • ③一个提案,提供Promise.try方法替代上面的写法:浏览器目前会报错

    1
    2
    3
    4
    5
    const f = () => console.log('now');
    Promise.try(f);
    console.log('next');
    // now
    // next

二、Iterator和for of循环

1. Iterator(遍历器)的概念

JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是Map,Map的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。

Iterator 接口是一种数据遍历的协议,只要调用遍历器对象的next方法,就会得到一个对象,表示当前遍历指针所在的那个位置的信息。

遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for…of循环,Iterator 接口主要供for…of消费。

Iterator 的遍历过程是这样的:

  • (1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
  • (2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
  • (3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
  • (4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

一个模拟next方法返回值的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var it = makeIterator(['a', 'b']);

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}

2.ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内。

1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
[Symbol.iterator] : function () {
return {
next: function () {
return {
value: 1,
done: true
};
}
};
}
};

3.原生具备 Iterator 接口的数据结构如下:不含对象

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

4.数组的Symbol.iterator属性:

1
2
3
4
5
6
7
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }

5.对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了。

6.有一些场合会默认调用 Iterator 接口(即Symbol.iterator方法):

  • ①解构赋值
  • ②扩展运算符:这样就可对有Iterator接口的数据结构使用扩展运算符转为数组,而对于没有Iterator接口的类数组,可采用Array.from转为数组,这样就具有了Iterator接口
  • ③yield*

yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let generator = function* () {
yield 1;
yield* [2,3,4];
yield 5;
};

var iterator = generator();

iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }

  • ④其他场合
    由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口:

  • for…of

  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()(比如new Map([[‘a’,1],[‘b’,2]]))
  • Promise.all()
  • Promise.race()

7. Iterator接口与Generator函数

Symbol.iterator方法的最简单实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let myIterable = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
}
}
[...myIterable] // [1, 2, 3]

// 或者采用下面的简洁写法

let obj = {
* [Symbol.iterator]() {
yield 'hello';
yield 'world';
}
};

for (let x of obj) {
console.log(x);
}
// "hello"
// "world"

8.for…of循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟for…in循环也不一样。

1
2
3
4
5
6
7
8
9
10
let arr = [3, 5, 7];
arr.foo = 'hello';

for (let i in arr) {
console.log(i); // "0", "1", "2", "foo" 这也说明了for in遍历了自身及原型上的可枚举属性
}

for (let i of arr) {
console.log(i); // "3", "5", "7"
}

9. Map遍历得到的是数组,Set遍历得到的是单个值:

1
2
3
4
5
6
7
8
9
10
11
12
13
let map = new Map().set('a', 1).set('b', 2);
Map; // {"a" => 1, "b" => 2}
for (let pair of map) {
console.log(pair);
}
// ['a', 1]
// ['b', 2]

for (let [key, value] of map) {
console.log(key + ' : ' + value);
}
// a : 1
// b : 2

10. 可用Array.from将不具有iterator接口的类数组对象转为数组,这样也就具有了iterator接口:

1
2
3
4
5
6
7
8
9
10
11
let arrayLike = { length: 2, 0: 'a', 1: 'b' };

// 报错
for (let x of arrayLike) {
console.log(x);
}

// 正确
for (let x of Array.from(arrayLike)) {
console.log(x);
}

11.循环对比:

  • for in 会遍历原型可枚举属性,为遍历对象而生,尽管对象没有iterator接口
  • forEach不能中途跳出循环
  • for of 可中途跳出循环,不会遍历原型可枚举属性,针对数组

三、Generator

1. 执行Generator(生成器)返回一个遍历器对象,这个遍历器对象可以依次遍历Generator函数内部的每一个状态,yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}

var hw = helloWorldGenerator();
hw.next() // 遇到第一个yield,暂停,然后返回yield后面的表达式的值
// { value: 'hello', done: false }

hw.next() // 从上次暂停的地方往下执行,遇到第二个yield后暂停,返回值
// { value: 'world', done: false }

hw.next() // 从上次暂停的地方往下执行,发现没有yield了,所以一直往下执行,直到遇到// return,如果没有return则返回undefined
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

定义方式:

1
2
3
4
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· } // 推荐这种写法
function*foo(x, y) { ··· }

2. 遍历器对象的next方法的运行逻辑如下:

  • (1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

  • (2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

  • (3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

  • (4)如果该函数没有return语句,则返回的对象的value属性值为undefined。

需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

3. Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。

1
2
3
4
5
6
7
8
9
function* f() {
console.log('执行了!')
}

var generator = f();

setTimeout(function () {
generator.next()
}, 2000);

4. yield表达式如果用在另一个表达式之中,必须放在圆括号里面:

1
2
3
4
5
6
7
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError

console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}

yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号:

1
2
3
4
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}

5.与 Iterator 接口的关系:

任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。

由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口:

1
2
3
4
5
6
7
8
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};

[...myIterable] // [1, 2, 3]

上面代码中,Generator 函数赋值给Symbol.iterator属性,从而使得myIterable对象具有了 Iterator 接口,可以被…运算符遍历了。

Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身:

1
2
3
4
5
6
7
8
function* gen(){
// some code
}

var g = gen();

g[Symbol.iterator]() === g
// true

6.yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
function* f() {
for(var i = 0; true; i++) {
var reset = yield i; // yield i表达式是没有返回值的,或者说返回undefined
if(reset) { i = -1; }
}
}

var g = f();

g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false } 相当于给reset赋值为true,重置了i的值

next参数的值是传给上一个yield表达式的返回值,所以这也意味着第一个next的参数是无效的,所以不需要传,即第一个next是用于启动遍历器对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false} yield (x + 1)的返回值是undefined,所以乘2再除3得到的是NaN
a.next() // Object{value:NaN, done:true} 5 + NaN + undefined为NaN

var b = foo(5);
b.next() // { value:6, done:false } 5+1得到6
b.next(12) // { value:8, done:false } 12赋给yield (x + 1),然后乘2除3得到8,即(y / 3)的值
b.next(13) // { value:42, done:true } 同理

//-----------------------------------------
function* dataConsumer() {
console.log('Started');
console.log(`1. ${yield}`);
console.log(`2. ${yield}`);
return 'result';
}

let genObj = dataConsumer();
genObj.next();
// Started
// {value: undefined, done: false}
genObj.next('a')
// 1. a
// {value: undefined, done: false}
genObj.next('b')
// 2. b
// {value: "result", done: true}

7. for of 循环:

for…of循环可以自动遍历 Generator 函数时生成的Iterator对象,且此时不再需要调用next方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}

for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5 没有6,因为一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象

8. Generator.prototype.throw()

Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
};

var i = g();
i.next(); // 要捕获错误,必须先执行一次next来启动遍历器对象

try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b

9. throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var gen = function* gen(){
try {
yield console.log('a');
} catch (e) {
// ...
}
yield console.log('b');
yield console.log('c');
}

var g = gen();
g.next() // a
g.throw() // b
g.next() // c

10. Generator.prototype.return()

Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数:

1
2
3
4
5
6
7
8
9
10
11
function* gen() {
yield 1;
yield 2;
yield 3;
}

var g = gen();

g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }

如果return方法调用时,不提供参数,则返回值的value属性为undefined:

1
2
3
4
5
6
7
8
9
10
function* gen() {
yield 1;
yield 2;
yield 3;
}

var g = gen();

g.next() // { value: 1, done: false }
g.return() // { value: undefined, done: true }

如果 Generator 函数内部有try…finally代码块,且正在执行try代码块,那么return方法会推迟到finally代码块执行完再执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
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方法。

11. 在一个 Generator 函数里面执行另一个 Generator 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function* bar() {
yield 'x';
yield* foo(); // 加了*号就是返回遍历器内部值,相当于调用了*后面变量的iterator接口,否则返回遍历器对象
yield 'y';
}

// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}

// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}

for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
3.1 异步应用

1.异步简单说就是不连续的执行任务,类似一个协程的过程:

1
2
3
4
5
function* asyncJob() {
// ...其他代码
var f = yield readFile(fileA);
// ...其他代码
}

其中的yield命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield命令是异步两个阶段的分界线,协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。

2.Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。

3.传值调用:先计算参数值再传入函数体内使用。
传名调用:直接将参数表达式传入函数体内,使用到时再进行求值。

4.Generator的异步应用中何时调用第一步,何时调用第二步,此时就需要使用thunk函数,相当于“传名调用”,编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体,这个临时函数就叫做 Thunk 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function f(m) {
return m * 2;
}

f(x + 5);

// 等同于

var thunk = function () {
return x + 5;
};

function f(thunk) {
return thunk() * 2;
}

5.JavaScript 语言的 Thunk 函数

JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数,类似柯里化:

1
2
3
4
5
6
7
8
9
10
11
12
// 正常版本的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);

四、async

1. async 函数其实就是 Generator 函数的语法糖,可以再等待第一阶段得到结果后自动执行第二阶段,而不是像Generator那样手动执行。*换成了async,yield换成了await。

2. async函数对 Generator 函数的改进,体现在以下加点:

  • ①内置执行器
  • ②更语义化
  • ③适应性
  • ④返回promise

3. async函数返回一个 Promise 对象。

async函数内部return语句返回的值,会成为then方法回调函数的参数:

1
2
3
4
5
6
async function f() {
return 'hello world';
}

f().then(v => console.log(v))
// "hello world"

如果没有return,则then方法回调函数的参数则得到的是undefined

1
2
3
4
async function f() {
await Promise.resolve('hello world'); // 不会执行
}
f().then(a=>{console.log(a)}) // undefined

4. Promise 对象的状态变化

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数:

1
2
3
4
5
6
7
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"

上面代码中,函数getTitle内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行then方法里面的console.log

5. 一般await后面是接promise对象,返回该对象的结果,如果不是promise对象,则直接返回对应的值:

1
2
3
4
5
6
7
8
async function f() {
// 等同于
// return 123;
return await 123; // return要放await前面,否则会报错
}

f().then(v => console.log(v))
// 123

6. await后面的promise状态如果为reject,则会被catch到:

1
2
3
4
5
6
7
8
async function f() {
await Promise.reject('出错了');
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出错了

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。

1
2
3
4
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}

为了防止有错误或reject中断代码的执行,则需要使用catch来处理,或者使用try catch:

1
2
3
4
5
6
7
8
9
10
async function f() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// 出错了
// hello world

如果有多个await命令,可以统一放在try…catch结构中:

1
2
3
4
5
6
7
8
9
10
11
12
async function main() {
try {
const val1 = await firstStep();
const val2 = await secondStep(val1);
const val3 = await thirdStep(val1, val2);

console.log('Final: ', val3);
}
catch (err) {
console.error(err);
}
}

7. 使用async注意点:

  • ①catch错误,防止代码中断
  • ②对于不存在继发关系的异步操作,应该让它们同步进行,而不是顺序执行:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let foo = await getFoo();
    let bar = await getBar();

    // 写法一
    let [foo, bar] = await Promise.all([getFoo(), getBar()]);

    // 写法二
    let fooPromise = getFoo();
    let barPromise = getBar();
    let foo = await fooPromise;
    let bar = await barPromise;
  • ③await命令只能用在async函数之中,如果用在普通函数,就会报错,如用在forEach中会报错,因为是并发执行,应该使用for循环:

    1
    2
    3
    4
    5
    6
    7
    8
    unction dbFuc(db) { //这里不需要 async
    let docs = [{}, {}, {}];

    // 可能得到错误结果
    docs.forEach(async function (doc) {
    await db.post(doc);
    });
    }

上面代码可能不会正常工作,原因是这时三个db.post操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用for循环。

1
2
3
4
5
6
7
async function dbFuc(db) {
let docs = [{}, {}, {}];

for (let doc of docs) {
await db.post(doc);
}
}

  • ④async 函数可以保留运行堆栈。
    1
    2
    3
    const a = () => {
    b().then(() => c());
    };

上面代码中,函数a内部运行了一个异步任务b()。当b()运行的时候,函数a()不会中断,而是继续执行。等到b()运行结束,可能a()早就运行结束了,b()所在的上下文环境已经消失了。如果b()或c()报错,错误堆栈将不包括a()。

改成async函数:

1
2
3
4
const a = async () => {
await b();
c();
};

上面代码中,b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦b()或c(),错误堆栈将包括a()。

8. async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里:

1
2
3
4
5
6
7
8
9
10
11
async function fn(args) {
// ...
}

// 等同于

function fn(args) {
return spawn(function* () {
// ...
});
}

9. 继发/并发顺序输出:

1
2
3
4
5
6
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}

上面代码确实大大简化,问题是所有远程操作都是继发。只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间。我们需要的是并发发出远程请求:

1
2
3
4
5
6
7
8
9
10
11
12
async function logInOrder(urls) {
// 并发读取远程URL
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});

// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}

10. 异步遍历器:asyncIterator,部署在Symbol.asyncIterator属性上面,最大的语法特点就是调用遍历器的next方法,返回的是一个 Promise 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 }
});

可改写为:

1
2
3
4
5
6
7
8
9
10
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方法是可以连续调用的,不必等到上一步产生的 Promise 对象resolve以后再调用。这种情况下,next方法会累积起来,自动按照每一步的顺序运行下去,所以也可以这样:

1
2
3
4
5
6
7
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

11. for await…of

for…of循环用于遍历同步的 Iterator 接口。新引入的for await…of循环,则是用于遍历异步的 Iterator 接口:

1
2
3
4
5
6
7
async function f() {
for await (const x of createAsyncIterable(['a', 'b'])) {
console.log(x);
}
}
// a
// b

如果next方法返回的 Promise 对象被reject,for await…of就会报错,要用try…catch捕捉。

1
2
3
4
5
6
7
8
9
async function () {
try {
for await (const x of createRejectingIterable()) {
console.log(x);
}
} catch (e) {
console.error(e);
}
}

注意,for await…of循环也可以用于同步遍历器:

1
2
3
4
5
6
7
(async function () {
for await (const x of ['a', 'b']) {
console.log(x);
}
})();
// a
// b

12. 异步 Generator 函数

就像 Generator 函数返回一个同步遍历器对象一样,异步 Generator 函数的作用,是返回一个异步遍历器对象:

1
2
3
4
5
6
async function* gen() {
yield 'hello';
}
const genObj = gen();
genObj.next().then(x => console.log(x));
// { value: 'hello', done: false }

13. yield* 语句

yield*语句也可以跟一个异步遍历器:

1
2
3
4
5
6
7
8
9
10
async function* gen1() {
yield 'a';
yield 'b';
return 2;
}

async function* gen2() {
// result 最终会等于 2
const result = yield* gen1();
}

与同步 Generator 函数一样,for await…of循环会展开yield*:

1
2
3
4
5
6
7
(async function () {
for await (const x of gen2()) { // 也是相当于执行了gen的遍历器
console.log(x);
}
})();
// a
// b

最后

因为比较多,所以目前只整理到这里,后续有些比较重要难懂的模块会分开更新,同时包括ES6的部分,希望对你有所帮助,如有不合理的地方欢迎指正,喜欢的就关注一波吧,后续会持续更新。