前言
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