柯里化 curry
柯里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
var add = (x, y) => x + y;
var curriedAdd = x => y => x + y; var addOne = curriedAdd(1);
var addTen = curriedAdd(10);
addOne(5);
addTen(5);
|
上述示例,将原本接收两个参数的 add
改造成了接收一个参数的 addOne
和 addTen
函数,每个函数都具有独立的语义。
这种方式有一个优点,可以把易变的参数固定下来。这个最典型的应用场景是使用 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
函数进行转换, 如 lodash
和 ramda
都提供有函数可以自动完成 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) => { 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)
var push = Array.prototype.push.unCurry(); var obj = {}; push(obj, "a")
|
在javascript里面,很多函数都不做对象的类型检测,而是只关心这些对象能做什么,如 Array
和 String
的 prototype
上的方法就被特意设计成了这种模式,这些方法不对 this
的数据类型做任何校验,因此 obj
可以冒用 Array
的 push
方法进行操作。这里再看一个 String
的例子:
var toUpperCase = String.prototype.toUpperCase.unCurry(); toUpperCase('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();
var b = { name: 'b' }
a.print.call(b);
var call = Function.prototype.call.unCurry(); call(a.print, b)
call(a.print, b, 'bb') a.name
b.name
|
unCurry
本身也是方法,它也可以被反柯里化:
var unCurry = Function.prototype.unCurry.unCurry(); var toUpperCase = unCurry(String.prototype.toUpperCase) toUpperCase('js');
|
反柯里化的实现:
实现的代码很简单,只有几行,但是比较绕:
Function.prototype.unCurry = function() { var _fn = this; return function() { return Function.prototype.call.apply(_fn, arguments); } }
|
参考