javaScript之函数柯里化

什么是柯里化

在数学和计算机科学中,柯里化是一种将使用多个参数的函数转换成一系列使用一个参数的函数的技术。
核心思想:

用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数

1
2
3
4
5
6
7
8
9
function add(a, b) {
return a + b;
}
// 执行add函数,一次传入两个参数即可
add(1, 2); // 3

// 假设有一个 curry 函数可以做到柯里化
var addCurry = curry(add);
addCurry(1)(2) //3

用途

柯里化的实际意义是什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 // 示意而已
function ajax(type, url, data) {
var xhr = new XMLHttpRequest();
xhr.open(type, url, true);
xhr.send(data);
}

// 虽然 ajax 这个函数非常通用,但是在重复调用的时候参数冗余
ajax('POST', "www.test.com", "name=kevin")
ajax('POST', "www.test2.com", "name=kevin")

//利用 curry
var ajaxCurry = curry(ajax);

// 以 POST 类型请求数据
var post = ajaxCurry("POST");
post('www.test.com',"name=kevin");

// 以 POST 类型请求来自于 www.test.com的数据
var postFromTest = post('www.test.com');
postFromTest('name=kevin');

目前的感觉,只是把传递参数的次数增加了,好像并没有什么实际的意义,但是如果我们是吧柯里化后的函数比如map?
比如说我们获得了这么一段数据:

1
var person = [{name: 'kevin'}, {name: 'daisy'}]

如果我们要获取所有的name值,可以这么做:

1
2
3
var name = person.map(function(item) {
return item.name;
})

当用上了curry函数:

1
2
3
4
var prop = curry(function(key, obj) {
return obj[key]
})
var name = person.map(prop('name'))

这里为了获得name属性还要再编写一个prop函数,会不会自添麻烦,但是可以注意到,prop函数编写一次后,以后可以多次使用,代码的可读性也提高了。

第一版

柯里化的应用在未来会很常见,现在我们就来编写这个curry函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function curry(fn){
var args = [].slice.call(arguments, 1);
return function() {
var newArgs = args.concat([].slice.call(arguments));
return fn.apply(this, newArgs);
}
}

var addCurry = curry(add, 1, 2);
console.log(addCurry())

var addCurry2 = curry(add, 1);
console.log(addCurry2(2));

var addCurry3 = curry(add)
console.log(addCurry3(1, 2))

已经有点柯里化的感觉了,但是还没有达到要求,不够我们可以把这个函数用作辅助函数,帮助我们写出一个真正的curry函数

第二版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function sub_curry(fn) {
var args = [].slice.call(arguments, 1);
return function() {
return fn.apply(this, args.concat([].slice.call(arguments)));
};
}

function curry(fn, length) {
//fn.length是fn的参数长度
length = length || fn.length;
var slice = Array.prototype.slice;

return function() {
//通过判断参数的长度来决定是否退出递归
if(arguments.length < length) {
var combined = [fn].concat(slice.call(arguments));
// length-arguments.length 来判断是否有二次参数传递
return curry(sub_curry.apply(this, combined), length - arguments.length);
}else {
return fn.apply(this, arguments);
}
};
}

使用一下我们编写的函数

1
2
3
4
5
6
7
8
9
10
11
var fn = curry(function(a, b, c) {
return [a, b, c];
});
//不递归
fn("a", "b", "c") // ["a", "b", "c"]
// 递归1次
fn("a", "b")("c") // ["a", "b", "c"]
//递归2次
fn("a")("b")("c") // ["a", "b", "c"]
//递归1次
fn("a")("b", "c") // ["a", "b", "c"]

似乎已经达到了我们的预期,然而这个curry函数的实现有点难以理解啊…

更易懂的实现

如果你没法理解的话,试试下面的方式,也能实现同样的效果:

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
28
29
30
function curry(fn, args) {
var length = fn.length;

args = args || [];

return function() {
var _args = args.slice(0),
arg, i;
for(i = 0; i < arguments.length; i++) {
arg = arguments[i];
_args.push(arg);
}
//如果传递的参数个数小于fn函数的参数个数就意味着需要curry化
if(_args.length < length) {
return curry.call(this, fn, _args);
}
else {
return fn.apply(this, _args);
}
}
}

var fn = curry(function(a, b, c) {
console.log([a, b, c]);
});

fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]

第三版的代码

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
function curry(fn, args, holes) {
length = fn.length;

args = args || [];

holes = holes || [];

return function() {

var _args = args.slice(0),
_holes = holes.slice(0),
argsLen = args.length,
holesLen = holes.length,
arg, i, index = 0;

for (i = 0; i < arguments.length; i++) {
arg = arguments[i];
// 处理类似 fn(1, _, _, 4)(_, 3) 这种情况,index 需要指向 holes 正确的下标
if (arg === _ && holesLen) {
index++
if (index > holesLen) {
_args.push(arg);
_holes.push(argsLen - 1 + index - holesLen)
}
}
// 处理类似 fn(1)(_) 这种情况
else if (arg === _) {
_args.push(arg);
_holes.push(argsLen + i);
}
// 处理类似 fn(_, 2)(1) 这种情况
else if (holesLen) {
// fn(_, 2)(_, 3)
if (index >= holesLen) {
_args.push(arg);
}
// fn(_, 2)(1) 用参数 1 替换占位符
else {
_args.splice(_holes[index], 1, arg);
_holes.splice(index, 1)
}
}
else {
_args.push(arg);
}

}
if (_holes.length || _args.length < length) {
return curry.call(this, fn, _args, _holes);
}
else {
return fn.apply(this, _args);
}
}
}
var _ = {};

var fn = curry(function(a, b, c, d, e) {
console.log([a, b, c, d, e]);
});

// 验证 输出全部都是 [1, 2, 3, 4, 5]
fn(1, 2, 3, 4, 5);
fn(_, 2, 3, 4, 5)(1);
fn(1, _, 3, 4, 5)(2);
fn(1, _, 3)(_, 4)(2)(5);
fn(1, _, _, 4)(_, 3)(2)(5);
fn(_, 2)(_, _, 4)(1)(3)(5)