【JS基础】从JavaScript中的for…of说起(上) – iterator 和 generator

38次阅读

共计 5470 个字符,预计需要花费 14 分钟才能阅读完成。

写在前面
本文首发于公众号:符合预期的 CoyPan
先来看一段很常见的代码:
const arr = [1, 2, 3];
for(const i of arr) {
console.log(i); // 1,2,3
}
上面的代码中,用 for…of 来遍历一个数组。其实这里说遍历不太准确,应该是说:for…of 语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。
iterator
ECMAScript 2015 规定了关于迭代的协议,这些协议可以被任何遵循某些约定的对象来实现。如果一个 js 对象想要能被迭代,那么这个对象或者其原型链对象必须要有一个 Symbol.iterator 的属性,这个属性的值是一个无参函数,返回一个符合迭代器协议的对象。这样的对象被称为符合【可迭代协议】。
typeof Array.prototype[Symbol.iterator] === ‘function’; // true
typeof Array.prototype[Symbol.iterator]() === ‘object’; // true
数组之所以可以被 for…of 迭代,就是因为数组的原型对象上拥有 Symbol.iterator 属性,这个属性返回了一个符合【迭代器协议】的对象。
一个符合【迭代器协议】的对象必须要有一个 next 属性,next 属性也是一个无参函数,返回一个对象,这个对象至少需要有两个属性:done, value, 大概长成下面这样:
{
next: function(){
return {
done: boolean, // 布尔值,表示迭代是否完成,如果没有这个属性,则默认为 false
value: any // 迭代器返回的任何 javascript 值。如果迭代已经完成,value 属性可以被省略
}
}
}
依旧来看一下数组:
typeof Array.prototype[Symbol.iterator]().next === ‘function’ // true
Array.prototype[Symbol.iterator]().next() // {value: undefined, done: true}

const iteratorObj = [1,2,3][Symbol.iterator]();
iteratorObj.next(); // { value: 1, done: false}
iteratorObj.next(); // { value: 2, done: false}
iteratorObj.next(); // { value: 3, done: false}
iteratorObj.next(); // { value: undefined, done: true}
我们自己来实现一个可以迭代的对象。
const myIterator = {
[Symbol.iterator]: function() {
return {
i: 0,
next: function() {
if(this.i < 2) {
return {value: this.i++ , done: false};
} else {
return {done: true};
}
}
}
}
}
for(const item of myIterator) {
console.log(item);
}

// 0
// 1
不光 for…of 会使用对象的 iterator 接口,下面这些用法也会默认使用对象的 iteretor 接口。(1) 解构赋值 (2) 扩展运算符 (3) yield*
generator
生成器对象和生成器函数
generator 表示一个生成器对象。这个对象符合【可迭代协议】和【迭代器协议】,是由生成器函数 (generator function) 返回的。
什么是生成器函数呢?MDN 上的描述如下:
生成器函数在执行时能暂停,后面又能从暂停处继续执行。调用一个生成器函数并不会马上执行它里面的语句,而是返回一个这个生成器的 迭代器(iterator)对象。当这个迭代器的 next() 方法被首次(后续)调用时,其内的语句会执行到第一个(后续)出现 yield 的位置为止,yield 后紧跟迭代器要返回的值。或者如果用的是 yield*(多了个星号),则表示将执行权移交给另一个生成器函数(当前生成器暂停执行)。next()方法返回一个对象,这个对象包含两个属性:value 和 done,value 属性表示本次 yield 表达式的返回值,done 属性为布尔类型,表示生成器后续是否还有 yield 语句,即生成器函数是否已经执行完毕并返回。
看下面的例子:
function* gen() { // gen 一个生成器函数
yield 1;
yield 2;
yield 3;
}
const g = gen(); // g 是一个生成器对象,是可迭代的
Object.prototype.toString.call(g) === “[object Generator]” // true
g.next(); // { value: 1, done: false}
g.next(); // { value: 2, done: false}
g.next(); // { value: 3, done: false}
g.next(); // { value: undefined, done: true}
因为生成器对象符合可迭代协议和迭代器协议,我们可以用 for…of 来进行迭代。for…of 会拿到迭代器返回值的 value,也就是说,在迭代 generator 时,for…of 拿到的是 yield 后面紧跟的那个值。
function* gen2() {
yield ‘a’;
yield ‘b’;
yield ‘c’;
}
const g2 = gen2();
for(const i of g2) {
console.log(i);
}
// a
// b
// c
生成器函数的 ” 嵌套 ”
function *gen1(i) {
yield i+1;
yield i+2;
yield *gen2(i+2); // 将执行权移交给 gen2
yield i+3;
}

function *gen2(i) {
yield i*2;
}

const g = gen1(0);
g.next(); // { value: 1, done: false}
g.next(); // { value: 2, done: false}
g.next(); // { value: 4, done: false}
g.next(); // { value: 3, done: false}
g.next(); // { value: undefined, done: true}

