用柯里化实现一个累加器

先看一道面试题, 如何实现下面这个函数?

1
2
3
4
add(1);// 1
add(1, 2);// 3
add(1)(2);// 3
add(1, 2, 3)(4, 10);// 20

先是 ES6 版本的答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const add = (...args) => {
const calculate = (arr) => {
return arr.length === 0 ? 0 : arr.length === 1 ? arr[0] : arr.reduce((ac, cv) => ac + cv);
}

let result = calculate(args);
const func = (...args) => {
result += calculate(args);
return func;
}

func.toString = func.valueOf = () => result;

return func;
}

测试

1
add(1, 2, 3)(1)()(3); //10

ES5 的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function add() {
function calculate() {
var arr = [].slice.call(arguments);
switch(arr.length) {
case 0:
return 0;
case 1:
return arr[0];
default:
return arr.reduce(function(ac, cv) {
return ac + cv;
})
}
}

var result = calculate.apply(null, arguments);
function func() {
result += calculate.apply(null, arguments);
return func;
}

func.toString = func.valueOf = function() {
return result;
}

return func;
}

测试

1
add(1, 2, 3)(1)()(3); //10

这道题和柯里化有什么关系呢?

把上面的函数简化一下就可以看出来了:

1
2
3
4
5
6
7
8
9
10
11
const add_currying = (num) => {
let result = num;
const func = (num) => {
result += num;
return func;
}

func.toString = func.valueOf = () => result;

return func;
}

测试

1
add_currying(1)(2); //3

在 JavaScript 中构造一个柯里化函数的关键

先看维基百科中, 对柯里化的定义:

在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
即:

  1. 需要保持事先传入的参数, 或者第一次传入的参数, 并且返回一个由这个参数构成的新的函数
  2. 接下来的计算都通过第 0 步返回的函数执行

在 JavaScript 中即为:

  1. 利用闭包, 将第一次传入的参数保持住
  2. 返回一个与第一次传入的参数一起构成的新函数

在实现的技巧上, 用到了 Function.toString() 方法和 Object.valueOf() 方法, 其中 func.toString 是当这个函数需要在 console 面板中显示时调用的方法,
func.valueOf 是在需要当做值进行传递时调用的方法, 而柯里化本身, 并与此无关, 正因为返回值是函数, 才能称作是柯里化。

可以对 add_currying 或者 add 的值进行 typeof

1
2
typeof add_currying(1)(2);// "function"
typeof add(1, 2, 3)(1, 2);// "function"

在实现的过程中, 写了一个错误的答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function add() {
function calculate() {
var arr = [].slice.call(arguments);
switch(arr.length) {
case 0:
return 0;
case 1:
return arr[0];
default:
return arr.reduce(function(ac, cv) {
return ac + cv;
})
}
}

var result = calculate(arguments);
function func() {
result += calculate(arguments);
return func;
}

func.toString = func.valueOf = function() {
return result;
}

return func;
}

测试

1
add(1, 2, 3)(1)()(3); //6[object Arguments][object Arguments][object Arguments]

错在哪里了呢?calculate(arguments)的传参上, argumentsarray-like, 而不是 array, 直接当做实参传进去的时候, 会被调用 toString() 方法, 形参得到的就是 '[object Arguments]', 而不是一个 arguments 或者 Array 了。而在 JavaScript 中, 能直接接受 arguments 当做参数的, 只有 apply, 所以正确的传值方式是 calculate.apply(null, arguments)