Linux中國

JavaScript 函數式編程介紹

當 Brendan Eich 在 1995 年創造 JavaScript 時,他原本打算將 Scheme 移植到瀏覽器里 。Scheme 作為 Lisp 的方言,是一種函數式編程語言。而當 Eich 被告知新的語言應該是一種可以與 Java 相比的腳本語言後,他最終確立了一種擁有 C 風格語法的語言(也和 Java 一樣),但將函數視作一等公民。而 Java 直到版本 8 才從技術上將函數視為一等公民,雖然你可以用匿名類來模擬它。這個特性允許 JavaScript 通過函數式範式編程。

JavaScript 是一個多範式語言,允許你自由地混合和使用面向對象式、過程式和函數式的編程範式。最近,函數式編程越來越火熱。在諸如 AngularReact 這樣的框架中,通過使用不可變數據結構可以切實提高性能。不可變是函數式編程的核心原則,它以及純函數使得編寫和調試程序變得更加容易。使用函數來代替程序的循環可以提高程序的可讀性並使它更加優雅。總之,函數式編程擁有很多優點。

什麼不是函數式編程

在討論什麼是函數式編程前,讓我們先排除那些不屬於函數式編程的東西。實際上它們是你需要丟棄的語言組件(再見,老朋友):

  • 循環:
    • while
    • do...while
    • for
    • for...of
    • for...in
  • var 或者 let 來聲明變數
  • 沒有返回值的函數
  • 改變對象的屬性 (比如: o.x = 5;)
  • 改變數組本身的方法:
    • copyWithin
    • fill
    • pop
    • push
    • reverse
    • shift
    • sort
    • splice
    • unshift
  • 改變映射本身的方法:
    • clear
    • delete
    • set
  • 改變集合本身的方法:
    • add
    • clear
    • delete

脫離這些特性應該如何編寫程序呢?這是我們將在後面探索的問題。

純函數

你的程序中包含函數不一定意味著你正在進行函數式編程。函數式範式將 純函數 pure function 非純函數 impure function 區分開。鼓勵你編寫純函數。純函數必須滿足下面的兩個屬性:

  • 引用透明:函數在傳入相同的參數後永遠返回相同的返回值。這意味著該函數不依賴於任何可變狀態。
  • 無副作用:函數不能導致任何副作用。副作用可能包括 I/O(比如向終端或者日誌文件寫入),改變一個不可變的對象,對變數重新賦值等等。

我們來看一些例子。首先,multiply 就是一個純函數的例子,它在傳入相同的參數後永遠返回相同的返回值,並且不會導致副作用。

function multiply(a, b) {
  return a * b;
}

下面是非純函數的例子。canRide 函數依賴捕獲的 heightRequirement 變數。被捕獲的變數不一定導致一個函數是非純函數,除非它是一個可變的變數(或者可以被重新賦值)。這種情況下使用 let 來聲明這個變數,意味著可以對它重新賦值。multiply 函數是非純函數,因為它會導致在 console 上輸出。

let heightRequirement = 46;

// Impure because it relies on a mutable (reassignable) variable.
function canRide(height) {
  return height >= heightRequirement;
}

// Impure because it causes a side-effect by logging to the console.
function multiply(a, b) {
  console.log('Arguments: ', a, b);
  return a * b;
}

下面的列表包含著 JavaScript 內置的非純函數。你可以指出它們不滿足兩個屬性中的哪個嗎?

  • console.log
  • element.addEventListener
  • Math.random
  • Date.now
  • $.ajax (這裡 $ 代表你使用的 Ajax 庫)

理想的程序中所有的函數都是純函數,但是從上面的函數列表可以看出,任何有意義的程序都將包含非純函數。大多時候我們需要進行 AJAX 調用,檢查當前日期或者獲取一個隨機數。一個好的經驗法則是遵循 80/20 規則:函數中有 80% 應該是純函數,剩下的 20% 的必要性將不可避免地是非純函數。

使用純函數有幾個優點:

  • 它們很容易導出和調試,因為它們不依賴於可變的狀態。
  • 返回值可以被緩存或者「記憶」來避免以後重複計算。
  • 它們很容易測試,因為沒有需要模擬(mock)的依賴(比如日誌,AJAX,資料庫等等)。

你編寫或者使用的函數返回空(換句話說它沒有返回值),那代表它是非純函數。

不變性

讓我們回到捕獲變數的概念上。來看看 canRide 函數。我們認為它是一個非純函數,因為 heightRequirement 變數可以被重新賦值。下面是一個構造出來的例子來說明如何用不可預測的值來對它重新賦值。

let heightRequirement = 46;

function canRide(height) {
  return height >= heightRequirement;
}

// Every half second, set heightRequirement to a random number between 0 and 200.
setInterval(() => heightRequirement = Math.floor(Math.random() * 201), 500);

const mySonsHeight = 47;

// Every half second, check if my son can ride.
// Sometimes it will be true and sometimes it will be false.
setInterval(() => console.log(canRide(mySonsHeight)), 500);

我要再次強調被捕獲的變數不一定會使函數成為非純函數。我們可以通過只是簡單地改變 heightRequirement 的聲明方式來使 canRide 函數成為純函數。

const heightRequirement = 46;

function canRide(height) {
  return height >= heightRequirement;
}

通過用 const 來聲明變數意味著它不能被再次賦值。如果嘗試對它重新賦值,運行時引擎將拋出錯誤;那麼,如果用對象來代替數字來存儲所有的「常量」怎麼樣?

const constants = {
  heightRequirement: 46,
  // ... other constants go here
};

function canRide(height) {
  return height >= constants.heightRequirement;
}

我們用了 const ,所以這個變數不能被重新賦值,但是還有一個問題:這個對象可以被改變。下面的代碼展示了,為了真正使其不可變,你不僅需要防止它被重新賦值,你也需要不可變的數據結構。JavaScript 語言提供了 Object.freeze 方法來阻止對象被改變。

'use strict';

// CASE 1: 對象的屬性是可變的,並且變數可以被再次賦值。
let o1 = { foo: 'bar' };

// 改變對象的屬性
o1.foo = 'something different';

// 對變數再次賦值
o1 = { message: "I'm a completely new object" };

// CASE 2: 對象的屬性還是可變的,但是變數不能被再次賦值。
const o2 = { foo: 'baz' };

// 仍然能改變對象
o2.foo = 'Something different, yet again';

// 不能對變數再次賦值
// o2 = { message: 'I will cause an error if you uncomment me' }; // Error!

// CASE 3: 對象的屬性是不可變的,但是變數可以被再次賦值。
let o3 = Object.freeze({ foo: "Can't mutate me" });

// 不能改變對象的屬性
// o3.foo = 'Come on, uncomment me. I dare ya!'; // Error!

// 還是可以對變數再次賦值
o3 = { message: "I'm some other object, and I'm even mutable -- so take that!" };

// CASE 4: 對象的屬性是不可變的,並且變數不能被再次賦值。這是我們想要的!!!!!!!!
const o4 = Object.freeze({ foo: 'never going to change me' });

// 不能改變對象的屬性
// o4.foo = 'talk to the hand' // Error!

// 不能對變數再次賦值
// o4 = { message: "ain't gonna happen, sorry" }; // Error

不變性適用於所有的數據結構,包括數組、映射和集合。它意味著不能調用例如 Array.prototype.push 等會導致本身改變的方法,因為它會改變已經存在的數組。可以通過創建一個含有原來元素和新加元素的新數組,而不是將新元素加入一個已經存在的數組。其實所有會導致數組本身被修改的方法都可以通過一個返回修改好的新數組的函數代替。

'use strict';

const a = Object.freeze([4, 5, 6]);

// Instead of: a.push(7, 8, 9);
const b = a.concat(7, 8, 9);

// Instead of: a.pop();
const c = a.slice(0, -1);

// Instead of: a.unshift(1, 2, 3);
const d = [1, 2, 3].concat(a);

// Instead of: a.shift();
const e = a.slice(1);

// Instead of: a.sort(myCompareFunction);
const f = R.sort(myCompareFunction, a); // R = Ramda

// Instead of: a.reverse();
const g = R.reverse(a); // R = Ramda

// 留給讀者的練習:
// copyWithin
// fill
// splice

映射集合 也很相似。可以通過返回一個新的修改好的映射或者集合來代替使用會修改其本身的函數。

