這裏只傳授最高端的編程技巧...
好久沒講技術了,先回憶一下啥是函數式編程(FP)吧,比如FP要求使用表達式,不允許出現語句,這樣更接近自然語言。
表達式取代經典語句
什麼叫語句呢?學校編程課本上教的變量聲明語句,循環語句,條件判斷語句,枚舉語句,這些都是語句,也就是說我們再熟悉不過的if/else語句,for/while循環,switch以及try/catch都不給用了!
沒有這些語句還編個P程啊?我當時也有一種“這些年編程白學了”的衝動,雖然官方說每一種語句都可以用對應的表達式來替代,比如在JavaScript領域,變量聲明省略掉關鍵詞後就變成了表達式:
變量聲明語句
// 變量聲明語句+賦值
let test = 123;
// 變量申明+賦值表達式
test = 123;
因爲變量總是屬於當前函數的變量對象(variable object),聲明變量等同於給對象添加屬性,所以變量申明表達式返回賦的值或者undefined。
if/else語句
函數式替換if/else語句也很簡單,我們本來就有條件運算符(… ? … : …)可用:
// 條件語句
if(convention){}
else {}
// 條件表達式
convention ? expression1 : expression2;
switch語句
switch語句的話可以用js散列表來模擬,也就是對象,用散列表來枚舉比switch遍歷快許多
// 狀態枚舉語句
switch (expression) {
case value1:
break;
case value2:
break;
default:
break;
}
// 字典表達式
({
value1(){},
value2(){},
})[expression] || default();
try&catch語句
至於try/catch/finally可以將同步流包裹進promise,再給他監聽一個catch方法:
// 異常處理語句
try{
// 代碼塊
}catch(err){
}finally{}
// 異常處理表達式
new Promise((res,rej)=>{
// 代碼塊
}).catch(err=>{
}).finally(()=>{})
以上這些表達式都完美替換了經典語句,但是我在“如何取代循環語句”問題上思考了很久,循環語句不同於上面幾種,循環問題是最複雜的,光語句語法就有for和while等好幾種,如何取代這些傻吊語句成了一個問題。下面我來一一討論一下,表達式是否能夠完美的替換循環語句。
數組問題
Array對象(數組或者叫列表)是JavaScript裏最重要的一個類,也是原型鏈上方法最多的一個。事實上JS裏一切對象都是(散)列表。首先,所有循環都要使用數組,因爲數組的長度(n)是衡量循環的時間複雜度的標準,通常循環一遍的複雜度就是O(n)。
循環遍歷
我們最常見的循環就是遍歷一個數組,那直接可以利用數組的forEach方法來遍歷:
// 遍歷數組語句
for(let i=0; i<list.length; i++){
}
// 遍歷數組方法
list.forEach(item=>{
})
指定循環次數
for循環語句中經常出現需要指定循環的次數而沒有數組,我們可以通過構造一個定長數組來遍歷:
// 指定次數循環語句
for(let i=0; i<n; i++){
}
// 指定次數循環表達式
Array(n).fill(true).forEach(()=>{
})
continue中斷本次迭代
continue關鍵詞的作用是提前結束本次迭代進程,趕緊進入下一次迭代。在函數式數組的遍歷中只要使用return結束當前回調的執行就行啦。
// continue語句
while (expression) {
if (condition) {
continue;
}
}
// 用return結束當前迭代函數
list.forEach(()=>{
if (condition) {
return;
}
})
break結束循環
和continue不同,break關鍵詞會結束整個循環,forEach傳的回調函數永遠會執行列表的長度遍,所以forEach沒用,同理map和filter等一系列數組遍歷方法都不能用。可喜的是,數組有一些“可中斷的遍歷方法”,比如find方法本意是尋找一個數組元素,找到後就可以中斷遍歷;比如some方法本意是是否有“一些”元素符合回調條件,遍歷時一旦匹配到一個就會停止向下匹配;比如every方法本意是是否“所有”元素都符合回調條件,遍歷時只要發現1個元素不符合就會停止向下匹配。所以函數式編程中有3個數組方法可以實現循環的break。
// 傳統break語句
for(let item of list){
if(condition)break;
}
// 函數式break
// find
list.find(item=>{
if(condition)return true;
})
// some
list.some(item=>{
if(condition)return true;
})
// every
list.some(item=>{
if(condition)return false;
})
無限循環
取代無限循環語句只要遞歸調用自己就好啦~
// 無限循環語句
while(true){}
// 無限循環表達式
(function loop(){
loop();
})();
異步循環(劃重點)
異步循環是最難的模擬的一個。假如我們有一個異步任務列表asyncTasks,想要串行執行而不是並行執行,也就是一個接着一個運行,如果想要並行執行任務非常簡單,只要Promise.all(asyncTasks)就行了,但能不能實現一個Promise.sequential呢?如果任務數量確定可以直接.then().then()...來鏈式調用,但如果數量是動態的就得用循環了。首先模擬一個tasks列表,其中每個元素都是async函數,即返回promise的函數:
tasks = [2000, 1000, 3000].map(time => async () => {
await new Promise(res => setTimeout(res, time));
console.log(time);
})
使用循環語句來順序執行非常舒適,但如果你嘗試使用forEach來遍歷就會出現問題:
// 異步鏈用循環語句+await非常合適
for(task of tasks){
await task();
}
// 但是這樣你會發現,若干個異步任務併發執行了!
tasks.forEach(async (task)=>{
await task();
})
使用forEach,回調函數雖然是異步的,但是這個回調函數在一瞬間被併發執行了n次,每一次之間沒有等待,導致串行失敗。追根揭底,forEach無法順序執行異步任務的原因是,回調函數每次執行完全獨立,沒有關聯。貫穿Array原型鏈上幾十種遍歷方法中,似乎只有reduce和sort等寥寥幾個方法可以實現前後關聯。我們來模擬一個吧,利用reduce來polyfill一個Promise.sequential方法。
Promise.concurrent = Promise.all;
Promise.sequential = tasks => tasks.reduce(async (chain, nextTask) => {
await chain;
return nextTask();
}, Promise.resolve());
Promise.sequential(tasks)
.then(()=>console.log('finished'));
// 依次打印2000,1000,3000,'finished'
老衲的解釋:這裏利用reduce將一系列promise串了起來,合成了一個大的promise,本質上仍然是通過.then將一個個promise鏈起來。注意,在async函數中即使return了一個promise.resolve(123),函數返回值將是另一個promise,只是解析值都是123。
經過本文的分析,所有的JavaScript語句,無論是聲明,條件,枚舉,循環還是流程控制語句,統統可以用函數表達式來替換,讓JS成爲第一個只由表達式組成的通用編程語言。如果認爲我有遺漏的地方或者說還有哪些語句是不可取代的,歡迎在底下留言評論。
參考
https://css-tricks.com/why-using-reduce-to-sequentially-resolve-promises-works/
https://stackoverflow.com/questions/24586110/resolve-promises-one-after-another-i-e-in-sequence
https://jakearchibald.com/2017/await-vs-return-vs-return-await/
https://jimmy.blog.csdn.net/article/details/91038735
(完)
【日記】
看看本文的參考鏈接,可以發現外網站點都習慣於將文章的標題放在url上作爲文章ID,這種習慣的好處就是可以從url上直接讀出內容的主題,而我們的站點url很多都是一個個文章編號。不得不說,這些專業論壇的文章的不僅質量高,url的設計也很有語義。