JavaScript 柯里化

一、什麼是柯里化

Currying ——只傳遞給函數一部分參數來進行調用,並讓它返回一個函數去處理剩下的參數。

柯里化即 Currying,是一門編譯原理層面的技術,用途是實現多參函數,其爲實現多參函數提供了一個遞歸降解的實現思路——把接受多個參數的函數變換成接受第一個參數的函數,並且返回接受剩餘參數且返回結果的新函數

某些編程語言中(如 Haskell)就是通過柯里化技術支持多參函數這一語言特性的

二、JS 柯里化的實現

先來寫一個實現加法的函數 add

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

現在我們直接實現一個被柯里化的 add 函數,該函數名爲 curriedAdd,則根據上面的定義,curriedAdd 需要滿足以下條件:

curriedAdd(1)(3) === 4
// true

var increment = curriedAdd(1)
increment(2) === 3
// true

var addTen = curriedAdd(10)
addTen(2) === 12
// true

滿足以上條件的 curriedAdd 的函數可以用以下代碼段實現:

function curriedAdd(x) {
  return function (y) {
    return x + y
  }
}

這種實現方式並不通用,但表明了實現柯里化的一個基礎——柯里化延遲求值的特性需要用到 JavaScript 中的作用域——使用作用域來保存上一次傳進來的參數。

curriedAdd 進行抽象,可以得到如下函數 currying

function currying(fn, ...args1) {
  return function (...args2) {
    return fn(...args1, ...args2)
  }
}

var increment = currying(add, 1)
increment(2) === 3
// true

var addTen = currying(add, 10)
addTen(2) === 12
// true

在此實現中,currying 函數的返回值其實是一個接收剩餘參數並立即返回計算值的函數,即它的返回值並沒有自動被柯里化 。
所以我們可以通過遞歸來將柯里化返回的函數也自動柯里化。

function trueCurrying(fn, ...args) {
  if (args.length >= fn.length) {
    return fn(...args)
  }

  return function (...args2) {
    return trueCurrying(fn, ...args, ...args2)
  }
}

以上函數實現了柯里化的核心思想。

JavaScript 中的常用庫 Lodash 中的 curry 方法,其核心思想和以上相似,都是對比多次接受的參數總數函數定義時的入參數量,當接受參數的數量大於或等於被柯里化函數的傳入參數數量時,就返回計算結果,否則返回一個繼續接受參數的函數。

三、柯里化的應用

1、參數複用

固定不變的參數,實現參數複用是柯里化的主要用途之一:

var increment = curriedAdd(1)
increment(2) === 3
// true

var addTen = curriedAdd(10)
addTen(2) === 12
// true

上面的 increment, addTen 就是進行參數複用

2、延遲執行

延遲執行也是柯里化的一個重要使用場景,而 bind 和箭頭函數也能實現同樣的功能。

一個常見的場景就是爲標籤綁定 onClick 事件,同時考慮爲綁定的方法傳遞參數。

以下列出了幾種常見的方法,來比較優劣:

① 通過 data 屬性

<div data-name="name" onClick={handleOnClick} />

通過 data 屬性本質只能傳遞字符串的數據,如果需要傳遞複雜對象,只能通過 JSON.stringify(data) 來傳遞滿足 JSON 對象格式的數據,但對更加複雜的對象無法支持

② 通過 bind 方法

<div onClick={handleOnClick.bind(null, data)} />

bind 方法和以上實現的 currying 方法,在功能上有極大的相似,在實現上也幾乎差不多。

唯一的不同就是 bind 方法需要強制綁定 context,即 bind 的第一個參數會作爲原函數運行時的 this 指向,
currying 不需要此參數。

③ 通過箭頭函數

<div onClick={() => handleOnClick(data)} />

箭頭函數能夠實現延遲執行,同時也不像 bind 方法必需指定 context。

但在 react 中,不建議直接在 jsx 標籤內寫箭頭函數(直接在 jsx 標籤內寫業務邏輯)。

④ 通過 currying

<div onClick={currying(handleOnClick, data)} />

⑤ 性能對比

通過 jsPerf 測試四種方式的性能,結果爲:箭頭函數>bind>currying>trueCurrying

currying 函數相比 bind 函數,其原理相似,但是性能相差巨大,其原因是 bind 由瀏覽器實現,運行效率有加成。

從這個結果看柯里化性能是最差的,但另一方面就算最差的 trueCurrying 的實現,也能達到 50w Ops/s,說明這些性能其實影響非常小。

trueCurrying 方法中實現的自動柯里化,是另外三個方法所不具備的。

四、柯里化的優劣勢

1、優勢

① 爲了多參函數複用性

柯里化讓人眼前一亮的地方在於,讓人覺得函數還能這樣子複用。
通過一行代碼,將 add 函數轉換爲 increment,addTen 等。
對於柯里化的複雜實現中,以 Lodash 爲列,提供了 placeholder 對多參函數的複用玩出花樣:

import _ from 'loadsh'

function abc(a, b, c) {
  return [a, b, c];
}

var curried = _.curry(abc)

// Curried with placeholders.
curried(1)(_, 3)(2)
// => [1, 2, 3]

② 爲函數式編程而生

柯里化是爲函數式而生的東西,應運着有一整套函數式編程的東西,純函數、compose、container 等等。

假如要寫 Pointfree Javascript 風格的代碼,那麼柯里化是不可或缺的。使用 compose、container 等也需要柯里化

2、劣勢

① 柯里化的一些特性有其他解決方案

如果我們只是想提前綁定參數,那麼我們有很多好幾個現成的選擇,bind,箭頭函數等,而且性能比柯里化更好

② 柯里化陷於函數式編程

上面 trueCurrying 的實現是最符合柯里化定義的,也提供了 bind,箭頭函數等不具備的“新奇”特性——可持續的柯里化。

但柯里化是函數式編程的產物,它生於函數式編程,也服務於函數式編程,而 JavaScript 並非真正的函數式編程語言,相比 Haskell 等函數式編程語言,JavaScript 使用柯里化等函數式特性有額外的性能開銷,也缺乏類型推導。

假如沒有準備好去寫函數式編程規範的代碼,僅需要在 JSX 代碼中提前綁定一次參數,那麼 bind 或箭頭函數就足夠了。

五、總結

1、柯里化在 JavaScript 中是“低性能”的,但是這些性能在絕大多數場景,是可以忽略的。
2、柯里化的思想極大地助於提升函數的複用性。
3、柯里化生於函數式編程,也陷於函數式編程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章