超级面板
文章目录
最新文章
最近更新
文章分类
标签列表
文章归档

函数式编程-柯里化和反柯里化

柯里化 curry

柯里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

var add = (x, y) => x + y;

var curriedAdd = x => y => x + y;
var addOne = curriedAdd(1);

var addTen = curriedAdd(10);

addOne(5);
// 6

addTen(5);
// 15

上述示例,将原本接收两个参数的 add 改造成了接收一个参数的 addOneaddTen 函数,每个函数都具有独立的语义。

这种方式有一个优点,可以把易变的参数固定下来。这个最典型的应用场景是使用 bind 函数绑定 this 对象:

Function.prototype.bind = function(context) {
var _this = this
var _args = Array.prototype.slice.call(arguments, 1);
return function() {
return _this.apply(context, _args.concat(Array.prototype.slice.call(arguments)));
}
}

柯里化的另一个应用场景是在如果有多个不同的执行场景,可以提前确定当前执行环境。这个最典型的例子是兼容现代浏览器以及 IE 浏览器的事件监听:

var addEvent = (root, ele, type, fn, capture = false) => {
if (root.attachEvent) {
ele.attachEvent('on' + type, fn);
} else if (root.addEventListener) {
ele.addEventListener(type, fn, capture);
}
}

这个函数执行倒没有问题,只是每次调用都会执行一次 if...else,挺繁琐的,完全可以通过柯里化只做一次判定:

var addEvent = ((root) => {
if (root.attachEvent) {
return (ele, type, fn) => ele.attachEvent('on' + type, fn);
} else if (root.addEventListener) {
return (ele, type, fn, capture = false) => ele.addEventListener(type, fn, capture);
}
})(window);

这样addEvent函数实际上已经是本浏览器支持的事件添加方法。

curry 的实现

curry 的实现有两种方法,第一种是自己手工实现:

var add = (x, y, z) => x + y + z;

var curriedAdd = x => y => z => x + y + z;

还有一种方法就是使用 curry 函数进行转换, 如 lodashramda 都提供有函数可以自动完成 curry, 这里写一个简单的实现:

var curry = (fn, length = fn.length) => {
var args = [];
var _curryN = (...arguments) => {
args = args.concat(Array.prototype.slice.call(arguments));
if (args.length == length) {
return fn.apply(null, args);
}
return _curryN;
};
return _curryN;
}

就是,生成一个 curry 函数,每次调用 curry 函数的时候计算函数的参数是否满足定义时候的参数数量,如果不满足,则缓存当前的参数,否则,把多次调用 curry 函数的参数传入原始函数执行。

中间的 _curryN 是个挺有用的工具,可以将一个接受多个参数的函数转化为接受部分参数的 curry 函数,将其进一步分离,并优化代码结构如下:

var curryN = (fn, length) => {
//对原始函数的包装,合并多次调用的柯里化的函数的参数,作为原始函数fn的参数,调用fn
var _warpFunc = (fn, args) => (...arguments) => fn.apply(null, args.concat(Array.prototype.slice.call(arguments)));

return function() {
var args = Array.prototype.slice.call(arguments);
if (args.length < length) {
return curryN(_warpFunc(fn, args), length - args.length)
}
return fn.apply(null, args);
}
}

var curry = (fn, length = fn.length) => curryN(fn, length);

反柯里化 uncurry

反柯里化(uncurry)从字面上就可以看出和柯里化(curry)的含义正好相反,如果说柯里化的作用是固定部分参数,势函数针对性更强,那么反柯里化的作用就是扩大一个函数的应用范围,使一个函数适用于其他的对象。

如果说 curry 是预先传入一些参数,那么 uncurry 就是把原来已经固定的参数或者 this 上下文当作参数延迟到问来传递,也就是把 this.method 的调用模式转化成 method(this,arg1,arg2....)

比如,Array 上有一个 push 的方法,想让 push 这个函数不仅仅支持数组,还能够被其他对象使用:push(obj,args) ,如下:

var arr = [1, 2, 3];
arr.push(4)
// [1, 2, 3, 4]

var push = Array.prototype.push.unCurry();
var obj = {};
push(obj, "a")
// Object {0: "a", length: 1}

在javascript里面,很多函数都不做对象的类型检测,而是只关心这些对象能做什么,如 ArrayStringprototype 上的方法就被特意设计成了这种模式,这些方法不对 this 的数据类型做任何校验,因此 obj 可以冒用 Arraypush 方法进行操作。这里再看一个 String 的例子:

var toUpperCase = String.prototype.toUpperCase.unCurry();
toUpperCase('js');
// JS

call 方法也可以被 unCurry

var a = {
name: 'a',
print: function() {
console.log(this.name)
},
change: function(name) {
this.name = name
console.log(this.name)
},
}

a.print();
// a

var b = {
name: 'b'
}

a.print.call(b);
// b

var call = Function.prototype.call.unCurry();
call(a.print, b)
// b

call(a.print, b, 'bb')
a.name
// "aa"

b.name
// "bb"

unCurry 本身也是方法,它也可以被反柯里化:

var unCurry = Function.prototype.unCurry.unCurry();
var toUpperCase = unCurry(String.prototype.toUpperCase)
toUpperCase('js');
// JS

反柯里化的实现:

实现的代码很简单,只有几行,但是比较绕:

/**
* 为 Function 原型添加 unCurry 方法,这样所有的 function 都可以被修改适用范围;
* 需要返回一个修改后的适用范围的函数,此时需要借用 call 方法实现
* 但是需要处理参数,此时借用 apply传入参数
*/
Function.prototype.unCurry = function() {
// _fn 在本例中是 Array.prototype.push
var _fn = this;
return function() {
// 这里有点绕,做个说明:
// return Function.prototype.call.apply(_fn, arguments);
//
// 等价于:
// var fCall = Function.prototype.call;
// return fCall.apply(_fn, arguments);
//
// 等价于:
// return _fn.fCall(...arguments);
//
// 等价于:
// return _fn.apply(arguments[0], [].slice.call(arguments, 1));
//
// 即修改 _fn 中的 this 指向第一个参数,在本例中是 obj,
// 剩下的参数传入原函数_fn
return Function.prototype.call.apply(_fn, arguments);
}
}

参考