1. 程式人生 > >為什麼要避免寫 for 迴圈

為什麼要避免寫 for 迴圈

終於我還是單獨寫一篇文章來說明不寫 for 迴圈的理由了。

我在寫《如何在 JS 程式碼中消滅 for 迴圈》的時候,以為我所倡導的應該已經是一個共識,但沒想到會有這麼大爭議,甚至有些程式設計經驗豐富的前輩也反對。所以,我覺得還是有必要再說明一下。我沒想說服所有人,只是想盡到把問題說清楚的責任。

一,效能問題

這是我看到的關注最多的點了。先推薦去 FunFunFunction 看下 MPJ 老師對這個問題怎麼說 Fast code is NOT important. 微觀層面的極小效能差異,不會成為你整個效能的應用瓶頸。

還見到有朋友說,V8 引擎對 for 迴圈有優化。拜託,V8 引擎對高階函式也有優化啊。就我最近知道的例子,V8 引擎的 Array.prototype.sort 方法的底層實現,以前是在排序項少於10個的時候用插入排序,在排序項大於10個的時候用快速排序。就在幾個月前,V8 使用了更穩定的 TimSort 演算法替代了快排。我不瞭解 TimSort

, 但據說是目前效能最好的排序演算法。

如果你給幾十上百個元素排序(說實話我也不知道前端排序,需要考慮效能的閾值在哪,不測怎麼知道?),一開始就考慮效能不用高階 sort 函式,而是寫個 for 迴圈實現 TimSort,這不是對開發資源的浪費?

我前段在 Twitter 上看到一個小插曲,一個開發者自己寫了個方法(時間久了,我忘了是什麼方法),釋出到 npm 讓人去試用。別人問這個方法原生已經有了,你寫這個幹嘛?他說這個效能好!結果被人懟 "Use the language!" 這個故事我當時還發在掘金沸點了,見這裡

建議大家關注下 V8 團隊,看他們的工程師對 JS 開發者的建議是什麼。其實底層引擎和開發社群是一直在良性互動的,引擎團隊也想讓開發者體驗好一點,讓自家的產品更有競爭力。比如,React 最近搞出 Time Slicing 技術(Vue 3.0 也會跟進)後,V8 團隊就覺得為啥不讓瀏覽器原生支援這個呢?未來的 Chrome 可能會支援 Scheduler API.

效能也不是應用要考慮的唯一因素,如果是的話,那可能大部分應用都要用 WebAssembly 重寫了。某些情況下確實需要用到這種極端手段,但大多數時候,可維護性和開發效率,是優先於效能的。

二,指令式 V.S. 宣告式

指令式(Imperative)程式設計就是告訴程式每一步怎麼做。比如用 jQuery,告訴某個元素,先左移 10 px,再轉個圈,再變個顏色閃幾下,然後自己消失吧…… 每一步都要告訴操作物件具體指令是什麼。

宣告式(Declarative)程式設計就是告訴程式我想要達到怎樣的效果,至於是怎麼實現的,其它獨立模組已經寫好了,組合起來就行了。比如我在這篇文章裡用 Rx.js 實現的動畫:

const moveDown$ = duration(1500).pipe(
    map(easeInQuint),
    map(distance(700)),
    tap(y => (targetDiv.style.top = y + "px"))
);
複製程式碼

這裡的意思就是,告訴目標元素,在 1500 ms 內,以 easeInQuint 的曲線加速度,向下移動 700 px. 是不是很好懂?我只是說了我要什麼,具體怎麼做的,都在相應獨立模組裡面。而這些獨立模組寫一遍就行了,甚至能跨專案複用。想象一下指令式程式碼能這麼靈活和易讀嗎?

而 for 迴圈就是指令式的。指令式程式碼難伸縮,難複用,而且全是實施細節(implementation details),易讀性極差。有相當一部分人說高階函式易讀性差,for 迴圈易讀性好。我個人觀點是這些朋友需要鍛鍊下程式抽象能力,for 迴圈幾乎能做到零抽象,讀抽象層次低的指令式程式碼,當然好懂,但不意味著好讀。舉個例子:

// 在產品列表中找到相應產品,提取出價格,再把價格格式化
const formalizeData = compose(formatCurrency, pluckPrice, findProduct);

formalizeData(products)
複製程式碼

compose 裡面的獨立函式實現細節我就不寫了。這種寫法優勢在哪?首先,這種程式碼幾乎不用註釋,從右往左讀一遍函式名就知道在幹嘛。其次,compose 裡面的三個獨立函式由於是純函式,可以在其他地方複用。如果用 for 迴圈一步到位實現 formalizeData 函式,那就沒辦法複用了。

僅僅為了效能,程式碼伸縮性和擴充套件性都不考慮,實在是捨本逐末。

三,改變資料

for 迴圈由於太底層了,其設計初衷就是執行作用(effects),用來高效執行底層指令。而開發應用時,對於作用是要嚴格限制的。讓你的程式副作用散佈在程式各個角落,很容易造成難以發現的 bug。什麼是副作用?在一個函式執行計算時,產生了計算目的之外的行為,即是副作用。比如,有一個由數字組成的陣列,你想把每一個數字加一個貨幣符號,然後你用 for 迴圈把每一個數組元素加了個 $. 這就是副作用,你本來只想要一個格式化價格組成的陣列,結果你把原資料改了。如果用 map,就不會改變原資料,而是返回一個新的符合要求的資料。

你也可以說我在 for 迴圈開始宣告一個空物件/陣列,然後往這個空物件/數組裡 assign/push,這樣確實能避免上面說得到的問題。但這種規約靠自覺,而且指令式程式碼太自由了,難以保證每個人都清楚自己沒引起副作用。

副作用最大的問題是無法組合,所以函數語言程式設計才會從數學裡面引入抽象層次那麼高的 Monad 來解決這個問題。當然這個扯遠了,只是想說電腦科學家和數學家們為了馴服副作用費這麼大勁,是有原因的。