const map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three']
]);

// Instead of: map.set(4, 'four');
const map2 = new Map([...map, [4, 'four']]);

// Instead of: map.delete(1);
const map3 = new Map([...map].filter(([key]) => key !== 1));

// Instead of: map.clear();
const map4 = new Map();
const set = new Set(['A', 'B', 'C']);

// Instead of: set.add('D');
const set2 = new Set([...set, 'D']);

// Instead of: set.delete('B');
const set3 = new Set([...set].filter(key => key !== 'B'));

// Instead of: set.clear();
const set4 = new Set();

我想提一句如果你在使用 TypeScript(我非常喜歡 TypeScript),你可以用 Readonly<T>ReadonlyArray<T>ReadonlyMap<K, V>ReadonlySet<T> 介面來在編譯期檢查你是否嘗試更改這些對象,有則拋出編譯錯誤。如果在對一個對象字面量或者數組調用 Object.freeze,編譯器會自動推斷它是只讀的。由於映射和集合在其內部表達,所以在這些數據結構上調用 Object.freeze 不起作用。但是你可以輕鬆地告訴編譯器它們是只讀的變數。

![TypeScript Readonly Interfaces](/data/attachment/album/201710/23/233446z6jofno6l5elfxtj.png "TypeScript Readonly Interfaces")

TypeScript 只讀介面

好,所以我們可以通過創建新的對象來代替修改原來的對象,但是這樣不會導致性能損失嗎?當然會。確保在你自己的應用中做了性能測試。如果你需要提高性能,可以考慮使用 Immutable.js。Immutable.js 用持久的數據結構 實現了鏈表堆棧映射集合和其他數據結構。使用了如同 Clojure 和 Scala 這樣的函數式語言中相同的技術。

// Use in place of `[]`.
const list1 = Immutable.List([&apos;A&apos;, &apos;B&apos;, &apos;C&apos;]);
const list2 = list1.push(&apos;D&apos;, &apos;E&apos;);

console.log([...list1]); // [&apos;A&apos;, &apos;B&apos;, &apos;C&apos;]
console.log([...list2]); // [&apos;A&apos;, &apos;B&apos;, &apos;C&apos;, &apos;D&apos;, &apos;E&apos;]

// Use in place of `new Map()`
const map1 = Immutable.Map([
  [&apos;one&apos;, 1],
  [&apos;two&apos;, 2],
  [&apos;three&apos;, 3]
]);
const map2 = map1.set(&apos;four&apos;, 4);

console.log([...map1]); // [[&apos;one&apos;, 1], [&apos;two&apos;, 2], [&apos;three&apos;, 3]]
console.log([...map2]); // [[&apos;one&apos;, 1], [&apos;two&apos;, 2], [&apos;three&apos;, 3], [&apos;four&apos;, 4]]

// Use in place of `new Set()`
const set1 = Immutable.Set([1, 2, 3, 3, 3, 3, 3, 4]);
const set2 = set1.add(5);

console.log([...set1]); // [1, 2, 3, 4]
console.log([...set2]); // [1, 2, 3, 4, 5]

函數組合

記不記得在中學時我們學過一些像 (f ∘ g)(x) 的東西?你那時可能想,「我什麼時候會用到這些?」,好了,現在就用到了。你準備好了嗎?f ∘ g讀作 「函數 f 和函數 g 組合」。對它的理解有兩種等價的方式,如等式所示: (f ∘ g)(x) = f(g(x))。你可以認為 f ∘ g 是一個單獨的函數,或者視作將調用函數 g 的結果作為參數傳給函數 f。注意這些函數是從右向左依次調用的,先執行 g,接下來執行 f

關於函數組合的幾個要點:

  1. 我們可以組合任意數量的函數(不僅限於 2 個)。
  2. 組合函數的一個方式是簡單地把一個函數的輸出作為下一個函數的輸入(比如 f(g(x)))。
// h(x) = x + 1
// number -> number
function h(x) {
  return x + 1;
}

// g(x) = x^2
// number -> number
function g(x) {
  return x * x;
}

// f(x) = convert x to string
// number -> string
function f(x) {
  return x.toString();
}

// y = (f ∘ g ∘ h)(1)
const y = f(g(h(1)));
console.log(y); // &apos;4&apos;

Ramdalodash 之類的庫提供了更優雅的方式來組合函數。我們可以在更多的在數學意義上處理函數組合,而不是簡單地將一個函數的返回值傳遞給下一個函數。我們可以創建一個由這些函數組成的單一複合函數(就是 (f ∘ g)(x))。

// h(x) = x + 1
// number -> number
function h(x) {
  return x + 1;
}

// g(x) = x^2
// number -> number
function g(x) {
  return x * x;
}

// f(x) = convert x to string
// number -> string
function f(x) {
  return x.toString();
}

// R = Ramda
// composite = (f ∘ g ∘ h)
const composite = R.compose(f, g, h);

// Execute single function to get the result.
const y = composite(1);
console.log(y); // &apos;4&apos;

好了,我們可以在 JavaScript 中組合函數了。接下來呢?好,如果你已經入門了函數式編程,理想中你的程序將只有函數的組合。代碼里沒有循環(for, for...of, for...in, while, do),基本沒有。你可能覺得那是不可能的。並不是這樣。我們下面的兩個話題是:遞歸和高階函數。

遞歸

假設你想實現一個計算數字的階乘的函數。 讓我們回顧一下數學中階乘的定義:

n! = n * (n-1) * (n-2) * ... * 1.

n! 是從 n1 的所有整數的乘積。我們可以編寫一個循環輕鬆地計算出結果。

function iterativeFactorial(n) {
  let product = 1;
  for (let i = 1; i <= n; i++) {
    product *= i;
  }
  return product;
}

注意 producti 都在循環中被反覆重新賦值。這是解決這個問題的標準過程式方法。如何用函數式的方法解決這個問題呢?我們需要消除循環,確保沒有變數被重新賦值。遞歸是函數式程序員的最有力的工具之一。遞歸需要我們將整體問題分解為類似整體問題的子問題。

計算階乘是一個很好的例子,為了計算 n! 我們需要將 n 乘以所有比它小的正整數。它的意思就相當於:

n! = n * (n-1)!

啊哈!我們發現了一個解決 (n-1)! 的子問題,它類似於整個問題 n!。還有一個需要注意的地方就是基礎條件。基礎條件告訴我們何時停止遞歸。 如果我們沒有基礎條件,那麼遞歸將永遠持續。 實際上,如果有太多的遞歸調用,程序會拋出一個堆棧溢出錯誤。啊哈!

function recursiveFactorial(n) {
  // Base case -- stop the recursion
  if (n === 0) {
    return 1; // 0! is defined to be 1.
  }
  return n * recursiveFactorial(n - 1);
}

然後我們來計算 recursiveFactorial(20000) 因為……,為什麼不呢?當我們這樣做的時候,我們得到了這個結果:

![Stack overflow error](/data/attachment/album/201710/23/233447bg5wglljjz99gf2s.png "Stack overflow error")

堆棧溢出錯誤

這裡發生了什麼?我們得到一個堆棧溢出錯誤!這不是無窮的遞歸導致的。我們已經處理了基礎條件(n === 0 的情況)。那是因為瀏覽器的堆棧大小是有限的,而我們的代碼使用了越過了這個大小的堆棧。每次對 recursiveFactorial 的調用導致了新的幀被壓入堆棧中,就像一個盒子壓在另一個盒子上。每當 recursiveFactorial 被調用,一個新的盒子被放在最上面。下圖展示了在計算 recursiveFactorial(3) 時堆棧的樣子。注意在真實的堆棧中,堆棧頂部的幀將存儲在執行完成後應該返回的內存地址,但是我選擇用變數 r 來表示返回值,因為 JavaScript 開發者一般不需要考慮內存地址。

![The stack for recursively calculating 3! (three factorial)](/data/attachment/album/201710/23/233447plcywxldy9qldwq0.png "The stack for recursively calculating 3! (three factorial)")

遞歸計算 3! 的堆棧(三次乘法)

