面试常考的函数柯里化 curry 到底怎么写是最优解

640 阅读4分钟

前言

函数柯里化(Currying)是函数式编程中的一个重要概念,它将一个接受多个参数的函数转换成一系列接受单一参数的函数。在面试中面试官经常使用这个题目来考察面试者的函数式编程理解、代码抽象能力、闭包和作用域理解这几方面的能力。想要写好函数柯里化,那么我们需要知道什么是柯里化。

柯里化是什么

柯里化是一种关于函数的高阶技术。它不仅用于 JS,还被用于其他编程语言。它是一种函数的转换-->将一个函数从可调用的 f(a, b, c)转换为可调用的f(a)(b)(c)它不会调用我们的函数,它只是对函数进行转换。 下面来看一个例子:

function curry(f) {
  return function (a) {
    return function (b) {
      return f(a, b)
    }
  }
}

function sum(a, b) {
  return a + b
}
const curriedSum = curry(sum)
console.log(curriedSum(1)(2)) // 3

熟悉闭包的同学或许一眼就看出来,这里利用了闭包的特性来实现了一个基础的柯里化函数。但不熟悉的同学可能会问:为什么最内层的函数可以使用到外层函数的参数。接下来我简单讲下闭包的特性,熟悉的同学跳过即可。

  • 闭包:一个函数内部定义的函数可以访问其外部函数的变量。

也就是说,当一个函数在其内部定义另一个函数时,内部函数会形成一个闭包。这个闭包使得内部函数可以记住并访问其外部函数的作用域中的变量,即使外部函数已经执行完毕。拿下面这个例子来说:内层函数 a、b 都可以访问外部函数的全部变量

function curry(f) {
  return function (c) {
    function a(a) {
      console.log(f) // 访问外部函数的变量 f
      console.log(c) // 访问外部函数的变量 c
    }
    a(1) // 调用内部函数
    function b(a) {
      console.log(f) // 访问外部函数的变量 f
      console.log(c) // 访问外部函数的变量 c
    }
    b(2)
  }
}

柯里化的好处?目的?

了解完什么是柯里化,那它又有什么用处呢?接下来我们使用一个例子来理解柯里化的好处。

现在我们有一个用于格式化和输出信息的log函数log(date, importance, msg)。在开发中,此类函数具有很多有用的函数,例如通过网络发送日志(log)

function log(date, importance, msg) {
  console.log(`${date} ${importance}: ${msg}`)
}

// 柯里化
const log = curry(log)

// 柯里化后的log函数可以正常使用
log(new Date(), 'info', 'hello') // 2023-01-16 info: hello

// 但也可以以柯里化后形式运行
log(new Date())('DEBUG')('hello') // 2023-01-16 info: hello

// 为当前日志创建便捷函数
let logNow = log(new Date())
logNow('info')('hello') // 2023-01-16 info: hello

现在,logNow是具有固定第一个参数的函数,也就是更简短的部分应用函数(partially applied function)部分函数(partial)

接下来我们更进一步将 logNow 转化为更将便捷的函数

const debugNow = logNow('DEBUG')
debugNow('hello') // 2023-01-16 DEBUG: hello

可以看到,我们从最开始要传递三个参数,使用了柯里化之后,最终只需要传递一个参数即可,极大的简化了我们的代码。

最佳实践

相信同学们看完最开始的例子就会发现有一个问题:一个参数需要返回一个函数,两个参数的话还需要返回一个函数,以此类推。这就显得我们的函数非常的臃肿,那有没有什么方法可以传递多个参数的同时还不会造成函数的臃肿呢?那肯定有的,接下来我们来看看如何实现

// apply version
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args)
    } else {
      return function (...args2) {
        return curried.apply(this, args.concat(args2))
      }
    }
  }
}

// or --- bind and apply version
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args)
    } else {
      return curried.bind(this, ...args)
    }
  }
}

// or --- no apply/bind version
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn(...args)
    }
    return (...args2) => curried(...args, ...args2)
  }
}

// e.g.
function sum(a, b, c) {
  return a + b + c
}

const curriedSum = curry(sum)

console.log(curriedSum(1)(2)(3)) // 6
console.log(curriedSum(1)(2, 3)) // 6
console.log(curriedSum(1, 2)(3)) // 6
console.log(curriedSum(1, 2, 3)) // 6

相信大家看到这个实现方式会觉得有点复杂,但是它比较容易理解。

curry(fn)嗲用的记过是如下所示的包装器 curried

function curried(...args) {
  if (args.length >= fn.length) {
    // (1)
    return fn.apply(this, args)
  } else {
    return function (...args2) {
      // (2)
      return curried.apply(this, args.concat(args2))
    }
  }
}

当我们运行它时,这里有两个if执行分支: 1、如果传入的args长度与原始函数所定义的(fn.length)相同或者更长时,那么只需要使用 fn.apply 将调用传递给它即可。 2、否则,获取一个部分应用函数:这里还没有调用fn。取而代之的是,返回另外一个包装器(其实这就是将之前的参数收集起来合并到总的参数数组中)

总结

柯里化是一种转换,将fn(a, b, c)转换为可以被fn(a)(b)(c)的形式进行调用。JS 实现通常都保持该函数可以被正常调用,并且如果参数数量不足,则返回一个部分应用函数。