Iterator 遍历器的简单使用
Object接口 实现Iterator
我们可以使用 ES6 的展开运算符 … 和 for…of… 去遍历带有 Iterator 接口的数据结构,需要注意的是,Object 本身不具备 Iterator 接口,所以我们无法通过 … 把一个对象扩展到一个数组中,并且会报错,我们可以通过代码手动将 Object 类型实现 Iterator 接口。
没有实现Iterator 接口的Object
let obj = {
a: 1,
b: 2,
c: 3
};
let arr = [...obj];
结果:
实现Iterator 接口的Object
// 通过 Generator 函数给 Object 扩展 Iterator 接口
Object.prototype[Symbol.iterator] = function*() {
for (var key in this) {
yield this[key];
}
};
// 测试 Iterator 接口
let obj = {
a: 1,
b: 2,
c: 3
};
let arr = [...obj];
console.log(arr); // [1, 2, 3]
结果:
模拟 Generator
Generator 函数是一个生成器,调用后会返回给我们一个 Iterator 遍历器对象,在对象中有一个 next 方法,调用一次 next,帮我遍历一次,返回值为一个对象,内部有 value 和 done 两个属性,value 属性代表当前遍历的值,done 属性代表是否遍历完成,如果遍历完成后继续调用 next,返回的对象中 value 属性值为 undefined,done 属性值为 true,这个遍历器在进行数据遍历时更像给我们提供了一个暂停功能,每次都需要手动调用 next 去进行下一次遍历。
// 模拟遍历器生成函数
function iterator(arr) {
var i = 0;
return {
next: function() {
var done = i >= arr.length;
var value = !done ? arr[i++] : undefined;
return {
value: value,
done: done
};
}
};
}
测试一下模拟的遍历器生成函数:
// 测试 iterator 函数
var arr = [1, 3, 5];
// 遍历器
var result = iterator(arr);
result.next(); // {value: 1, done: false}
result.next(); // {value: 3, done: false}
result.next(); // {value: 5, done: false}
result.next(); // {value: undefined, done: true}
Generator 的基本使用
在普通的函数 function 关键字后加一个 * 就代表声明了一个生成器函数,执行后返回一个遍历器对象,每次调用遍历器的 next 方法时,遇到 yield 关键字暂停执行,并将 yield 关键字后面的值会作为返回对象中 value 的值,如果函数有返回值,会把返回值作为调用 next 方法进行遍历完成后返回的对象中 value 的值,果已经遍历完成,再次 next 调用这个 value 的值会变成 undefined。
// 生成器函数
function* gen() {
yield 1;
yield 2;
return 3;
}
// 遍历器
let it = gen();
it.next(); // {value: 1, done: false}
it.next(); // {value: 2, done: false}
it.next(); // {value: 3, done: true}
it.next(); // {value: undefined, done: true}
观察两个结果:
return
无return
在 Generator 函数中可以使用变量接收 yield 关键字执行后的返回值,只是接收的值并不是 yield 关键字后面表达式执行的结果,而是遍历器在下一次调用 next 方法时传入的参数。
在 Generator 函数中,如果在其他函数或方法调用的回调内部(函数的执行上/下文发生变化)不能直接使用 yield 关键字。
/ 正确的写法
function* gen(arr) {
for(let i = 0; i < arr.length; i++) {
yield arr[i];
}
}
如果在一个 Generator 函数中调用了另一个 Generator 函数,在调用外层函数返回遍历器的 next 方法时是不会遍历内部函数返回的遍历器的。
// 外层的生成器函数
function* genOut() {
yield "a";
yield genIn();
yield "c";
}
// 内层的生成器函数
function* genIn() {
yield "b";
}
// 遍历器
let it = genOut();
it.next(); // {value: 'a', done: false}
it.next(); // 返回 genIn 的遍历器对象
it.next(); // {value: 'c', done: false}
it.next(); // {value: undefined, done: true}
在 genOut 返回的遍历器调用 next 遇到 yield* 表达式时帮我们去遍历了 genIn 返回的遍历器,其实 yield* 内部做了处理,等同于下面代码:
// 外层的生成器
function* genOut() {
yield "a";
for (let v of genIn()) {
yield v;
}
yield "c";
}
// 内层的生成器
function* genIn() {
yield "b";
}
// 遍历器
let it = genOut();
it.next(); // {value: 'a', done: false}
it.next(); // {value: 'b', done: false}
it.next(); // {value: 'c', done: false}
it.next(); // {value: undefined, done: true}
Generators 与 Promise 结合
因为 Generator 函数在执行时遇到 yield 关键字会暂停执行,那么 yield 后面可以是异步操作的代码,比如 Promise,需要继续执行,就手动调用返回遍历器的 next 方法,因为中间有一个等待的过程,所以在执行异步代码的时候避免了回调函数的嵌套,在写法上更像同步,更容易理解。
我们来设计一个 Generator 函数与 Promise 异步操作结合的使用场景,假设我们需要使用 NodeJS 的 fs 模块读取一个文件 a.txt 的内容,而 a.txt 的内容是另一个需要读取文件 b.txt 的文件名,读取 b.txt 最后打印读取到的内容 “Hello world”。
// 引入依赖
let fs = require("fs");
let util = require("util");
// 将 readFile 方法转换成 Promise
let read = util.promisify(fs.readFile);
// 生成器函数
function* gen() {
let aData = yield read("1.txt", "utf8");
let bData = yield read(aData, "utf8");
return bData;
}
// 遍历器
let it = gen();
it.next().value.then(data => {
it.next(data).then(data => {
console.log(data); // Hello world
});
});
co 库的使用
co 库是基于 Generator 和 Promise 的,这个库能帮我们实现自动调用 Generator 函数返回遍历器的 next 方法,并执行 yield 后面 Promise 实例的 then 方法,所以每次 yield 后面的异步操作返回的必须是一个 Promise 实例,代码看起来像同步,执行其实是异步,不用自己手动进行下一次遍历.