JS 分步實現柯里化函式

迪斯馬斯克發表於2019-03-29

簡介

首先,柯里化(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 方法

相關文章