函数柯里化
参考博客:https://www.jianshu.com/p/2975c25e4d71
# 柯里化是什么?
维基百科上说道:柯里化,英语:Currying(果然是满满的英译中的既视感),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
// 多参数输入
function add(x, y) {
return x + y
}
// Currying后
function curryingAdd(x) {
return function (y) {
return x + y
}
}
// 区别在于输入方式的改变:(1,2) -> (1)(2)
add(1, 2) // 3
curryingAdd(1)(2) // 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 使用场景
# 1.函数复用
// 看下面例子:输入的第一个参数都是相同的
let fn1 = function(a,b)
let fn1 = function(a,c)
let fn1 = function(a,d)
// 使用柯里化可以实现
let fn2 = function(a) // 构造一个具有公共参数a的函数,此后所有的函数都是基于此调用
fn2(b) // 再调用参数
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
点击查看
// 正常正则验证字符串 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 2. 提前确认
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);
}
}
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
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
我们在做项目的过程中,封装一些dom操作可以说再常见不过,上面第一种写法也是比较常见,但是我们看看第二种写法,它相对一第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了会走哪一个方法,避免每次都进行判断。
# 3.bind函数的实现机制就是Currying
改变this
指向的函数:
fn.call(this,arg1,arg2); // 立即
fn.apply(this,[arg1,arg2]);// 立即
fn.bind(this,arg1,arg2);// 绑定,使用闭包绑定传入的参数(args)以及指针(_this)
1
2
3
2
3
⭐️完整案例:细看细看
Function.prototype.newbind = function (newEnv) {
let _this = this
console.log('this',this) // fn [调用对象的环境,这里是fn(在windows中),是指调用了newbind的对象]
console.log('newEnv',newEnv) //o [newEnv是新的环境]
let args = Array.prototype.slice.call(arguments, 1)
console.log('args',args) // newbind函数的多余实参
return function() {
return _this.apply(newEnv, args)
// 在本例中等价于fn.apply(o,[1,2]),通过闭包的方式,获取到了
// 1. 用 _this 指代了 fn
// 2. 将arg1,arg2 转为了 [arg1,arg2]
// 3. 新环境o,即形参中的newEnv
// 由于fn是调用新增原生方法newbind的函数名(指针),但是由于是在原型链上的新增,用_this指代调用的对象fn
}
}
// 新的环境
let o = {
name: 'andy'
}
function fn(a,b){
console.log(a + b);
}
let f = fn.newbind(o,1,2); // 由于是闭包不执行
f();
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
其中Currying
的过程:fn.bind(newEnv,arg1,arg2)
_this.apply(newEnv,[arg1,arg2,...])
上面封装有一个缺陷,就是Fuction.prototype.bind
?需要改进一下:
// 初步封装
var currying = function(fn) {
var args = Array.prototype.slice.call(arguments, 1) // arguments取决于currying中输入的实参
console.log(args)
return function() {
// 闭包,可以实现链式调用
console.log(arguments)
let newArgs = [...args,...Array.prototype.slice.call(arguments)] // 第一次接的实参,和调用后传入的实参进行拼接
console.log(newArgs)
return fn.apply(this, newArgs) // 通过this将newArgs环境给导出
}
}
let fn = function(){
var sum = 0;
for(let i = 0; i<arguments.length;i++){
sum += arguments[i]
console.log(sum)
}
console.log('final',sum)
}
currying(fn,1,2,3)(2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
使用递归实现多参数传递,例如currying(a)(b)(c)
function progressCurrying(fn,args){
let _this = this // 缓存当前的函数环境
let len = fn.length; // 计算当前有几个括号,如fn()()()有三个()?
let args = args || [];
return function(){
let _args = Array.prototype.slice.call(arguments); // call传入缓存arguments的空间指针
let args = [args,..._args];
if(_args.length < len) {
return progressCurrying.call(_this,fn,_args);
}
// 参数收集完毕,则执行fn
return fn.apply(this,_args);// 此时_args已经将(a)(b)(c)转换为(a,b,c)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
这边其实是在初步的基础上,加上了递归的调用,只要参数个数小于最初的fn.length,就会继续执行递归。
# 性能分析
Curry
的一些性能问题你只要知道下面四点就差不多了:
- 存取
arguments
对象通常要比存取命名参数要慢一点 - 一些老版本的浏览器在
arguments.length
的实现上是相当慢的 - 使用
fn.apply( … )
和fn.call( … )
通常比直接调用fn( … )
稍微慢点 - 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上
其实在大部分应用中,主要的性能瓶颈是在操作DOM节点上,这js的性能损耗基本是可以忽略不计的,所以curry是可以直接放心的使用。
# 经典面试题
// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数(只会执行一次)
var _args = Array.prototype.slice.call(arguments);
// 从第二个参数,开始封装一个内部函数,可以实现链式调用,例如fn(1)(2)(3)
var _adder = function() {
let _args = [..._args,...arguments];
return _adder;
};
// 最后一步,使用reduce实现**递归相加**
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
});
}
return _adder;
}
add(1)(2)(3) // 6
add(1, 2, 3)(4) // 10
add(1)(2)(3)(4)(5) // 15
add(2, 6)(1) // 9
// 如果要在node中看,可以将上述代码改写为console.log(add(1)(2)(3).toString())
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
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
编辑 (opens new window)
上次更新: 2022/04/06, 15:04:00