背景
在 JavaScript 中,柯里化和反柯里化是高阶函数的一种应用,在这之前我们应该清楚什么是高阶函数,通俗的说,函数可以作为参数传递到函数中,这个作为参数的函数叫回调函数,而拥有这个参数的函数就是高阶函数,回调函数在高阶函数中调用并传递相应的参数,在高阶函数执行时,由于回调函数的内部逻辑不同,高阶函数的执行结果也不同,非常灵活,也被叫做函数式编程。
柯里化
在 JavaScript 中,函数柯里化是函数式编程的重要思想,也是高阶函数中一个重要的应用,其含义是给函数分步传递参数,每次传递部分参数,并返回一个更具体的函数接收剩下的参数,这中间可嵌套多层这样的接收部分参数的函数,直至返回最后结果。
1、最基本的柯里化拆分
// 原函数
function addFunc(a, b, c) {
return a + b + c;
}
// 柯里化
function curryingAdd(a) {
return function (b) {
return function (c) {
return a + b + c;
}
}
}
// 调用原函数
addFunc(1, 2, 3); // 6
// 调用柯里化后的函数
curryingAdd(1)(2)(3); // 6
被柯里化的函数 curryingAdd
每次的返回值都为一个函数,并使用下一个参数作为形参,直到三个参数都被传入后,返回的最后一个函数内部执行求和操作,其实是充分的利用了闭包的特性来实现的。
2、柯里化通用式
上面的柯里化函数没涉及到高阶函数,也不具备通用性,无法转换形参个数任意或未知的函数,我们接下来封装一个通用的柯里化转换函数,可以将任意函数转换成柯里化。
// ES5 实现
function curryingFunc(func, args) {
// 取func的形参个数
var len = func.length;
// 上次传入的参数
var args = args || [];
return function () {
// 本次的参数
var _args = [].slice.call(arguments);
// 合并两次参数,并调整参数顺序
Array.prototype.unshift.apply(_args, args);
// 如果参数不够,则返回继续收集参数,否则返回func执行结果
if (_args.length < len) {
return curryingFunc.call(null, func, _args);
}
return func.apply(null, _args);
}
}
上面主要使用的是 ES5 的语法来实现,大量的使用了 call 和 apply,下面我们通过 ES6 的方式实现功能完全相同的柯里化转换通用式。
// ES6 实现
function curryingFunc(func, args = []) {
let len = func.length;
return function(..._args) {
_args.unshift(...args);
if (_args.length < len) {
return curryingFunc.call(null, func, _args);
}
return func(..._args);
}
}
函数 curryingFunc
算是比较高级的转换柯里化的通用式,可以随意拆分参数,假设一个被转换的函数有多个形参,我们可以在任意环节传入任意个数的参数进行拆分,举一个例子,假如 5 个参数,第一次可以传入 2 个,第二次可以传入 1 个, 第三次可以传入剩下的,也有其他的多种传参和拆分方案,因为在 curryingFunc
内部收集参数的同时按照被转换函数的形参顺序进行了更正。
柯里化的一个很大的好处是可以帮助我们基于一个被转换函数,通过对参数的拆分实现不同功能的函数,如下面的例子。
// 被转换函数用于检测字符串是否符合正则
function checkFunc(reg, str) {
return reg.test(str);
}
// 柯里化
let check = curryingFunc(checkFunc);
// 产生新功能的函数
let checkPhone = check(/^1[34578]\d{9}$/);
let checkNumber = check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);
上面的例子根据一个被转换的函数通过转换变成柯里化函数,并用 check
变量接收,以后每次调用 check
传递不同的正则就会产生一个检测不同类型字符串的功能函数。
这种使用方式同样适用于被转换函数是高阶函数的情况,比如下面的例子。
function mapFunc(func, arr) {
return arr.map(func);
}
// 柯里化
let getNewArr = curryingFunc(mapFunc);
// 产生新的功能函数
let percentArr = getNewArr(item => `${item}%`);
let doubleArr = getNewArr(item => item * 2);
// 调用
let arr = [1, 2, 3];
percentArr(arr); // ['1%', '2%', '3%']
doubleArr(arr); // [2, 4, 6]
3、柯里化与bind
bind
方法是经常使用的一个方法,它的作用是帮我们将调用 bind
函数内部的上下文对象 this
替换成我们传递的第一个参数,并将后面其他的参数作为调用 bind
函数的参数。
// bind 方法的模拟
Obejct.prototype.bind = function(context) {
var _self = this;
// 获取除第一个参数外的
var args = [].slice.call(arguments, 1);
return function() {
// 绑定this
return _self.apply(context, args);
}
}
通过上面代码可以看出,其实 bind
方法就是一个柯里化转换函数,将调用 bind
方法的函数进行转换,即通过闭包返回一个柯里化函数,执行该柯里化函数的时候,借用 apply
将调用 bind
的函数的执行上下文转换成了 context
并执行,只是这个转换函数没有那么复杂,没有进行参数拆分,而是函数在调用的时候传入了所有的参数。
反柯里化
反柯里化的思想与柯里化正好相反,如果说柯里化的过程是将函数拆分成功能更具体化的函数,那反柯里化的作用则在于扩大函数的适用性,使本来作为特定对象所拥有的功能函数可以被任意对象所使用。
1、反柯里化通用式
反柯里化通用式的参数为一个希望可以被其他对象调用的方法或函数,通过调用通用式返回一个函数,这个函数的第一个参数为要执行方法的对象,后面的参数为执行这个方法时需要传递的参数
ES5 实现
function unCurryingFunc(fn) {
return function() {
// 取走要执行fn的对象
var obj = [].shift.call(arguments);
return fn.apply(obj, arguments);
}
}
ES6 实现
function unCurryingFunc(fn) {
return function(...args) {
return fn.call(...args);
}
}
下面我们通过一个例子来感受一下反柯里化的应用。
// 构造函数F
function F() {}
// 拼接属性的方法
F.prototype.conactProps = function() {
let args = Array.from(argumengs);
return args.reduce((prev, next) => `${this[prev]}:${this[next]}`);
}
let obj = {
name: 'Mark',
age: 18,
};
// 反柯里化
let contactProps = unCurryingFunc(F.prototype.contactProps);
contactProps(obj, 'name', 'age'); // Mark: 18
反柯里化还有另外一个应用,用来代替直接使用 call
和 apply
,比如检测数据类型的 Object.prototype.toString
等方法,以往我们使用时是在这个方法后面直接调用 call 更改上下文并传参,如果项目中多处需要对不同的数据类型进行验证是很麻的,常规的解决方案是封装成一个检测数据类型的模块。
// 常规用法
function checkType(val) {
return Object.prototype.toString.call(val);
}
如果需要这样封装的功能很多就麻烦了,代码量也会随之增大,其实我们也可以使用另一种解决方案,就是利用反柯里化通用式将这个函数转换并将返回的函数用变量接收,这样我们只需要封装一个 uncurringFunc
通用式就可以了。
// 利用反柯里化创建检测数据类型的函数
let checkTypeFunc = unCurryingFunc(Object.prototype.toString);
checkTypeFunc(1); // [object Number]
checkTypeFunc('hi'); // [object String]
checkTypeFunc(true); // [object Boolean]
2、通过函数调用生成反柯里化函数
在 JavaScript 我们经常使用面向对象的编程方式,在两个类或构造函数之间建立联系实现继承,如果我们对继承的需求仅仅是希望一个构造函数的实例能够使用另一个构造函数原型上的方法,那进行繁琐的继承很浪费,简单的继承父子类的关系又不那么的优雅,还不如之间不存在联系
Function.prototype.uncurrying = function () {
var self = this;
return function() {
return Function.prototype.call.apply(self, arguments);
}
}
之前的问题通过上面给函数扩展的 uncurrying
方法完全得到了解决,比如下面的例子。
// 构造函数
function F() {}
F.prototype.sayHi = function () {
return 'Hi, I am ' + this.name + ', i am ' + this.age + ' years old'
}
// 希望sayHi方法被任何对象使用
let sayHi = F.prototype.sayHi.uncurrying();
sayHi({name: 'Mark', age: 18}); // Hi, I am Mark, i am 18 years old
在 Function
的原型对象上扩展的 uncurrying
中,难点是理解 Function.prototype.call.apply
,我们知道在 call
的源码逻辑中 this
指的是调用它的函数,在 call
内部用第一个参数替换了这个函数中的 this
,其余作为形参执行了函数。
而在 Function.prototype.call.apply
中 apply
的第一个参数更换了 call
中的 this
,这个用于更换 this
的就是例子中调用 uncurrying
的方法 F.prototype.sayHi
,所以等同于 F.prototype.sayHi.call
,arguments
内的参数会传入 call
中,而 arguments
的第一项正是用于修改 F.prototype.sayHi
中 this
的对象。
最后
看到这里你应该对柯里化和反柯里化有了一个初步的认识了,但要熟练的运用在开发中,还需要我们更深入的去了解它们内在的含义。