Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ES6 遍历、枚举与迭代 #675

Open
libin1991 opened this issue Dec 5, 2018 · 0 comments
Open

ES6 遍历、枚举与迭代 #675

libin1991 opened this issue Dec 5, 2018 · 0 comments
Labels

Comments

@libin1991
Copy link
Owner

libin1991 commented Dec 5, 2018

JavaScript 遍历、枚举与迭代的骚操作

前言

[JavaScript 遍历、枚举与迭代的骚操作(上篇)](JavaScript 遍历、枚举与迭代的骚操作(上篇))总结了一些�常用对象的遍历方法,大部分情况下是可以满足工作需求的。但下篇介绍的内容,在工作中95%的情况下是用不到的,仅限装逼。俗话说:装得逼多必翻车!若本文有翻车现场,请轻喷。

ES6 迭代器(iterator)、生成器(generator)

�上一篇提到,for of循环是依靠对象的迭代器工作的,如果用for of循环遍历一个非可迭代对象(即无默认迭代器的对象),for of循环就会报错。那迭代器到底是何方神圣?��

迭代器是一种特殊的对象,其有一个next方法,每一次枚举(for of每循环一次)都会调用此方法一次,且返回一个对象,此对象包含两个值:

  • value属性,表示此次调用的返回值(for of循环只返回此值);
  • done属性,Boolean值类型,标志此次调用是否已结束。

生成器,顾名思义,就是迭代器他妈;生成器是�返回迭代器的特殊函数,�迭代器由生成器生成。

�生成器声明方式跟普通函数相似,仅在函数名前面加一个*号(*号左右有空格也是可以正确�运行的,但为了代码可读性,建议左边留空格,右边不留);函数内部使用yield关键字指定每次迭代返回值。

// 生成器function *iteratorMother() {
        yield'we';
        yield'are';
        yield'the BlackGold team!';
    }

    // 迭代器let iterator = iteratorMother();

    console.log(iterator.next());  // { value: "we", done: false }console.log(iterator.next());  // { value: "are", done: false }console.log(iterator.next());  // { value: "the BlackGold team!", done: false }console.log(iterator.next());  // { value: undefined, done: false }console.log(iterator.next());  // { value: undefined, done: false }复制代码

上面的�例子展示声明了一个生成器函数iteratorMother的方式,调用此函数�返回一个迭代器iterator。

yield是ES6中的关键字,它指定了iterator对象每一次调用next方法时返回的值。如第一个yield�关键字后面的字符串"we"即为iterator对象第一次调用next方法返回的值,以此类推,直到�所有的yield语句执行完毕。

注意:当yield语句执行完毕后,调用iterator.next()会一直返回{ value: undefined, done: true },so,别用for of循环遍历同一个迭代器两次

function *iteratorMother() {
        yield'we';
        yield'are';
        yield'the BlackGold team!';
    }

    let iterator = iteratorMother();

    for (let element of iterator) {
        console.log(element);
    }

    // �we// are// the BlackGold team!for (let element of iterator) {
        console.log(element);
    }

    // nothing to be printed// 这个时候�迭代器iterator已经完成他的使命,如果想要再次迭代,应该生成另一个迭代器对象以进行遍历操作复制代码

注意:可以指定生成器的�返回值,当运行到return语句时,无论后面的代码是否有yield关键字�都不会再执行;且返回值只返回一次,再次调用next方法也只是返回{ value: undefined, done: true }

function *iteratorMother() {
        yield'we';
        yield'are';
        yield'the BlackGold team!';
        return'done';

        // 不存在的,这是不可能的yield'�0 error(s), 0 warning(s)'
    }

    // 迭代器let iterator = iteratorMother();

    console.log(iterator.next());  // { value: "we", done: false }console.log(iterator.next());  // { value: "are", done: false }console.log(iterator.next());  // { value: "the BlackGold team!", done: false }console.log(iterator.next());  // { value: "done", done: false }console.log(iterator.next());  // { value: undefined, done: false }复制代码

注意third time:yield关键字仅可在生成器函数内部�使用,一旦在生成器外使用(包括在生成器内部的函数例使用)就会报错,so,使用时注意别跨越函数边界

function *iteratorMother() {
        let arr = ['we', 'are', 'the BlackGold team!'];

        // 报错了// 以下代码实际上是在�forEach方法的参数函数里面使用yield
        arr.forEach(item =>yield item);
    }
复制代码

上面的例子,在JavaScript引擎进行函数声明提升的时候就报错了,而非在实例化一个�迭代器实例的时候才报错。

注意fourth time:别尝试在生成器内部获取yield指定的返回值,否则会得到一个undefined

