JavaScript 遍歷、枚舉與迭代的騷操做(上篇)總結了一些經常使用對象的遍歷方法,大部分狀況下是能夠知足工做需求的。但下篇介紹的內容,在工做中95%的狀況下是用不到的,僅限裝逼。俗話說:裝得逼多必翻車!若本文有翻車現場,請輕噴。web
上一篇提到,for of循環是依靠對象的迭代器工做的,若是用for of循環遍歷一個非可迭代對象(即無默認迭代器的對象),for of循環就會報錯。那迭代器究竟是何方神聖?
面試
迭代器是一種特殊的對象,其有一個next方法,每一次枚舉(for of每循環一次)都會調用此方法一次,且返回一個對象,此對象包含兩個值:數組
生成器,顧名思義,就是迭代器他媽;生成器是返回迭代器的特殊函數,迭代器由生成器生成。
promise
生成器聲明方式跟普通函數類似,僅在函數名前面加一個*號(*號左右有空格也是能夠正確運行的,但爲了代碼可讀性,建議左邊留空格,右邊不留);函數內部使用yield關鍵字指定每次迭代返回值。app
// 生成器
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語句執行完畢。
async
注意:當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 }post
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