前言
柯里化(currying)就是将使用多个参数的函数
转换成一系列使用部分参数的函数
的技术。
使用柯里化(currying)使我们把注意力集中于函数本身,而不必在意冗长的参数个数,使执行函数的代码更简洁,写出Pointfree的程序。
柯里化是JavaScript函数式编程的重点,其中应用函数闭包
,call和apply
,高阶函数
,递归
等知识点,所以也是Javascript中的难点。
本文整理了JavaScript柯里化的基本概念,实现和应用场景。抛砖引玉一下,帮助读者掌握函数柯里化的基本知识点,能够在实际的开发中应用起来。
原创不易,您的点赞是我继续写作的动力,如果文章中有纰漏和错误,还望指出,谢谢!
概述
首先我们具象一下柯里化的概念。假设有一个接收3个参数的函数A
function A(a,b,c){
//todo something
}
如果我们使用一个柯里化转换函数curry
,这个函数接受函数作为参数,并返回函数
const _A = curry(A);
函数_A
可以接受1个或者多个参数,当总计传入的参数等于函数定义的参数个数时,输出结果。如下所示:
_A(1,2,3);
_A(1)(2)(3);
_A(1)(2,3);
_A(1,2)(3);
上述结果一次或者多次调用函数_A
都返回相同的结果。
那么我们将curry
称为对函数A
的柯里化。因为curry
接受一个函数并返回一个函数,curry
又称为高阶函数
。
先撇开curry
,我们先对一个简单的函数进行柯里化,如下:
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
的柯里化。然而
- 对于参数个数少的函数,柯里化相对简单,但是一旦参数增多,手动去柯里化不太现实;
- 我们也需要一个工具函数
curry
,去柯里化我们任意一个函数,用户只需要专注函数业务的实现- 上述柯里化之后的参数顺序不一定可变,例如减法
subtraction(10,1)
,上述柯里化之后只能subtraction(10)(1)
不能subtraction(_,1)(10)
实现
上述阐述了柯里化的概念和实现效果。本节我们来实现工具函数curry
。
在开始之前,我们需要了解柯里化的思想。在概述中提过
总计传入的参数等于函数定义的参数个数时,输出结果
故柯里化思想就是一个积累函数参数,当参数个数一旦达到函数执行要求,执行函数,返回结果的过程。
积累参数的过程,正如大坝后面的水库,积累参数的过程称为闭包,后面的水库就是内存
参数一旦到达要求,返回结果,大坝一泻千里
于是我们的实现函数如下:
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));
效果如下:
下面是柯里化的骚气实现,对于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;
}
引申
在上述实现的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]
上述getProp经过柯里化,可以提升函数的复用性
延迟执行
原生事件监听的方法在现代浏览器和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函数的柯里化》