JS中的curry化(柯里化)
什么是 curry 化
curry 化也是一个常见的概念,维基百科对其解释为:
在计算机科学中,柯里化(currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。这个技术由克里斯托弗·斯特雷奇以逻辑学家哈斯凯尔·加里命名的。
再简洁一些就是:柯里化是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
还是不懂,没关系,下面就通过几个例子一步一步去了解柯里化。
怎么实现 curry 化
实现一个函数,对数组进行过滤,过滤掉小于10的项。
传统的做法是:
const filterLowerThan10 = (array) => {
let result = [];
for (let i = 0; i < array.length; i++) {
let currentValue = array[i];
if (currentValue < 10) {
result.push(currentValue);
}
}
return result;
};
实现起来并没有难度,但是当前要过滤的是小于10的项,如果这个阈值更改了呢,我们可以借用 curry 化的思想将其改造:
const filterLowerNumber = (number) => {
return (array) => {
let result = [];
for (let i = 0; i < array.length; i++) {
let currentValue = array[i];
if (currentValue < number) {
result.push(currentValue);
}
}
return result;
};
};
const filterLowerThan10 = filterLowerNumber(10);
filterLowerThan10([1, 11, 8, 21, 2]); // [1,8,2]
// 也可以这样简写
// filterLowerNumber(10)([1, 11, 8, 21, 2]);
另一个场景
实现一个求两数之和得方法
普通函数:
function add(x, y) {
return x + y;
}
add(1, 2); // 3
curry 化函数:
var add = function (x) {
return (y) => x + y;
};
add(1)(2); // 3
在此基础上提交更复杂得要求
按要求实现 add 方法:
add(1)(2); // 结果为3
add(1)(3)(5); // 结果为9
add(1)...(n); // 结果为sum
大家可能比较眼熟哈,很多 curry 化的面试题都是以此为原型的。
解题:
- 由调用方式可知,add 函数每次执行后一定返回一个函数,以供后续调用,且返回的函数依然要返回自身,供多级调用;
- 当最后一次调用结束,返回的是一个函数,为了满足题意,需要改写内部返回的函数 toString (代码中也解释);
- 为了进行求和,需要在 add 函数内部维护一个闭包变量 args,args 是个数组,存放了第一次调用 add 和 后续调用 fn 函数时传入的参数;
- 在调用 fn 的 toString 方法时,意味着最后一次调用结束,返回函数,那么就计算 args 数组中的所有值得和即可求出结果。
const add = (arg1) => {
let args = [arg1];
const fn = (arg2) => {
args.push(arg2);
return fn;
};
// 因为最后一次执行完毕后会返回 fn 函数体,相当于调用了 fn 的 toString 方法,所以改写 toString 方法求和即可
fn.toString = function () {
return args.reduce((prev, item) => prev + item, 0);
};
return fn;
};
add(1)(2)(3); // 6
这里只实现了每次调用传入单个参数,为了支持每次调用可以传入多参数,改动为:
const add = (...arg1) => {
let args = [...arg1];
const fn = (...arg2) => {
args = [...args, ...arg2];
return fn;
};
fn.toString = function () {
return args.reduce((prev, item) => prev + item, 0);
};
return fn;
};
add(1)(2, 3, 4)(5); // 17
虽然可以正确计算出结果,但是如果用 === 把表达式和结果进行一个判断
add(1)(2)(3) === 6; //false
add(1)(2, 3, 4)(5) === 15; //false
无一例外输出都是false,其实并不奇怪,上面代码中也说过,调用 add 函数返回的永远都是 Function ,这里只是通过修改了 fn 的 toString 方法达到了输出计算结果的目的,但是这并不能改变返回值的类型,依然是 Function。
反 curry 化
反 curry 化的意义在于扩大函数的适用性,使本来作为特定对象所拥有的功能函数可以被任意对象所使用。
function Person() {
this.message = "wowowo";
}
Person.prototype = {
speak: function () {
console.log(this.message);
},
};
Person 实例均可使用 speak 方法:
new Person().speak();
如果有一个变量对象:
const dog = {
message: "wang wang wang!",
};
该对象也想使用 Person 原型上的 speak 方法,就需要反 curry 化:
const unCurrySpeak = unCurry(Person.prototype.speak);
unCurrySpeak(dog);
unCurry 就是我们要实现的反 curry 化的方法。
分析可知: unCurry 的参数是一个“希望被其他对象所调用的方法”,unCurry 执行后返回一个新的函数,该函数的第一个参数是预期要执行方法的对象(dog),后面的参数是执行这个方法时需要传递的参数。
function unCurry(fn) {
return function () {
var obj = [].shift.call(arguments);
return fn.apply(obj, arguments);
};
}
如此实现即可,当然也可以将 uncurry 挂载在函数原型上实现。