javascript
如何在 JS 代码中消灭 for 循环
Edit: 在我入職上一家公司的第一天,看到代碼庫(kù)里面一堆的 for 循環(huán),內(nèi)心有些崩潰,于是做了一次技術(shù)分享,展示怎樣在代碼中避免 for 循環(huán)。這篇文章是那次分享的總結(jié)。至于為什么我提倡避免 for 循環(huán),參考我寫的這篇文章。本文并不完美,其中遞歸的部分其實(shí)不應(yīng)該在生產(chǎn)環(huán)境中用的。重點(diǎn)其實(shí)應(yīng)該是怎樣用 reduce 和其它高階函數(shù),至于這些高階函數(shù)底層用的是 while 循環(huán)還是 for 循環(huán),都不重要,我們可以不在乎這些細(xì)節(jié)。在實(shí)際寫代碼的時(shí)候,只要被允許,我都會(huì)盡量使用 Ramda。如果對(duì) Ramda 感興趣,可參考我的另一篇文章優(yōu)雅代碼指北 -- 巧用 Ramda
一,用好 filter,map,和其它 ES6 新增的高階遍歷函數(shù)
問(wèn)題一:?將數(shù)組中的 falsy 值去除
const?arrContainsEmptyVal?=?[3,?4,?5,?2,?3,?undefined,?null,?0,?""];答案:
const?compact?=?arr?=>?arr.filter(Boolean);?
問(wèn)題二:?將數(shù)組中的 VIP 用戶余額加 10
const?users?=?[{?username:?"Kelly",?isVIP:?true,?balance:?20?},{?username:?"Tom",?isVIP:?false,?balance:?19?},{?username:?"Stephanie",?isVIP:?true,?balance:?30?} ];答案:
users.map(user?=>?(user.isVIP???{?...user,?balance:?user.balance?+?10?}?:?user) );補(bǔ)充:經(jīng)網(wǎng)友提醒,這個(gè)答案存在淺拷貝的問(wèn)題。操作引用型數(shù)據(jù)確實(shí)是一個(gè)麻煩的問(wèn)題。下面提供兩個(gè)方案:
用 Ramda:
import?R?from?"ramda";const?add10IfVIP?=?R.ifElse(R.propEq("isVIP",?true),R.evolve({?balance:?R.add(10)?}),R.identity );const?updateUsers?=?R.map(add10IfVIP); updateUsers(users);用 Immer
如果你習(xí)慣寫 mutable 的代碼,可以試下 Immer,用 mutable 的風(fēng)格寫 immutable 的代碼。
import?produce?from?"immer";const?updatedUsers?=?produce(users,?nextState?=>?{nextState.forEach(user?=>?{if?(user.isVIP)?{user.balance?+=?10;}}); });?
問(wèn)題三:?判斷字符串中是否含有元音字母
const?randomStr?=?"hdjrwqpi";答案:
const?isVowel?=?char?=>?["a",?"e",?"o",?"i",?"u"].includes(char); const?containsVowel?=?str?=>?[...str].some(isVowel);containsVowel(randomStr);?
問(wèn)題四:?判斷用戶是否全部是成年人
const?users?=?[{?name:?"Jim",?age:?23?},{?name:?"Lily",?age:?17?},{?name:?"Will",?age:?25?} ];答案:
users.every(user?=>?user.age?>=?18);?
問(wèn)題五:?找出上面用戶中的第一個(gè)未成年人
答案:
const?findTeen?=?users?=>?users.find(user?=>?user.age?<?18);findTeen(users);?
問(wèn)題六:?將數(shù)組中重復(fù)項(xiàng)清除
const?dupArr?=?[1,?2,?3,?3,?3,?3,?6,?7];答案:
const?uniq?=?arr?=>?[...new?Set(arr)];uniq(dupArr);?
問(wèn)題七:?生成由隨機(jī)整數(shù)組成的數(shù)組,數(shù)組長(zhǎng)度和元素大小可自定義
答案:
const?genNumArr?=?(length,?limit)?=>Array.from({?length?},?_?=>?Math.floor(Math.random()?*?limit));genNumArr(10,?100);?
二,理解和熟練使用 reduce
問(wèn)題八:?不借助原生高階函數(shù),定義 reduce
答案:
const?reduce?=?(f,?acc,?arr)?=>?{if?(arr.length?===?0)?return?acc;const?[head,?...tail]?=?arr;return?reduce(f,?f(head,?acc),?tail); };?
問(wèn)題九:?將多層數(shù)組轉(zhuǎn)換成一層數(shù)組
const?nestedArr?=?[1,?2,?[3,?4,?[5,?6]]];答案:
const?flatten?=?arr?=>arr.reduce((flat,?next)?=>?flat.concat(Array.isArray(next)???flatten(next)?:?next),[]);?
問(wèn)題十:?將下面數(shù)組轉(zhuǎn)成對(duì)象,key/value 對(duì)應(yīng)里層數(shù)組的兩個(gè)值
const?objLikeArr?=?[["name",?"Jim"],?["age",?18],?["single",?true]];答案:
const?fromPairs?=?pairs?=>pairs.reduce((res,?pair)?=>?((res[pair[0]]?=?pair[1]),?res),?{});fromPairs(objLikeArr);?
問(wèn)題十一:?取出對(duì)象中的深層屬性
const?deepAttr?=?{?a:?{?b:?{?c:?15?}?}?};答案:
const?pluckDeep?=?path?=>?obj?=>path.split(".").reduce((val,?attr)?=>?val[attr],?obj);pluckDeep("a.b.c")(deepAttr);?
問(wèn)題十二:?將用戶中的男性和女性分別放到不同的數(shù)組里:
const?users?=?[{?name:?"Adam",?age:?30,?sex:?"male"?},{?name:?"Helen",?age:?27,?sex:?"female"?},{?name:?"Amy",?age:?25,?sex:?"female"?},{?name:?"Anthony",?age:?23,?sex:?"male"?}, ];答案:
const?partition?=?(arr,?isValid)?=>arr.reduce(([pass,?fail],?elem)?=>isValid(elem)???[[...pass,?elem],?fail]?:?[pass,?[...fail,?elem]],[[],?[]],);const?isMale?=?person?=>?person.sex?===?"male";const?[maleUser,?femaleUser]?=?partition(users,?isMale);?
問(wèn)題十三:?reduce 的計(jì)算過(guò)程,在范疇論里面叫 catamorphism,即一種連接的變形。和它相反的變形叫 anamorphism。現(xiàn)在我們定義一個(gè)和 reduce 計(jì)算過(guò)程相反的函數(shù) unfold(注:reduce 在 Haskell 里面叫 fold,對(duì)應(yīng) unfold)
const?unfold?=?(f,?seed)?=>?{const?go?=?(f,?seed,?acc)?=>?{const?res?=?f(seed);return?res???go(f,?res[1],?acc.concat(res[0]))?:?acc;};return?go(f,?seed,?[]); };根據(jù)這個(gè) unfold 函數(shù),定義一個(gè) Python 里面的 range 函數(shù)。
答案:
const?range?=?(min,?max,?step?=?1)?=>unfold(x?=>?x?<?max?&&?[x,?x?+?step],?min);?
三,用遞歸代替循環(huán)(可以break!)
Edit: 雖然遞歸爆棧的問(wèn)題可以用代碼解決,但遞歸確實(shí)性能趕不上循環(huán)。這部分內(nèi)容純粹當(dāng)做遞歸函數(shù)案例了。如何解決遞歸爆棧,可以參考我的另一篇文章不懂遞歸?讀完這篇保證你懂
問(wèn)題十四:?將兩個(gè)數(shù)組每個(gè)元素一一對(duì)應(yīng)相加。注意,第二個(gè)數(shù)組比第一個(gè)多出兩個(gè),不要把第二個(gè)數(shù)組遍歷完。
const?num1?=?[3,?4,?5,?6,?7]; const?num2?=?[43,?23,?5,?67,?87,?3,?6];答案:
const?zipWith?=?f?=>?xs?=>?ys?=>?{if?(xs.length?===?0?||?ys.length?===?0)?return?[];const?[xHead,?...xTail]?=?xs;const?[yHead,?...yTail]?=?ys;return?[f(xHead)(yHead),?...zipWith(f)(xTail)(yTail)]; };const?add?=?x?=>?y?=>?x?+?y;zipWith(add)(num1)(num2);?
問(wèn)題十五:?將 Stark 家族成員提取出來(lái)。注意,目標(biāo)數(shù)據(jù)在數(shù)組前面,使用 filter 方法遍歷整個(gè)數(shù)組是浪費(fèi)。
const?houses?=?["Eddard?Stark","Catelyn?Stark","Rickard?Stark","Brandon?Stark","Rob?Stark","Sansa?Stark","Arya?Stark","Bran?Stark","Rickon?Stark","Lyanna?Stark","Tywin?Lannister","Cersei?Lannister","Jaime?Lannister","Tyrion?Lannister","Joffrey?Baratheon" ];答案:
const?takeWhile?=?f?=>?([head,?...tail])?=>f(head)???[head,?...takeWhile(f)(tail)]?:?[];const?isStark?=?name?=>?name.toLowerCase().includes("stark");takeWhile(isStark)(houses);?
問(wèn)題十六:?找出數(shù)組中的奇數(shù),然后取出前4個(gè):
const?numList?=?[1,?3,?11,?4,?2,?5,?6,?7];答案:
const?takeFirst?=?(limit,?f,?arr)?=>?{if?(limit?===?0?||?arr.length?===?0)?return?[];const?[head,?...tail]?=?arr;return?f(head)??[head,?...takeFirst(limit?-?1,?f,?tail)]:?takeFirst(limit,?f,?tail); };const?isOdd?=?n?=>?n?%?2?===?1;takeFirst(4,?isOdd,?numList);?
四,使用高階函數(shù)遍歷數(shù)組時(shí)可能遇到的陷阱
問(wèn)題十七:?從長(zhǎng)度為 100 萬(wàn)的隨機(jī)整數(shù)組成的數(shù)組中取出偶數(shù),再把所有數(shù)字乘以 3
//?用我們剛剛定義的輔助函數(shù)來(lái)生成符合要求的數(shù)組 const?bigArr?=?genNumArr(1e6,?100);能運(yùn)行的答案:
const?isEven?=?num?=>?num?%?2?===?0; const?triple?=?num?=>?num?*?3;bigArr.filter(isEven).map(triple);注意,上面的解決方案將數(shù)組遍歷了兩次,無(wú)疑是浪費(fèi)。如果寫 for 循環(huán),只用遍歷一次:
const?results?=?[]; for?(let?i?=?0;?i?<?bigArr.length;?i++)?{if?(isEven(bigArr[i]))?{results.push(triple(bigArr[i]));} }在我的電腦上測(cè)試,先 filter 再 map 的方法耗時(shí) 105.024 ms,而采用 for 循環(huán)的方法耗時(shí)僅 25.598 ms!那是否說(shuō)明遇到此類情況必須用 for 循環(huán)解決呢? No!
?
五,死磕到底,Transduce!
我們先用 reduce 來(lái)定義 filter 和 map,至于為什么這樣做等下再解釋。
const?filter?=?(f,?arr)?=>arr.reduce((acc,?val)?=>?(f(val)?&&?acc.push(val),?acc),?[]);const?map?=?(f,?arr)?=>?arr.reduce((acc,?val)?=>?(acc.push(f(val)),?acc),?[]);重新定義的 filter 和 map 有共有的邏輯。我們把這部分共有的邏輯叫做 reducer。有了共有的邏輯后,我們可以進(jìn)一步地抽象,把 reducer 抽離出來(lái),然后傳入 filter 和 map:
const?filter?=?f?=>?reducer?=>?(acc,?value)?=>?{if?(f(value))?return?reducer(acc,?value);return?acc; };const?map?=?f?=>?reducer?=>?(acc,?value)?=>?reducer(acc,?f(value));現(xiàn)在 filter 和 map 的函數(shù) signature 一樣,我們就可以進(jìn)行函數(shù)組合(function composition)了。
const?pushReducer?=?(acc,?value)?=>?(acc.push(value),?acc);bigNum.reduce(map(triple)(filter(isEven)(pushReducer)),?[]);但是這樣嵌套寫法易讀性太差,很容易出錯(cuò)。我們可以寫一個(gè)工具函數(shù)來(lái)輔助函數(shù)組合:
const?pipe?=?(...fns)?=>?(...args)?=>?fns.reduce((fx,?fy)?=>?fy(fx),?...args);然后我們就可以優(yōu)雅地組合函數(shù)了:
bigNum.reduce(pipe(filter(isEven),map(triple))(pushReducer),[] );經(jīng)過(guò)測(cè)試(用?console.time()/console.timeEnd()),上面的寫法耗時(shí) 33.898 ms,僅比 for 循環(huán)慢 8 ms。為了代碼的易維護(hù)性和易讀性,這點(diǎn)性能上的微小犧牲,我認(rèn)為是可以接受的。
這種寫法叫 transduce。有很多工具庫(kù)提供了 transducer 函數(shù)。比如?transducers-js。除了用 transducer 來(lái)遍歷數(shù)組,還能用它來(lái)遍歷對(duì)象和其它數(shù)據(jù)集。功能相當(dāng)強(qiáng)大。
?
六,for 循環(huán)和 for … of 循環(huán)的區(qū)別
for … of 循環(huán)是在 ES6 引入 Iterator 后,為了遍歷 Iterable 數(shù)據(jù)類型才產(chǎn)生的。EcmaScript 的 Iterable 數(shù)據(jù)類型有數(shù)組,字符串,Set 和 Map。for … of 循環(huán)屬于重型的操作(具體細(xì)節(jié)我也沒(méi)了解過(guò)),如果用 AirBNB 的 ESLint 規(guī)則,在代碼中使用 for … of 來(lái)遍歷數(shù)組是會(huì)被禁止的。
那么,for … of 循環(huán)應(yīng)該在哪些場(chǎng)景使用呢?目前我發(fā)現(xiàn)的合理使用場(chǎng)景是遍歷自定義的 Iterable。來(lái)看這個(gè)題目:
問(wèn)題十八:?將 Stark 家族成員名字遍歷,每次遍歷暫停一秒,然后將當(dāng)前遍歷的名字打印來(lái),遍歷完后回到第一個(gè)元素再重新開(kāi)始,無(wú)限循環(huán)。
const?starks?=?["Eddard?Stark","Catelyn?Stark","Rickard?Stark","Brandon?Stark","Rob?Stark","Sansa?Stark","Arya?Stark","Bran?Stark","Rickon?Stark","Lyanna?Stark" ];答案:
function*?repeatedArr(arr)?{let?i?=?0;while?(true)?{yield?arr[i++?%?arr.length];} }const?infiniteNameList?=?repeatedArr(starks);const?wait?=?ms?=>new?Promise(resolve?=>?{setTimeout(()?=>?{resolve();},?ms);});(async?()?=>?{for?(const?name?of?infiniteNameList)?{await?wait(1000);console.log(name);} })();?
七,放棄倔強(qiáng),實(shí)在需要用 for 循環(huán)了
前面講到的問(wèn)題基本覆蓋了大部分需要使用 for 循環(huán)的場(chǎng)景。那是否我們可以保證永遠(yuǎn)不用 for 循環(huán)呢?其實(shí)不是。我講了這么多,其實(shí)是在鼓勵(lì)大家不要寫 for 循環(huán),而不是不用 for 循環(huán)。我們常用的數(shù)組原型鏈上的 map,filter 等高階函數(shù),底層其實(shí)是用 for 循環(huán)實(shí)現(xiàn)的。在需要寫一些底層代碼的時(shí)候,還是需要寫 for 循環(huán)的。來(lái)看這個(gè)例子:
Number.prototype[Symbol.iterator]?=?function*()?{for?(let?i?=?0;?i?<=?this;?i++)?{yield?i;} };[...6];?//?[0,?1,?2,?3,?4,?5,?6]注意,這個(gè)例子只是為了好玩。生產(chǎn)環(huán)境中不要直接修改 JS 內(nèi)置數(shù)據(jù)類型的原型鏈。原因是 V8 引擎有一個(gè)原型鏈快速推測(cè)機(jī)制,修改原型鏈會(huì)破壞這個(gè)機(jī)制,造成性能問(wèn)題。
總結(jié)
以上是生活随笔為你收集整理的如何在 JS 代码中消灭 for 循环的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 干货 | 解决分布式场景下数据一致性问题
- 下一篇: Java当中的常量池