前言
函式柯里化(currying)就是將使用多個引數的函式轉換成一系列使用一個引數的函式
的技術。
柯里化讓我們把注意力集中到函式本身,而不是冗長的資料引數之中,使程式碼更簡潔,幫助我們寫出Pointfree的程式。
概述
首先我們具象一下柯里化的概念。假設有一個接收3個引數的函式A
function A(a,b,c){
//todo something
}
複製程式碼
如果我們使用一個柯里化轉換函式createCurrying,這個函式接受函式作為引數,並返回函式
const _A = createCurrying(A);
複製程式碼
函式_A可以接受1個或者多個引數,當總計的引數等於函式定義的引數個數時,輸出結果。如下所示:
_A(1,2,3);
_A(1)(2)(3);
_A(1)(2,3);
_A(1,2)(3);
複製程式碼
上述結果一次或者多次呼叫函式_A都返回相同的結果。
那麼我們將createCurrying稱為對函式A的柯里化。
先撇開createCurrying,我們先對一個簡單的函式進行改造,讓其滿足我們的柯里化要求,如下:
function add(a,b){
return a+b;
}
console.log(add(1+2)); //輸出3
function _add(a){
return function(b){
return a+b;
}
}
console.log(_add(1)(2));//輸出3
console.log(_add(2)(1));//輸出3
複製程式碼
上述 _add
是對 add
的柯里化。然而
- 對於引數個數少的函式,柯里化相對簡單,但是一旦引數增多,手動去柯里化不太現實,
- 我們也需要一個工具函式createCurrying,去柯里化我們任意一個函式,使用者只需要專注函式業務的實現
實現
上述主要闡述了柯里化的概念,實現效果。本節我們來實現createCurrying。
在實現之前,我們需要了解柯里化的思想。
什麼是柯里化思想?正如概述中所說
總計的引數等於函式定義的引數個數時,輸出結果
所以柯里化思想就是一個積累函式引數的過程,引數一旦達到函式執行要求,直接返回函式結果。
此處積累函式引數的手段,我們可以使用閉包。實現函式如下:
function createCurrying(fn,...args){
let argsLength = fn.length; //函式定義的形參個數
return function() {
var newArgs = args.concat([].slice.call(arguments)); //將上一次呼叫函式的引數和本次的引數合併
if(newArgs.length >= argsLength){
return fn.apply(this,newArgs); //如果引數和執行的函式相等,執行函式
}
return createCurrying.call(this,fn,...newArgs); //否則遞迴呼叫
}
}
複製程式碼
驗證一下:
function add(a,b,c){
return a+b+c;
}
let _add = createCurrying(add);
console.log(_add(1,2)(3));
console.log(_add(1)(2,3));
console.log(_add(1)(2)(3));
複製程式碼
效果如下:
下面是柯里化的騷氣實現,對於ES6想深入的童鞋,可以好好看一下
var curry = fn =>
judge = (...args) =>
args.length === fn.length? fn(...args): (...arg) => judge(...args, ...arg)
複製程式碼
我把它轉化成ES5,幫助理解。
var curry = function (fn){
let judge = function(...args){
if(args.length === fn.length){
return fn(...args)
}else{
return function (...arg){
return judge(...args, ...arg);
}
}
}
return judge;
}
複製程式碼
引申
在上述實現的createCurrying還是存在缺點,即柯里化之後的函式,只支援引數的順序呼叫,如果要支援亂序,實現方式如下:
function curry(fn, args, holes) {
length = fn.length;
args = args || [];
holes = holes || [];
return function() {
var _args = args.slice(0),
_holes = holes.slice(0),
argsLen = args.length,
holesLen = holes.length,
arg, i, index = 0;
for (i = 0; i < arguments.length; i++) {
arg = arguments[i];
// 處理類似 fn(1, _, _, 4)(_, 3) 這種情況,index 需要指向 holes 正確的下標
if (arg === _ && holesLen) {
index++
if (index > holesLen) {
_args.push(arg);
_holes.push(argsLen - 1 + index - holesLen)
}
}
// 處理類似 fn(1)(_) 這種情況
else if (arg === _) {
_args.push(arg);
_holes.push(argsLen + i);
}
// 處理類似 fn(_, 2)(1) 這種情況
else if (holesLen) {
// fn(_, 2)(_, 3)
if (index >= holesLen) {
_args.push(arg);
}
// fn(_, 2)(1) 用引數 1 替換佔位符
else {
_args.splice(_holes[index], 1, arg);
_holes.splice(index, 1)
}
}
else {
_args.push(arg);
}
}
if (_holes.length || _args.length < length) {
return curry.call(this, fn, _args, _holes);
}
else {
return fn.apply(this, _args);
}
}
}
複製程式碼
應用
柯里化的作用包括提高函式引數複用,延遲計算等,一般有如下幾種應用:
偏函式
偏函式(Partial function),在python中應用較多,詳情可檢視這裡,在Javascript也可以應用,如有一個int函式,如下:
function int(chars,hex=10){
//將字串chars轉換成以hex進位制的整數
}
int('10') //將10轉換成10進位制
int('10',2)//將10轉換成2進位制
int('10',8)//將10轉換成8進位制
複製程式碼
該函式可以將預設的數字字串轉化成10進位制整數,也可以指定hex的值。此處我們可以引申的柯里化函式,如下
let int2 = createCurrying(int,_,2);
int2('10');
let int8 = createCurrying(int,_,8);
int8('10');
複製程式碼
簡化回撥
var persons = [{name: 'kevin', age: 11}, {name: 'daisy', age: 24}]
let getProp = createCurrying(function (key, obj) {
return obj[key]
});
let names2 = persons.map(getProp('name'))
console.log(names2); //['kevin', 'daisy']
let ages2 = persons.map(getProp('age'))
console.log(ages2); //[11,24]
複製程式碼
延遲執行
原生事件監聽的方法在現代瀏覽器和IE瀏覽器會有相容問題,解決該相容性問題的方法是進行一層封裝,若不考慮柯里化函式,我們正常情況下會像下面這樣進行封裝,如下:
/*
* @param ele Object DOM元素物件
* @param type String 事件型別
* @param fn Function 事件處理函式
* @param isCapture Boolean 是否捕獲
*/
var addEvent = function(ele, type, fn, isCapture) {
if(window.addEventListener) {
ele.addEventListener(type, fn, isCapture)
} else if(window.attachEvent) {
ele.attachEvent("on" + type, fn)
}
}
addEvent(document.getElementById('button'), "click", function() {
alert("function currying");
}, false)
複製程式碼
柯里化之後,如下:
var addEvent = (function(){
if (window.addEventListener) {
return function(el, sType, fn, capture) {
el.addEventListener(sType, function(e) {
fn.call(el, e);
}, (capture));
};
} else if (window.attachEvent) {
return function(el, sType, fn, capture) {
el.attachEvent("on" + sType, function(e) {
fn.call(el, e);
});
};
}
})();
addEvent(document.getElementById('button'), "click", function() {
alert("function currying");
}, false)
複製程式碼
此處使用柯里化延遲執行的特點,返回新函式,在新函式呼叫相容的事件方法。等待addEvent
新函式呼叫,延遲執行。
參考文獻
《用場景去理解函式柯里化》
《JavaScript專題之函式柯里化》
《JS中的柯里化(currying)》
《前端基礎進階(八):深入詳解函式的柯里化》
《掌握JavaScript函式的柯里化》