JavaScript | 函数柯里化

275 阅读6分钟

函数柯里化是什么?

首先来看下维基百科上对柯里化的定义:

计算机科学中,柯里化(英语:Currying),又译为卡瑞化加里化,是把接受多个参数函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。——维基百科_柯里化

听上去比较抽象,下面来举一个例子,方便大家理解:

  • 首先定义一个函数,作用是返回传入参数的和。
// 首先定义一个函数
function sum(a, b, c) {
  return a + b + c;
}

let result = sum(1, 2, 3)

console.log(result) // 6
  • 下面我们来看该函数柯里化后的代码。
function sum(a) {
  return function(b) {
    return function (c) {
      return a + b +c
    }
  }
}

let result2 = sum(1)(2)(3)

console.log('sum-result::',result) // sum-result::6

来解析下上面的代码,在柯里化后,sum函数出现了下面的几个特征:

  1. 函数作为返回值,内部的a、b、c变量因为访问了外部作用域的变量,形成了闭包
  2. 在调用的时,如果该函数的参数的数量不满足原函数的参数数量,则内部会向外返回一个函数,该函数可以接受并处理剩余的参数。
  3. 当参数数量和原函数一致的时候,触发后的结果和原有函数一致。

所以可以根据维基百科给出的定义,以及上文中的代码,我们做一个小结:

  • 柯里化是将一种函数转换为另一种函数的技术。
  • 原有函数为多个参数的函数,在柯里化后,该函数会转换为能够接收并处理剩余参数的新函数。

函数柯里化的特性

当使用函数柯里化时,参数的复用是一个非常有用的特性。 通过函数柯里化,可将一个函数拆分成多个部分,每个部分接受一个参数,并返回一个新的函数,这样就可以在不同的上下文中重复使用这些参数。

函数柯里化的应用

上文可知函数柯里化的显著特性就是可以实现参数的复用,那么这种特性如何运用于日常开发中呢? 可以这样整理思路:

  • 比如为在求值的过程中,如果需要多次调用相同的函数,但是每次调用都需要传入部分相同的参数,这样写,代码看上去是否冗余会有些冗余呢?
  • 那么为什么不能直接固定部分参数,减少后续求值的中所需要写的重复代码。

带着这个思路,下面举个例子,方便大家理解:

例:日志记录

比如在开发过程中,我们难免会遇到需要记录日志的例子:

function log(date, importance, message) {
  console.log(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`);
}

log(new Date('2024/01/01 15:30:00'), "INFO", "test")
log(new Date('2024/01/01 15:30:00'), "DANGER", "danger test")
log(new Date('2024/01/01 15:30:00'), "WARNING", "warning test")

可以看到如果我们需要记录日志,假设每次都需要传入同样的日期,而后面参数的importancemessage需要经常变动,那么为何不可以固定这里的第一个参数呢? 这里我们将该函数柯里化(使用lodash内置的柯里化函数API),看看具体的效果吧!

function log(date, importance, message) {
  console.log(
    `[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`
  )
}
let curryLog = _.curry(log)
// log(new Date('2024/01/01 15:30:00'), 'INFO', 'test')
// log(new Date('2024/01/01 15:30:00'), 'DANGER', 'danger test')
// log(new Date('2024/01/01 15:30:00'), 'WARNING', 'warning test')
// 固定时间
curryLog(new Date('2024/01/01 15:30:00'))('INFO')('test')
// 同样也可以把importance(重要等级)固定下来封装为一个新的函数
const info = curryLog(new Date('2024/01/01 15:30:00'))('INFO')
info('test2')

运行结果:

image.png

柯里化的实现

在了解完成柯里化的应用后,现在来看看柯里化是怎么实现的吧! 下面是柯里化函数的简易实现代码:

function curry(func) {
  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args)
    } else {
      return function (...args2) {
        return curried.apply(this, [...args, ...args2])
      }
    }
  }
}

实现思路:

  1. 首先定义一个函数,参数为需要柯里化的函数。
  2. 返回curried函数,参数为...args
  3. 判断该函数传入的参数的长度是否大于该函数的期望的参数数量
  4. 当该函数传入的参数的长度大于该函数的期望的参数数量的时候,返回该函数自身的拷贝。
  5. 如果不满足函数传入的参数的长度大于该函数的期望的参数数量,则返回一个新的匿名函数,此时创建args2用于保存后续传入的参数,然后将之前传入的参数args和新传入的参数args2做一个合并,变成一个新的数组,后返回curried的函数拷贝。
  6. 回到第3步。

测试:

function log(date, importance, message) {
  console.log(
    `[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`
  )
}
function curry(func) {
  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args)
    } else {
      return function (...args2) {
        return curried.apply(this, [...args, ...args2])
      }
    }
  }
}

let curryLog = curry(log)
// 固定时间
curryLog(new Date('2024/01/01 15:30:00'))('INFO')('test')
// 同样也可以把importance固定下来封装为一个新的函数
const info = curryLog(new Date('2024/01/01 15:30:00'))('INFO')
info('test2')

测试结果:

image.png

柯里化的缺点

  • 因为依赖闭包以及查找作用域链的特性,所以会造成额外的开销,进而影响代码性能。

总结

函数柯里化的作用:

  1. 参数的复用:将一个函数拆分成多个部分,每个部分接受一个参数,并返回一个新的函数,这样就可以在不同的上下文中重复使用这些参数,实现参数的复用和灵活的代码组合。
  2. 简化函数调用:帮助我们简化函数的调用方式,提高代码的可读性和可维护性。

函数柯里化的缺点:

  1. 性能损耗:因为依赖闭包以及查找作用域链的特性,所以会造成额外的开销,进而影响代码性能。

参考文献