函數式編程 ( Functional Programming ) 是一種以函數為基礎的編程方式和代碼組織方式,能夠帶來更好的代碼調試及項目維護的優(yōu)勢。
1. 函數
在函數式編程中,任何代碼可以都是函數,且要求具有返回值肘交,如下示例
// 非函數式
var title = "Functional Programming";
var saying = "This is not";
console.log(saying + title); // => This is not Functional Programming
// 函數式
var say = title => "This is " + title;
var text = say("Functional Programming"); // => This is Functional Programming
2. 純函數
純函數在這里指函數內外間是“無”關聯的。主要有下面兩點
- 沒有副作用(side effect)不會涉及到外部變量的使用或修改
- 引用透明函數內只會依賴傳入參數扑馁,在任何時候對函數輸入相同的參數時涯呻,總能輸出相同的結果
// 非純函數(函數內依賴函數外的變量值)
var title = "Functional Programming";
var say = ()=> "This is not" + title; // <= 依賴了全局變量 title
// 純函數
var say = (title)=>"This is " + title; // <= 依賴了以參數 title 傳入
say("Functional Programming");
我們來深入一下純函數.
2.1 什么是"純函數"
純函數是指 不依賴于且不改變它作用域之外的變量狀態(tài)
的函數。
也就是說腻要,
純函數的返回值只由它調用時的參數決定
复罐,它的執(zhí)行不依賴于系統(tǒng)的狀態(tài)(比如:何時、何處調用它——譯者注)雄家。
純函數是 函數式編程 的一個基礎效诅。
2.2 例子
var values = { a: 1 };
function impureFunction ( items ) {
var b = 1;
items.a = items.a * b + 2;
return items.a;
}
var c = impureFunction( values );
// 現在 `values.a` 變成 3, impureFunction 改變了它。
在上面的代碼中咳短,我們改變了參數對象中的一個屬性填帽。由于我們定義的函數改變的對象在我們的函數作用域之外,導致這個函數成為“不純”的函數咙好。
var values = { a: 1 };
function pureFunction ( a ) {
var b = 1;
a = a * b + 2;
return a;
}
var c = pureFunction( values.a );
// `values.a` 沒有被改變, 它的值仍然是 1
上面的代碼篡腌,我們只計算了作用域內的局部變量,沒有任何作用域外部的變量被改變勾效,因此這個函數是“純函數”嘹悼。
var values = { a: 1 };
var b = 1;
function impureFunction ( a ) {
a = a * b + 2;
return a;
}
var c = impureFunction( values.a );
// 實際上, `c` 的值依賴于外部變量 `b`.
// 你可能容易忽略這種情況,外部變量的變化也可能會導致函數出現不確定結果层宫。
上面的代碼里杨伙, b 不在作用域中,函數執(zhí)行結果依賴于上下文環(huán)境萌腿,因此函數也是“不純”的限匣。
var values = { a: 1 };
var b = 1;
function pureFunction ( a, c ) {
a = a * c + 2;
return a;
}
var c = pureFunction( values.a, b );
// 這樣從定義上明確 `c` 依賴于參數 `b`,避免函數不確定結果毁菱。
上面這樣改就成了“純函數”米死。
2.3 具體應用
考慮以下代碼:
var getMinQuantity = function getMinQuantity ( name ) {
// 一個純函數根據傳入的名字返回對應的數量
我們看一下在一個實際項目中的代碼例子:
var popover = {
// A bunch of code…
addQuantityText: function ( quantity ) {
var quantityTextOptions = {
namespace: "quantity",
initialChildIndex: 2,
quantity: quantity
};
try {
this.formatQuantityText( quantityTextOptions );
} catch ( err ) {
console.log( "Couldn"t add quantity text!" );
}
},
formatQuantityText: function ( options ) {
if ( !this.$$boxContainer ) {
throw new Error( "$$boxContainer is not configured" );
}
var namespace = options.namespace || "quantity";
var quantity = options.quantity || 0;
var initialChildIndex = options.initialChildIndex || 0;
var $$quantity = new Canvas(); // implementation details hidden
$$quantity.name = namespace;
$$quantity.value = quantity;
this.setQuantityTextColor( $$quantity );
this.$$boxContainer.addChild( $$quantity, initialChildIndex );
return $$quantity;
},
setQuantityTextColor: function ( $$quantity ) {
if ( !$$quantity ) return;
var minQuantity = getMinQuantity( $$quantity.name );
var quantity = $$quantity.value || minQuantity;
var hasEnoughQuantity = (quantity >= minQuantity);
$$quantity.color = (hasEnoughQuantity) ? "green" : "red";
},
// A bunch of code…
};
上面的這三個函數 addQuantityText() , formatQuantityText() 以及 setQuantityTextColor() 都不是純函數。
我們使用 addQuantityText() 在 $$boxContainer 容器里展示數量贮庞。 這個方法是個操作入口峦筒,其中做一些細節(jié)的操作。當 $$quantity 出錯的時候窗慎,你需要在這一整坨代碼里面里面查錯物喷,有時候這復雜得像是在尋寶 —— 當然這不是令人心情愉快尋寶游戲卤材。
這種代碼組織方式往往意味著長期維護會很麻煩。
當問題變得復雜
在這個例子里峦失,函數內執(zhí)行次序變得重要扇丛。
僅僅交換2行代碼,程序就會出錯尉辑。這看起來很顯然晕拆,但是它確實不好調試。
var popover = {
// A bunch of code…
addQuantityText: function ( quantity ) {
var quantityTextOptions = {
namespace: "quantity",
initialChildIndex: 2,
quantity: quantity
};
try {
this.formatQuantityText( quantityTextOptions );
} catch ( err ) {
console.log( "Couldn"t add quantity text!" );
}
},
formatQuantityText: function ( options ) {
if ( !this.$$boxContainer ) {
throw new Error( "$$boxContainer is not configured" );
}
var namespace = options.namespace || "quantity";
var quantity = options.quantity || 0;
var initialChildIndex = options.initialChildIndex || 0;
var $$quantity = new Canvas(); // implementation details hidden
//this.setQuantityTextColor 提前寫了材蹬,造成錯誤
this.setQuantityTextColor( $$quantity );
$$quantity.name = namespace;
$$quantity.value = quantity;
this.$boxContainer.addChild( $$quantity, initialChildIndex );
return $$quantity;
},
setQuantityTextColor: function ( $$quantity ) {
if ( !$$quantity ) return;
var minQuantity = getMinQuantity( $$quantity.name );
var quantity = $$quantity.value || minQuantity;
var hasEnoughQuantity = (quantity >= minQuantity);
$$quantity.color = (hasEnoughQuantity) ? "green" : "red";
},
// A bunch of code…
};
上面的代碼產生了錯誤实幕,問題是這個錯誤有時候還比較難被發(fā)現。
setQuantityTextColor() 本應該只負責處理 $$quantity 的顏色堤器,但你卻需要從頭閱讀三個函數的每一行代碼去判斷究竟哪些操作改變了object中的屬性值昆庇,然后重新梳理整個代碼流程去弄明白其中的哪一步出錯了。
在這個時候闸溃,你甚至會后悔將 formatQuantityText() 分解為了更細粒度的方法來簡化每個方法的具體實現細節(jié)整吆。
總而言之,在你調試的時候辉川,你需要檢查許多代碼表蝙。如果你開始思考將一個大方法拆分成若干小方法為什么反而讓調試變得困難,那么 純函數 概念就變得對你非常有意義乓旗。
使用純函數思想解決問題
我們盡量使用純函數來改寫我們的代碼:
var popover = {
// A bunch of code…
// 所以對系統(tǒng)狀態(tài)改變的操作都封裝在這個方法里
// 插入 DOM 元素只由單一函數來負責府蛇,限制副作用
// 這使得 debug 變得簡單
addQuantityText: function ( quantity ) {
if ( !this.$$boxContainer ) {
throw new Error( "$$boxContainer is not configured" );
}
var quantityTextOptions = {
namespace: "quantity",
quantity: quantity
};
var $$quantity = this.formatQuantityText( quantityTextOptions );
this.$$boxContainer.addChild( $$quantity, 2 );
},
// 這個方法沒有副作用,它僅調用另一個純函數屿愚。
// 它創(chuàng)建和返回所需要的汇跨、正確配置的 canvas 對象
formatQuantityText: function ( options ) {
var namespace = options.namespace || "quantity";
var quantity = options.quantity || 0;
var $$quantity = new Canvas(); // implementation details hidden
$$quantity.name = namespace;
$$quantity.value = quantity;
$$quantity.color = this.getQuantityTextColor( quantity, namespace );
return $$quantity;
},
// 這個函數也沒有副作用,它根據參數 quantity 返回對應的顏色值
getQuantityTextColor: function ( quantity, namespace ) {
var minQuantity = getMinQuantity( namespace );
var hasEnoughQuantity = (quantity && quantity >= minQuantity);
return (hasEnoughQuantity) ? "green" : "red";
},
// A bunch of code…
};
從設計上來講妆距,上面的代碼并沒有根本性的變化穷遂,然而,這樣改寫帶來了顯而易見的好處娱据。
我們做了什么蚪黑?
- 我們用 getQuantityTextColor() 代替了 setQuantityTextColor()
- 這個方法根據 quantity 返回一個顏色值,而不是之前那樣直接改變 object 屬性
- 方法不依賴于它們作用域之外的變量
- 方法只調用純函數方法
- 我們將 $$quantity 對象的創(chuàng)建和修改DOM分開來了
- 我們將對系統(tǒng)狀態(tài)的改變統(tǒng)一封裝到 addQuantityText() 內部
通過上面的步驟中剩,我們移除了帶有副作用的方法忌穿,從而簡化了代碼維護的工作量。如果 $$quantity 發(fā)生錯誤咽安,我們只需要檢查一個函數伴网。
簡化接口
我們之前的的兩個方法是public的蓬推,更完美的做法是將它們改成private的妆棒。
事實上,做這個優(yōu)化和我們的API無關,它們存在只是為了簡化接口糕珊。如果你對這不理解动分,可以先閱讀原作者之前寫得 這篇文章 。
由于它們已經是純函數红选,將它們提出來簡直易如反掌澜公,因為它們的輸出不依賴任何外部環(huán)境,只由參數決定:
function getQuantityTextColor ( quantity, namespace ) {
var minQuantity = getMinQuantity( namespace );
var hasEnoughQuantity = (quantity && quantity >= minQuantity);
return (hasEnoughQuantity) ? "green" : "red";
};
function formatQuantityText ( options ) {
var namespace = options.namespace || "quantity";
var quantity = options.quantity || 0;
var $$quantity = new Canvas(); // implementation details hidden
$$quantity.name = namespace;
$$quantity.value = quantity;
$$quantity.color = getQuantityTextColor( quantity, namespace );
return $$quantity;
};
簡化后的接口代碼:
var popover = {
// A bunch of code…
addQuantityText: function ( quantity ) {
if ( !this.$$boxContainer ) {
throw new Error( "$$boxContainer is not configured" );
}
var quantityTextOptions = {
namespace: "quantity",
quantity: quantity
};
var $$quantity = formatQuantityText( quantityTextOptions );
this.$$boxContainer.addChild( $$quantity, 2 );
},
// A bunch of code…
};
2.4 使用純函數的好處
最主要的好處是沒有副作用喇肋。純函數不會修改作用域之外的狀態(tài)坟乾,做到這一點,代碼就變得足夠簡單和清晰:當你調用一個純函數蝶防,你只要關注它的返回值甚侣,而不用擔心因為別處的問題導致錯誤。
純函數是健壯的间学,改變執(zhí)行次序不會對系統(tǒng)造成影響殷费,因此純函數的操作可以并行執(zhí)行。
純函數非常容易進行單元測試低葫,因為不需要考慮上下文環(huán)境详羡,只需要考慮輸入和輸出。
最后嘿悬,盡可能使用純函數 讓你的代碼保持簡單和靈活
实柠。
2.5 設計問題
當你在使用面向對象編程時,你或許會覺得函數式編程的概念沒啥用善涨。這種想法是錯誤的主到,因為 面向對象編程和函數式編程無疑是相容的 。
事實上躯概,我們的目的很簡單: 通過盡可能限制能對系統(tǒng)造成影響的函數的數量來簡化你的代碼 登钥。
如果你認真去思考如何能盡量多使用純函數,你就可以更輕松調試和維護你的代碼娶靡,你的程序人生也能因此更美好牧牢。
好了,你已經知道該怎么做了吧姿锭。這實際上是在程序設計實踐中經常遇到的一系列問題塔鳍,如同上面的這種在 get 和 set 中選擇的問題。
3. 不可變數據(immutable)
這里主要是指變量值的不可變呻此。當需要基于原變量值改變時轮纫,可通過產生新的變量來確保原變量的不變性,如下
// 可變數據
var arr = ["Functional", "Programming"];
arr[0] = "Other"; // <= 修改了arr[0]的值
console.log(arr) // => ["Other", "Programming"] // 變量arr值已經被修改
// 不可變數據
var arr = ["Functional", "Programming"];
// 得到新的變量焚鲜,不修改了原來的值
var newArr = arr.map(item => {
if(item === "Functional"){
return "Other";
} else {
return item;
}
})
console.log(arr); // => ["Functional", "Programming"] 變量arr值不變
console.log(newArr); // => ["Other", "Programming"] 產生新的變量newArr
之所以使用這種不變值掌唾,除了更好的函數式編程外放前,還能夠維持線程安全可靠,落地在業(yè)務中糯彬,實際上也能讓代碼更加清晰凭语。設想,如果你定義了一個變量A撩扒,A在其他地方被其他人修改了似扔,這樣是不方便定位A的當前值的。關于定義多個變量引發(fā)的內存等問題搓谆,可以通過重用結構或部分引用的方式來減輕炒辉,可參考 immutable.js
使用 map, reduce 等數據處理函數
強大的 JavaScript 有著越來越多的高能處理數據函數,其中包含了 map泉手、 reduce辆脸、 filter 等。
map 能夠對原數組中的值進行逐個處理并產生新的數組螃诅,一個簡單例子
// map
var data = [1, 2, 3];
var squares = data.map( (item, index, array) => item * item );
console.log(squares); // => [1, 4, 9]
console.log(data);// => [1, 2, 3] data 還是那個 data
reduce 能夠對原數組中的各個值進行結合處理啡氢,來產生新的值,如下面例子中术裸,previous 代表上一個結果值倘是,current 代表當前值,reduce 函數可以傳入第二個參數作為 previous 初始值袭艺,不傳時則 previous 初始值為數組中第一個值搀崭。
// reduce
var sum = [1, 2, 3].reduce( (previous, current, index, array) => previous + current );
console.log(sum); // => 6
4. 函數柯里化 Currying
柯里化 是將多參函數轉換成一系列的單參函數。結合下面例子來說明下
// 一個多參函數
var add = (a, b) => a + b;
add(1, 2); // => 3
將上面的多參函數進行柯里化猾编,如下
// 柯里化函數
var add = a => b => a + b;
上面柯里化后的函數調用方式也有所轉變瘤睹,第一次傳入一個參數返回了一個函數,再傳入參數則完成整體的調用答倡,這也是利用的閉包的特性
var add1 = add(1);
add1(2); // => 3
柯里化后的函數轰传,也可以應用在生產 “ 函數 ” 上,如下示例
var say = title => type => title + " is " + type;
var sayFP = say("Functional Programming");
var sayOther = say("Other Programming");
sayFP("good"); // => Functional Programming is good
sayOther("good"); // => Other Programming is good
5. 組合函數compose
顧名思義瘪撇,組合函數是將多個函數進行組合成一個函數获茬。舉個例子
var compose = (fn1, fn2) => (arg) => fn1(fn2(arg));
var a = arg => arg + 'a';
var b = arg => arg + 'b';
var c = compose(a, b); // 將a,b函數進行組合
c('c'); // => cba
上面示例中,當調用組合函數 c 時倔既,傳入的參數會經過 b 函數恕曲,接著將 b 函數的返回值作為 a 函數的參數值,從而輸出最終結果渤涌。組合函數 c 就像管道一樣佩谣,將水流( 返回值 )流經各個函數中進行處理。
當想要組合很多函數成一條很長很長的“管道”時实蓬,那么顯然上面的 compose 函數已經不夠用了茸俭。下面看看 redux 是怎么做這個 compose 工具函數的吊履。
// 源自: redux/src/compose.js
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
} else {
const last = funcs[funcs.length - 1]
const rest = funcs.slice(0, -1)
return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}
}
代碼很簡潔,主要利用了遞歸方式和數組的 reduceRight 方法來處理瓣履,reduceRight 跟上邊提到的 reduce 方法功能是一樣的,不同的是 reduceRight 是從數組的末尾向前逐個處理练俐。就這樣袖迎,想拼多長的就多長。
以上腺晾,便是筆者在項目實踐中應用較多的函數式編程內容燕锥,如有不妥,請斧正悯蝉。
附: 一些可供學習函數式編程的內容
- Immutable.js (https://facebook.github.io/immutable-js/)
- Underscore (http://underscorejs.org/)
- Lodash (https://lodash.com/)
- Ramda (http://ramdajs.com/)
- Mori (http://swannodette.github.io/mori/)
- Monads (http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html)