Currying是函数式编程的一个特性,将多个参数的处理转化成单个参数的处理,类似链式调用。
柯里化(Currying)
柯里化有3个常见作用:1. 参数复用;2. 提前返回;3. 延迟计算/运行。
通俗的柯里化函数原型如下:
var currying = function(fn) {
var args = [].slice.call(arguments, 1);
return function() {
var newArgs = args.concat([].slice.call(arguments));
return fn.apply(null, newArgs);
};
};
柯里化的通用实现
我们来定义一个比较通用的currying函数:
// 第一个参数为要应用的function,第二个参数是需要传入的最少参数个数
function curry(func, minArgs) {
if (minArgs == undefined) {
minArgs = 1;
}
function A(frozenargs) {
return function() { // 优化处理,如果调用时没有参数,返回该函数本身
var args = Array.prototype.slice.call(arguments);
var newArgs = frozenargs.concat(args);
if (newArgs.length >= minArgs) {
return func.apply(this, newArgs);
} else {
return A(newArgs);
}
};
}
return A([]);
}
柯里化应用
这样,我们就可以随意定义我们的业务行为了,比如定义加法:
var plus = curry(function () {
var result = 0;
for (var i = 0; i < arguments.length; ++i) {
result += arguments[i];
}
return result;
}, 2);
plus(3, 2); // 正常调用,返回 5
plus(3); // 偏应用,返回一个函数(返回值为3+参数值)
plus(3)(2); // 完整应用(返回5)
plus()(3)()()(2); // 返回 5
plus(3, 2, 4, 5); // 可以接收多个参数,返回 14
plus(3)(2, 3, 5); // 同理,返回13
如下是减法的例子:
var minus = curry(function(x) {
var result = x;
for (var i = 1; i < arguments.length; ++i) {
result -= arguments[i];
}
return result;
}, 2);
minus(5,3);//正常调用,返回 2
minus(5)(3);//完整应用,返回 2
minus()(3)()()(2);//返回 1
minus(8)(2, 3, 5);// 接受多个参数,返回 -2
多次柯里化
或者如果你想交换参数的顺序,你可以这样定义:
var flip = curry(function(f) {
return curry(function(a, b) {
return f(b, a);
}, 2);
});
var flip_minus = flip()(minus);//返回一个具有柯里化能力的函数
flip_minus(2)(10);//8
flip_minus()(1)()(6);//5
这里为什么要调用两次 curry 呢, 第一次调用是为了固定 func ,返回了curry 内部的A的内部方法,即返回:
function(){ //该函数即flip,以下将称作`方法①`
var args = Array.prototype.slice.call(arguments);
var newArgs = frozenargs.concat(args);
if (newArgs.length >= minArgs) {
return func.apply(this, newArgs);
} else {
return A(newArgs);
}
}
//其中func指的是curry传入的第一个参数,表示一个方法,即如下:
function(f) {//该方法表示上述func,以下将称作`方法②`
return curry(function(a, b) {
return f(b, a);
}, 2);
}
分析
- 此时调用 flip(minus) , 实际上是执行了一遍 “方法①” , 由于实参长度等于minArgs(即1), 因此返回 func.apply(this, newArgs);
- 由于 newArgs=[ minus ], 然后将执行 “方法②” ;
- 由于 f=minus, 在 “方法②” 中将第二次执行curry, 最终返回交换参数后的minus.
因此 flip 是一个柯里化后的方法, 具有柯里化的典型特征: 能够将多个参数的处理转化成单个参数的处理. 不仅如此, flip 可以连续两次柯里化参数.
如上述栗子, 第一次柯里化的参数是方法 minus, 由于长度未做限制, 默认为1, 即至少要向 flip 传递一个方法后才能返回一个柯里化的 flip_minus, 同样, 返回的 flip_minus 也具备柯里化的能力.flip_minus()(1)()(6) 等的执行结果充分说明了这点.
反柯里化(unCurrying)
函数柯里化,是固定部分参数,返回一个接受剩余参数的函数,也称为部分计算函数,目的是为了固定参数, 延迟计算等。
那么反柯里化函数,从字面讲,意义和用法跟函数柯里化相比正好相反,扩大适用范围,创建一个应用范围更广的函数。使得本来只有特定对象才适用的方法,扩展到更多的对象。
反柯里化的三种实现
看一下通用函数①:
Function.prototype.unCurrying = function() {
var self = this;
return function() {
return Function.prototype.call.apply(self, arguments);
}
}
以上这段代码做了3件事:
- 在Function原型上增加 unCurrying 方法, 方便所有方法继承;
- 返回方法, 即暴露方法对外的接口;
- 借用call, call 的参数由 apply提供;
上述代码先后调用了call, apply 方法 来保证参数传递正常. 自然也可以直接调用apply(因arguments类似数组,调用apply较为方便). 如下函数②:
Function.prototype.unCurrying = function() {
var self = this;
return function() {
var a = arguments;
return self.apply(a[0], [].slice.call(a, 1));分割arguments,方便apply传参
};
};
当然, 还可以利用Function.prototype.bind()方法返回一个新函数,从而有如下函数③:
Function.prototype.unCurrying = function () {
return this.call.bind(this);
};
bind() 方法会创建一个新函数, 称为绑定函数, 当调用这个绑定函数时, 绑定函数会以创建它时传入bind() 方法的第一个参数作为this ,也就是说, 传入的this将成为最终的上下文, 从第二个参数开始的参数, 将按照顺序作为原函数的参数来调用原函数.
也就是说, 谁(假设为方法f)调用 unCurrying 方法, 将返回一个如下的方法.
function(){
Function.prototype.call.apply(f, arguments);
}
原理都相同,最终就是把 this.method 转化成 method(this,arg1,arg2….) 以实现方法借用和this的泛化。
以上,函数②比较好理解,函数①和③有些晦涩,下面我将分别推导。
反柯里化推导
如上所示,Function.prototype.call.apply(self, arguments);
看起来有些绕,其实很好理解。接下来我会先分析第①种实现,再分析第③种实现。第①种实现是这样的:
Function.prototype.unCurrying = function() {
var self = this;
return function() {
return Function.prototype.call.apply(self, arguments);
}
}
var push = Array.prototype.push.uncurrying(); // 借用push举例
谁调用uncurrying
,谁就等于this
或self
. 这意味着self
就是数组的push方法
.
替换掉self
,最终外部的push
等同如下函数:
function(){
return Function.prototype.call.apply(Array.prototype.push, arguments);
};
函数放在这里,我们先来理解apply
函数,apply
有分解数组为一个个参数的作用。
推导公式:a.apply(b, arguments)
意味着把b当做this上下文,相当于是在b上调用a方法,并且传入所有的参数,如果b中本身就含有a方法,那么就相当于 b.a(arg1, arg2,…)
得到公式1:a.apply(b, arguments) === b.a(arg1, arg2,…)
由于call
和 apply
除参数处理不一致之外,其他作用一致,那么公式可以进一步变化得到:
公式2:a.call(b, arg) === b.a(arg)
将公式1这些代入上面的函数,有:
a = Function.prototype.call
即a等于call方法。
我们接着代入公式,有:
b = Array.prototype.push
即b等于数组的push方法
那么 Function.prototype.call.apply(Array.prototype.push, arguments)
就相对于:
Array.prototype.push.call(arg1, arg2,…)
,那么:
push([], 1)
就相当于 Array.prototype.push.call([], 1)
,再代入公式2,相当于:[].push(1)
答案已经呼之欲出了,就是往数组中末尾添加数字1。
接下来我来分析反柯里化的第③种实现:
对于this.call.bind(this);
部分,this
相当于Array.prototype.push
,那么整体等同于如下:
Array.prototype.push.call.bind(Array.prototype.push)
这里的难点在于bind方法,bind的实现比较简单,如下:
Function.prototype.bind = function(thisArg){
var _this = this;
var _arg = _slice.call(arguments,1);
return function(){
var arg = _slice.call(arguments);
arg = _arg.concat(arg);
return _this.apply(thisArg,arg);
}
}
进一步简化bind
的原理,等同于谁调用bind
,就返回一个新的function。
我们假设函数fn
调用bind
方法如fn.bind([1, 2])
,经过简化,忽略bind
绑定参数的部分,最终返回如下:
function(){
return fn.apply([1, 2], arguments);
}
以上,将fn
替换为 Array.prototype.push.call
,[1, 2]
替换为 Array.prototype.push
,那么:
Array.prototype.push.call.bind(Array.prototype.push)
将等同于:
function(){
return Array.prototype.push.call.apply(Array.prototype.push, arguments);
}
这个看起来和反柯里化的第二种实现有些不大相同,不要急,虽然表面上看起来不一致,但骨子里还是一致的。请耐心往下看:
不同的地方在于前半部分 Array.prototype.push.call
,这里它是一个整体,实际上想代表的就是call方法。而我们都知道,所有函数的call方法,最终都是Function.prototype
的 call
方法。那么,就有如下恒等式成立:
Array.prototype.push.call === Function.prototype.call //true
那么以上函数将等同于:
function(){
return Function.prototype.call.apply(Array.prototype.push, arguments);
}
褪去代入的参数,函数可还原为:
function(){
return Function.prototype.call.apply(self, arguments);
}
综上,最终反柯里化的第③种实现将和第①种实现完全一致。
反柯里化前因
接下来我们来回顾下前一篇文章<详解JS之Arguments对象>中所讲到的鸭式辩型: 如果一个对象可以像鸭子一样走路,游泳,并且嘎嘎叫,就认为这个对象是鸭子,哪怕它并不是从鸭子对象继承过来的。
Array构造器和String构造器的prototype上的方法就被特意设计成了鸭子类型。这些方法不对this的数据类型做任何校验。这也就是为什么arguments能冒充array调用push方法.
下面我们来看下v8引擎里面 Array.prototype.push 的代码:
function ArrayPush() {
var n = TO_UINT32(this.length);
var m = % _ArgumentsLength();
for (var i = 0; i < m; i++) {
this[i + n] = % _Arguments(i); //属性拷贝
this.length = n + m; //修正length
return this.length;
}
}
Array.prototype.push 方法并没有去判断调用对象是不是数组, 这给对象冒充提供了天然条件, 基于此函数反柯里化(unCurrying) 才具有可行性.
反柯里化应用
下面我们让普通对象具有push方法的能力:
var push = Array.prototype.push.unCurrying(),
obj = {};
push(obj, '123', '456');
console.log(obj); //Object {0: "123", 1: "456", length: 2}
obj本来是一个空对象, 它被push了两个元素 “123” 和 “456”, 并且拥有了 length 属性. obj实际上已经变成了一个数组(即[ “123”, “456” ]).
下面我们来看看更多的例子:
/*反柯里化toUpperCase*/
var toUpperCase = String.prototype.toUpperCase.unCurrying(); console.log(toUpperCase('abc')); // ABC
/*反柯里化toLowerCase*/
var toLowerCase = String.prototype.toLowerCase.unCurrying();
console.log(toLowerCase('DEF')); // def
/*反柯里化call*/
var call = Function.prototype.call.unCurrying();
function f(action){
console.log(this.name+" is "+action);
}
var obj = {
name:'louis'
};
call(f,obj,'working.');//call的3个参数分别为 函数, 上下文对象, 形参
其中反柯里化 call 可能不大好理解, 它表示 obj 对象借用方法 f , 替换了其中的this(上下文).
反柯里化自身
更有趣的是, unCurrying本身也是方法, 它同样可以被反柯里化, 这就是反柯里化的值得玩味的地方.
var unCurrying = Function.prototype.unCurrying.unCurrying();
var sort = unCurrying(Array.prototype.sort);
var array = sort([5,2,3,4,1],function(a,b){
return a-b;
});
console.log(array);//[1,2,3,4,5]
以上是关于 javaScript
柯里化与反柯里化的一些理解.
柯里化体现的思想是”归一”, 多个参数化为一个参数, 然后逐个处理, 便于产生偏函数, 实现链式调用; 反柯里化体现的思想是”延伸”, 通过拓展方法的作用域, 使得它变得更通用, 提高了代码的复用性. 它们都提升了代码的优雅性.
版权声明:转载需注明作者和出处。
本文作者:louis
本文链接:http://louiszhai.github.io/2015/12/16/currying/
参考文章