生成器函数里的参数传递
function* gen3() {
let a = yield 1;
console.log(‘a:’, a);
let b = yield a + 1;
yield b + 10;
}
const g = gen3();
g.next(); // { value: 1, done: false} 这个时候,代码执行到 gen3 里第一行等号右边
g.next(100); // a: 100 , {value: 101, done: false}。代码执行第一行等号的左边,我们传入了 100,这个 100 会作为 a 的值,接着执行第二行的 log, 然后执行到第三行等号的右边。
g.next(); // { value: NaN, done: false}。代码执行第三行等号的左半部分,由于我们没有传值,b 就是 undefined, undefined + 10 就是 NaN 了。
g.next(); // { value: undefined, done: true}
如果我们使用 for…of 来遍历上述的生成器对象,由于 for…of 拿到的是迭代器返回值的 value,所以会得到以下的结果:
function* gen4() {
let a = yield 1;
let b = yield a + 1;
yield b + 10;
}
const g4 = gen4();
for(const i of g4) {
console.log(i);
}
// 1
// NaN
// NaN
下面是一个使用 generator 和 for…of 输出斐波拉契数列的经典例子:
function* fibonacci() {
let [prev, curr] = [0, 1];
while(1){
[prev, curr] = [curr, prev + curr];
yield curr;
}
}
for (let n of fibonacci()) {
if (n > 100) {
break
}
console.log(n);
}
稍微总结一下,generator 给了我们控制暂停代码执行的能力,我们可以自己来控制代码执行。那是否可以用 generator 来写异步操作呢 ?
iterator,generator 与异步操作
一个很常见的场景: 页面发起一个 ajax 请求,请求返回后,执行一个回调函数。在这个回调函数里,我们使用第一个请求返回的 url,再次发起一个 ajax 请求。(这里先不考虑使用 Promise)
// 我们先定义发起 ajax 的函数,这里用 setTimeout 模拟一下
function myAjax(url, cb) {
setTimeout(function(){
const data = ‘ajax 返回了 ’;
cb && cb(resData);
}, 1000);
}

// 一般情况下,要实现需求,一般可以这样写
myAjax(‘https://xxxx’, function(url){
myAjax(url, function(data){
console.log(data);
});
});
我们尝试用 generator 的写法来实现上面的需求.

// 先把 ajax 函数改造一下, 把 url 提出来作为一个参数,然后返回一个只接受回调函数作为参数的 newAjax 函数
// 这种只接受回调函数作为参数的函数被称为 thunk 函数。
function thunkAjax(url) {
return function newAjax(cb){
myAjax(url, cb);
}
}

// 我们定义一个 generator function
function* gen() {
const res1 = yield thunkAjax(‘http://url1.xxxx’);
console.log(‘res1’, res1);
const res2 = yield thunkAjax(res1);
console.log(‘res2’, res2);
}

// 实现需求。
const g = gen();
const y1 = g.next(); // y1 = { value: ƒ, done: false}. 这里的 value,就是一个 newAjax 函数,接受一个回调函数作为参数
y1.value(url => { // 执行 y1.value 这个函数,并且传入了一个回调函数作为参数
const y2 = g.next(url); // 传入 url 作为参数,最终会赋值给上面代码中的 res1。y2 = {value: f, done: false}
y2.value(data => {
g.next(data); // 传入 data 作为参数,会赋值给上面代码中的 res2。至此,迭代也完成了。
});
});

// 最终的输出为:
// 1s 后输出:res1 ajax 返回了
// 1s 后输出:res2 ajax 返回了
在上面的代码中,我们使用 generator 实现了依次执行两个异步操作。上面的代码看起来是比较复杂的。整个的逻辑在 gen 这个 generator function 里,然后我们手动执行完了 g 这个 generator。按照上面的代码,如果我们想再加入一个 ajax 请求,需要先修改 generator function,然后修改 generator 的执行逻辑。我们来实现一个自动的流程,只需要定义好 generator,让它自动执行。
function autoRun(generatorFun) {
const generator = generatorFun();
const run = function(data){
const res = generator.next(data);
if(res.done) {
return;
}
return res.value(run);
}
run();
}
这下,我们就可以专注于 generator function 的逻辑了。
function* gen() {
const res1 = yield thunkAjax(‘http://url1.xxxx’);
console.log(‘res1’, res1);
const res2 = yield thunkAjax(res1);
console.log(‘res2’, res2);
const res3 = yield thunkAjax(res2);
console.log(‘res3’, res3);

}
// 自动执行
autoRun(gen);
著名的 co 就是一个自动执行 generator 的库。
上面的代码中,gen 函数体内,我们用同步代码的写法,实现了异步操作。可以看到,用 gererator 来执行异步操作,在代码可读性、可扩展性上面,是很有优势的。如今,我们或许会像下面这样来写上面的逻辑:
const fn = async function(){
const res1 = await func1;
console.log(res1);
const res2 = await func2;
console.log(res2);

}
fn();
写在后面
本文从 for..of 入手,梳理了 javascript 中的两个重要概念:iterator 和 generator。并且介绍了两者在异步操作中的应用。符合预期。下一篇文章中,将介绍 async、await,任务队列的相关内容,希望能对 js 中的异步代码及其写法有一个更深入,全面的认识。

正文完
 0