[譯] 函數式 JavaScript 快速入門

函數式 JavaScript 快速入門

函數式編程是目前最熱門的趨勢之一,有不少好的論點解釋了人們爲何想在代碼中使用它。我並不打算在這裏詳細介紹全部函數式編程的概念和想法,而是會盡力給你演示在平常狀況下和 JavaScript 打交道的時候如何用上這種編程。javascript

函數式編程是一種編程範例,它將計算機運算視爲數學上的函數計算,而且避免了狀態的改變和易變的數據。前端

從新定義函數

在深刻接觸 JavaScript 的函數式編程範例以前,我們得先知道什麼是高階函數、它的用途以及這個定義自己究竟有什麼含義。高階函數既能夠把函數當成參數來接收,也能夠做爲把函數做爲結果輸出。你須要記住 函數其實也是一種值,也就是說你能夠像傳遞變量同樣去傳遞函數。java

因此呢,在 JavaScript 裏你能夠這麼作:android

// 建立函數
function f(x){
  return x * x;
}
// 調用該函數
f(5); // 25

// 建立匿名函數
// 並賦值一個變量
var g = function(x){
  return x * x;
}
// 傳遞函數
var h = g;
// And use it
h(5); // 25
複製代碼
把函數當成值來使用

一旦使用上面這個技巧,你的代碼更容易被重複利用,同時功能也更增強大。我們都經歷過這樣的狀況:想要把一個函數傳到另外一個函數裏去執行任務,但須要寫一些額外的代碼來實現這一點,對吧?使用函數式編程的話,你將再也不須要寫額外的代碼,而且可使你的代碼變得很乾淨、易於理解。ios

有一點要注意,正確的泛函代碼的特色是沒有反作用,也就是說函數應該只依賴於它們的參數輸入,而且不該以任何方式影響到外界環境。這個特色有重要的含義,舉個例子:若是傳遞進函數的參數相同,那麼輸出的結果也老是相同的;若是一個被調用的函數所輸出的結果並無被用到,那麼這個結果即便被刪掉也不會影響別的代碼。git


使用數組原型的內置方法

Array.prototype 應該是你學習 JavaScript 函數式編程的第一步,它涵蓋了不少數組轉化的實用方法,這些方法在現代網頁應用裏至關的常見。github

先來看看這個叫 Array.prototype.sort() 的方法會很不錯,由於這個轉化挺直白的。顧名思義,咱能夠用這個方法來給數組排序.sort() 只接收一個參數(即一個用於比較兩個元素的函數)。若是第一個元素在第二個元素的前面,結果返回的是負值。反之,則返回正值。面試

排序聽起來很是簡單,然而當你須要給比通常數字數組複雜得多的數組排序時,可能就不那麼簡單了。在下面這個例子裏,咱們有一個對象的數組,裏面存的是以磅(lbs)或千克(kg)爲單位的體重,我們須要對這些人的體重進行升序排列。代碼看起來會是這樣:編程

// 我們這個比較函數的定義
var sortByWeight = function(x,y){
  var xW = x.measurement == "kg" ? x.weight : x.weight * 0.453592;
  var yW = y.measurement == "kg" ? y.weight : y.weight * 0.453592;
  return xW > yW ? 1 : -1;
}

// 兩組數據有細微差異
// 要根據體重來對它們進行排序
var firstList = [
  { name: "John", weight: 220, measurement: "lbs" },
  { name: "Kate", weight: 58, measurement: "kg" },
  { name: "Mike", weight: 137, measurement: "lbs" },
  { name: "Sophie", weight: 66, measurement: "kg" },
];
var secondList = [
  { name: "Margaret", weight: 161, measurement: "lbs", age: 51 },
  { name: "Bill", weight: 76, measurement: "kg", age: 62 },
  { name: "Jonathan", weight: 72, measurement: "kg", age: 43 },
  { name: "Richard", weight: 74, measurement: "kg", age: 29 },
];

