函数科里化(Curring)

函数科里化

什么是函数科里化?

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

通常,我们是怎么接收参数的呢;
对于一个函数,正常人的想法,一次性传入所有的参数。
那如果不定参数呢。
JavaScript提供了arguments来存储参数。

1. 什么是arguments?

arguments 是一个对应于传递给函数的参数的类数组对象
MDN JavaScript arguments

对于每一个函数function都有一个实例化对象arguments,(该对象并不能显式创建,比如new一个arguments,时不可能成功的),arguments表示函数的参数详情,是一个类数组对象。
什么是类数组对象呢?

众所周知,数组是一个特殊的对象,其结构为对应下标的位置存储对应的内容,并且有一个length属性。

那么,对于键值对,也就是对象类型的数据结构,可以转化为数组吗?

答案是:当然可以。

如何转化,这里就提出了类数组对象,类数组对象就是对象,与数组的区别在于他的形式是对象形式,下标为属性名,属性值为对应下标的数组值,为什么“类”呢?因为类数组对象也有一个属性length,表示转化为数组后其长度。

实际上,具有length属性的对象都可以被转化为数组,但是只有属性值为数字的,且属性值数字小于length的才能被正确的转化为数组,否则其对应数组值为 empty;

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var fun=function(){
console.log(arguments)
}
fun(1,2,3,4)
/*
控制台打印:
Arguments(4) [1, 2, 3, 4, callee: ƒ, Symbol(Symbol.iterator): ƒ]
0: 1
1: 2
2: 3
3: 4
callee: ƒ ()
length: 4
Symbol(Symbol.iterator): ƒ values()
__proto__: Object
*/

仔细观察这些参数,再结合函数使用参数,想一想callee的作用~

2. 科里化实例

介绍完了arguments,那科里化到底是什么形式呢?

接下来看一个简单的函数例子;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var fun=function(){
return fun2=function(){
console.log(2)
}
}
fun;
//控制台打印:
// function(){
// return fun2=function(){
// console.log(2)
// }
// }
fun();
//控制台打印:
// fun2=function(){
// console.log(2)
// }
fun()();
//控制台打印:
// 2;

大家都知道,函数中如果存在return关键字,则返回一个东西。

return也可以返回一个函数,但是返回函数并不代表执行函数,正如上面执行fun;一样,那么执行一个fun()呢?显然相当于执行fun2;只有执行fun()()时,才相当于执行了一个fun2();

既然可以多次执行,那么也可以多次传入参数。

我们可以利用这个多次传参选择性隐式执行的特点,做些什么呢?

1. 参数复用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 正常正则验证字符串 reg.test(txt)

// 函数封装后
function check(reg, txt) {
return reg.test(txt)
}

check(/\d+/g, 'test') //false
check(/[a-z]+/g, 'test') //true

// Currying后
function curryingCheck(reg) {
return function(txt) {
return reg.test(txt)
}
}

var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)

hasNumber('test1') // true
hasNumber('testtest') // false
hasLetter('21212') // false

如果有很多地方都要校验是否有数字,其实就是需要将第一个参数reg进行复用,这样别的地方就能够直接调用hasNumber,hasLetter等函数,让参数能够复用,调用起来也更方便。

2. 提前确认
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
var on = function(element, event, handler) {
if (document.addEventListener) {
if (element && event && handler) {
element.addEventListener(event, handler, false);
}
} else {
if (element && event && handler) {
element.attachEvent('on' + event, handler);
}
}
}

var on = (function() {
if (document.addEventListener) {
return function(element, event, handler) {
if (element && event && handler) {
element.addEventListener(event, handler, false);
}
};
} else {
return function(element, event, handler) {
if (element && event && handler) {
element.attachEvent('on' + event, handler);
}
};
}
})();

//换一种写法可能比较好理解一点,上面就是把isSupport这个参数给先确定下来了
var on = function(isSupport, element, event, handler) {
isSupport = isSupport || document.addEventListener;
if (isSupport) {
return element.addEventListener(event, handler, false);
} else {
return element.attachEvent('on' + event, handler);
}
}

第二种写法,它相对一第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了会走哪一个方法,避免每次都进行判断

3. 延迟运行
1
2
3
4
5
6
7
8
Function.prototype.bind = function (context) {
var _this = this
var args = Array.prototype.slice.call(arguments, 1)

return function() {
return _this.apply(context, args)
}
}

bind实现的机制就是Currying.


关于科里化性能:

  1. 存取arguments对象通常要比存取命名参数要慢一点
  2. 一些老版本的浏览器在arguments.length的实现上是相当慢的
  3. 使用fn.apply( … ) 和 fn.call( … )通常比直接调用fn( … ) 稍微慢点
  4. 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上

科里化经典面试题:

实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

答案:(实现多样,好用即可~)

1
2
3
4
5
6
7
8
9
10
11
12
13
var add = function () {
var _args = Array.prototype.slice.call(arguments);
var _adder = function () {
_args.push(...arguments);
return _adder;
}
_adder.toString = function () {
return _args.reduce(function(a,b){
return a+b;
})
}
return _adder;
}

了解相关知识点:
Function.prototype.toString()
Array.prototype.slice.call()
Array.prototype.reduce()