function *iteratorMother() {
        let a = yield'we';
        let b = yield a + ' ' +  'are';
        yield b + ' ' + 'the BlackGold team!';
    }

    let iterator = iteratorMother();

    for (let element of iterator) {
        console.log(element);
    }

    // �we// undefined are// undefined the BlackGold team!复制代码

note:可以使用匿名函数表达式声明一个生成器,只要在function关键字后面加个可爱的�*号就好,例子就不写了;但是不可以使用箭头函数声明生成器

为对象添加生成器

使用for of循环去遍历一个对象的时候,会先去寻找此对象有没有生成器,若有则使用其默认的生成器生成一个迭代器,�然后遍历此迭代器;若无,报错!�

上篇也�提到,像Set、Map、Array等特殊的对象类型,都有多个生成器,但是自定义的对象是没有内置生成器的,不知道为啥;就跟别人有女朋友而我没有女朋友一样,不知道为啥。没关系,自己动手,丰衣足食;我们为自定义对象添加一个生成器(至于怎么解决女朋友的问题,别问我)

let obj = {
        arr: ['we', 'are', 'the BlackGold team!'],
        *[Symbol.iterator]() {
            for (let element ofthis.arr) {
                yield element;
            }
        }
    }

    for (let key of obj) {
        console.log(key);
    }

    // we// are// the BlackGold team!复制代码

好吧,我承认上面的例子有点�脱了裤子放P的味道,当然�不是说这个例子臭,而是有点多余;毕竟我们希望遍历的是对象的属性,那就换个方式搞一下吧

let father = {
        *[Symbol.iterator]() {
            for (let key ofReflect.ownKeys(this)) {
                yield key;
            }
        }
    };

    let obj = Object.create(father);

    obj.a = 1;
    obj[0] = 1;
    obj[Symbol('PaperCrane')] = 1;
    Object.defineProperty(obj, 'b', {
        writable: true,
        value: 1,
        enumerable: false,
        configurable: true
    });

    for (let key of obj) {
        console.log(key);
    }

    /* 看起来什么鬼属性都能被Reflect.ownKeys方法获取到 */// 0// a// b// Symbol(PaperCrane)复制代码

通过上面例子的展示的方式包装对象,确实可以�使用for of来遍历对象的属性,但是使用起来还是有点点的麻烦,�目前没有较好的解决办法。我们在创建自定义的类�(构造器)的时候,可以加上�Symbol.iterator生成器,那么类的实例就可以使用for of循环遍历了。

note:Reflect对象是反射对象,其提供的方法默认特性与底层提供的方法表现一致,如Reflect.ownKeys的表现就相当于Object.keys、Object.getOwnPropertyNames、Object.getOwnPropertySymbols三个操作加起来的操作。上篇有一位ID为“webgzh907247189”的朋友提到还有这种获取对象属性名的方法,这一篇就演示一下,同时也非常感谢这位朋友的宝贵意见。

迭代器传值

上面提到过,�如果在迭代器内部获取yield指定的返回值,将会得到一个undefined,但代码逻辑如果依赖前面的返回值的话,就需要通过给�迭代器的next方法传参达到此目的

function *iteratorMother() {
        let a = yield'we';
        let b = yield a + ' ' +  'are';
        yield b + ' ' + 'the BlackGold team!';
    }

    let iterator = iteratorMother(),
        first, second, third;

    // 第一次调用�next方法时�,传入的参数将不起任何作用
    first = iterator.next('anything,even an Error instance');
    console.log(first.value);                // we
    second = iterator.next(first.value);
    console.log(second.value);               // we are
    third = iterator.next(second.value);
    console.log(third.value);                // we are the BlackGold team!复制代码

往next方法传的参数,将会成为上一次调用next对应的yield关键字的返回值,在生成器内部可以获得此值。所以调用next方法时,会执行对应yield关键字右侧至上一个yield关键字左侧的代码块;生成器内部变量a的声明和赋值是在第二次调用next方法的时候进行的。

note:往第一次调用的next方法传参时,将不会对迭代有任何的影响。此外,也可以往next方法传递一个Error实例,当迭代器报错时,后面的代码将不会执行。

解决回调地狱

每当面试时问到如何解决回调地狱问题时,我们的第一反应�应该是使用Promise对象;如果�你是大牛,可以随手甩面试官Promise的实现原理;但是万一�不了解Promise原理,又想装个逼,可以试试使用迭代器解决回调地狱问题