// 用開頭定義的函數
// 對兩組數據進行排序
firstList.sort(sortByWeight); // Kate, Mike, Sophie, John 
secondList.sort(sortByWeight); // Jonathan, Margaret, Richard, Bill
複製代碼
用函數式編程來對兩個數組進行排序的例子

在上面的例子裏,你能夠很清楚地觀察到使用高階函數帶來的好處:節省了空間、時間,也讓你的代碼更能被讀懂、更容易被重複利用。若是你不打算用 .sort() 來寫的話,你得另外寫兩個循環並重復大部分的邏輯。坦率來講,那樣將致使更冗長、臃腫且不易理解的代碼。後端


一般你對數組的操做也不單只是排序而已。就個人經驗而言,根據屬性來過濾一個數組很常見,並且沒有什麼方法比 Array.prototype.filter() 更加合適。過濾數組並不困難,由於你只需將一個函數做爲參數,對於那些須要被過濾掉的元素,該函數會返回 false。反之,該函數會返回 true。很簡單,不是嗎?我們來看看實例:

// 一羣人的數組
var myFriends = [
  { name: "John", gender: "male" },
  { name: "Kate", gender: "female" },
  { name: "Mike", gender: "male" },
  { name: "Sophie", gender: "female" },
  { name: "Richard", gender: "male" },
  { name: "Keith", gender: "male" }
];

// 基於性別的簡易過濾器
var isMale = function(x){
  return x.gender == "male";
}

myFriends.filter(isMale); // John, Mike, Richard, Keith
複製代碼
關於過濾的一個簡單例子

雖然 .filter() 會返回數組中全部符合條件的元素,你也能夠用 Array.prototype.find() 提取數組中第一個符合條件的元素,或是用 Array.prototype.findIndex() 來提取數組中第一個匹配到的元素索引。同理,你可使用 Array.prototype.some() 來測試是否至少有一個元素符合條件,抑或是用 Array.prototype.every() 來檢查是否全部的元素都符合條件。這些方法在某些應用中能夠變得至關有用,因此我們來看一個囊括了這幾種方法的例子:

// 一組關於分數的數組
// 不是每一項都標註了人名
var highScores = [
  {score: 237, name: "Jim"},
  {score: 108, name: "Kit"},
  {score: 91, name: "Rob"},
  {score: 0},
  {score: 0}
];

// 這些簡單且能重複使用的函數
// 是用來查看每一項是否有名字
// 以及分數是否爲正數
var hasName = function(x){
  return typeof x['name'] !== 'undefined';
}
var hasNotName = function(x){
  return !hasName(x);
}
var nonZeroHighScore = function(x){
  return x.score != 0;
}

// 填充空白的名字,直到全部空白的名字都有「---」
while (!highScores.every(hasName)){
  var highScore = highScores.find(hasNotName);
  highScore.name = "---";
  var highScoreIndex = highScores.findIndex(hasNotName);
  highScores[highScoreIndex] = highScore;
}

// 檢查非零的分數是否存在
// 並在 console 裏輸出
if (highScores.some(nonZeroHighScore))
  console.log(highScores.filter(nonZeroHighScore));
else 
  console.log("No non-zero high scores!");

複製代碼
使用函數式編程來構造數據

到這一步,你應該會有些融會貫通的感受了。上面的例子清楚地體現出高階函數是如何使你避免了大量重複且難以理解的代碼。這個例子雖然簡單,但你也能看出代碼的簡潔之處,與你在未使用函數式編程範例時所編寫的內容造成鮮明對比。


先撇開上面例子裏複雜的邏輯,我們有的時候只想要將數組轉化成另外一個數組,且無需對數組裏的數據作那麼多的改變。這個時候 Array.prototype.map() 就派上用場了,咱們能夠用這個方法來轉化數組中的對象。.map()和以前例子所用到的方法並不相同,區別在於其做爲參數的高階函數會返回一個對象,能夠是任何你想寫的對象。讓我用一個簡單的例子來演示一下:

