柯里化(Currying),又称部分求值(Partial Evaluation),可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数。柯里化,是一个逐步接收参数的过程。
柯里化有3个常见作用:
- 参数复用
- 提前返回
- 延迟计算/运行
先看一个简单示例,实现 add(1)(2, 3)(4)() = 10
的效果:
let currying = (fn) => {
let arr = [];
// 相当于参数 fn 及 return 语句之前定义的变量都被保存下来了
let next = (...args) => {
if (args.length > 0) {
arr.push(...args);
// 实现链式调用
return next;
} else {
return fn(...arr);
}
}
return next;
// ES5 写法
// return function() {
// if (arguments.length === 0) {
// return fn.call(null, arr);
// } else {
// arr = arr.concat([].slice.call(arguments));
// }
// }
}
let add = currying((...args) => {
let sum = 0;
for (let item of args) {
sum += item;
}
return sum;
})
add(1);
add(2, 3);
add(4);
let sum = add(5)();
console.log(sum); // 15
从上例可以看出,柯里化有延迟计算/运行的作用,而且可以保存变量,因此,我们可以通过柯里化实现参数复用:
// 监听事件
function nodeListen(node, eventName){
return function(fn){
node.addEventListener(eventName, function(){
fn.apply(this, Array.prototype.slice.call(arguments));
}, false);
}
}
var bodyClickListen = nodeListen(document.body, 'click');
bodyClickListen(function(){
console.log('first listen');
});
bodyClickListen(function(){
console.log('second listen');
});
// 有点懒,就不搞成 ES6 的写法了
提前返回,很常见的一个例子,兼容现代浏览器以及IE浏览器的事件添加方法,可以减少判断次数(参考张鑫旭的JS中的柯里化(currying)):
// 原先的写法
var addEvent = function(el, type, fn, capture) {
if (window.addEventListener) {
el.addEventListener(type, function(e) {
fn.call(el, e);
}, capture);
} else if (window.attachEvent) {
el.attachEvent("on" + type, function(e) {
fn.call(el, e);
});
}
};
// 柯里化,只走一次判断
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);
});
};
}
})();
以 add(1)(2, 3)(4)(5) = 15
为例,如何知道柯里化函数的执行时机?
总结了3种方法:
判断最后一次调用时传入的参数特征
以上文中的将 add 函数柯里化的例子 ,判断传入参数的个数以结束函数调用,但这种方法不够优雅
隐式类型转化:valueOf
和toString
修改上文 add 函数柯里化的例子
let currying = (fn) => {
let arr = [];
let next = (...args) => {
arr.push(...args);
return next;
}
// 字符类型
next.toString = () => {
console.log('toString')
return fn(...arr);
};
// 数值类型
next.valueOf = () => {
console.log('valueOf')
return fn(...arr);
};
return next;
}
let add = currying((...args) => {
let sum = 0;
for (let item of args) {
sum += item;
}
return sum;
})
// 注意 ‘+’,这里进行了隐式类型转化
let sum = +add(1)(2, 3)(4);
console.log(sum);
//valueOf
//10
如果我们没有重新定义
valueOf
和toString
,其**隐式转换会调用默认的toString()
**方法,将函数本身内容作为字符串返回;如果我们自己重新定义toString
/valueOf
方法,其中valueOf
比toString
优先级更高
可以参考 有趣的JavaScript隐式类型转换,简单了解一下隐式类型转换
判断原函数(被柯里化函数的参数数量)
主要用到了函数的 length
属性,示例参照下一节通用柯里化函数
三行代码实现柯里化函数:
const curry = (fn) => {
if (fn.length <= 1) return fn;
const generator = (args) => (args.length === fn.length ? fn(...args) : arg => generator([...args, arg]));
// 上述函数拆分后
// const generator = (args) => {
// if (args.length === fn.length) {
// return fn(...args);
// } else {
// return arg => generator([...args, arg])
// }
// }
return generator([], fn.length);
};
const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);
const res = curriedSum(1)(2)(3)
console.log(res); // 6
还有一个一行实现的:
const curry = (fn, arr = []) => (...args) => (
arg => arg.length === fn.length
? fn(...arg)
: curry(fn, arg)
)([...arr, ...args])
emmmmm,我的小脑筋不适合这个…
与 call/apply 方法直接执行不同,bind 方法将第一个参数设置为函数执行的上下文,其他参数依次传递给调用方法(函数的主体本身不执行,可以看成是延迟执行),并动态创建返回一个新的函数(绑定函数), 这符合柯里化特点。
function addArguments(arg1, arg2) {
return arg1 + arg2
}
// 创建一个函数,它拥有预设的第一个参数
var addThirtySeven = addArguments.bind(null, 37);
var result1 = addThirtySeven(5);
// 37 + 5 = 42
var result2 = addThirtySeven(6, 10);
// 37 + 6 = 42 ,第二个参数被忽略
反柯里化,是一个泛型化的过程。它使得被反柯里化的函数,可以接收更多参数。使本来只有特定对象才适用的方法,扩展到更多的对象。
即把如下给定的函数签名,
obj.func(arg1, arg2)
转化成一个函数形式,签名如下:
func(obj, arg1, arg2)
Function.prototype.uncurrying = function() {
var that = this;
return function() {
return Function.prototype.call.apply(that, arguments);
}
};
function sayHi () {
return "Hello " + this.value +" "+[].slice.call(arguments);
}
var sayHiuncurrying=sayHi.uncurrying();
console.log(sayHiuncurrying({value:'world'},"hahaha"));
uncurrying
是定义在Function
的prototype
上的方法,因此对所有的函数都可以使用此方法。调用时候:sayHiuncurrying=sayHi.uncurrying()
,所以uncurrying
中的 this
指向的是 sayHi
函数; (一般原型方法中的 this
不是指向原型对象prototype
,而是指向调用对象,在这里调用对象是另一个函数,在javascript中函数也是对象)call.apply(that, arguments)
把 that
设置为 call
方法的上下文,然后将 arguments
传给 call
方法。上例中,that
实际指向 sayHi
,所以调用 sayHiuncurrying(arg1, arg2, ...)
相当于 sayHi.call(arg1, arg2, ...)
(apply的作用);sayHi.call(arg1, arg2, ...)
, call
函数把 arg1
当做 sayHi
的上下文,然后把 arg2,...
等剩下的参数传给 sayHi
,因此最后相当于 arg1.sayHi(arg2,...)
(call的作用);sayHiuncurrying(obj,args)
等于 obj.sayHi(args)
。var uncurrying= function (fn) {
return function () {
return Function.prototype.call.apply(fn,arguments);
}
};
举例:
var $ = {};
console.log($.push); // undefined
var pushUncurrying = uncurrying(Array.prototype.push);
$.push = function (obj) {
pushUncurrying(this,obj);
};
$.push('first');
console.log($.length); // 1
console.log($[0]); // first
console.log($.hasOwnProperty('length')); // true
这里模仿了一个“类似jquery库” 实现时借用 Array 的 push
方法。 我们知道对象是没有 push
方法的,所以 console.log(obj.push)
返回 undefined
,可以借用Array
来处理 push
,由原生的数组方法(js引擎)来维护 伪数组对象的 length 属性和数组成员。
参考文献
JS中的柯里化(currying)
JavaScript 函数式编程技巧 - 柯里化
柯里化与反柯里化
三行代码实现 JS 柯里化
浅析 JavaScript 中的 函数 uncurrying 反柯里化