// 执行迭代器的函数,�参数iteratorMother是一个生成器let iteratorRunner = iteratorMother => {
        let iterator = iteratorMother(),
            result = iterator.next(); // 开始执行迭代器let run = () => {
            if (!result.done) {
                // 假如上一次迭代的返回值是一个函数// 执行result.value,传入一个回调函数,当result.value执行完毕时执行下一次迭代if ((typeof result.value).toUpperCase() === 'FUNCTION') {
                    result.value(params => {
                        result = iterator.next(params);

                        // 继续迭代
                        run();
                    });
                } else {
                    // 上一次迭代的返回值不是一个函数,直接进入下一次迭代
                    result = iterator.next(result.value);
                    run();
                }
            }
        }

        // 循环执行迭代器,直到迭代器�迭代完毕
        run();
    }

        // �异步函数包装器,为了解决向异步函数传递参数问题let asyncFuncWrapper = (asyncFunc, param) => resolve => asyncFunc(param, resolve),
        // 模拟的异步函数
        asyncFunc = (param, callback) => setTimeout(() => callback(param), 1000);

    iteratorRunner(function *() {
        // 按照同步的方式快乐的写代码let a = yield asyncFuncWrapper(asyncFunc, 1);
        a += 1;
        let b = yield asyncFuncWrapper(asyncFunc, a);
        b += 1;
        let c = yield asyncFuncWrapper(asyncFunc, b);

        let d = yield c + 1;
        console.log(d);          // �4
    });
复制代码

上面的例子中,使用setTimeout来模拟一个异步函数asyncFunc,此异步函数接受两个参数:param和回调函数callback;在生成器内部,每一个yield关键字返回的值都为一个包装了异步函数的函数,用于往异步函数传入参数;执行迭代器的�函数iteratorRunner,用于循环执行迭代器,并运行迭代器返回的函数。最后,我们可以在匿名生成器里面以同步的方式处理我们的代码逻辑。

以上的方式虽然解决了回调地狱的问题,但本质上依然是使用回调的方式调用代码,只是换了代码的组织方式。生成器内部的�代码组织方式,有点类似ES7的async、await语法;所不同的是,async函数可以返回一个promise对象,搬砖工作者可以继续使用此promise对象以同步方式调用异步函数。

let asyncFuncWrapper = (asyncFunction, param) => {
            returnnewPromise((resolve, reject) => {
                asyncFunction(param, data => {
                    resolve(data);
                });
            });
        },
        asyncFunc = (param, callback) => setTimeout(() => callback(param), 1000);

    asyncfunctionasyncFuncRunner() {
        let a = await asyncFuncWrapper(asyncFunc, 1);
        a += 1;
        let b = await asyncFuncWrapper(asyncFunc, a);
        b += 1;
        let c = await asyncFuncWrapper(asyncFunc, b);

        let d = await c + 1;
        return d;
    }

    asyncFuncRunner().then(data =>console.log(data));    // 三秒后输出 4复制代码

委托生成器

在这个讲求DRY(Don't Repeat Yourself)的时代,生成器也可以进行复用。

function *iteratorMother() {
        yield'we';
        yield'are';
    }

    function *anotherIteratorMother() {
        yield'the BlackGold team!';
        yield'get off work now!!!!!!';
    }

    function *theLastIteratorMother() {
        yield *iteratorMother();
        yield *anotherIteratorMother();
    }

    let iterator = theLastIteratorMother();

    for (let key of iterator) {
        console.log(key);
    }

    // we// are// the BlackGold team!// get off work now!!!!!!复制代码

上面的例子中,生成器theLastIteratorMother定义里面,复用了生成器iteratorMother、anotherIteratorMother两个生成器,相当于在生成器theLastIteratorMother内部声明了两个相关的迭代器,然后进行迭代。需要注意的是,复用生成器是,yield关键字后面有星号。

几个循环语句性能

上一篇有小伙伴提到对比一下遍历方法的性能,我这边简单对比一下各个循环遍历数组的性能,测试数组长度为1000万,测试代码如下:

let arr = newArray(10 * 1000 * 1000).fill({ test: 1 });

    console.time();
    for (let i = 0, len = arr.length; i < len; i++) {}
    console.timeEnd();

    console.time();
    for (let i in arr) {}
    console.timeEnd();

    console.time();
    for (let i of arr) {}
    console.timeEnd();

    console.time();
    arr.forEach(() => {});
    console.timeEnd();
复制代码

结果如下图(单位为ms,不考虑IE):

以上的结果可能在不同的环境下略有差异,但是基本可以说明,原生的循环速度最快�,forEach次之,for of循环�再次之,forin循环�又次之。其实,如果数据量不大,�遍历的方法基本不会成为性能的瓶颈,考虑如何减少循环遍历或许更实际一点。

总结

��含泪写完这一篇,我要下班了,再见各位。

@author: PaperCrane

@libin1991 libin1991 added the ES6 label Dec 5, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant