大佬,JavaScript 柯里化,瞭解一下?

簡介

柯里化從何而來

柯里化, 即 Currying 的音譯。 Currying 是編譯原理層面實現多參函數的一個技術。javascript

在說JavaScript 中的柯里化前,能夠聊一下原始的 Currying 是什麼,又從何而來。html

在編碼過程當中,身爲碼農的咱們本質上所進行的工做就是——將複雜問題分解爲多個可編程的小問題。前端

Currying 爲實現多參函數提供了一個遞歸降解的實現思路——把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數並且返回結果的新函數,在某些編程語言中(如 Haskell),是經過 Currying 技術支持多參函數這一語言特性的。java

因此 Currying 本來是一門編譯原理層面的技術,用途是實現多參函數react

柯里化去向哪裏

在 Haskell 中,函數做爲一等公民,Currying 從編譯原理層面的技術應運而成了一個語言特性。 在語言特性層面,Currying 是什麼?git

在《Mostly adequate guide》一書中,這樣總結了 Currying ——只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數程序員

因此 Currying 是應函數式編程而生,在有了 Currying 後,你們再去探索去發掘了它的用途及意義。 而後由於這些用途和意義,你們才積極地將它擴展到其餘編程語言中。github

在 JavaScript 中實現 Currying

爲了實現只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數這句話所描述的特性。 咱們先寫一個實現加法的函數 add編程

function add (x, y) {

  return (x + y)

}
複製代碼

如今咱們直接實現一個被 Curryingadd 函數,該函數名爲 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

  }
}
複製代碼

固然以上實現是有一些問題的:它並不通用,而且咱們並不想經過從新編碼函數自己的方式來實現 Currying 化。

可是這個 curriedAdd 的實現代表了實現 Currying 的一個基礎 —— Currying 延遲求值的特性須要用到 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 函數的返回值實際上是一個接收剩餘參數而且當即返回計算值的函數。即它的返回值並無自動被 Currying化 。因此咱們能夠經過遞歸來將 currying 的返回的函數也自動 Currying 化。

function trueCurrying(fn, ...args) {

    if (args.length >= fn.length) {

        return fn(...args)

    }

    return function (...args2) {

        return trueCurrying(fn, ...args, ...args2)

    }
}
複製代碼

以上函數很簡短,可是已經實現 Currying 的核心思想了。JavaScript 中的經常使用庫 Lodash 中的 curry 方法,其核心思想和以上並無太大差別——比較屢次接受的參數總數與函數定義時的入參數量,當接受參數的數量大於或等於被 Currying 函數的傳入參數數量時,就返回計算結果,不然返回一個繼續接受參數的函數。

Lodash 中實現 Currying 的代碼段較長,由於它考慮了更多的事情,好比綁定 this 變量等。在此處就不直接貼出 Lodash 中的代碼段,感興趣的同窗能夠去看看看 Lodash 源碼,比較一下這兩種實現會致使什麼樣的差別。

然而 Currying 的定義和實現都不是最重要的,本文想要闡述的重點是:它可以解決編碼和開發當中怎樣的問題,以及在面對不一樣的問題時,選擇一個合適的 Currying,來最恰當的解決問題

Currying 使用場景

參數複用

固定不變的參數,實現參數複用是 Currying 的主要用途之一。

上文中的increment, addTen是一個參數複用的實例。對add方法固定第一個參數爲 10 後,改方法就變成了一個將接受的變量值加 10 的方法。

延遲執行

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

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

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

  1. 經過 data 屬性

    <div data-name="name" onClick={handleOnClick} />
    複製代碼

    經過 data 屬性本質只能傳遞字符串的數據,若是須要傳遞複雜對象,只能經過 JSON.stringify(data) 來傳遞知足 JSON 對象格式的數據,但對更加複雜的對象沒法支持。(雖然大多數時候也無需傳遞複雜對象)

  2. 經過bind方法

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

    bind 方法和以上實現的 currying 方法,在功能上有極大的類似,在實現上也幾乎差很少。可能惟一的不一樣就是 bind 方法須要強制綁定 context,也就是 bind 的第一個參數會做爲原函數運行時的 this 指向。而 currying 不須要此參數。因此使用 currying 或者 bind 只是一個取捨問題。

  3. 箭頭函數

    <div onClick={() => handleOnClick(data))} />
    複製代碼

    箭頭函數可以實現延遲執行,同時也不像 bind 方法必需指定 context。可能惟一須要顧慮的就是在 react 中,會有人反對在 jsx 標籤內寫箭頭函數,這樣子容易致使直接在 jsx 標籤內寫業務邏輯。

  4. 經過currying

    <div onClick={currying(handleOnClick, data)} />
    複製代碼

性能對比

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

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

從這個結果看 Currying 性能無疑是最差的,可是另外一方面就算最差的 trueCurrying 的實現,也能在本人的我的電腦上達到 50w Ops/s 的狀況下,說明這些性能是無需在乎的。

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

到底需不須要 Currying

爲何須要 Currying

  1. 爲了多參函數複用性

    Currying 讓人眼前一亮的地方在於,讓人以爲函數還能這樣子複用。

    經過一行代碼,將 add 函數轉換爲 increment,addTen 等。

    對於 Currying 的複雜實現中,以 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]
    複製代碼
  2. 爲函數式編程而生

    Currying 是爲函數式而生的東西。應運着有一整套函數式編程的東西,純函數composecontainer等等事物。(可閱讀《mostly-adequate-guide》

    假如要寫 Pointfree Javascript 風格的代碼,那麼Currying是不可或缺的。

    要使用 compose,要使用 container 等事物,咱們也須要 Currying。

爲何不須要 Currying

  1. Currying 的一些特性有其餘解決方案

    若是咱們只是想提早綁定參數,那麼咱們有不少好幾個現成的選擇,bind,箭頭函數等,並且性能比Curring更好。

  2. Currying 陷於函數式編程

    在本文中,提供了一個 trueCurrying 的實現,這個實現也是最符合 Currying 定義的,也提供 了bind,箭頭函數等不具有的「新奇」特性——可持續的 Currying(這個詞是本人臨時造的)。

    可是這個「新奇」特性的應用並不是想象得那麼普遍。

    其緣由在於,Currying 是函數式編程的產物,它生於函數式編程,也服務於函數式編程。

    而 JavaScript 並不是真正的函數式編程語言,相比 Haskell 等函數式編程語言,JavaScript 使用 Currying 等函數式特性有額外的性能開銷,也缺少類型推導。

    從而把 JavaScript 代碼寫得符合函數式編程思想和規範的項目都較少,從而也限制了 Currying 等技術在 JavaScript 代碼中的廣泛使用。

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

結論

  1. Currying 在 JavaScript 中是「低性能」的,可是這些性能在絕大多數場景,是能夠忽略的。
  2. Currying 的思想極大地助於提高函數的複用性。
  3. Currying 生於函數式編程,也陷於函數式編程。假如沒有準備好寫純正的函數式代碼,那麼 Currying 有更好的替代品。
  4. 函數式編程及其思想,是值得關注、學習和應用的事物。因此在文末再次安利 JavaScript 程序員閱讀此書 —— 《mostly-adequate-guide》

參考連接

相關文章
相關標籤/搜索