你可能會想像當計算 n = 20000 時堆棧會更高。我們可以做些什麼優化它嗎?當然可以。作為 ES2015 (又名 ES6) 標準的一部分,有一個優化用來解決這個問題。它被稱作 尾調用優化 proper tail calls optimization (PTC)。當遞歸函數做的最後一件事是調用自己並返回結果的時候,它使得瀏覽器刪除或者忽略堆棧幀。實際上,這個優化對於相互遞歸函數也是有效的,但是為了簡單起見,我們還是來看單一遞歸函數。

你可能會注意到,在遞歸函數調用之後,還要進行一次額外的計算(n * r)。那意味著瀏覽器不能通過 PTC 來優化遞歸;然而,我們可以通過重寫函數使最後一步變成遞歸調用以便優化。一個竅門是將中間結果(在這裡是 product)作為參數傳遞給函數。

&apos;use strict&apos;;

// Optimized for tail call optimization.
function factorial(n, product = 1) {
  if (n === 0) {
    return product;
  }
  return factorial(n - 1, product * n)
}

讓我們來看看優化後的計算 factorial(3) 時的堆棧。如下圖所示,堆棧不會增長到超過兩層。原因是我們把必要的信息都傳到了遞歸函數中(比如 product)。所以,在 product 被更新後,瀏覽器可以丟棄掉堆棧中原先的幀。你可以在圖中看到每次最上面的幀下沉變成了底部的幀,原先底部的幀被丟棄,因為不再需要它了。

![The optimized stack for recursively calculating 3! (three factorial) using PTC](/data/attachment/album/201710/23/233448xpfd9wdfkqaqv9ip.png "The optimized stack for recursively calculating 3! (three factorial) using PTC")

遞歸計算 3! 的堆棧(三次乘法)使用 PTC

現在選一個瀏覽器運行吧,假設你在使用 Safari,你會得到 Infinity(它是比在 JavaScript 中能表達的最大值更大的數)。但是我們沒有得到堆棧溢出錯誤,那很不錯!現在在其他的瀏覽器中呢怎麼樣呢?Safari 可能現在乃至將來是實現 PTC 的唯一一個瀏覽器。看看下面的兼容性表格:

![PTC compatibility](/data/attachment/album/201710/23/233449yle0huli5njzl1dl.png "PTC compatibility")

PTC 兼容性

其他瀏覽器提出了一種被稱作 語法級尾調用 syntactic tail calls (STC)的競爭標準。「語法級」意味著你需要用新的語法來標識你想要執行尾遞歸優化的函數。即使瀏覽器還沒有廣泛支持,但是把你的遞歸函數寫成支持尾遞歸優化的樣子還是一個好主意。

高階函數

我們已經知道 JavaScript 將函數視作一等公民,可以把函數像其他值一樣傳遞。所以,把一個函數傳給另一個函數也很常見。我們也可以讓函數返回一個函數。就是它!我們有高階函數。你可能已經很熟悉幾個在 Array.prototype 中的高階函數。比如 filtermapreduce 就在其中。對高階函數的一種理解是:它是接受(一般會調用)一個回調函數參數的函數。讓我們來看看一些內置的高階函數的例子:

const vehicles = [
  { make: &apos;Honda&apos;, model: &apos;CR-V&apos;, type: &apos;suv&apos;, price: 24045 },
  { make: &apos;Honda&apos;, model: &apos;Accord&apos;, type: &apos;sedan&apos;, price: 22455 },
  { make: &apos;Mazda&apos;, model: &apos;Mazda 6&apos;, type: &apos;sedan&apos;, price: 24195 },
  { make: &apos;Mazda&apos;, model: &apos;CX-9&apos;, type: &apos;suv&apos;, price: 31520 },
  { make: &apos;Toyota&apos;, model: &apos;4Runner&apos;, type: &apos;suv&apos;, price: 34210 },
  { make: &apos;Toyota&apos;, model: &apos;Sequoia&apos;, type: &apos;suv&apos;, price: 45560 },
  { make: &apos;Toyota&apos;, model: &apos;Tacoma&apos;, type: &apos;truck&apos;, price: 24320 },
  { make: &apos;Ford&apos;, model: &apos;F-150&apos;, type: &apos;truck&apos;, price: 27110 },
  { make: &apos;Ford&apos;, model: &apos;Fusion&apos;, type: &apos;sedan&apos;, price: 22120 },
  { make: &apos;Ford&apos;, model: &apos;Explorer&apos;, type: &apos;suv&apos;, price: 31660 }
];

