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

简介: 【JS基础】从JavaScript中的for...of说起(上) - iterator 和 generator

写在前面


先来看一段很常见的代码:


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'; // truetypeof 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' // trueArray.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


01

生成器对象和生成器函数

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]" // trueg.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


02

生成器函数的嵌套


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 }


03

生成器函数的参数传递


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请求。


// 我们先定义发起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 functionfunction* 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
0
0
0
29
分享
相关文章
JavaScript 中通过Array.sort() 实现多字段排序、排序稳定性、随机排序洗牌算法、优化排序性能,JS中排序算法的使用详解(附实际应用代码)
Array.sort() 是一个功能强大的方法,通过自定义的比较函数,可以处理各种复杂的排序逻辑。无论是简单的数字排序,还是多字段、嵌套对象、分组排序等高级应用,Array.sort() 都能胜任。同时,通过性能优化技巧(如映射排序)和结合其他数组方法(如 reduce),Array.sort() 可以用来实现高效的数据处理逻辑。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
JavaWeb JavaScript ③ JS的流程控制和函数
通过本文的详细介绍,您可以深入理解JavaScript的流程控制和函数的使用,进而编写出高效、可维护的代码。
75 32
JavaScript中通过array.filter()实现数组的数据筛选、数据清洗和链式调用,JS中数组过滤器的使用详解(附实际应用代码)
用array.filter()来实现数据筛选、数据清洗和链式调用,相对于for循环更加清晰,语义化强,能显著提升代码的可读性和可维护性。博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
【JavaScript】——JS基础入门常见操作(大量举例)
JS引入方式,JS基础语法,JS增删查改,JS函数,JS对象
springboot解决js前端跨域问题,javascript跨域问题解决
本文介绍了如何在Spring Boot项目中编写Filter过滤器以处理跨域问题,并通过一个示例展示了使用JavaScript进行跨域请求的方法。首先,在Spring Boot应用中添加一个实现了`Filter`接口的类,设置响应头允许所有来源的跨域请求。接着,通过一个简单的HTML页面和jQuery发送AJAX请求到指定URL,验证跨域请求是否成功。文中还提供了请求成功的响应数据样例及请求效果截图。
springboot解决js前端跨域问题,javascript跨域问题解决
Moment.js与其他处理时间戳格式差异的JavaScript库相比有什么优势?
Moment.js与其他处理时间戳格式差异的JavaScript库相比有什么优势?
除了 Generator 函数,还有哪些 JavaScript 异步编程解决方案?
【10月更文挑战第30天】开发者可以根据具体的项目情况选择合适的方式来处理异步操作,以实现高效、可读和易于维护的代码。

热门文章

最新文章