资讯专栏INFORMATION COLUMN

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

wslongchen / 2842人阅读

摘要:当这个迭代器的方法被首次后续调用时,其内的语句会执行到第一个后续出现的位置为止,后紧跟迭代器要返回的值。在这个回调函数里,我们使用第一个请求返回的,再次发起一个请求。

写在前面
本文首发于公众号:符合预期的CoyPan

后续文章:【JS基础】从JavaScript中的for...of说起(下) - async和await

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

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中的异步代码及其写法有一个更深入,全面的认识。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/103309.html

相关文章

  • JS基础JavaScript中的for...of说起(下) - asyncawait

    摘要:基础从中的说起上和在异步操作中使用和是一件比较费劲的事情,而给我们提供了更为简便的和。表达式会暂停当前的执行,等待处理完成。若正常处理,其回调的函数参数作为表达式的值,继续执行。若处理异常,表达式会把的异常原因抛出。 写在前面 本文首发于公众号:【符合预期的CoyPan】 在上一篇文章中,梳理了javascript中的两个重要概念:iterator和generator,并且介绍了两者在...

    hufeng 评论0 收藏0
  • ES6 的 for..of Generator,伪数组 jQuery 对象说起

    摘要:引用自可迭代对象和迭代器不以规矩,不成方圆为了使某个对象成为可迭代对象象,它必须实现方法,也就是说,它得有一个是的属性。的遍历,绝对应该用。 pseudo 英 [sju:dəʊ] 美 [su:doʊ]adj.假的,虚伪的n.[口]假冒的人,伪君子 pseudo-array 英 [sju:dəʊəreɪ] 美 [sju:dəʊəreɪ][计] 伪数组 jQuery 对象是伪数组 两个...

    Harriet666 评论0 收藏0
  • ES2018 新特征之:异步迭代器 for-await-of

    摘要:不幸的是,迭代器不能用来表示这样的数据源。即使是的迭代器也是不够的,因为它的是异步的,但是迭代器需要同步确定状态。异步迭代器一个异步迭代器就像一个迭代器,除了它的方法返回一个的。 ES2018 新特性 异步迭代器(本文) 正则表达式反向(lookbehind)断言 正则表达式 Unicode 转义 非转义序列的模板字符串 正则表达式 s/dotAll 模式 正则表达式命名捕获组 对...

    klivitamJ 评论0 收藏0
  • 贺老微博引出的“遍历器(Iterators)加速那些奥秘”

    摘要:我关注的贺老贺师俊前辈最近发表个这样一条微博虽然这条微博没有引起大范围的关注和讨论,但是作为新人,我陷入了思考。通过贺老的微博,对一个问题进行探究,最终找到核心成员的一文,进行参考并翻译。 我关注的贺老—贺师俊前辈@johnhax 最近发表个这样一条微博: showImg(https://segmentfault.com/img/remote/1460000010452807); 虽然...

    XUI 评论0 收藏0
  • JavaScript ES6相关的一些知识(/let、const/箭头函数/Promise/gene

    摘要:的精髓在于,用维护状态传递状态的方式使得回调函数能够及时调用,比传递要简单灵活的其他方法用于指定发生错误时的回调函数,等同于部分和的区别在发生异常,在中捕获不到能够捕获异常。 ES6是个啥 ECMAScript是国际通过的标准化脚本语言JavaScript由ES,BOM,DOM组成ES是JavaScript的语言规范,同时JavaScript是ES的实现和扩展6就是JavaScript...

    opengps 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<