前言
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: true }
console.log(iterator.next()); // { value: undefined, done: true }
复制代码
上面的例子展示声明了一个生成器函数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: true }
console.log(iterator.next()); // { value: undefined, done: true }
复制代码
注意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循环又次之。其实,如果数据量不大,遍历的方法基本不会成为性能的瓶颈,考虑如何减少循环遍历或许更实际一点。
总结
含泪写完这一篇,我要下班了,再见各位。
@Author: PaperCrane