本文首发于公众号:符合预期的CoyPan
后续文章:【JS基础】从JavaScript中的for...of提及(下) - async和awaitjavascript
先来看一段很常见的代码:java
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 对象等等)上建立一个迭代循环,调用自定义迭代钩子,并为每一个不一样属性的值执行语句。git
ECMAScript 2015规定了关于迭代的协议,这些协议能够被任何遵循某些约定的对象来实现。若是一个js对象想要能被迭代,那么这个对象或者其原型链对象必需要有一个Symbol.iterator的属性,这个属性的值是一个无参函数,返回一个符合迭代器协议的对象。这样的对象被称为符合【可迭代协议】。github
typeof Array.prototype[Symbol.iterator] === 'function'; // true typeof Array.prototype[Symbol.iterator]() === 'object'; // true
数组之因此能够被for...of迭代,就是由于数组的原型对象上拥有Symbol.iterator属性,这个属性返回了一个符合【迭代器协议】的对象。ajax
一个符合【迭代器协议】的对象必需要有一个next属性,next属性也是一个无参函数,返回一个对象,这个对象至少须要有两个属性:done, value, 大概长成下面这样:segmentfault
{ 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*async
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来写异步操做呢 ?
一个很常见的场景: 页面发起一个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中的异步代码及其写法有一个更深刻,全面的认识。