博客 / 詳情

返回

JavaScript 中的函數式編程:函數、組合與柯里化

原文鏈接:https://blog.bitsrc.io/functi...

豆皮粉兒,又見面啦!今天字節跳動數據平台的"陽羨"小哥哥給大家帶來一篇翻譯文章"JavaScript 中的函數式編程:函數、組合與柯里化",乾貨滿滿,不容錯過!!!

本文作者:陽羨

面向對象編程和函數式編程是兩種截然不同的編程範式,有各自的規則,也有各自的優缺點。

但是,JavaScript,並非一直使用一種編程範式,而是兼具兩者的特徵。給你提供了普通OOP語言一些方面的能力,比如類、對象、繼承等。但同時,它也給你提供了一些函數式概念,比如高階函數,也提供了組合與柯里化的能力。

高階函數

先説説我在本文中涉及的三個概念中最重要的一個:高階函數。

擁有對高階函數的訪問權意味着函數不僅僅是一個你可以從代碼中定義和調用的構造,事實上,你可以將它們作為可賦值的變量,即函數是一等公民。

如果你寫過一些JavaScript的話,這一點應該不會讓人感到驚訝,畢竟,你應該已經能夠簡單地從網上按照例子將匿名函數賦值給變量了。將函數賦值給變量在日常使用中並不罕見。

如果JavaScript是你用來學習編程的第一門語言,那麼你可能會對上述邏輯在許多其他語言中是無效的而感到驚訝。像賦值一個整數一樣賦值一個函數其實非常有用,事實上,本文所涉及的大部分主題都是由此衍生而來。

高階函數的優勢:封裝行為

通過高階函數,我們不僅可以像上面一樣給變量賦值函數,而且,我們還可以在函數調用時將其作為參數傳遞。這又為創建動態邏輯打開了大門,你可以通過直接將複雜的行為作為參數傳遞來重用邏輯。

想象一下,在一個純粹的面向對象的環境中工作,你需要重用一個邏輯,你知道一個基本邏輯可以被擴展並作為複雜邏輯的一部分。在這種情況下,你可能會選擇使用繼承,通過將該邏輯封裝在一個抽象類中,然後將其擴展為一組派生類,這些類利用該通用邏輯並對其進行補充。這是完美而高效的OOP準則,但讓我們看看我們剛才做了什麼。我們:

  1. 創建了一個抽象的結構來封裝我們的可重用邏輯。
  2. 創建了一個二級的派生類
  3. 讓後者在前者的基礎上進行邏輯擴展。

但是,在函數式的環境下,為了複用邏輯,我們可以簡單地將需要複用的邏輯提取到一個函數中,然後將這個函數作為參數傳遞給任何其他可以從這種封裝行為中受益的函數。我們只是在創建函數,而不是創建“模版”。

下面的例子試圖展示我上面解釋的內容。第一段代碼展示了你如何在OOP環境中去重用一個格式化並輸出的邏輯。

然而,第二個例子表明,通過將邏輯提取出來,並使用函數封裝,你可以用很少的成本來創建你所需要的邏輯。你可以繼續添加更多的格式化( format)和輸出(output)函數,然後只需用一行代碼將它們組合在一起就可以了。

我的意思是,這兩種方法都有優點,而且都是非常高效的,沒有高低優劣之分。函數式有多麼令人難以置信的靈活性,以及我們如何使用基本的函數式原理,這僅僅是因為我們有能力將行為(即函數)作為參數傳遞,就好像它們是一個基本類型,如整數或字符串。

高階函數的優勢:整潔代碼

整潔代碼的最好例子就是數組方法,如forEach ,map ,reduce等。在非函數式語言中,例如C語言,迭代一個數組的元素,並對它們進行轉換,需要使用for循環或其他循環結構。它們要求你以一種非常命令式的方式編寫代碼(換句話説,你需要表達事情如何在循環內發生),而函數式則允許更多的聲明式編程風格(你最終指定需要發生什麼)。

你的代碼實際上是在説:

聲明一個新的變量i作為myArray的索引 它的值範圍從0到myArray的數組長度。

遍歷i的值,然後把i位置的myArray的值乘2,並將其添加到transformedArray數組中。

它當然是可行的,而且比較容易理解,但是,邏輯的複雜度會迅速升級,並且閲讀邏輯所需的認知成本也會增加。然而,表達同樣的邏輯,函數式可能更具有可讀性:

本質上,這段代碼是説

用double函數映射(map)myArray的元素,並將結果賦值給transformedArray。

因為邏輯被隱藏在兩個函數(map和double)中,所以你不必擔心理解它們的工作原理。你也可以在第一個例子中把乘法邏輯隱藏在一個函數裏面,但是仍然需要暴露迭代邏輯,晦澀的迭代邏輯是你作為一個閲讀代碼的人,必須在頭腦中解析以理解其工作原理的重要部分。

