柯里化(currying)就是將使用多個參數的函數
轉換成一系列使用部分參數的函數
的技術。
使用柯里化(currying)使咱們把注意力集中於函數自己,而沒必要在乎冗長的參數個數,使執行函數的代碼更簡潔,寫出Pointfree的程序。
柯里化是JavaScript函數式編程的重點,其中應用函數閉包
,call和apply
,高階函數
,遞歸
等知識點,因此也是Javascript中的難點。
本文整理了JavaScript柯里化的基本概念,實現和應用場景。拋磚引玉一下,幫助讀者掌握函數柯里化的基本知識點,可以在實際的開發中應用起來。
原創不易,您的點贊是我繼續寫做的動力,若是文章中有紕漏和錯誤,還望指出,謝謝!javascript
首先咱們具象一下柯里化的概念。假設有一個接收3個參數的函數A前端
function A(a,b,c){
//todo something
}
複製代碼
若是咱們使用一個柯里化轉換函數curry
,這個函數接受函數做爲參數,並返回函數java
const _A = curry(A);
複製代碼
函數_A
能夠接受1個或者多個參數,當總計傳入的參數等於函數定義的參數個數時,輸出結果。以下所示:python
_A(1,2,3);
_A(1)(2)(3);
_A(1)(2,3);
_A(1,2)(3);
複製代碼
上述結果一次或者屢次調用函數_A
都返回相同的結果。
那麼咱們將curry
稱爲對函數A
的柯里化。由於curry
接受一個函數並返回一個函數,curry
又稱爲高階函數
。
先撇開curry
,咱們先對一個簡單的函數進行柯里化,以下:git
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
的柯里化。然而github
- 對於參數個數少的函數,柯里化相對簡單,可是一旦參數增多,手動去柯里化不太現實;
- 咱們也須要一個工具函數
curry
,去柯里化咱們任意一個函數,用戶只須要專一函數業務的實現- 上述柯里化以後的參數順序不必定可變,例如減法
subtraction(10,1)
,上述柯里化以後只能subtraction(10)(1)
不能subtraction(_,1)(10)
上述闡述了柯里化的概念和實現效果。本節咱們來實現工具函數curry
。
在開始以前,咱們須要瞭解柯里化的思想。在概述中提過編程
總計傳入的參數等於函數定義的參數個數時,輸出結果瀏覽器
故柯里化思想就是一個積累函數參數,當參數個數一旦達到函數執行要求,執行函數,返回結果的過程。 閉包
積累參數的過程,正如大壩後面的水庫,積累參數的過程稱爲閉包,後面的水庫就是內存app
參數一旦到達要求,返回結果,大壩一瀉千里
因而咱們的實現函數以下:
function curry(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 curry.call(this,fn,...newArgs); //不然遞歸調用
}
}
複製代碼
驗證一下:
function add(a,b,c){
return a+b+c;
}
let _add = curry(add);
console.log(_add(1,2)(3));
console.log(_add(1)(2,3));
console.log(_add(1)(2)(3));
複製代碼
效果以下:
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;
}
複製代碼
在上述實現的curry
仍是存在缺點,即柯里化以後的函數,只支持參數的順序調用,若是要支持亂序,實現方式以下:
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)
複製代碼
此處使用IIFE,執行if...else...語句提早返回咱們須要的func,這樣後續咱們就不用每次都去判斷,提升性能。
主要應用有節流和防抖,能夠參見這篇文章。
《用場景去理解函數柯里化》
《JavaScript專題之函數柯里化》
《JS中的柯里化(currying)》
《前端基礎進階(八):深刻詳解函數的柯里化》
《掌握JavaScript函數的柯里化》