最近在學習函數式編程,看到柯里化函數這個東東,原覺得是個新的概念,沒想到一查,居然老早就存在了,且已經成熟運用多年,深感慚愧啊,我到如今都還沒接觸過。javascript
先是查看了柯里化的介紹java
在計算機科學中,柯里化(Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數且返回結果的新函數的技術。編程
這句話聽起來有點拗口啊,反正我沒能徹底理解,不過不要緊,咱們能夠查看網上大牛們的解釋,通過一頓搜索,算是屢清楚了頭緒。segmentfault
咱們先來看個栗子,這是一個求和的函數,求4個參數的和,那麼咱們在使用這個函數的時候,就要先知道每一個參數的具體的值,而後才能調用這個函數。數組
function add(a, b, c, d) {
return a + b + c + d;
};
console.log(add(2,3,4,5)) // 14
複製代碼
而假如,咱們想讓調用更加靈活一些,好比改爲下面這種調用方式app
console.log(add(2)(3)(4)(5)) // 14
複製代碼
能夠看到,這種調用方式有個好處,不須要等待全部參數明確後在執行具體方法,當在參數不知足的狀況下,每次調用返回的是函數,好比 add(2)
返回的是一個函數,返回的函數不斷的接受新的參數,直到知足預約好的4個參數時,便輸出結果。 而 add(2,3,4,5) => add(2)(3)(4)(5)
這一個過程即是函數柯里化的過程。這樣子解釋,不知道大夥好理解不?函數式編程
talk is cheap , show me the code,ok 那咱們看下具體的實現,先寫一個函數將 add()
轉換成柯里化函數函數
/** * 轉換函數 * @param {*} fn 目標函數 * @param {...any} args 其餘參數 */
const createCurry = (fn, ...args) => {
// 獲取目標函數的參數個數
let length = fn.length;
return (...rest) => {
// 將已有的參數和新的參數合併
let allArgs = args.slice(0);
allArgs.push(...rest);
// 若參數個數已經知足目標函數的參數要求,則執行目標函數。不然繼續返回轉換函數
if (allArgs.length < length) {
return createCurry.call(this, fn, ...allArgs)
} else {
return fn.apply(this, allArgs)
}
}
}
function add(a, b, c, d) {
return a + b + c + d;
};
複製代碼
使用以下:學習
const curryAdd = createCurry(add,2);
const sum = curryAdd(3)(4)(5);
console.log(sum) // 14
複製代碼
轉換柯里化函數有個關鍵的點,那就是要明確觸發條件,好比說上面的栗子中,咱們的觸發條件是參數的個數,根據參數的個數來區分返回的是函數仍是具體的結果。ui
經過上面的栗子咱們能夠看出柯里化函數有這麼幾個特色:
光說理論太枯燥,那麼咱們把上面的栗子在稍微擴展下,把add
函數改爲不定參數,就是說,我能夠傳n個參數,求這n個參數的和,調用以下
const curryAdd = createCurry(add);
const sum = curryAdd(3)(4)(5); // 12
const sum = curryAdd(3)(4)(5)(6); // 18
複製代碼
OK,首先咱們要對add
函數作下修改
/** * 不定參數求和 * @param {...any} arg */
function add(...arg){
return arg.reduce((result,value)=>{ return result += value },0)
}
複製代碼
那麼這個函數使用就變成 add(1,2,3)
,但咱們的目標是這樣的 add(1)(2)(3)
,在寫轉換函數以前,咱們先要肯定咱們的觸發條件,我想到的條件是:當新傳入的參數個數爲 0 的時候,就是沒有參數,就執行目標函數,不然返回函數。那麼柯里化函數的調用方式須要稍微改爲以下
add(1)(2)(3)();
複製代碼
具體的轉換函數和調用以下
const createCurry = (fn, ...args) => {
return (...rest) => {
let allArgs = args.slice(0);
allArgs.push(...rest)
// 觸發條件
if (rest.length !== 0) {
return createCurry.call(this, fn, ...allArgs)
} else {
return fn.apply(this, allArgs)
}
}
}
const curryAdd = createCurry1(add);
console.log(curryAdd(1)(2)(3)(4)()) // 10
複製代碼
經過上面兩個小栗子,大夥應該有個大體概念了,因此柯里化是一種設計思想,主動掌握了函數的控制權,根據咱們的須要,設定相應的觸發機制。有句話,讓代碼編寫代碼,柯里化算是有那麼點意思吧,函數生成函數,函數去調用函數,咱們只要編寫原子性的函數和設定好條件。
寫個通用的數組處理柯里化函數,currying
的第一個參數是目標函數,currying
直接返回函數,沒有特定的觸發條件,在使用mapSQ([1, 2, 3, 4, 5])
會將數組和currying
的第二個參數一塊兒傳給目標函數,即currying
第一個參數,詳細以下:
function currying(fn) {
var slice = Array.prototype.slice,
__args = slice.call(arguments, 1);
return function () {
var __inargs = slice.call(arguments);
return fn.apply(null, __args.concat(__inargs));
};
}
function square(i) {
return i * i;
}
function double(i) {
return i *= 2;
}
function map(handeler, list) {
return list.map((value) => {
return handeler(value)
});
}
var mapSQ = currying(map, square); // 平方
mapSQ([1, 2, 3, 4, 5]); //[1, 4, 9, 16, 25]
var mapTwo = currying(map, double); // 兩倍
mapSQ([1, 2, 3, 4, 5]); //[2,4,6,8,10]
複製代碼
這個栗子是在 30secondsofcode 上看到,我以爲寫得很是精簡直觀,給你們分享下
const curry = (fn, arity = fn.length, ...args) => arity <= args.length ? fn(...args) : curry.bind(null, fn, arity, ...args);
curry(Math.pow)(2)(10); // 1024
curry(Math.min, 3)(10)(50)(2); // 2
複製代碼
官方寫成了一行,閱讀可能不太方便,我改爲通俗版,加了些註釋
/** * 生成柯里化函數,以參數長度達標做爲觸發條件 * @param {*} fn 目標函數 * @param {*} arity 目標函數參數個數 * @param {...any} args 調用傳入的參數 */
const curry = (fn, arity = fn.length, ...args) => {
if (arity <= args.length) {
return fn(...args)
} else {
return curry.bind(null, fn, arity, ...args);
}
}
複製代碼
函數柯里化能夠給咱們帶來不少想象,能夠將耦合的業務邏輯拆解,使得函數編程更加純粹。不過我我的以爲柯里化函數要是太複雜,對大大下降代碼的可閱讀性和可維護性,因此柯里化雖然看着高大上,但仍是不能濫用。