const averageSUVPrice = vehicles
  .filter(v => v.type === &apos;suv&apos;)
  .map(v => v.price)
  .reduce((sum, price, i, array) => sum + price / array.length, 0);

console.log(averageSUVPrice); // 33399

注意我們在一個數組對象上調用其方法,這是面向對象編程的特性。如果我們想要更函數式一些,我們可以用 Rmmda 或者 lodash/fp 提供的函數。注意如果我們使用 R.compose 的話,需要倒轉函數的順序,因為它從右向左依次調用函數(從底向上);然而,如果我們想從左向右調用函數就像上面的例子,我們可以用 R.pipe。下面兩個例子用了 Rmmda。注意 Rmmda 有一個 mean 函數用來代替 reduce

const vehicles = [
  { make: &apos;Honda&apos;, model: &apos;CR-V&apos;, type: &apos;suv&apos;, price: 24045 },
  { make: &apos;Honda&apos;, model: &apos;Accord&apos;, type: &apos;sedan&apos;, price: 22455 },
  { make: &apos;Mazda&apos;, model: &apos;Mazda 6&apos;, type: &apos;sedan&apos;, price: 24195 },
  { make: &apos;Mazda&apos;, model: &apos;CX-9&apos;, type: &apos;suv&apos;, price: 31520 },
  { make: &apos;Toyota&apos;, model: &apos;4Runner&apos;, type: &apos;suv&apos;, price: 34210 },
  { make: &apos;Toyota&apos;, model: &apos;Sequoia&apos;, type: &apos;suv&apos;, price: 45560 },
  { make: &apos;Toyota&apos;, model: &apos;Tacoma&apos;, type: &apos;truck&apos;, price: 24320 },
  { make: &apos;Ford&apos;, model: &apos;F-150&apos;, type: &apos;truck&apos;, price: 27110 },
  { make: &apos;Ford&apos;, model: &apos;Fusion&apos;, type: &apos;sedan&apos;, price: 22120 },
  { make: &apos;Ford&apos;, model: &apos;Explorer&apos;, type: &apos;suv&apos;, price: 31660 }
];

// Using `pipe` executes the functions from top-to-bottom. 
const averageSUVPrice1 = R.pipe(
  R.filter(v => v.type === &apos;suv&apos;),
  R.map(v => v.price),
  R.mean
)(vehicles);

console.log(averageSUVPrice1); // 33399

// Using `compose` executes the functions from bottom-to-top.
const averageSUVPrice2 = R.compose(
  R.mean,
  R.map(v => v.price),
  R.filter(v => v.type === &apos;suv&apos;)
)(vehicles);

console.log(averageSUVPrice2); // 33399

使用函數式方法的優點是清楚地分開了數據(vehicles)和邏輯(函數 filtermapreduce)。面向對象的代碼相比之下把數據和函數用以方法的對象的形式混合在了一起。

柯里化

不規範地說, 柯里化 currying 是把一個接受 n 個參數的函數變成 n 個每個接受單個參數的函數的過程。函數的 arity 是它接受參數的個數。接受一個參數的函數是 unary,兩個的是 binary,三個的是 ternaryn 個的是 n-ary。那麼,我們可以把柯里化定義成將一個 n-ary 函數轉換成 nunary 函數的過程。讓我們通過簡單的例子開始,一個計算兩個向量點積的函數。回憶一下線性代數,兩個向量 [a, b, c][x, y, z] 的點積是 ax + by + cz

function dot(vector1, vector2) {
  return vector1.reduce((sum, element, index) => sum += element * vector2[index], 0);
}

const v1 = [1, 3, -5];
const v2 = [4, -2, -1];

console.log(dot(v1, v2)); // 1(4) + 3(-2) + (-5)(-1) = 4 - 6 + 5 = 3

dot 函數是 binary,因為它接受兩個參數;然而我們可以將它手動轉換成兩個 unary 函數,就像下面的例子。注意 curriedDot 是一個 unary 函數,它接受一個向量並返回另一個接受第二個向量的 unary 函數。