// 一個有 4 個對象的數組
var myFriends = [
  { name: "John", surname: "Smith", age: 52},  
  { name: "Sarah", surname: "Smith", age: 49},  
  { name: "Michael", surname: "Jones", age: 46},  
  { name: "Garry", surname: "Thomas", age: 48}
];

// 一個簡單的函數
// 用來把名和姓放在一塊兒
var fullName = function(x){
  return x.name + " " + x.surname;
}

myFriends.map(fullName);
// 應輸出
// ["John Smith", "Sarah Smith", "Michael Jones", "Garry Thomas"]
複製代碼
對數組裏的對象進行 mapping 操做

從上面這個例子能夠看出,一旦對數組使用了 .map() 方法,很容易就能獲得一個僅包含我們所需屬性的數組。在這個例子裏,咱只想要對象中 namesurname 這兩行字符串,因此才使用簡單的 mapping(譯者注:即便用 map 方法) 來利用原來包含不少對象的數組上建立了另外一個只包含字符串的數組。Mapping 這種方式可能比你想象的還要經常使用,它在每一個網頁開發者的口袋中能夠成爲很強大的工具。因此說,這整篇文章裏你若是別的沒記住的話,不要緊,但千萬要記住如何使用 .map()


最後還有一點很是值得你注意,那就是常規目的數組轉化中的 Array.prototype.reduce().reduce() 與上面提到的全部方法都有所不一樣,由於它的參數不只僅是一個高階函數,還包含一個累加器。一開始聽起來可能有些使人困惑,因此先看一個例子來幫助你理解 .reduce() 背後的基礎概念吧:

// 關於不一樣公司支出的數組
var oldExpenses = [
  { company: "BigCompany Co.", value: 1200.10},
  { company: "Pineapple Inc.", value: 3107.02},
  { company: "Office Supplies Inc.", value: 266.97}
];
var newExpenses = [
  { company: "Office Supplies Inc.", value: 108.11},
  { company: "Megasoft Co.", value: 1208.99}
];

// 簡單的求和函數
var sumValues = function(sum, x){
  return sum + x.value;
}

// 將第一個數組降爲幾個數值之和
var oldExpensesSum = oldExpenses.reduce(sumValues, 0.0);
// 將第二個數組降爲幾個數值之和
console.log(newExpenses.reduce(sumValues, oldExpensesSum)); // 5891.19
複製代碼
將數組降爲和值

對於任何曾經把數組中的值求和的人來講,理解上面這個例子應該不會特別困難。一開始我們定義了一個可重複使用的高階函數,用於把數組中的 value 都加起來。以後,我們用這個函數來給第一個數組中的支出數值求和,並把求出來的值當成初始值,而不是從零開始地去累加第二個數組中的支出數值。因此最後得出的是兩個數組的支出數值總和。

固然了,.reduce() 能夠作的事情遠不止在數組中求和而已。大多數別的方法解決不了的複雜轉化,均可以使用 .reduce() 與一個數組或對象的累加器來輕鬆解決。一個實用的例子是轉化一個有不少篇文章的數組,每一篇文章有一個標題和一些標籤。原來的數組會被轉化成標籤的數組,每一項中有使用該標籤的文章數目以及這些文章的標題構成的數組。我們來看看代碼:

// 一個帶有標籤的文章的數組
var articles = [
  {title: "Introduction to Javascript Scope", tags: [ "Javascript", "Variables", "Scope"]},
  {title: "Javascript Closures", tags: [ "Javascript", "Variables", "Closures"]},
  {title: "A Guide to PWAs", tags: [ "Javascript", "PWA"]},
  {title: "Javascript Functional Programming Examples", tags: [ "Javascript", "Functional", "Function"]},
  {title: "Why Javascript Closures are Important", tags: [ "Javascript", "Variables", "Closures"]},
];

