函數式編程簡介:JavaScript 範式

已發表: 2022-03-11

函數式編程是一種使用表達式和函數構建計算機程序而不改變狀態和數據的範例。

通過尊重這些限制,函數式編程旨在編寫更易於理解且更能抵抗錯誤的代碼。 這是通過避免使用流控制語句( forwhilebreakcontinuegoto )來實現的,這些語句會使代碼更難理解。 此外,函數式編程要求我們編寫純粹的、確定性的函數,這些函數不太可能出錯。

在本文中,我們將討論使用 JavaScript 進行函數式編程。 我們還將探索使之成為可能的各種 JavaScript 方法和功能。 最後,我們將探索與函數式編程相關的不同概念,並了解它們為何如此強大。

然而,在進入函數式編程之前,需要了解純函數和不純函數之間的區別。

純函數與非純函數

純函數接受一些輸入並給出固定輸出。 此外,它們對外界沒有副作用。

 const add = (a, b) => a + b;

這裡, add是一個純函數。 這是因為,對於ab的固定值,輸出將始終相同。

 const SECRET = 42; const getId = (a) => SECRET * a;

getId不是純函數。 原因是它使用全局變量SECRET來計算輸出。 如果SECRET發生變化, getId函數將為相同的輸入返回不同的值。 因此,它不是一個純函數。

 let id_count = 0; const getId = () => ++id_count;

這也是一個不純函數,這也是出於幾個原因——(1)它使用一個非局部變量來計算其輸出,以及(2)它通過修改其中的一個變量而在外部世界中產生副作用世界。

getId 是不純的插圖

如果我們必須調試此代碼,這可能會很麻煩。

id_count的當前值是多少? 還有哪些其他函數正在修改id_count ? 還有其他依賴id_count的功能嗎?

由於這些原因,我們在函數式編程中只使用純函數。

純函數的另一個好處是它們可以並行化和記憶化。 看看前面的兩個函數。 不可能並行化或記憶它們。 這有助於創建高性能代碼。

函數式編程的原則

到目前為止,我們已經了解到函數式編程依賴於一些規則。 它們如下。

  1. 不要改變數據
  2. 使用純函數:固定輸出固定輸入,無副作用
  3. 使用表達式和聲明

當我們滿足這些條件時,我們可以說我們的代碼是正常的。

JavaScript 中的函數式編程

JavaScript 已經有一些啟用函數式編程的函數。 示例:String.prototype.slice、Array.protoype.filter、Array.prototype.join。

另一方面,Array.prototype.forEach、Array.prototype.push 是不純函數。

有人可能會爭辯說Array.prototype.forEach在設計上並不是一個不純的函數,但請仔細想想——除了改變非本地數據或產生副作用之外,它不可能做任何事情。 因此,可以將其歸入不純函數的範疇。

此外,JavaScript 有一個 const 聲明,非常適合函數式編程,因為我們不會改變任何數據。

JavaScript 中的純函數

下面我們來看看 JavaScript 給出的一些純函數(方法)。

篩選

顧名思義,這會過濾數組。

 array.filter(condition);

這裡的條件是一個獲取數組每一項的函數,它應該決定是否保留該項並為此返回真實的布爾值。

 const filterEven = x => x%2 === 0; [1, 2, 3].filter(filterEven); // [2]

注意filterEven是一個純函數。 如果它是不純的,那麼它會使整個過濾器調用不純。

地圖

map將數組的每一項映射到一個函數,並根據函數調用的返回值創建一個新數組。

 array.map(mapper)

mapper是一個函數,它將數組的一項作為輸入並返回輸出。

 const double = x => 2 * x; [1, 2, 3].map(double); // [2, 4, 6]

減少

reduce將數組縮減為單個值。

 array.reduce(reducer);

reducer是一個函數,它獲取累加值和數組中的下一項並返回新值。 對數組中的所有值一個接一個地調用它。

 const sum = (accumulatedSum, arrayItem) => accumulatedSum + arrayItem [1, 2, 3].reduce(sum); // 6 

減少通話插圖

康卡特

concat將新項目添加到現有數組以創建新數組。 它與push()的不同之處在於push()會改變數據,從而使其不純。

 [1, 2].concat([3, 4]) // [1, 2, 3, 4]