function curriedDot(vector1) {
  return function(vector2) {
    return vector1.reduce((sum, element, index) => sum += element * vector2[index], 0);
  }
}

// Taking the dot product of any vector with [1, 1, 1]
// is equivalent to summing up the elements of the other vector.
const sumElements = curriedDot([1, 1, 1]);

console.log(sumElements([1, 3, -5])); // -1
console.log(sumElements([4, -2, -1])); // 1

很幸運,我們不需要把每一個函數都手動轉換成柯里化以後的形式。Ramdalodash 等庫可以為我們做這些工作。實際上,它們是柯里化的混合形式。你既可以每次傳遞一個參數,也可以像原來一樣一次傳遞所有參數。

function dot(vector1, vector2) {
  return vector1.reduce((sum, element, index) => sum += element * vector2[index], 0);
}

const v1 = [1, 3, -5];
const v2 = [4, -2, -1];

// Use Ramda to do the currying for us!
const curriedDot = R.curry(dot);

const sumElements = curriedDot([1, 1, 1]);

console.log(sumElements(v1)); // -1
console.log(sumElements(v2)); // 1

// This works! You can still call the curried function with two arguments.
console.log(curriedDot(v1, v2)); // 3

Ramda 和 lodash 都允許你「跳過」一些變數之後再指定它們。它們使用置位符來做這些工作。因為點積的計算可以交換兩項。傳入向量的順序不影響結果。讓我們換一個例子來闡述如何使用一個置位符。Ramda 使用雙下劃線作為其置位符。

const giveMe3 = R.curry(function(item1, item2, item3) {
  return `
    1: ${item1}
    2: ${item2}
    3: ${item3}
  `;
});

const giveMe2 = giveMe3(R.__, R.__, &apos;French Hens&apos;);   // Specify the third argument.
const giveMe1 = giveMe2(&apos;Partridge in a Pear Tree&apos;);  // This will go in the first slot.
const result = giveMe1(&apos;Turtle Doves&apos;);               // Finally fill in the second argument.

console.log(result);
// 1: Partridge in a Pear Tree
// 2: Turtle Doves
// 3: French Hens

在我們結束探討柯里化之前最後的議題是 偏函數應用 partial application 。偏函數應用和柯里化經常同時出場,儘管它們實際上是不同的概念。一個柯里化的函數還是柯里化的函數,即使沒有給它任何參數。偏函數應用,另一方面是僅僅給一個函數傳遞部分參數而不是所有參數。柯里化是偏函數應用常用的方法之一,但是不是唯一的。

JavaScript 擁有一個內置機制可以不依靠柯里化來做偏函數應用。那就是 function.prototype.bind 方法。這個方法的一個特殊之處在於,它要求你將 this 作為第一個參數傳入。 如果你不進行面向對象編程,那麼你可以通過傳入 null 來忽略 this

1function giveMe3(item1, item2, item3) {
  return `
    1: ${item1}
    2: ${item2}
    3: ${item3}
  `;
}

const giveMe2 = giveMe3.bind(null, &apos;rock&apos;);
const giveMe1 = giveMe2.bind(null, &apos;paper&apos;);
const result = giveMe1(&apos;scissors&apos;);

console.log(result);
// 1: rock
// 2: paper
// 3: scissors

總結

我希望你享受探索 JavaScript 中函數式編程的過程。對一些人來說,它可能是一個全新的編程範式,但我希望你能嘗試它。你會發現你的程序更易於閱讀和調試。不變性還將允許你優化 Angular 和 React 的性能。

這篇文章基於 Matt 在 OpenWest 的演講 JavaScript the Good-er Parts. OpenWest 在 6/12-15 ,2017 在 Salt Lake City, Utah 舉行。

作者簡介:

Matt Banz - Matt 於 2008 年五月在猶他大學獲得了數學學位畢業。一個月後他得到了一份 web 開發者的工作,他從那時起就愛上了它!在 2013 年,他在北卡羅萊納州立大學獲得了計算機科學碩士學位。他在 LDS 商學院和戴維斯學區社區教育計劃教授 Web 課程。他現在是就職於 Motorola Solutions 公司的高級前端開發者。

via: https://opensource.com/article/17/6/functional-javascript

作者:Matt Banz 譯者:trnhoe 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國