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

沃趣葫蘆娃發表於2018-05-08

簡介

柯里化從何而來

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

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

在編碼過程中,身為碼農的我們本質上所進行的工作就是——將複雜問題分解為多個可程式設計的小問題。

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

所以 Currying 原本是一門編譯原理層面的技術,用途是實現多參函式

柯里化去向哪裡

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

在《Mostly adequate guide》一書中,這樣總結了 Currying ——只傳遞給函式一部分引數來呼叫它,讓它返回一個函式去處理剩下的引數

所以 Currying 是應函數語言程式設計而生,在有了 Currying 後,大家再去探索去發掘了它的用途及意義。 然後因為這些用途和意義,大家才積極地將它擴充套件到其他程式語言中。

在 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)} />
    複製程式碼

效能對比

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

通過 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》

參考連結

相關文章