柯里化(currying)

函數柯里化是指將一個多參數的函數變成一個少參數的函數,並將部分參數固定下來的編程思想。讓我用一個例子來解釋。

現在,如果你想做的是將10加到一系列值上,你可以調用add10,而不是每次都用相同的第二個參數調用adder。我知道這可能是一個簡單的例子,當你尋找柯里化時,可能到處都是這個例子,但考慮到你正在做的事情:你正在利用adder函數的邏輯,並創建該函數的專門版本,換句話説,你正在擴展該函數,就像你使用一個類一樣。

你可以把柯里化看作是函數式編程的繼承,按照這個思路,再回到上面格式化輸出的例子,你可以這樣編寫你的代碼:

本質上,你有一個叫做log的函數,它需要三個參數,而我們把它柯里化成專門的版本,只需要一個,因為另外兩個已經被我們選好了。

需要注意的是,我把log函數當作一個抽象類,只是因為在我的例子中,你不會想直接使用它,然而這樣做沒有任何限制,因為這只是一個普通的函數。如果我們使用的是抽象類,你就不能直接實例化它。

組合(Composition)

最後,函數組合是高階函數的另一個非常有趣的衍生品。乍一看,人們很容易把組成混淆為柯里化的情況,或者也許反過來説,有柯里化的函數而不是直接的值(就像我們在上面的記錄儀例子中做的那樣)可以被認為是函數組合。

這些觀點其實都沒有錯,當你開始使用函數式時,這兩個概念之間有一條非常細微的界限。具體來説,組成的定義如下:

在計算機科學中,函數組合是一種將簡單的函數組合起來建立更復雜的函數的行為或機制。與數學中通常的函數組合一樣,每個函數的結果作為下一個函數的參數傳遞,最後一個函數的結果就是整個函數的結果。

這是維基百科上關於函數組成的定義,最後加粗的部分是我強調的,因為那是關鍵部分。在柯里化中,你沒有這個限制,你可以很容易地使用預先設定的函數參數,如果它們是函數,它們不必一個接一個地調用,讓第一個函數的結果成為第二個函數的輸入,以此類推。

與柯里化不同,這是一個強大的工具,因為在這裏,只有部分功能,每個功能都在完成一個特定的任務,等待着被組成更大更復雜的東西。想想看,就好像函數是樂高積木一樣,通過組合,只要你把正確的邏輯柯里化,並以正確的順序組合在一起(即只要你以正確的順序組合成正確的函數),你就能創造出任何你能想到的東西。

如果你以前使用過Linux發行版,你可能已經注意到,Linux中的CLI工具遵循一個非常確定的模式:它們只做一件事,並且能夠從標準輸入中讀取結果,並將其輸出到標準輸出。因此,允許用户將多個命令組合成一句功能強大的命令,例如:

$ cat myfile.txt | wc -l

如上所示,我是在讀取一個文件,並計算它的行數,然而,如果以不同的方式或與其他命令組合,輸出可能會有很大的不同。同樣的情況也發生在函數上,如果你在設計函數的時候,讓一個函數的輸出可以成為另一個函數的輸入,你也可以像這樣組合它們。

看看上面和最後的例子,我創建了四個不同的處理字符串的函數,並將它們組合成三個不同的函數。你可以通過組合函數來創建新函數。這就是組合的魅力所在。

仔細看一下代碼,有幾處值得關注。

  • 有些函數(replace和findMatches)實際上是接受參數並返回一個函數。這是為了使它們更通用,由於JS將返回的函數的上下文與函數本身一起保存(即閉包),我們能夠將這些參數作為被返回函數的 "全局 "變量,並作為組合的一部分使用。
  • 請注意compose函數,它是利用ES6的結構操作符,簡單地在函數參數上進行迭代,並執行它們,然後將其結果發送給下一個函數。reduceRight的使用保證了我們在函數列表中從右到左進行執行,這就是為什麼我總是把小寫字母加到最後一個。如果你想讓順序反過來,你可以直接使用 reduce 來代替。

結論

如果使用得當,高階函數以及柯里化和組合都是非常強大的工具。我知道,如果你不習慣於使用函數式的思維模式,而是更願意使用類和對象,這些技術可能看起來有悖於直覺,但它們本質上並不晦澀難懂,只是需要換個角度去思考。

感受並享受JavaScript的函數式編程吧!

你之前使用過這些工具嗎?你更喜歡用函數式編程思想寫代碼嗎?還是你更喜歡OOP開發?歡迎在評論區發表你的看法。

感謝閲讀,我們下期再見!

The End

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.