本文將簡要介紹函數式編程的概念,並闡述五種提升 JavaScript 代碼函數式風格的方法。
核心要點
const
聲明變量來實現。 for
循環,因為它們依賴於可變狀態。應改用遞歸和高階數組方法。此外,應避免類型強制以保持類型一致性。這可以通過在聲明函數之前編寫類型聲明註釋來實現。 什麼是函數式編程?
函數式編程是一種編程範式,它使用函數及其應用,而不是在命令式編程語言中使用的命令列表。
這是一種更抽象的編程風格,其根源在於數學——特別是稱為 lambda 演算的數學分支,該分支由數學家 Alonzo Church 於 1936 年設計為可計算性的形式模型。它由表達式和函數組成,這些表達式和函數將一個表達式映射到另一個表達式。從根本上說,這就是我們在函數式編程中所做的:我們使用函數將值轉換為不同的值。
本文作者近年來愛上了函數式編程。我們開始使用鼓勵更函數式風格的 JavaScript 庫,然後通過學習如何在 Haskell 中編寫代碼直接跳入深水區。
Haskell 是一種純函數式編程語言,開發於 20 世紀 90 年代,類似於 Scala 和 Clojure。使用這些語言,您將被迫採用函數式風格進行編碼。學習 Haskell 使我們真正了解了函數式編程提供的所有優勢。
JavaScript 是一種多範式語言,因為它可以用於以命令式、面向對像或函數式風格進行編程。但是,它確實特別適合函數式風格,因為函數是第一類對象,這意味著它們可以賦值給變量。這也意味著函數可以作為參數傳遞給其他函數(通常稱為回調),也可以作為其他函數的返回值。返回其他函數或接受其他函數作為參數的函數稱為高階函數,它們是函數式編程的基礎部分。
近年來,以函數式風格編程 JavaScript 變得越來越流行,尤其是在 React 興起之後。 React 使用適合函數式方法的聲明式 API,因此紮實掌握函數式編程原理將改進您的 React 代碼。
為什麼函數式編程如此優秀?
簡而言之,函數式編程語言通常會導致代碼簡潔、清晰且優雅。代碼通常更容易測試,並且可以在多線程環境中運行而不會出現任何問題。
如果您與許多不同的程序員交談,您可能會從每個人那裡獲得關於函數式編程的完全不同的意見——從那些絕對討厭它的人到那些絕對喜歡它的人。我們(本文作者)站在“喜歡它”的一端,但我們完全理解它並非每個人的菜,尤其因為它與通常教授的編程方式大相徑庭。
但是,一旦您掌握了函數式編程,並且一旦思維過程點擊到位,它就會成為第二天性,並改變您編寫代碼的方式。
規則 1:淨化您的函數
函數式編程的關鍵部分是確保您編寫的函數是“純”的。如果您不熟悉這個術語,純函數基本上滿足以下條件:
純函數必須至少有一個參數,並且必須返回值。如果您仔細考慮,如果它們不接受任何參數,它們將沒有任何數據可以使用,如果它們不返回值,那麼函數的意義何在?
純函數一開始可能看起來並非完全必要,但使用不純函數會導致程序發生整體變化,從而導致一些嚴重的邏輯錯誤!
例如:
<code class="language-javascript">// 不纯 let minimum = 21; const checkAge = (age) => age >= minimum; // 纯 const checkAge = (age) => { const minimum = 21; return age >= minimum; };</code>
在不純函數中,checkAge
函數依賴於可變變量 minimum
。例如,如果稍後在程序中更新 minimum
變量,則 checkAge
函數可能會使用相同的輸入返回布爾值。
假設我們運行以下代碼:
<code class="language-javascript">checkAge(20); // false</code>
現在,假設稍後在代碼中,changeToUK()
函數將 minimum
的值更新為 18。
然後,假設我們運行以下代碼:
<code class="language-javascript">// 不纯 let minimum = 21; const checkAge = (age) => age >= minimum; // 纯 const checkAge = (age) => { const minimum = 21; return age >= minimum; };</code>
現在,checkAge
函數評估為不同的值,儘管給出了相同的輸入。
純函數使您的代碼更易於移植,因為它們不依賴於作為參數提供的任何值之外的任何其他值。返回值永遠不會改變的事實使純函數更容易測試。
一致地編寫純函數還可以消除發生突變和副作用的可能性。
突變是函數式編程中的一大危險信號,如果您想了解更多信息,可以閱讀關於它們在《JavaScript 中變量賦值和突變指南》中的內容。
為了使您的函數更易於移植,請確保您的函數始終保持純淨。
規則 2:保持變量不變
聲明變量是任何程序員學習的第一件事之一。它變得微不足道,但在使用函數式編程風格時卻極其重要。
函數式編程的關鍵原則之一是,一旦設置了變量,它就會在整個程序中保持該狀態。
這是顯示代碼中變量的重新賦值/重新聲明如何成為災難的最簡單的示例:
<code class="language-javascript">checkAge(20); // false</code>
如果您仔細考慮,n
的值不可能同時為 10 和 11;這在邏輯上說不通。
命令式編程中的一種常見編碼實踐是使用以下代碼遞增值:
<code class="language-javascript">checkAge(20); // true</code>
在數學中,語句 x = x 1
是不合邏輯的,因為如果您從兩邊減去 x
,您將得到 0 = 1
,這顯然是不正確的。
因此,在 Haskell 中,您不能將變量賦值給一個值,然後將其重新賦值給另一個值。為了在 JavaScript 中實現這一點,您應該遵循始終使用 const
聲明變量的規則。
規則 3:使用箭頭函數
在數學中,函數的概念是將一組值映射到另一組值的概念。下圖顯示了通過平方將左側的值集映射到右側的值集的函數:
這就是使用箭頭表示法在數學中編寫它的方式:f: x → x²。這意味著函數 f 將值 x 映射到 x²。
我們可以使用箭頭函數幾乎以相同的方式編寫此函數:
<code class="language-javascript">const n = 10; n = 11; // TypeError: "Attempted to assign to readonly property."</code>
在 JavaScript 中使用函數式風格的一個關鍵特徵是使用箭頭函數而不是常規函數。當然,這確實歸結為風格,使用箭頭函數而不是常規函數實際上不會影響代碼的“函數式”程度。
但是,在使用函數式編程風格時,最難適應的事情之一是將每個函數都視為輸入到輸出的映射的思維方式。沒有所謂的過程。我們發現使用箭頭函數可以幫助我們更好地理解函數的過程。
箭頭函數具有隱式返回值,這確實有助於可視化此映射。
箭頭函數的結構——尤其是它們的隱式返回值——有助於鼓勵編寫純函數,因為它們的結構實際上是“輸入映射到輸出”:
<code class="language-javascript">// 不纯 let minimum = 21; const checkAge = (age) => age >= minimum; // 纯 const checkAge = (age) => { const minimum = 21; return age >= minimum; };</code>
另一件事是我們喜歡強調的,尤其是在編寫箭頭函數時,是使用三元運算符。如果您不熟悉三元運算符,它們是內聯 if...else
語句,形式為 condition ? value if true : value if false
。
您可以閱讀更多關於它們在《快速提示:如何在 JavaScript 中使用三元運算符》中的內容。
在函數式編程中使用三元運算符的主要原因之一是 else
語句的必要性。程序必須知道如果不滿足原始條件該怎麼做。例如,Haskell 會強制執行 else
語句,如果沒有給出 else
語句,它將返回錯誤。
使用三元運算符的另一個原因是它們是始終返回值的表達式,而不是可以用於執行可能具有副作用的操作的 if-else
語句。這對於箭頭函數特別有用,因為它意味著您可以確保存在返回值並保持輸入到輸出映射的圖像。如果您不確定語句和表達式之間的細微差別,那麼關於語句與表達式的指南非常值得一讀。
為了說明這兩個條件,以下是一個使用三元運算符的簡單箭頭函數示例:
<code class="language-javascript">checkAge(20); // false</code>
action
函數將根據 state
參數的值返回“eat”或“sleep”的值。
因此,總之:在使您的代碼更具函數式時,您應該遵循以下兩條規則:
if...else
語句替換為三元運算符規則 4:刪除 for 循環
鑑於使用 for
循環編寫迭代代碼在編程中非常常見,說要避免它們似乎很奇怪。事實上,當我們第一次發現 Haskell 甚至沒有任何類型的 for
循環操作時,我們難以理解如何實現某些標準操作。但是,有一些非常好的理由說明為什麼 for
循環不會出現在函數式編程中,我們很快發現每種類型的迭代過程都可以在不使用 for
循環的情況下實現。
不使用 for
循環最重要的原因是它們依賴於可變狀態。讓我們來看一個簡單的求和函數:
<code class="language-javascript">checkAge(20); // true</code>
如您所見,我們必須在 for
循環本身以及我們在 for
循環中更新的變量中使用 let
。
如前所述,這通常是函數式編程中的不良做法,因為函數式編程中的所有變量都應該是不可變的。
如果我們想編寫所有變量都是不可變的代碼,我們可以使用遞歸:
<code class="language-javascript">const n = 10; n = 11; // TypeError: "Attempted to assign to readonly property."</code>
如您所見,沒有變量被更新。
我們當中的數學家顯然會知道所有這些代碼都是不必要的,因為我們可以只使用巧妙的求和公式 0.5*n*(n 1)。但這是一種很好的方法,可以說明 for
循環的可變性與遞歸之間的區別。
但是,遞歸併不是解決可變性問題的唯一解決方案,尤其是在處理數組時。 JavaScript 具有許多內置的高階數組方法,這些方法可以遍歷數組中的值而無需更改任何變量。
例如,假設我們要將 1 加到數組中的每個值。使用命令式方法和 for
循環,我們的函數可能如下所示:
<code class="language-javascript">// 不纯 let minimum = 21; const checkAge = (age) => age >= minimum; // 纯 const checkAge = (age) => { const minimum = 21; return age >= minimum; };</code>
但是,我們可以使用 JavaScript 的內置 map
方法並編寫如下所示的函數:
<code class="language-javascript">checkAge(20); // false</code>
如果您以前從未見過map
函數,那麼絕對值得了解它們——以及JavaScript 的所有內置高階數組方法,例如filter
,尤其如果您真的對JavaScript 中的函數式編程感興趣。您可以在《不可變數組方法:如何編寫非常清晰的 JavaScript 代碼》中找到更多關於它們的信息。
Haskell 完全沒有 for
循環。為了使您的 JavaScript 更具函數式,請嘗試通過使用遞歸和內置的高階數組方法來避免使用 for
循環。
規則 5:避免類型強制
在使用 JavaScript 等不需要類型聲明的語言進行編程時,很容易忘記數據類型的重要性。 JavaScript 中使用的七種原始數據類型是:
Haskell 是一種強類型語言,需要類型聲明。這意味著在任何函數之前,您都需要使用 Hindley-Milner 系統指定進入的數據類型和輸出的數據類型。
例如:
<code class="language-javascript">checkAge(20); // true</code>
這是一個非常簡單的函數,它將兩個數字(x 和 y)加在一起。對於每個函數,包括像這樣的非常簡單的函數,都必須向程序解釋數據類型似乎有點荒謬,但這最終有助於顯示函數的預期工作方式及其預期返回的內容。這使得代碼更容易調試,尤其是在代碼變得更複雜時。
類型聲明遵循以下結構:
<code class="language-javascript">const n = 10; n = 11; // TypeError: "Attempted to assign to readonly property."</code>
類型強制在使用 JavaScript 時可能是一個大問題,JavaScript 具有各種可以用來(甚至濫用)繞過數據類型不一致的技巧。以下是最常見的技巧以及如何避免它們:
if
語句中的 0 值等效於 false。這可能導致懶惰的編程技術,忽略檢查數值數據是否等於 0。 例如:
<code class="language-javascript">// 不纯 let minimum = 21; const checkAge = (age) => age >= minimum; // 纯 const checkAge = (age) => { const minimum = 21; return age >= minimum; };</code>
這是一個評估數字是否為偶數的函數。它使用 !
符號將 n % 2
的結果強制轉換為布爾值,但 n % 2
的結果不是布爾值,而是一個數字(0 或 1)。
像這樣的技巧,雖然看起來很聰明,並且減少了您編寫的代碼量,但它們破壞了函數式編程的類型一致性規則。因此,編寫此函數的最佳方法如下所示:
<code class="language-javascript">checkAge(20); // false</code>
另一個重要概念是確保數組中的所有數據值都是相同類型。 JavaScript 不會強制執行此操作,但如果沒有相同類型,當您想要使用高階數組方法時可能會導致問題。
例如,一個將數組中所有數字相乘並返回結果的乘積函數可以用以下類型聲明註釋編寫:
<code class="language-javascript">checkAge(20); // true</code>
在這裡,類型聲明清楚地表明函數的輸入是一個包含 Number 類型元素的數組,但它只返回一個數字。類型聲明清楚地說明了此函數的預期輸入和輸出。顯然,如果數組不只包含數字,則此函數將無法工作。
Haskell 是一種強類型語言,而 JavaScript 是一種弱類型語言,但為了使您的 JavaScript 更具函數式,您應該在聲明函數之前編寫類型聲明註釋,並確保避免類型強制快捷方式。
我們還應該在這裡提到,如果您想要 JavaScript 的強類型替代方案,該替代方案將為您強制執行類型一致性,那麼您可以轉向 TypeScript。
結論
總而言之,以下五個規則將幫助您實現函數式代碼:
for
循環。 雖然這些規則不能保證您的代碼是純函數式的,但它們將在很大程度上使其更具函數式,並有助於使其更簡潔、更清晰且更容易測試。
我們真的希望這些規則能像幫助我們一樣幫助您!我們兩人都是函數式編程的忠實粉絲,我們強烈建議任何程序員使用它。
如果您想進一步深入研究函數式 JavaScript,我們強烈建議您閱讀《弗里斯比教授關於函數式編程的大部分充分指南》,該指南可以在線免費獲取。如果您想全力以赴學習 Haskell,我們建議您使用 Try Haskell 交互式教程並閱讀優秀的《學習 Haskell 以獲得更大的好處》一書,該書也可以在線免費閱讀。
關於 JavaScript 函數式編程的常見問題
什麼是 JavaScript 中的函數式編程? 函數式編程是一種編程範式,它將計算視為數學函數的評估,並避免更改狀態和可變數據。在 JavaScript 中,它涉及使用函數作為一等公民並避免副作用。
什麼是 JavaScript 中的一等函數? JavaScript 中的一等函數意味著函數被視為與任何其他變量一樣。它們可以賦值給變量,作為參數傳遞給其他函數,並作為值從其他函數返回。
什麼是函數式編程中的不變性? 不變性是指一旦創建對象,就不能更改它。在 JavaScript 函數式編程的上下文中,這意味著在初始化變量或數據結構後避免修改它們。
什麼是高階函數? 高階函數是將其他函數作為參數或返回函數作為結果的函數。它們支持函數的組合,使創建模塊化和可重用代碼更容易。
是否有任何庫/框架可以促進 JavaScript 中的函數式編程? 是的,一些庫和框架(例如 Ramda 和 lodash)提供了支持 JavaScript 函數式編程概念的實用程序和函數。它們可以幫助簡化和增強函數式編程實踐。
以上是使您的JavaScript更具功能性的5種方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!