JavaScript 中的函數式編程
一、是什麼
函數式編程是一種 "編程範式"(programming paradigm),一種編寫程序的方法論
主要的編程範式有三種:命令式編程,聲明式編程和函數式編程
相比命令式編程,函數式編程更加強調程序執行的結果而非執行的過程,倡導利用若干簡單的執行單元讓計算結果不斷漸進,逐層推導複雜的運算,而非設計一個複雜的執行過程
舉個例子,將數組每個元素進行平方操作,命令式編程與函數式編程如下
// 命令式編程
var array = [0, 1, 2, 3]
for(let i = 0; i < array.length; i++) {
array[i] = Math.pow(array[i], 2)
}
// 函數式方式
[0, 1, 2, 3].map(num => Math.pow(num, 2))
簡單來講,就是要把過程邏輯寫成函數,定義好輸入參數,只關心它的輸出結果
即是一種描述集合和集合之間的轉換關係,輸入通過函數都會返回有且只有一個輸出值
可以看到,函數實際上是一個關係,或者說是一種映射,而這種映射關係是可以組合的,一旦我們知道一個函數的輸出類型可以匹配另一個函數的輸入,那他們就可以進行組合
二、概念
純函數
函數式編程旨在儘可能的提高代碼的無狀態性和不變性。要做到這一點,就要學會使用無副作用的函數,也就是純函數
純函數是對給定的輸入返還相同輸出的函數,並且要求你所有的數據都是不可變的,即純函數 = 無狀態 + 數據不可變
舉一個簡單的例子
let double = value=>value*2;
特性:
-
函數內部傳入指定的值,就會返回確定唯一的值
-
不會造成超出作用域的變化,例如修改全局變量或引用傳遞的參數
優勢:
- 使用純函數,我們可以產生可測試的代碼
test('double(2) 等於 4', () => {
expect(double(2)).toBe(4);
})
-
不依賴外部環境計算,不會產生副作用,提高函數的複用性
-
可讀性更強 ,函數不管是否是純函數 都會有一個語義化的名稱,更便於閱讀
-
可以組裝成複雜任務的可能性。符合模塊化概念及單一職責原則
高階函數
在我們的編程世界中,我們需要處理的其實也只有 “數據” 和“關係”,而關係就是函數
編程工作也就是在找一種映射關係,一旦關係找到了,問題就解決了,剩下的事情,就是讓數據流過這種關係,然後轉換成另一個數據,如下圖所示
在這裏,就是高階函數的作用。高級函數,就是以函數作爲輸入或者輸出的函數被稱爲高階函數
通過高階函數抽象過程,注重結果,如下面例子
const forEach = function(arr,fn){
for(let i=0;i<arr.length;i++){
fn(arr[i]);
}
}
let arr = [1,2,3];
forEach(arr,(item)=>{
console.log(item);
})
上面通過高階函數 forEach
來抽象循環如何做的邏輯,直接關注做了什麼
高階函數存在緩存的特性,主要是利用閉包作用
const once = (fn)=>{
let done = false;
return function(){
if(!done){
fn.apply(this,fn);
}else{
console.log("該函數已經執行");
}
done = true;
}
}
柯里化
柯里化是把一個多參數函數轉化成一個嵌套的一元函數的過程
一個二元函數如下:
let fn = (x,y)=>x+y;
轉化成柯里化函數如下:
const curry = function(fn){
return function(x){
return function(y){
return fn(x,y);
}
}
}
let myfn = curry(fn);
console.log( myfn(1)(2) );
上面的curry
函數只能處理二元情況,下面再來實現一個實現多參數的情況
// 多參數柯里化;
const curry = function(fn){
return function curriedFn(...args){
if(args.length<fn.length){
return function(){
return curriedFn(...args.concat([...arguments]));
}
}
return fn(...args);
}
}
const fn = (x,y,z,a)=>x+y+z+a;
const myfn = curry(fn);
console.log(myfn(1)(2)(3)(1));
關於柯里化函數的意義如下:
-
讓純函數更純,每次接受一個參數,鬆散解耦
-
惰性執行
組合與管道
組合函數,目的是將多個函數組合成一個函數
舉個簡單的例子:
function afn(a){
return a*2;
}
function bfn(b){
return b*3;
}
const compose = (a,b)=>c=>a(b(c));
let myfn = compose(afn,bfn);
console.log( myfn(2));
可以看到compose
實現一個簡單的功能:形成了一個新的函數,而這個函數就是一條從 bfn -> afn
的流水線
下面再來看看如何實現一個多函數組合:
const compose = (...fns)=>val=>fns.reverse().reduce((acc,fn)=>fn(acc),val);
compose
執行是從右到左的。而管道函數,執行順序是從左到右執行的
const pipe = (...fns)=>val=>fns.reduce((acc,fn)=>fn(acc),val);
組合函數與管道函數的意義在於:可以把很多小函數組合起來完成更復雜的邏輯
三、優缺點
優點
-
更好的管理狀態:因爲它的宗旨是無狀態,或者說更少的狀態,能最大化的減少這些未知、優化代碼、減少出錯情況
-
更簡單的複用:固定輸入 -> 固定輸出,沒有其他外部變量影響,並且無副作用。這樣代碼複用時,完全不需要考慮它的內部實現和外部影響
-
更優雅的組合:往大的說,網頁是由各個組件組成的。往小的說,一個函數也可能是由多個小函數組成的。更強的複用性,帶來更強大的組合性
-
隱性好處。減少代碼量,提高維護性
缺點:
-
性能:函數式編程相對於指令式編程,性能絕對是一個短板,因爲它往往會對一個方法進行過度包裝,從而產生上下文切換的性能開銷
-
資源佔用:在 JS 中爲了實現對象狀態的不可變,往往會創建新的對象,因此,它對垃圾回收所產生的壓力遠遠超過其他編程方式
-
遞歸陷阱:在函數式編程中,爲了實現迭代,通常會採用遞歸操作
參考文獻
-
https://zhuanlan.zhihu.com/p/81302150
-
https://zh.wikipedia.org/zh-hans/%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Do8lTY9j0IB5PGZe0rQj8Q