javascript函数高级技巧中的柯里化是函数式编程的重要标志。它的特点是函数可以作为一个参数被传递,同时也可以作为一个函数的返回值被返回,在返回的闭包中再传入一些参数进行下一步的运算。先来一个简单的例子:
var sum = function(num1, num2){
return num1 + num2;
}
var currying = function(num){
return sum(5, num);
}
currying(3); //8
这里定义了两个变量sum和currying,在currying中返回的是sum函数并传入了一个固定的参数5作为sum的第一个参数,再把调用currying传入的参数作为sum的第二个参数。这不是一个严格的柯里化函数,但可以清晰的展现柯里化的机制。对上面的例子进行改写,将函数作为sum的一个明显返回值。
var sum = function(num1){
return function(num2){
console.log(num1 + num2);
}
}
sum3 = sum(3);
sum5 = sum(5);
sum3(5) // 8
sum5(5) // 10
sum3为一个闭包,其中有对之前传入的3的一个引用,调用sum3时将之前保存的3和后面传入的5一起给闭包使用,运算得到8,sum5与之类似。
通常的柯里化由以下步骤创建,调用一个函数并传入要柯里化的函数和参数。如下:
var currying = function(fn){
// var args = Array.prototype.slice.call(arguments, 1); //下面的效果相同
var args = [].slice.call(arguments, 1);
return function(){
var innerArgs = [].slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return fn.apply(null, finalArgs);
}
}
var sum = function(num1, num2){
return num1 + num2;
}
var curriedNum2 = currying(sum, 2);
curriedNum2(3); // 5
var curriedNums = currying(sum, 2, 8);
curriedNums(); // 10
初次调用currying函数时传入sum函数和参数,返回一个闭包。在currying函数中将传入的参数存在args变量中,在闭包内将匿名函数的参数同args中的参数拼接,调用传入的sum函数并传给它拼接后的参数数组。
如果传入的参数未知,且约定规则在调用时,函数没有参数,执行的是“Getter”,而有参数的话,则是执行“Setter”。例子如下:
// 传入的参数为一个函数,并返回一个闭包函数
var currying = function(fn){
var args = [];
// return function(){
return function res(){
if(arguments.length === 0){
return fn.apply(this,args);
}else{
// [].push.apply(args,arguments);
Array.prototype.push.apply(args,arguments);
// arguments.callee当前正在执行的函数,即currying的return值(一个闭包函数),此前的args,fn都得以保存,并能访问
// 在ES6中不建议用arguments.callee,因此用一个具名函数代替
// return arguments.callee;
return res;
}
}
};
var count = function(...args){
var sum = 0;
for(item of args){
sum += item;
}
return sum;
}
var calc = currying(count);
calc(100);
calc(200);
calc(300);
calc(); // 600
calc(100, 200, 300);
calc(400);
calc(); // 1600
第一次调用currying函数并传入count函数,在currying函数内返回一个闭包,闭包匿名函数的参数不为 0 时,将传入的参数添加到args数组中,并返回该匿名函数;为 0 时就调用fn函数并传给fn函数args参数。
在最后再放一个之前遇到的面试题。
题为:实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3)() = 6;
add(1, 2, 3)(4)() = 10;
add(1)(2)(3)(4)(5)() = 15;
实现如下:
function add() {
// 第一次执行时,定义一个数组用来存储所一次次传进来的参数
var _args = [].slice.call(arguments); // Array.prototype.slice.call同效果
// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
var _adder = function () {
if (arguments.length) {
_args.push(...arguments);
return _adder;
} else {
return _adder.toString();
}
};
// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
_adder.toString = function () {
return _args.reduce((a, b) => a + b, 0);
}
return _adder;
}
console.log(add(1)(2)(3)()); // 6
console.log(add(1, 2, 3)(4)()); // 10
console.log(add(1)(2)(3)(4)(5)()); // 15
console.log(add(2, 6)(1)()); // 9
另一个curry的封装方式sum求和函数的参数是固定的,参数个数倒计,把传进来的参数拼接起来,参数达到sum需要的个数时就执行sum,达不到就返回一个包含内部函数的包装函数,具体代码如下:
function sum(x, y, z, w) {
return x + y + z + w
}
function curry(fn) {
function inner(len, arg) {
if (len === 0) {
return fn.apply(null, arg)
}
return function(x) {
return inner(len - 1, arg.concat(x))
}
}
return inner(fn.length, [])
}
console.log(curry(sum)) // ƒ (x) { return inner(len - 1, arg.concat(x)) }
console.log(curry(sum)('a')) // ƒ (x) { return inner(len - 1, arg.concat(x)) }
console.log(curry(sum)('a')('b')) // ƒ (x) { return inner(len - 1, arg.concat(x)) }
console.log(curry(sum)('a')('b')('c')) // ƒ (x) { return inner(len - 1, arg.concat(x)) }
console.log(curry(sum)('a')('b')('c')('d')) // abcd
function curry2(fn) {
function inner(len, arg) {
if (len <= 0) {
return fn.apply(null, arg)
}
return function() {
return inner(len - arguments.length, arg.concat(Array.apply([], arguments)))
}
}
return inner(fn.length, [])
}
console.log(curry2(sum)('A')('B', 'C')('D')) // ABCD
console.log(curry2(sum)('A')()('B', 'C')()('D')) // ABCD