JS 分步实现柯里化函数

4,571

简介

首先,柯里化(Currying)是什么呢?

简单说,假如有一个函数,接受多个参数,那么一般来说就是一次性传入所有参数并执行。
而对其执行柯里化后,就变成了可以分多次接收参数

实现

阶段1

现在有一个加法函数:

function add(x, y, z) {
  return x + y + z
}

调用方式是 add(1, 2, 3)

如果执行柯里化,变成了 curriedAdd(),从效果来说,大致就是变成 curriedAdd(1)(2)(3) 这样子。

现在先不看怎么对原函数执行柯里化,而是根据这个调用方式重新写一个函数。
代码可能是这样的:

function curriedAdd1(x) {
  return function (y) {
    return function (z) {
      return x + y + z
    }
  }
}

阶段2

假如现在想要升级一下,不止可以接受三个参数。
可以使用 arguments,或者使用展开运算符来处理传入的参数。

但是有一个衍生的问题。因为之前每次只能传递一个,总共只能传递三个,才保证了调用三次之后参数个数刚好足够,函数才能执行。

既然我们打算修改为可以接受任意个数的参数,那么就要规定一个终点。比如说,可以规定为当不再传入参数的时候,就执行函数。

下面是使用 arguments 的实现。

function getCurriedAdd() {
  // 在外部维护一个数组保存传递的变量
  let args_arr = []
  // 返回一个闭包
  let closure = function () {
    // 本次调用传入的参数
    let args = Array.prototype.slice.call(arguments)
    // 如果传进了新的参数
    if (args.length > 0) {
      // 保存参数
      args_arr = args_arr.concat(args)
      // 再次返回闭包,等待下次调用
      // 也可以 return arguments.callee
      return closure
    }
    // 没有传递参数,执行累加
    return args_arr.reduce((total, current) => total + current)
  }
  return closure
}
curriedAdd = getCurriedAdd()
curriedAdd(1)(2)(3)(4)()

阶段3

这时可以发现,上面的整个函数里,与函数具体功能(在这里就是执行加法)有关的,就只是当没有传递参数时的部分,其他部分都是在实现怎样多次接收参数。

那么,只要让 getCurriedAdd 接受一个函数作为参数,把没有传递参数时的那一行代码替换一下,就可以实现一个通用的柯里化函数了。

把上面的修改一下,实现一个通用柯里化函数,并把一个阶乘函数柯里化:

function currying(fn) {
  let args_arr = []
  let closure =  function (...args) {
    if (args.length > 0) {
      args_arr = args_arr.concat(args)
      return closure
    }
    // 没有新的参数,执行函数
    return fn(...args_arr)
  }
  return closure
}
function multiply(...args) {
  return args.reduce((total, current) => total * current)
}
curriedMultiply = currying(multiply)
console.log(curriedMultiply(2)(3, 4)())

阶段4

上面的代码里,对于函数执行时机的判断,是根据是否有参数传入。
但是更多时候,更合理的依据是原函数可以接受的参数的总数。

函数名的 length 属性就是该函数接受的参数个数。比如:

function test1(a, b) {}
function test2(...args){}
console.log(test1.length) // 2
console.log(test2.length) // 0

改写一下:

function currying(fn) {
  let args_arr = [], max_length = fn.length
  let closure = function (...args) {
    // 先把参数加进去
    args_arr = args_arr.concat(args)
    // 如果参数没满,返回闭包等待下一次调用
    if (args_arr.length < max_length) return closure
    // 传递完成,执行
    return fn(...args_arr)
  }
  return closure
}
function add(x, y, z) {
  return x + y + z
}
curriedAdd = currying(add)
console.log(curriedAdd(1, 2)(3))

Lodash 中的柯里化

Lodash 中的柯里化就灵活得多了,可以先放置占位符之后再传值。
可以参考一下《实现 lodash 的 curry 方法》,这里就不分析了。

参考链接

大佬,JavaScript 柯里化,了解一下?
JS 函数柯里化
实现 lodash 的 curry 方法