- 原文地址:Master the JavaScript Interview: What is Functional Programming?
- 原文做者:Eric Elliott
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:zoomdong
- 校對者:Roc,Long Xiong
「掌握 JavaScript 面試」 是一系列的帖子,爲了幫助求職者在面試中高級 JavaScript 職位時可能碰見的常見問題作準備。這些是我在真實面試場景中常常會問到的一些問題。javascript
函數式編程已經成爲 JavaScript 領域中一個很是熱門的話題。就在幾年前,甚至不多有 JavaScript 程序員知道什麼是函數式編程,可是我在過去 3 年看到的每一個大型應用程序代碼庫中都大量使用了函數式編程思想。前端
函數式編程(一般縮寫爲 FP)是經過組合純函數,避免狀態共享、可變數據和反作用來構建軟件的過程。函數式編程是聲明式的,而不是命令式的,應用程序狀態經過純函數流動。與面向對象編程不一樣,在面向對象編程中,應用程序狀態一般與對象中的方法共享和協做。java
函數式編程是一種編程範式,這意味着它是一種基於一些基本的、定義性的原則(如上所列)來思考軟件構建的方法。其餘編程範式包括面向對象編程和麪向過程編程。node
與命令式或面向對象的代碼相比,函數式代碼每每更簡潔、更可預測、更易於測試 —— 但若是你不熟悉它以及與之相關的常見模式,函數式代碼看起來也會密集得多,並且相關的文檔對於新人來講多是難以理解的。android
若是你開始在 Google 上搜索函數式編程術語,你很快就會遇到一堵學術術語的牆,這對初學者來講是很是可怕的。說它有一個學習曲線是很是保守的說法。可是若是你已經使用 JavaScript 編程了一段時間,那麼你極可能已經在實際的軟件應用中使用了大量的函數式編程的概念和實用工具。ios
不要讓全部生詞嚇跑你。它們比聽起來容易多了。git
最困難的部分是吸取(或者理解)這些詞彙。在你開始理解函數式編程的含義以前,上面這個看似簡單的定義中有不少須要理解的概念:程序員
換句話說,若是你想知道函數式編程在實踐中意味着什麼,你必須首先理解這些核心概念。github
純函數指的是具備下列特徵的函數:面試
純函數有許多在函數式編程中很重要的屬性,包括引用透明性(你可使用函數一次調用的結果值代替其他對該函數的調用操做,這樣並不會對程序產生影響)。閱讀「什麼是純函數?」瞭解更多。
組合函數是將兩個或兩個以上的函數組合起來以產生一個新函數或進行某種計算的過程。例如,f . g
組合(. 的意思是組合)在 JavaScript 中等價於 f(g(x))
。理解組合函數是理解如何使用函數式編程構建軟件的重要一步。閱讀 「什麼是組合函數?」瞭解更多。
狀態共享是指共享做用域中存在的任何變量、對象或內存空間,或者是在做用域之間傳遞的對象的屬性。共享做用域能夠包括全局做用域或閉包做用域。一般,在面向對象編程中,經過向其餘對象添加屬性,在做用域之間共享對象。
例如,計算機遊戲可能有一個主遊戲對象,其中的角色和遊戲項存儲爲該對象所擁有的屬性。函數式編程避免了狀態共享,而是依賴不可變的數據結構和純計算從現有數據中派生出新數據。有關函數式軟件如何處理應用程序狀態的更多詳細信息,請參閱「10個更好的 Redux 架構提示」。
共享狀態的問題在於,爲了瞭解函數的效果,你必需要了解函數使用或影響的每一個共享變量的所有歷史記錄。
假設你有一個須要保存的 user
對象。saveUser()
函數向服務器上的 API 發出請求。在此過程當中,用戶使用 updateAvatar()
更改他們的我的頭像,並觸發另外一個 saveUser()
請求。在保存時,服務器發送回一個規範的 user
對象,爲了同步服務端或者其餘客戶端 API 引發的更改,該對象應該替換掉內存中對應的對象。
不幸的是,第二個響應在第一個響應以前被接收,因此當第一個(如今已通過時了)響應被返回時,新的我的頭像會在內存中被刪除並替換爲舊的我的頭像。這就是一個競爭條件的例子 —— 與狀態共享相關的很是常見的錯誤。
與共享狀態相關的另外一個常見問題是,更改調用函數的順序可能會致使一系列故障,由於做用於共享狀態的函數依賴於時序:
// 在共享狀態下,函數調用的順序會更改函數調用的結果。
const x = {
val: 2
};
const x1 = () => x.val += 1;
const x2 = () => x.val *= 2;
x1();
x2();
console.log(x.val); // 6
// 這個例子和上面徹底相同,除了對象名稱
const y = {
val: 2
};
const y1 = () => y.val += 1;
const y2 = () => y.val *= 2;
// 函數調用的順序被顛倒了
y2();
y1();
// 從而改變告終果的值
console.log(y.val); // 5
複製代碼
當避免狀態共享時,函數調用的時間和順序不會更改調用函數的結果。對於純函數,給定相同的輸入,老是獲得相同的輸出。這使得函數調用時徹底獨立於其餘函數調用,這能夠從根本上簡化更改和重構。一個函數的變化,或者函數調用的時間不會影響程序的其餘部分。
const x = {
val: 2
};
const x1 = x => Object.assign({}, x, { val: x.val + 1});
const x2 = x => Object.assign({}, x, { val: x.val * 2});
console.log(x1(x2(x)).val); // 5
const y = {
val: 2
};
// 因爲不依賴於外部變量
// 咱們不須要不一樣的函數來操做不一樣的變量
// 此處故意留空
// 由於函數不會發生變化
// 因此能夠按任意順序屢次調用這些函數
// 而沒必要更改其餘函數調用的結果
x2(y);
x1(y);
console.log(x1(x2(y)).val); // 5
複製代碼
在上面的例子中,咱們使用 Object.assign()
並傳入一個空對象做爲第一個參數來複制 x
的屬性,而不是在原數據上進行修改。在以前的示例中,它至關於從零開始建立一個新對象,而不使用 object.assign()
,但這是 JavaScript 中建立現有狀態副本的常見模式,而不是使用突變的常見模式,咱們在第一個示例中證實了這一點。
若是仔細觀察這個例子中的 console.log()
語句,你應該會注意到我已經提到的一些東西:組合函數。回想一下前面,組合函數相似這樣:f(g(x))
。在本例中,咱們將組合 x1 . x2
中的 f()
和 g()
替換爲 x1()
和 x2()
。
固然,若是你改變了組合的順序,輸出結果一樣會改變。操做的順序一樣很重要。f(g(x))
並不老是和 g(f(x))
相同,但再也不重要的是函數外的變量發生了什麼,這很重要。對於非純函數,除非你知道函數使用或影響的每一個變量的整個歷史記錄,不然不可能徹底理解函數的做用。
移除函數調用計時依賴項,就消除了一整類的潛在 bug。
不可變對象是指建立後不能修改的對象。相反,可變對象是在建立後能夠修改的對象。
不變性是函數式編程的核心概念,由於沒有它,程序中的數據流是有損的。狀態歷史被拋棄,奇怪的 bug 可能會潛入你的軟件。更多關於不變性的意義,請參閱「不變性之道」。
在 JavaScript 中,重要的是不要混淆 const
和不變性。const
建立一個變量名綁定,該綁定在建立後不能從新分配。const
不建立不可變對象。不能更改綁定所引用的對象,但仍然能夠更改對象的屬性,這意味着使用 const
建立的綁定是可變的,而不是不可變的。
不可變對象根本不能更改。經過深度凍結對象,可使值真正不可變。JavaScript 有一種方法能夠將對象凍結一層:
const a = Object.freeze({
foo: 'Hello',
bar: 'world',
baz: '!'
});
a.foo = 'Goodbye';
// Error: Cannot assign to read only property 'foo' of object Object
複製代碼
可是凍結的對象只是表面上不可變。例如,如下對象是可變的:
const a = Object.freeze({
foo: { greeting: 'Hello' },
bar: 'world',
baz: '!'
});
a.foo.greeting = 'Goodbye';
console.log(`${ a.foo.greeting }, ${ a.bar }${a.baz}`);
複製代碼
正如你所看到的,凍結對象的頂層基本屬性不能改變,可是裏面的任何對象屬性(包括數組等)仍然能夠改變 —— 因此即便是凍結的對象也不是不可變的,除非你遍歷整個對象樹並凍結每一個對象屬性。
在許多函數式編程語言中,有一種特殊的不可變的數據結構稱爲 trie 數據結構(發音同「tree」),它其實是深度凍結的 —— 這意味着不管屬性處於對象層次結構中的哪一個層級,都不能夠改變。
Tries 使用了共享結構在不可變對象被複制以後爲對象共享引用內存地址,該方法使用較少的內存,而且使得在一些操做下的性能獲得提高。
例如,你能夠在對象對的根節點進行一致性比較來比較兩個對象是否一致。若是一致的話,你就不須要再遍歷整個對象樹查找差別了。
JavaScript 中有幾個庫使用到了 tries,包括 Immutable.js 和 Mori。
我嘗試過這兩種方法,而且傾向於在須要大量不可變狀態的大型項目中使用 Immutable.js。有關更多信息,請參見「10個更好的Redux架構技巧」。
反作用是指任何應用程序狀態的改變都是能夠在被調用函數以外觀察到的,除了返回值。反作用包括:
在函數式編程中,一般會避免產生反作用,這使得程序的做用更易於理解和測試。
Haskell 和其餘函數語言常用 monad 從純函數中分離和封裝反作用。有關 monad 的話題的深度足以寫一本書來討論,因此咱們之後再談。
你如今須要知道的是,反作用操做須要與軟件的其餘部分隔離開來。若是你將反作用與其餘的程序邏輯隔離開,你的軟件將更容易擴展、重構、調試、測試和維護。
這就是大多數前端框架鼓勵用戶在單獨的、鬆散耦合的模塊中管理狀態和組件渲染的緣由。
函數式編程傾向於重用一組通用的函數式實用程序來處理數據。面向對象編程傾向於將方法和數據集中在對象中。這些協做方法只能對它們被設計用於操做的數據類型進行操做,並且一般只能對特定對象實例中包含的數據進行操做。
在函數式編程中,任何類型的數據都是平等的。同一個 map()
api 能夠映射對象、字符串、數字或任何其餘數據類型,由於它以一個函數做爲參數,該參數適當地處理給定的數據類型。FP 使用了高階函數完成它的通用實用技巧。
JavaScript 中函數是頭等公民,這些函數容許,它容許咱們將函數做爲數據 —— 將其賦給變量、傳遞給其餘函數、從函數返回等等。
高階函數是那些函數做爲參數、返回值爲函數或二者兼有的函數。高階函數一般用於:
函子是能夠映射的。換句話說,它是一個容器,它有一個接口,可用於將函數應用於其中的值。當你看到函子這個詞時,你應該想到「可映射」。
前面咱們瞭解了 map()
工具程序能夠做用於各類數據類型。它經過提高映射操做來使用函子 API 來實現這一點。map()
使用的重要流控制操做利用了該接口。對於 array.prototype.map()
,容器是一個數組,可是其餘數據結構也能夠是函子 —— 只要它們提供了映射 API。
讓咱們看看 Array.prototype.map()
如何容許你從映射實用程序中提取數據類型,使 map()
可用於任何數據類型。咱們將建立一個簡單的 double()
映射,它將傳入的任何值乘以 2:
const double = n => n * 2;
const doubleMap = numbers => numbers.map(double);
console.log(doubleMap([2, 3, 4])); // [ 4, 6, 8 ]
複製代碼
若是咱們想要在遊戲中對目標進行操做以使他們所得到的點數翻倍該怎麼辦?咱們所要作的就是對 double()
函數作一些細微的修改,而後將其傳遞給 map()
,這樣一切仍然能夠正常工做:
const double = n => n.points * 2;
const doubleMap = numbers => numbers.map(double);
console.log(doubleMap([
{ name: 'ball', points: 2 },
{ name: 'coin', points: 3 },
{ name: 'candy', points: 4}
])); // [ 4, 6, 8 ]
複製代碼
爲了使用通用實用函數來操做任意數量的不一樣數據類型,須要使用像函子和高階函數這樣的抽象,這個概念是很重要的。你將看到一個相似的概念以各類不一樣的方式應用。
「隨着時間推移表示的列表是一個流。」
如今你須要瞭解的是,數組和函子並非容器和容器中的值這一律念應用的惟一方式。例如,數組只是事物的列表。隨着時間的推移,一個列表是一個流,所以你可使用相同類型的實用程序來處理傳入事件的流 —— 當你開始用 FP 構建真正的軟件時,你會看到不少這種狀況。
函數式編程是一種聲明性的範式,這意味着程序邏輯的表達沒有顯式地描述流控制。
命令式程序花費幾行代碼來描述用於實現預期結果的特定步驟 —— 流控制:如何作事情。
聲明性程序抽象了流控制過程,花費幾行代碼來描述數據流:應該作什麼。如何被抽象出來。
例如,這個命令式映射接受一個數字數組,並返回一個新數組,其中每一個數字都被乘以2:
const doubleMap = numbers => {
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
return doubled;
};
console.log(doubleMap([2, 3, 4])); // [4, 6, 8]
複製代碼
這個聲明式映射也作了一樣的事情,可是使用Array.prototype.map()
函數式實用程序將流控件抽象出來,它容許你更清楚地表示數據流:
const doubleMap = numbers => numbers.map(n => n * 2);
console.log(doubleMap([2, 3, 4])); // [4, 6, 8]
複製代碼
命令式代碼常用語句。語句是執行某些操做的一段代碼。經常使用的語句包括 for
、if
、switch
、throw
等。
聲明式代碼更多地依賴於表達式。表達式是計算某個值的一段代碼。表達式一般是函數調用、值和運算符的組合,它們被用於計算出結果。
下面是表達式的一些例子:
2 * 2
doubleMap([2, 3, 4])
Math.max(4, 3, 2)
複製代碼
一般在代碼中,你會看到表達式被分配給標識符、從函數返回或傳遞到函數中。在被分配、返回或傳遞以前,表達式會先進行計算,實際使用的是其結果值。
函數式編程傾向於:
學習和練習這些函數式數組的核心功能:
.map()
.filter()
.reduce()
This post was included in the book 「Composing Software」. Buy the Book | Index | < Previous | Next >
Eric Elliott 是一名分佈式系統專家,而且是 《組合軟件》和《編寫 JavaScript 程序》這兩本書的做者。做爲 EricElliottJS.com 和 DevAnywhere.io 的聯合創始人,他教開發人員遠程工做和實現工做以及生活平衡所需的技能。他建立了加密項目的開發團隊,併爲他們提供建議。他還在軟件體驗上爲 Adobe 系統、Zumba Fitness、華爾街日報、ESPN、BBC 以及包括 Usher、Frank Ocean、Metallica 等在內的頂級唱片藝術家作出了貢獻。
他和世界上最漂亮的女人一塊兒享受着遠程(工做)的生活方式。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。