JavaScript 遍历、枚举与迭代的骚操作(下篇)
栏目: JavaScript · 发布时间: 5年前
内容简介:上一篇提到,for of循环是依靠对象的迭代器工作的,如果用for of循环遍历一个非可迭代对象(即无默认迭代器的对象),for of循环就会报错。那迭代器到底是何方神圣?迭代器是一种特殊的对象,其有一个next方法,每一次枚举(for of每循环一次)都会调用此方法一次,且返回一个对象,此对象包含两个值:
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 of this.arr) { yield element; } } } for (let key of obj) { console.log(key); } // we // are // the BlackGold team! 复制代码
好吧,我承认上面的例子有点脱了裤子放P的味道,当然不是说这个例子臭,而是有点多余;毕竟我们希望遍历的是对象的属性,那就换个方式搞一下吧
let father = { *[Symbol.iterator]() { for (let key of Reflect.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) => { return new Promise((resolve, reject) => { asyncFunction(param, data => { resolve(data); }); }); }, asyncFunc = (param, callback) => setTimeout(() => callback(param), 1000); async function asyncFuncRunner() { 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 = new Array(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循环又次之。其实,如果数据量不大,遍历的方法基本不会成为性能的瓶颈,考虑如何减少循环遍历或许更实际一点。
以上所述就是小编给大家介绍的《JavaScript 遍历、枚举与迭代的骚操作(下篇)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- JavaScript 遍历、枚举与迭代的骚操作(上篇)
- c# – 枚举时项目发生变化时是否会影响枚举?
- 迭代器萃取与反向迭代器
- 测者的测试技术手册:Junit单元测试遇见的一个枚举类型的坑(枚举类型详解)
- 浅谈python可迭代对象,迭代器
- 可迭代对象,迭代器(对象),生成器(对象)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Little Schemer
[美] Daniel P. Friedman、[美] Matthias Felleisen / 卢俊祥 / 电子工业出版社 / 2017-7 / 65.00
《The Little Schemer:递归与函数式的奥妙》是一本久负盛名的经典之作,两位作者Daniel P. Friedman、Matthias Felleisen在程序语言界名声显赫。《The Little Schemer:递归与函数式的奥妙》介绍了Scheme的基本结构及其应用、Scheme的五法十诫、Continuation-Passing-Style、Partial Function、......一起来看看 《The Little Schemer》 这本书的介绍吧!