您也可以使用擴展運算符執行相同操作。

 [1, 2, ...[3, 4]]

對象.assign

Object.assign將值從提供的對象複製到新對象。 由於函數式編程基於不可變數據,我們使用它來基於現有對象創建新對象。

 const obj = {a : 2}; const newObj = Object.assign({}, obj); newObj.a = 3; obj.a; // 2

隨著 ES6 的出現,這也可以使用擴展運算符來完成。

 const newObj = {...obj};

創建自己的純函數

我們也可以創建我們的純函數。 讓我們來複製一個字符串n次。

 const duplicate = (str, n) => n < 1 ? '' : str + duplicate(str, n-1);

此函數將一個字符串複製n次並返回一個新字符串。

 duplicate('hooray!', 3) // hooray!hooray!hooray!

高階函數

高階函數是接受函數作為參數並返回函數的函數。 通常,它們用於添加功能的功能。

 const withLog = (fn) => { return (...args) => { console.log(`calling ${fn.name}`); return fn(...args); }; };

在上面的示例中,我們創建了一個withLog高階函數,它接受一個函數並返回一個函數,該函數在包裝函數運行之前記錄一條消息。

 const add = (a, b) => a + b; const addWithLogging = withLog(add); addWithLogging(3, 4); // calling add // 7

withLog HOF 也可以與其他函數一起使用,並且它可以在沒有任何衝突或編寫額外代碼的情況下工作。 這就是 HOF 的美妙之處。

 const addWithLogging = withLog(add); const hype = s => s + '!!!'; const hypeWithLogging = withLog(hype); hypeWithLogging('Sale'); // calling hype // Sale!!!

也可以在不定義組合函數的情況下調用它。

 withLog(hype)('Sale'); // calling hype // Sale!!!

咖哩

柯里化意味著將一個接受多個參數的函數分解為一個或多個級別的高階函數。

讓我們來看看add功能。

 const add = (a, b) => a + b;

當我們要對它進行 curry 時,我們重寫它,將參數分配到多個級別,如下所示。

 const add = a => { return b => { return a + b; }; }; add(3)(4); // 7

柯里化的好處是記憶。 我們現在可以記住函數調用中的某些參數,以便以後可以重用它們而無需重複和重新計算。

 // assume getOffsetNumer() call is expensive const addOffset = add(getOffsetNumber()); addOffset(4); // 4 + getOffsetNumber() addOffset(6);

這肯定比在任何地方都使用這兩個參數要好。

 // (X) DON"T DO THIS add(4, getOffsetNumber()); add(6, getOffsetNumber()); add(10, getOffsetNumber());

我們還可以重新格式化我們的 curried 函數以使其看起來簡潔。 這是因為柯里化函數調用的每一級都是單行返回語句。 因此,我們可以使用 ES6 中的箭頭函數對其進行重構,如下所示。

 const add = a => b => a + b;

作品

在數學中,組合被定義為將一個函數的輸出傳遞給另一個函數的輸入,從而創建一個組合輸出。 由於我們使用的是純函數,因此在函數式編程中也是如此。

為了展示一個例子,讓我們創建一些函數。

第一個函數是 range,它接受一個起始數字a和一個結束數字b並創建一個由從ab的數字組成的數組。

 const range = (a, b) => a > b ? [] : [a, ...range(a+1, b)];

然後我們有一個乘法函數,它接受一個數組並將其中的所有數字相乘。

 const multiply = arr => arr.reduce((p, a) => p * a);

我們將一起使用這些函數來計算階乘。

 const factorial = n => multiply(range(1, n)); factorial(5); // 120 factorial(6); // 720

上述計算階乘的函數類似於f(x) = g(h(x)) ,從而證明了組合屬性。

結束語

我們學習了純函數和非純函數、函數式編程、有助於它的新 JavaScript 特性,以及函數式編程中的一些關鍵概念。

我們希望這篇文章能激起您對函數式編程的興趣,並可能激勵您在代碼中嘗試它。 我們確信這將是一次學習體驗,也是您軟件開發之旅中的里程碑。

函數式編程是一種經過充分研究且健壯的計算機程序編寫範例。 隨著 ES6 的引入,JavaScript 提供了比以往更好的函數式編程體驗。