Jacleklm's Blog

JS函数柯里化

2019/11/30

定义

柯里化是指将一个函数分解为一系列函数的过程,每个函数都只接收一个参数
函数柯里化,英语:Currying,高阶函数的一个特殊用法。是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术

一个简单的例子:

1
2
3
4
5
6
7
8
9
10
function add(a, b) {
return a + b
}
add(1, 5) // 6
function curryingadd(x) {
return function(y) {
return x + y
}
}
curryadd(1)(5) // 6

柯里化的用途

参数复用

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

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

提前确认

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)
}
}

我们在做项目的过程中,封装一些 dom 操作可以说再常见不过,上面第一种写法也是比较常见,但是我们看看第二种写法,它相对一第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了会走哪一个方法,避免每次都进行判断

延迟运行

1
2
3
4
5
6
7
Function.prototype.bind = function(context) {
var _this = this
var args = [...arguments].slice(1)
return function() {
return _this.apply(context, args.concat(...arguments))
}
}

bind 实现的机制就是 Currying,虽然上面这个 bind 有点简单没有考虑所有情况

柯里化的封装

其实和之前写的 bind 的实现很像

1
2
3
4
5
6
7
8
9
// 初步封装
var currying = function(fn) {
// args 获取第一个方法内的全部参数
var args = [...arguments].slice(1)
return function() {
// 把合并后的参数通过apply作为fn的参数并执行
return fn.apply(this, args.concat(...arguments))
}
}

这样返回的话其实只能多扩展一个参数,currying(a)(b)(c)的情况无法支持,所以需要递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 支持多参数传递
function progressCurrying(fn, ...args) { // fn 是原本的接受很多个参数的复杂函数
var _this = this
var len = fn.length // fn.length === fn 原本参数个数
var args = args || []

return function() {
var _args = [...arguments]
args.push(...arguments)

// 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
if (_args.length < len) {
return progressCurrying.call(_this, fn, _args)
}

// 参数收集完毕,则执行fn
return fn.apply(this, args)
}
}

一道经典面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6
add(1, 2, 3)(4) = 10
add(1)(2)(3)(4)(5) = 15

function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
let _args = [...arguments]
// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
let _result = function() { // 这里不能用箭头函数
_args.push(...arguments)
return _result
}
// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
_result.toString = function() { // 这里不能用箭头函数
return _args.reduce((acc, curr) => acc + curr, 0)
}
return _result
}

add(1)(2)(3) // 6
add(1, 2, 3)(4) // 10
add(1)(2)(3)(4)(5) // 15
add(2, 6)(1) // 9

toString 或 valueOf 的改写

上面那道题的的 toString 的改写也是非常有意思的一个知识点

当变量对象(函数也是对象)遇到一些情况的时候,会偷偷地调用 toString 和 valueOf 方法(隐式调用),那我们就可以改写这两个方法做一些事

这些情况会触发隐式调用:

  • 加减乘除
  • 比较运算符>
  • ==
  • String()
  • Number()

两个都重写的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj = {
i: 10,
valueOf: function() {
console.log('执行了valueOf()')
return this.i + 20
},
toString: function() {
console.log('执行了toString()')
return this.valueOf() + 20
}
}
console.log(+obj) // 执行了valueOf() 30
console.log(obj > 40) // 执行了valueOf() false
console.log(obj == 30) // 执行了valueOf() true

只重写了 toString

1
2
3
4
5
6
7
8
9
10
11
12
var aa = {
i: 10,
toString: function() {
console.log('toString')
return this.i
}
}
console.log(+aa) // 10 toString
console.log('' + aa) // 10 toString
console.log(String(aa)) // 10 toString
console.log(Number(aa)) // 10 toString
console.log(aa == '10') // true toString

只重写了 valueOf

1
2
3
4
5
6
7
8
9
10
11
12
var aa = {
i: 10,
valueOf: function() {
console.log('valueOf')
return this.i
}
}
console.log(+aa) // 10 valueOf
console.log('' + aa) // 10 valueOf
console.log(String(aa)) // [object Object]
console.log(Number(aa)) // 10 valueOf
console.log(aa == '10') // true valueOf

结论:如果只重写了 toString,对象转换时会无视 valueOf 的存在来进行转换。但是,如果只重写了 valueOf 方法,在要转换为字符串的时候会优先考虑 valueOf 方法。在不能调用 toString 的情况下,只能让 valueOf 上阵了
当两者都重写的时候,优先 valueOf

结论二:所以上面那道面试题,是触发了隐式调用,偷偷地执行了重写的toString方法。虽然还是没懂是怎么触发隐式调用的好像都不是我所知的那几种情况0.0

参考资料
张鑫旭-JS 中的柯里
详解 JS 函数柯里化
JavaScript 中 valueOf、toString 的隐式调用
JavaScript 中 valueOf 函数与 toString 方法深入理解

CATALOG
  1. 1. 定义
  2. 2. 柯里化的用途
    1. 2.1. 参数复用
    2. 2.2. 提前确认
    3. 2.3. 延迟运行
  3. 3. 柯里化的封装
  4. 4. 一道经典面试题
    1. 4.1. toString 或 valueOf 的改写