今天看到一个问题,问一个怎么实现
这样子的问题。
那看到这个问题的第一反应应该就是函数的柯里化。
维基百科 中对函数柯里化的解释是这样的:
柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
函数柯里化有什么作用呢,为什么要柯里化呢。其实也没有什么作用,一般编程中,基本不会用到柯里化,但是假如在函数式编程中,还是很有用的。另一方面,就是面试的时候很多面试官会问。
那么我们看看下面实现的几种例子:
1. 如果固定函数的调用次数
const a = function(x) {
return function(y) {
return function(z) {
return x * y * z;
};
};
};
改成 ES6 的写法会更简单
const result = x => y => z => x * y * z;
那这种方式只能实现固定三次调用,如果想有更通用的方法呢?那就看方法二:
2. 通用方法
function curry(fn) {
const len = fn.length;
return function curried() {
const args = Array.prototype.slice.call(arguments);
if (args.length >= len) {
return fn.apply(this, args);
}
return function () {
return curried.apply(this, args.concat(Array.prototype.slice.call(arguments)));
};
};
}
如果使用 ES6 的写法,可以简化为(使用了 ES6 的剩余参数(Rest Parameters)):
function curry(fn, ...args){
return args.length === fn.length ? fn(...args) : (...next_args) => curry(fn, ...args, ...next_args);
}
然后将需要柯里化的函数传入,即可得到一个柯里化的函数。
const result = curry(function (a, b, c) {
return a * b * c;
});
那同样的,可以得到
const result2 = curry(function (a, b, c) {
return a + b + c;
});
可是,如果我想得到
let x = a(3)(4)
let y = x(4);
console.log(x); // 12
console.log(y); // 48
又该怎么办呢?有些人想既然 x 输出为 12 了,那就肯定不能再被调用了,所以 x(4)
肯定会报错。
但是假如 数字12, 并不是一个真正的数字呢?来看例子:
3. 特殊的柯里化
function currying(x){
var sum = x
var curried = function(y){
sum = sum * y
return curried
}
curried.toString=function(){
return sum
}
return curried
}
调用
console.log(currying(1)) //1
console.log(currying(1)(2)) //2
console.log(currying(1)(2)(3)) //6
console.log(currying(1)(2)(3)(4)) //24
就是可以得到的,那是为什么呢?主要是利用了函数的 toString 方法。实现了障眼法,在控制台中调用的时候,因为返回的是函数,所以会调用函数的 toString 方法,故而就会显示出来数字。但是在实际代码调用过程中,其实返回的还是函数。所以,这种不算严格的柯里化。只是形式上类似而已。并且还会存在单例的问题,也即此函数任何时候调用,都是同一个值累积。
对于此函数还是好理解的,但是对于第二种通用柯里化其实比较难以理解。那么就大概解释下吧。
先解释两个概念,可能很多人会弄混乱。
1. 函数 a 执行之后返回一个函数 b,函数 b 中 的 arguments 是 调用函数 b 时候传入的参数。
2. Array.prototype.slice.call
是在类数组上调用数组的 slice 方法,将类数组变为真正的数组,例如调用函数是传入的 arguments 就是一个类数组,他是没有 slice 等数组的方法的,所以使用 Array.prototype.slice.call
来将其变为真正的数组。
给 1 举个例子,2 的例子上面的通用函数里面已经有了。
function a(){
return function (){
console.log(arguments);
}
}
var b = a();
此时,b 应该是一个函数.
function (){
console.log(arguments);
}
那么这个时候再调用b(1,2,3,4,5);
那 在刚才定义的那个函数中 arguments 应该是 ≈[1,2,3,4,5];
这个是很重要的一点,很多人这里会被弄混。
那接下来继续看通用柯里化函数。
当调用
const a = function (a, b, c) {
return a + b + c;
}
const result2 = curry(a);
时。此时 result2 应该是一个函数。
result2 = function curried() {
const args = Array.prototype.slice.call(arguments);
if (args.length >= len) {
return fn.apply(this, args);
}
return function () {
return curried.apply(this, args.concat(Array.prototype.slice.call(arguments)));
};
};
});
但是在这个函数中,却使用了闭包保留了函数 a 的参数,对于此例子来说,就是 a,b,c, 它的长度是 3.即,我调用 result2(3)
的时候,是能够访问到调用 curry(a)
时,a 传的参数(的个数)的。
那,当调用 result2(3)(4)(4);
时,是个什么情况呢?
先看第一步let result3 = result2(3);
此时会执行 curried() 这个函数。而执行此函数的时候,
const args = Array.prototype.slice.call(arguments);
中的 arguments 就是 3.所以 [3] 的长度小于刚才 a 的长度 3.那么就会继续返回一个函数。此时
result3 = function () {
return curried.apply(this, args.concat(Array.prototype.slice.call(arguments)));
}
在调用let result4 = result3(4);
和调用 result2(3) 是一样的结果,但是 由于每次都是通过 curried.apply 来调用 curried,并且每次调用 curried 的时候,都会
args.concat(Array.prototype.slice.call(arguments))
所以每次调用,curried 的参数都会增加。上面的例子,
const args = Array.prototype.slice.call(arguments);
中的 arguments 就应该是[3, 4]了。
所以当最后一次调用的时候 args.length >= len
的条件为真。所以就调用了 fn.apply(this, args)
等同于 fn(3,4,4)
最终就会返回相应的结果了。
此文章主要的灵感来自于:segmentfault ,使用了其中的函数代码并做修改,特此感谢。