// 一個可以將文章數組降爲標籤數組的函數
// 
var tagView = function(accumulator, x){
  // 針對文章的標籤數組(原數組)裏的每個標籤
  x.tags.forEach(function(currentTag){
    // 寫一個函數看看標籤是否匹配
    var findCurrentTag = function(y) { return y.tag == currentTag; };
    // 檢查是否該標籤已經出如今累積器數組
    if (accumulator.some(findCurrentTag)){
      // 找到標籤並得到索引
      var existingTag = accumulator.find(findCurrentTag);
      var existingTagIndex = accumulator.findIndex(findCurrentTag);
      // 更新使用該標籤的文章數目,以及文章標題的列表
      accumulator[existingTagIndex].count += 1;
      accumulator[existingTagIndex].articles.push(x.title);
    }
    // 不然就在累積器數組中增添標籤
    else {
      accumulator.push({tag: currentTag, count: 1, articles: [x.title]});
    }
  });
  // 返回累積器數組
  return accumulator;
}

// 轉化原數組
articles.reduce(tagView,[]);
// 輸出:
/*
[
 {tag: "Javascript", count: 5, articles: [
    "Introduction to Javascript Scope", 
    "Javascript Closures",
    "A Guide to PWAs", 
    "Javascript Functional Programming Examples",
    "Why Javascript Closures are Important"
 ]},
 {tag: "Variables", count: 3, articles: [
    "Introduction to Javascript Scope", 
    "Javascript Closures",
    "Why Javascript Closures are Important"
 ]},
 {tag: "Scope", count: 1, articles: [ 
    "Introduction to Javascript Scope" 
 ]},
 {tag: "Closures", count: 2, articles: [
    "Javascript Closures",
    "Why Javascript Closures are Important"
 ]},
 {tag: "PWA", count: 1, articles: [
    "A Guide to PWAs"
 ]},
 {tag: "Functional", count: 1, articles: [
    "Javascript Functional Programming Examples"
 ]},
 {tag: "Function", count: 1, articles: [
    "Javascript Functional Programming Examples"
 ]}
]
*/
複製代碼
使用 reduce() 來進行一項複雜的轉化

上面這個例子可能看起來會有些小複雜,因此須要一步一步來研究。首先呢,咱想要的最終結果是一個數組,因此累加器的初始值就成了[]。而後,咱想要數組中的每個對象都包含標籤名、使用該標籤的文章數目以及文章標題的列表。不但如此,每個標籤在數組中只能出現一次,因此咱必須用 .some().find().findIndex() 來檢查標籤是否存在,以後將現有標籤的對象進行轉化,而不是另加一個新的對象。

棘手的地方在於,咱不能定義一個函數來檢查每一個標籤是否都存在(不然須要 7 個不一樣的函數)。因此我們纔在當前標籤的循環裏定義高階函數,這樣一來就能夠再次使用高階函數,避免重寫代碼。對了,其實這也能夠經過 Currying 來完成,但我不會在本文中解釋這個技巧。

當我們在累加器數組中獲取標籤的對象以後,只須要把使用該標籤的文章數目遞增,而且將當前標籤下的文章添加到其文章數組中就好了。最後,我們返回累加器,大功告成。仔細閱讀的話會發現代碼不但很是簡短,並且很容易理解。相同狀況下,非函數式編程的代碼將會看起來很是使人困惑,並且明顯會更冗雜。

結語

函數式編程做爲目前最熱門的趨勢之一,是有其充分緣由的。它使我們在寫出更清晰、更精簡和更「吝嗇」代碼的同時,沒必要去擔憂反作用和狀態的改變。JavaScript 的 [Array.prototype](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/prototype) 方法在許多平常狀況下很是實用,而且讓我們在對數組進行簡單和複雜的轉化,也沒必要去寫太多重複的代碼。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索