函数柯里化详解

什么是函数柯里化

柯里化(Currying)又称部分求值,一个柯里化的函数首先会接收一些参数,接收了这些参数后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)或者f(a, b)(c)或者f(a)(b, c)

通俗的来说:固定部分参数,返回一个接受剩余参数的函数,也称为部分计算函数,目的是为了缩小适用范围,创建一个针对性更强的函数。核心思想是把多参数传入的函数拆成一个个的单参数(或部分)函数,内部再返回调用下一个单参数(或部分)函数,依次处理剩余的参数。有点类似俄罗斯套娃,一层包一层

备注:柯里化不会调用函数。它只是对函数进行转换。

函数柯里化示例

我们先用以下的例子来解释普通函数和柯里化之后的函数有什么区别:

比如,我们要实现一个日志打印的功能,日志打印,一般输出格式是时间+项目名+信息,如2022-07-29 xxx后台管理系统 mm接口异常,要实现这个功能,分别需要时间、项目名、信息三个参数。接下来我们分别用普通函数的写法和柯里化函数的写法,来实现这一功能

  1. 普通函数:
    普通函数就是三个参数:date, project, message

    // 参数:date, project, message
    const log = (date, project, message) => {
          
          
      return `${
            
            date} ${
            
            project} ${
            
            message}`
    }
    
    const logMsg = log('2022-07-29', 'xxx后台管理系统', 'mm接口异常');
    console.log(logMsg) // 输出 2022-07-29 xxx后台管理系统 mm接口异常
    
  2. 函数柯里化
    通常来说,输出日志,一般当天日期是不变的,同一个项目的项目名也是不变的(不过不同的项目名是变化的),唯有信息是时刻变化,那么我们可以将日期这个参数存储起来,这样就不用每次都传日期;当遇到同一个项目的时候,也可以把项目名这个参数存起来,不用每次都传项目名;接下来调用只需要传信息这个参数即可

如下,我们将函数拆成了一个个的单参数函数,再通过俄罗斯套娃的形式返回出去,这样我们就不用每次调用函数的时候都去反复传递这些相同的参数了,而且还能依据不同情况下,组合出不同的参数

现在,sameDateLog是具有固定date参数的函数,sameDateProjectNameLog是具有固定date、projectName参数的函数,换句话说,sameDateLog、sameDateProjectNameLog都是更简短的“偏应用函数(partially applied function)”或“偏函数(partial)”。

const log = (date) => {
    
    
  
  return (projectName) => {
    
    
    
    return (message) => {
    
    

      return `${
      
      date} ${
      
      projectName} ${
      
      message}`

    }
  
  }

}


/* 如果日期、项目名、信息都不同的情况下输出日志 */
// 日期为“2022-07-29”,项目名为“A项目”,输出日志
const logMsg1 = log('2022-07-29')('A项目')('接口报错');
console.log(logMsg1); // 打印 2022-07-29 A项目 接口报错
// 日期为“2022-07-29”,项目名为“A项目”,输出日志
const logMsg2 = log('2022-08-01')('B项目')('接口成功');
console.log(logMsg2); // 打印 2022-08-01 B项目 接口成功


/* 如果日期相同,项目名、信息不同的情况下输出日志 */
const sameDateLog = log('2022-07-29');
// 项目名为“A项目”,输出日志
const logMsg3 = sameDateLog('A项目')('接口异常');
console.log(logMsg3); // 打印 2022-07-29 B项目 接口异常
// 项目名为“B项目”,输出日志
const logMsg4 = sameDateLog('B项目')('接口超时');
console.log(logMsg4); // 打印 2022-07-29 B项目 接口超时


/* 如果日期、项目名相同,信息不同的情况下输出日志 */
const sameDateProjectNameLog = log('2022-07-29')('A项目');
// 输出日志
const logMsg5 = sameDateProjectNameLog('网络异常')
console.log(logMsg5); // 打印 2022-07-29 A项目 网络异常

实现一个函数,将普通函数柯里化

// 函数柯里化,利用递归和闭包实现
const curry = function(fn) {
    
    
  const len = fn.length; // 获取初始函数fn的形参个数
  
  // curry返回改造后的函数
  return function t() {
    
    
    const innerLength = arguments.length; // 获取t的实参个数
    const args = Array.prototype.slice.call(arguments); // 将类数组arguments对象转为真正的数组(类数组arguments对象是函数传入的实际参数,类似数组,拥有数组属性,但不是数组)
      
    if (innerLength >= len) {
    
     // 递归出口,如果t实参个数已经大于fn形参个数,则终止递归
      return fn.apply(undefined, args) // 执行改造后的函数

    } else {
    
     // 如果t的实参个数少于fn的形参个数,说明柯里化并没有完成,则继续执行柯里化
      return function () {
    
    
        const innerArgs = Array.prototype.slice.call(arguments); // 将类数组arguments对象转为真正的数组(类数组arguments对象是函数传入的实际参数,类似数组,拥有数组属性,但不是数组)
        const allArgs = args.concat(innerArgs);
        return t.apply(undefined, allArgs)
      }
    }
  }
}

// 测试
function add (num1, num2, num3, num4, num5) {
    
    
  return num1 + num2 + num3 + num4 + num5;
}


const finalFun = curry(add);
const result1 = finalFun(1)(2)(3)(4)(5);
const result2 = finalFun(1, 2)(3)(4)(5);
const result3 = finalFun(1,2,3)(4)(5);
const result4 = finalFun(1,2,3)(4, 5);

console.log(result1, result2, result3, result4); // 15 15 15 15

备注:在使用 apply 递归调用的时候,默认传入 undefined, 在其它场景下,可能需要传入 context, 绑定指定环境
实际开发,推荐使用 lodash.curry , 具体实现,可以参考下curry源码

经典面试题

请实现一个add函数实现以下功能 :
add(1) // 1
add(1)(2) // 3
add(1)(2)(3) // 6
add(1)(2)(3)(4) // 10
add(1)(2,3) // 6
add(1,2)(3) // 6
add(1,2,3) // 6

函数柯里化应用

  1. 参数复用:即如果函数有重复使用到的参数,可以利用柯里化,将复用的参数存储起来,不需要每次都传相同的参数
  2. 延迟执行:传入参数个数没有满足原函数入参个数,都不会立即返回结果,而是返回一个函数。(bind方法就是柯里化的一个示例)
  3. 函数式编程中,作为compose, functor, monad 等实现的基础

bind方法是函数柯里化应用的经典场景

(function(){
    
    
//context就是传入的obj用来改变this指向的,如果没有就默认写的是window
function myBind(context=window,...outerArgs){
    
        
  let _this = this;
  return function(...innerArgs){
    
    
    _this.call(context,...innerArgs.concat(outerArgs))
  }
}
Function.prototype.myBind = myBind;
})()

let obj = {
    
    
  name:'OBJ'
}
document.body.onclick = fn.myBind(obj,100,200)

函数柯里化的优缺点

优点:

  1. 柯里化之后,我们没有丢失任何参数:log 依然可以被正常调用。
  2. 我们可以轻松地生成偏函数,例如用于生成今天的日志的偏函数。
  3. 入口单一。
  4. 易于测试和复用。

缺点:

  1. 函数嵌套多
  2. 占内存,有可能导致内存泄漏(因为本质是配合闭包实现的)
  3. 效率差(因为使用递归)
  4. 变量存取慢,访问性很差(因为使用了arguments)

参考

https://segmentfault.com/a/1190000018265172

猜你喜欢

转载自blog.csdn.net/Boale_H/article/details/126058783