JS 基础:柯里化与反柯里化
柯里化
柯里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
柯里化,作为高阶函数的一种应用,是一个逐步接收参数的过程。
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
改造成了接收一个参数的 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) => {
//对原始函数的包装,合并多次调用的柯里化的函数的参数,作为原始函数 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
里面,很多函数都不做对象的类型检测,而是只关心这些对象能做什么,如 Array
和 String
的 prototype
上的方法就被特意设计成了这种模式,这些方法不对 this
的数据类型做任何校验,因此 obj
可以冒用 Array
的 push
方法进行操作。这里再看一个 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);
};
};
柯里化扩展
实现 add(1)(2, 3)(4)(5) = 15
的效果。 很多人这里就犯嘀咕了:我怎么知道执行的时机? 其实,这里有个忍者技艺:valueOf
和 toString
。 js
在获取当前变量值的时候,会根据语境,隐式调用 valueOf
和 toString
方法进行获取需要的值。
function currying(fn) {
var allArgs = [];
function next() {
var args = [].slice.call(arguments);
allArgs = allArgs.concat(args);
return next;
}
// 字符类型
next.toString = function () {
return fn.apply(null, allArgs);
};
// 数值类型
next.valueOf = function () {
return fn.apply(null, allArgs);
};
return next;
}
var add = currying(function () {
var sum = 0;
for (var i = 0; i < arguments.length; i++) {
sum += arguments[i];
}
return sum;
});