# 高阶函数应用 ———— 柯里化与反柯里化
# 引子
一道经典面试题:
//JS实现一个无限累加的add函数
add(1) //1
add(1)(2) //3
add(1)(2)(3) //6
当大家看到这个面试题的时候,能否在第一时间想到使用高阶函数实现?
在 JavaScript 中,柯里化和反柯里化是高阶函数的一种应用。
高阶函数的定义,通俗的说,函数可以作为参数传递到函数中,这个作为参数的函数叫回调函数,而拥有这个参数的函数就是高阶函数,回调函数在高阶函数中调用并传递相应的参数,在高阶函数执行时,由于回调函数的内部逻辑不同,高阶函数的执行结果也不同,非常灵活,也被叫做函数式编程。
# 柯里化
在 JavaScript 中,函数柯里化是函数式编程的重要思想,也是高阶函数中一个重要的应用,其含义是给函数分步传递参数,每次传递部分参数,并返回一个更具体的函数接收剩下的参数,这中间可嵌套多层这样的接收部分参数的函数,直至返回最后结果。
# 1、最基本的柯里化拆分
// 原函数
function add(a, b, c) {
return a + b + c;
}
// 柯里化函数
function addCurrying(a) {
return function (b) {
return function (c) {
return a + b + c;
}
}
}
// 调用原函数
add(1, 2, 3); // 6
// 调用柯里化函数
addCurrying(1)(2)(3) // 6
被柯里化的函数 addCurrying 每次的返回值都为一个函数,并使用下一个参数作为形参,直到三个参数都被传入后,返回的最后一个函数内部执行求和操作,其实是充分的利用了闭包的特性来实现的。
# 2、柯里化通用式
上面的柯里化函数没涉及到高阶函数,也不具备通用性,无法转换形参个数任意或未知的函数,我们接下来封装一个通用的柯里化转换函数,可以将任意函数转换成柯里化。
// add的参数不固定,看有几个数字累计相加
function add (a, b, c, d) {
return a+b+c+d
}
function currying (fn, ... args) {
// fn 要封装成柯里化的回调函数
// args 上一次传入的参数
// fn. length 回调函数的参数的总和
// args. lengtth curring函数 后面的参数总和
if (fn. length === args. length) { // 如:currying(add, 1, 2, 3, 4)
return fn(...args)
} else {
// 继续分步传递参数 newArgs 新一次传递的参数
return function(...newArgs) {
// 将先传递的参数和后传递的参数 结合在一起
let allArgs = [...args, ...newArgs]
return currying(fn, ...allArgs)
}
}
}
let fn1 = currying(add, 1, 2) // 3
let fn2 = fn1(3) // 6
let fn3 = fn2(4) // 10
函数 currying 算是比较高级的转换柯里化的通用式,可以随意拆分参数,假设一个被转换的函数有多个形参,我们可以在任意环节传入任意个数的参数进行拆分,举一个例子,假如 5 个参数,第一次可以传入 2 个,第二次可以传入 1 个, 第三次可以传入剩下的,也有其他的多种传参和拆分方案,因为在 currying 内部收集参数的同时按照被转换函数的形参顺序进行了更正.
# 柯里化的优势
# 1、参数复用
// 正常正则验证字符串 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函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数reg进行复用,这样别的地方就能够直接调用hasNumber,hasLetter等函数,让参数能够复用,调用起来也更方便。
# 2、bind的延迟运行(预处理函数)
Function. prototype. bind = function (context) {
var _this = this
var args = Array.prototype.slice.call(arguments, 1)
return function() {
return _this.apply(context, args)
}
}
像我们js中经常使用的bind,实现的机制就是Currying.
# 反柯里化
反柯里化通用式的参数为一个希望可以被其他对象调用的方法或函数,通过调用通用式返回一个函数,这个函数的第一个参数为要执行方法的对象,后面的参数为执行这个方法时需要传递的参数。
# 1、反柯里化通用式 ES5
function unCurrying(fn) {
return function () {
let obj = []. shift. call(arguments) // shift() 方法从数组中删除第一个元素,并返回该元素的值。改变原数组
return fn. apply(obj, arguments)
}
}
let toString = unCurrying(Object. prototype. toString)
console. log(toString('hello')) // [object String]
TIP