浅析函数柯里化与反柯里化
1. 柯里化
柯里化(Currying),又称部分求值(Partial Evaluation),可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数。柯里化,是一个逐步接收参数的过程。
1.1 作用
柯里化有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);
});
};
}
})();
1.2 函数的执行时机
以 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属性,示例参照下一节通用柯里化函数
1.3 通用柯里化函数
三行代码实现柯里化函数:
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,我的小脑筋不适合这个…
1.4 Function.prototype.bind 方法也是柯里化应用
与 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 ,第二个参数被忽略
2. 反柯里化
反柯里化,是一个泛型化的过程。它使得被反柯里化的函数,可以接收更多参数。使本来只有特定对象才适用的方法,扩展到更多的对象。
即把如下给定的函数签名,
obj.func(arg1, arg2)
转化成一个函数形式,签名如下:
func(obj, arg1, arg2)
2.1 简单实现
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)。
2.2 通用反柯里化函数
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 属性和数组成员。
参考文献

1133

被折叠的 条评论
为什么被折叠?



