迭代器

在 javaScript 中,如果一个对象具有 next 方法,并且该方法返回一个对象,格式如下:

1
{ value: 值, done: 是否迭代完成 }

则认为该对象是一个迭代器

  • next 方法用于得到下一个数据
  • value 下一个数据的值
  • done 是否迭代完成

Iterator接口

迭代器创建函数: 用于创建一个迭代器

ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,该方法是一个迭代器创建函数

原生具备 Iterator 接口的数据结构

  • ArrayMapSetStringarguments 等原生类型
  • NodeList

调用 Iterator 接口的地方

  • 展开运算符(除了对象的展开)本质上是调用的是 Iterator 接口

  • 数组和 set 的解构本质上调用的也是 Iterator 接口

  • 部署了 Symbol.iterator 接口,即可被 for...of 循环所遍历

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

  • Array.fromMapSetPromise.all 等方法

其他方法

遍历器对象除了具有 next 方法,还可以具有 return 方法和 throw 方法,next 方法是必须部署的,return 方法和 throw 方法是否部署是可选的。

return 方法的使用场合:如果 for...of 循环提前退出,就会调用 return 方法。

如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return 方法。

return 方法必须返回一个对象,这是 Generator 语法决定的

生成器

调用生成器函数会返回一个生成器,生成器就是一个迭代器。该迭代器代表该生成器函数的内部指针,控制函数的执行。

该迭代器的原型链上部署了 Symbol.iterator 接口,调用返回自己。

1
2
3
function* gen() {}
const g = gen();
console.log(g[Symbol.iterator]() === g); // true

每次调用 next 方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式(或 return 语句)为止。

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

throw

Generator 函数返回的生成器,都有一个 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。

  • throw 方法抛出的错误要被内部捕获,前提是必须至少执行过一次 next 方法
  • throw 方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法
1
2
3
4
5
6
7
8
9
10
11
12
function* gen(){
try {
yield 'a';
} catch (e) { }
yield 'b';
yield 'c';
}

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

return

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

迭代器调用 return 方法后,返回值的 value 属性就是 return 方法的参数

如果 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;
}
const 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 }

yield*

任何数据结构只要有 Iterator 接口,就可以被 yield* 遍历。

yield* 可用来在一个 Generator 函数里面执行另一个 Generator 函数。

如果被代理的 Generator 函数有 return 语句,那么就可以向代理它的 Generator 函数返回数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function* foo() {
yield 2;
yield 3;
return "foo";
}

function* bar() {
yield 1;
var v = yield* foo();
console.log("v: " + v);
yield 4;
}

const it = bar();
it.next(); // {value: 1, done: false}
it.next(); // {value: 2, done: false}
it.next(); // {value: 3, done: false}
it.next(); // "v: foo" // {value: 4, done: false}
it.next(); // {value: undefined, done: true}

执行上下文

JavaScript 代码运行时,会产生一个全局的上下文环境(context),包含了当前所有的变量和对象。然后,执行函数(或块级代码)的时候,又会在当前上下文环境的上层,产生一个函数运行的上下文,变成当前(active)的上下文,由此形成一个上下文环境的堆栈(context stack)。

这个堆栈是“后进先出”的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行完成它下层的上下文,直至所有代码执行完成,堆栈清空。

Generator 函数不是这样,它执行产生的上下文环境,一旦遇到 yield 命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行 next 命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。

异步应用

Generator 函数可以将异步的代码以同步的方式书写。

但是由于 Generator 函数无法自动执行,需要借助外部来执行。

Thunk函数

Thunk 函数是自动执行 Generator 函数的一种方法。

Thunk 函数的作用是将多参数函数替换成一个只接受回调函数作为参数的单参数函数。

1
2
3
4
5
6
7
function Thunk(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};

实现自动执行的执行器:

1
2
3
4
5
6
7
8
9
10
11
12
13
function run(fn) {
const gen = fn();

function next(err, data) {
const result = gen.next(data);
if (result.done) {
return;
}
result.value(next); // result.value 是一个函数
}

next();
}

co模块

co 模块是著名程序员 TJ Holowaychuk 于 2013 年 6 月发布的一个小工具,用于 Generator 函数的自动执行。

co 模块可以让你不用编写 Generator 函数的执行器

1
2
var co = require('co');
co(gen);

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

Thunk 函数并不是 Generator 函数自动执行的唯一方案。因为自动执行的关键是,必须有一种机制,自动控制 Generator 函数的流程,接收和交还程序的执行权。回调函数可以做到这一点,Promise 对象也可以做到这一点。

co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator 函数的yield命令后面,只能是 Thunk 函数或 Promise 对象。

如果数组或对象的成员,全部都是 Promise 对象,也可以使用 co,实现并发的异步操作。

简易实现

可以很简单的实现一个能够自动执行 Generator 函数的执行器。Thunk 函数太绕了,可以只考虑使用 Promise,很容易就能实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function run(gen) {
const i = gen();
next(i.next());

function next(res) {
if (res.done) {
return;
} else if (typeof res.value.then === "function") {
res.value.then(data => next(i.next(data)))
} else {
next(i.next(res.value));
}
}
}

async函数

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。

async 简单来说是 Generator 函数的语法糖,区别在于 async函数可以自执行,不需要执行器。而且语义更好、兼容性更好。

async函数的实现原理

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

1
2
3
4
5
6
async function fn(args) { }

// 等同于
function fn(args) {
return spawn(function* () { });
}

spawn 函数就是自动执行器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function spawn(gen) {
return new Promise((resolve, reject) => {
const i = gen();
step(() => i.next())
function step(next) {
let res;
try {
res = next();
} catch (e) {
return reject(e);
}
const { value, done } = res;
if (done) {
return resolve(value);
}
Promise.resolve(value).then(
v => step(() => i.next(v)),
e => step(() => i.throw(e))
)
}
})
}

异步迭代器

异步遍历器的最大的语法特点,就是调用迭代器的 next 方法,返回的是一个 Promise 对象。

一个对象的同步遍历器的接口,部署在 Symbol.iterator 属性上面。同样地,异步遍历器接口,部署在 Symbol.asyncIterator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const obj = {
async *[Symbol.asyncIterator]() {
const arr = ['a', 'b'];
for (const item of arr) {
yield Promise.resolve(item)
}
}
}

async function f() {
const asyncIterator = obj[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 }
}

f();

for await…of

for...of循环用于遍历同步的 Iterator 接口,for await...of 循环,则是用于遍历异步的 Iterator 接口。

1
2
3
4
5
6
async function f() {
const asyncIterator = obj[Symbol.asyncIterator]();
for await (const value of asyncIterator) {
console.log(value); // a b
}
}

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

for await...of 循环也可以用于同步迭代器。

异步Generator函数

就像 Generator 函数返回一个同步迭代器对象一样,异步 Generator 函数的作用,是返回一个异步迭代器对象。

1
2
3
4
5
6
async *asyncGenerator() {
const arr = ['a', 'b'];
for (const item of arr) {
yield Promise.resolve(item)
}
}

yield*

在异步生成器函数中 yield* 也可以跟一个异步遍历器

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

async function* gen2() {
const result = yield* gen1();
console.log(result); // 2
}