# 高阶函数应用 ———— 柯里化与反柯里化

# 引子

一道经典面试题:

//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]