學會JavaScript面試: 什么是函數(shù)式編程

原文: Master the JavaScript Interview: What is Functional Programming?

譯文: 什么是函數(shù)式編程?什么是命令式?聲明式 --- 一起學習可以watch,歡迎star

image.png

“掌握JavaScript面試”是一系列帖子,旨在幫助候選人準備他們在申請中高級JavaScript職位時可能遇到的常見問題。這些是我經(jīng)常在現(xiàn)實面試中使用的問題。

函數(shù)式編程已成為js領(lǐng)域里一個非常熱門的話題。就在幾年前,甚至很少有JavaScript程序員知道函數(shù)式編程是什么,但是我在過去3年中看到的每個大型應(yīng)用程序代碼庫都大量使用了函數(shù)式編程思想。

函數(shù)式編程(通??s寫為FP)是通過編寫純函數(shù),避免共享狀態(tài)、可變數(shù)據(jù)、副作用 來構(gòu)建軟件的過程。數(shù)式編程是聲明式 的而不是命令式 的,應(yīng)用程序的狀態(tài)是通過純函數(shù)流動的。與面向?qū)ο缶幊绦纬蓪Ρ?,面向?qū)ο笾袘?yīng)用程序的狀態(tài)通常與對象中的方法共享和共處。

函數(shù)式編程是一種編程范式 ,這意味著它是一種基于一些基本的定義原則(如上所列)思考軟件構(gòu)建的方式。當然,編程范示的其他示例也包括面向?qū)ο缶幊毯瓦^程編程。

函數(shù)式的代碼往往比命令式或面向?qū)ο蟮拇a更簡潔,更可預測,更容易測試 - 但如果不熟悉它以及與之相關(guān)的常見模式,函數(shù)式的代碼也可能看起來更密集雜亂,并且 相關(guān)文獻對新人來說是不好理解的。

如果你開始使用谷歌搜索功能性編程術(shù)語,你將很快發(fā)現(xiàn)學術(shù)術(shù)語的一堵墻,對于初學者來說這可能是望而生畏的。說它有一個學習曲線是一個嚴重的輕描淡寫。但是如果你已經(jīng)用JavaScript編程了一段時間,很可能你在真實程序中使用了很多函數(shù)式編程概念和實用程序。

不要讓所有的新詞匯嚇到你。它比聽起來容易得多。

最難的部分是那些你不熟悉的詞匯。在你開始掌握函數(shù)式編程的思想之前,上面定義中有很多想法需要去理解:

  • Pure functions(純函數(shù))
  • Function composition(函數(shù)組合)
  • Avoid shared state(避免狀態(tài)共享)
  • Avoid mutating state(避免狀態(tài)改變)
  • Avoid side effects(避免副作用)

換句話說,如果你想知道函數(shù)式編程在實踐中有什么意義,你必須首先理解這些核心概念。

pure function(純函數(shù))是這樣一個函數(shù):

  • 輸入的參數(shù)相同,返回的結(jié)果相同
  • 無副作用的

純函數(shù)在函數(shù)式編程中有許多重要的屬性,包括引用透明性(就是可以使用其結(jié)果值替換函數(shù)調(diào)用而不更改程序的含義)。閱讀 “什么是純函數(shù)?” 獲取更多信息。

共享狀態(tài)

享狀態(tài)是存在于共享作用域中的任何變量,對象或內(nèi)存空間,或者是在作用域之間傳遞的屬性。共享范圍可以包括全局范圍或閉包范圍。通常,在面向?qū)ο蟮木幊讨?,通過向其他對象添加屬性來在此范圍之間共享對象。

例如,計算機游戲可能具有主游戲?qū)ο?,其中角色和游戲物品存儲為該對象擁有的屬性。函?shù)式編程避免了共享狀態(tài) - 而是依賴于不可變數(shù)據(jù)結(jié)構(gòu)和單純計算來從現(xiàn)有數(shù)據(jù)中獲取新數(shù)據(jù)。有關(guān)功能軟件如何處理應(yīng)用程序狀態(tài)的更多詳細信息,請參閱10個更好的Redux架構(gòu)提示。

共享狀態(tài)的問題在于,為了理解函數(shù)的效果,必須得知道函數(shù)使用或影響的每個共享變量的完整歷史記錄。

想象下,你有一個需要保存的用戶對象。你有一個saveUser函數(shù)去請求服務(wù)端API。當發(fā)生這種情況時,用戶使用updateAvatar方法更改其個人資料圖片并觸發(fā)另一個saveUser請求。在保存時,服務(wù)器返回一個規(guī)范的用戶對象,該對象應(yīng)該替換內(nèi)存中的任何內(nèi)容,以便與服務(wù)器上發(fā)生的更改或響應(yīng)其他API調(diào)用同步。

不幸的是,第二個響應(yīng)在第一個響應(yīng)之前被接收,所以當?shù)谝粋€(現(xiàn)在過時的)響應(yīng)被返回時,新的內(nèi)容在內(nèi)存中被清除并被舊的替換。這是競爭條件的一個示例 - 與共享狀態(tài)相關(guān)的非常常見的錯誤。

與共享狀態(tài)相關(guān)的另一個常見問題是,更改調(diào)用函數(shù)的順序可能會導致級聯(lián)失敗,因為作用于共享狀態(tài)的函數(shù)與時序有關(guān):

// With shared state, the order in which function calls are made
// changes the result of the function calls.
const x = {
  val: 2
};

const x1 = () => x.val += 1;

const x2 = () => x.val *= 2;

x1();
x2();

console.log(x.val); // 6

// This example is exactly equivalent to the above, except...
const y = {
  val: 2
};

const y1 = () => y.val += 1;

const y2 = () => y.val *= 2;

// ...the order of the function calls is reversed...
y2();
y1();

// ... which changes the resulting value:
console.log(y.val); // 5

當您避免共享狀態(tài)時,函數(shù)調(diào)用的時間和順序不會更改調(diào)用函數(shù)的結(jié)果。使用純函數(shù),給定相同的輸入,您將始終獲得相同的輸出。這使得函數(shù)調(diào)用完全獨立于其他函數(shù)調(diào)用,這可以從根本上簡化更改和重構(gòu)。一個函數(shù)的更改或函數(shù)調(diào)用的時間不會波動并破壞程序的其他部分。

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
};

// Since there are no dependencies on outside variables,
// we don't need different functions to operate on different
// variables.

// this space intentionally left blank


// Because the functions don't mutate, you can call these
// functions as many times as you want, in any order, 
// without changing the result of other function calls.
x2(y);
x1(y);

console.log(x1(x2(y)).val); // 5

筆者: 看完這個例子你可能覺得不是很恰當,因為最后都是調(diào)用的時候的區(qū)別,上面的這個例子如果改變調(diào)用順序也是不可行的。x2(x1(y)).val ==> 6. 但是這個只是用來闡述一個狀態(tài)共享區(qū)別的思想。下面有解釋。

在上面的示例中,我們使用Object.assign并傳入一個空對象作為第一個參數(shù)來復制x的屬性而不是將其改變。在這種情況下,它只相當于從頭開始創(chuàng)建一個新對象,沒有Object.assign,但這是JavaScript中常見的模式,用于創(chuàng)建現(xiàn)有狀態(tài)的副本而不是使用突變。

如果你仔細觀察這個例子中的console.log()語句,你應(yīng)該注意到我已經(jīng)提到過的東西:函數(shù)組合?;叵胍幌轮?,函數(shù)組成如下所示:f(g(x))。在這種情況下,我們用組合的x1x2替換fgx1 . X2。

當然,如果更改組合的順序,輸出將會改變。運行秩序仍然很重要。f(g(x))并不總是等于g(f(x)),但是現(xiàn)在無關(guān)緊要的是函數(shù)之外的變量會發(fā)生什么,這很重要。使用非純函數(shù),除非你知道使用函數(shù)影響的每個變量的完整歷史記錄,否則無法完全理解函數(shù)的作用。

刪除函數(shù)調(diào)用時序依賴性,并消除整個類的潛在錯誤。

不可變性

不可變對象是在創(chuàng)建后無法修改的對象。相反,可變對象是可以在創(chuàng)建后修改的任何對象。

不可變性是函數(shù)式編程的核心概念,因為沒有它,程序中的數(shù)據(jù)流是有損的。狀態(tài)歷史被拋棄,奇怪的錯誤可能會蔓延到的軟件中。有關(guān)不變性的重要性的更多信息,請參閱不可變性之道。

在JavaScript中,要提醒的是不要將const與不可變性混淆。 const創(chuàng)建一個變量名綁定,在創(chuàng)建后無法重新分配。const不會創(chuàng)建不可變對象。不能更改綁定引用的對象,但仍然可以更改對象的屬性,這意味著使用const創(chuàng)建的綁定是可變的,而不是不可變的。(筆者認為:這個看情況,對于對象是這樣的,但是對于原始類型的就不是這樣的說法了。)

不可變對象根本無法更改。你可以通過深度凍結(jié)對象來使值真正不可變。JavaScript有一個凍結(jié)對象深度的方法(筆者:推薦這個鏈接查看freeze):

const a = Object.freeze({
  foo: 'Hello',
  bar: 'world',
  baz: '!'
});

a.foo = 'Goodbye';
// Error: Cannot assign to read only property 'foo' of object Object

但凍結(jié)的對象只是表面上不可變的。例如,以下對象是可變的:

const a = Object.freeze({
  foo: { greeting: 'Hello' },
  bar: 'world',
  baz: '!'
});

a.foo.greeting = 'Goodbye';

console.log(`${ a.foo.greeting }, ${ a.bar }${a.baz}`);

從上面可知,凍結(jié)對象的頂級原始屬性不能更改,但任何屬性為對象的(包括數(shù)組等)仍然可以更改。因此,即使凍結(jié)對象也不是不可變的,除非遍歷整個對象樹并凍結(jié)每個對象屬性。

在許多函數(shù)式編程語言中,有一些特殊的不可變數(shù)據(jù)結(jié)構(gòu)稱為trie數(shù)據(jù)結(jié)構(gòu) (發(fā)音為“tree”),它們被有效地深度凍結(jié) - 意味著無論屬性層次中的屬性級別如何,都不能更改任何屬性。

trie使用結(jié)構(gòu)共享來共享對象的所有部分的引用內(nèi)存位置,這些部分在操作復制對象之后保持不變,這使用較少的存儲器,并且對于某些類型的操作實現(xiàn)性能改進。

例如,可以在對象樹的根處使用標識進行比較。如果標識相同,則不必遍歷整個樹來檢查差異。

JavaScript中有幾個庫利用了嘗試,包括Immutable.jsMori。

我已經(jīng)對兩者進行了實驗,并且傾向于在需要大量不可變狀態(tài)的大型項目中使用Immutable.js。如需要了解更多,請查閱更好的Redux架構(gòu)的10個技巧

副作用

副作用是在被調(diào)用函數(shù)之外可以觀察到的除了返回值之外的任何應(yīng)用程序的狀態(tài)更改。副作用包括:

  • 修改任何外部變量或?qū)ο髮傩裕ɡ?,全局變量或父函?shù)作用域鏈中的變量)
  • 在控制臺log
  • 寫入屏幕
  • 寫入文件
  • 網(wǎng)絡(luò)請求
  • 觸發(fā)外部的程序
  • 調(diào)用帶有副作用的函數(shù)

在函數(shù)式編程中盡量避免副作用,這使得程序更容易理解,并且更容易測試。

Haskell和其他函數(shù)式語言經(jīng)常使用monad來隔離和封裝來自純函數(shù)的副作用。monads的主題足夠深入,可以寫一本書,所以我們將保存以供日后使用。

你現(xiàn)在需要知道的是,副作用操作需要與其他部分隔離開來。如果將副作用與程序邏輯的其余部分分開,則軟件將更容易擴展,重構(gòu),調(diào)試,測試和維護。

這就是大多數(shù)前端框架鼓勵用戶在單獨的,松散耦合的模塊中管理狀態(tài)和組件呈現(xiàn)的原因。

通過高階函數(shù)的可重用性

函數(shù)式編程傾向于重用一組通用的函數(shù)來處理數(shù)據(jù)。面向?qū)ο蟮木幊虄A向于在對象中共存方法和數(shù)據(jù)。這些方法只能對它們設(shè)計用于操作的數(shù)據(jù)類型進行操作,并且通常只對該特定對象實例中包含的數(shù)據(jù)進行操作。

在函數(shù)式編程中,任何類型的數(shù)據(jù)都是公平的。相同的map()函數(shù)可以映射對象,字符串,數(shù)字或任何其他數(shù)據(jù)類型,因為它將函數(shù)作為適當處理給定數(shù)據(jù)類型的參數(shù)。FP使用高階函數(shù)來實現(xiàn)。

avaScript有一等函數(shù),它允許我們將函數(shù)視為數(shù)據(jù) - 將它們分配給變量,將它們傳遞給其他函數(shù),從函數(shù)返回等等......

高階函數(shù)是將函數(shù)作為參數(shù),返回函數(shù)或兩者的任何函數(shù)。高階函數(shù)通常用于:

  • 使用回調(diào)函數(shù),promise,monad等抽象或隔離動作,作用(指副作用)或異步流控制......
  • 創(chuàng)建可以處理各種數(shù)據(jù)類型的工具
  • 部分將函數(shù)應(yīng)用于其參數(shù)或創(chuàng)建curried函數(shù)以用于重用或函數(shù)組合
  • 獲取函數(shù)列表并返回這些輸入函數(shù)的一些組合

容器(宿主),F(xiàn)unctors, 列表和流

仿函數(shù)(Functors)是可以映射的東西。換句話說,它是一個宿主,它有一個接口,可用于將函數(shù)應(yīng)用于其中的值。當你看到functor這個詞時,你應(yīng)該想到“可映射”。

之前我們了解到相同的map可以對各種數(shù)據(jù)類型起作用。它通過提升映射操作以使用仿函數(shù)API來實現(xiàn)。map使用的重要流控制操作利用了該接口。對于Array.prototype.map(),宿主是一個數(shù)組,但其他數(shù)據(jù)結(jié)構(gòu)也可以是仿函數(shù) - 只要它們提供映射API。

我們看看Array.prototype.map()如何允許您從映射實用程序中抽象數(shù)據(jù)類型,以使map()可用于任何數(shù)據(jù)類型。我們將創(chuàng)建一個簡單的double()映射,簡單地將任何傳入的值乘以2:

const double = n => n * 2;
const doubleMap = numbers => numbers.map(double);
console.log(doubleMap([2, 3, 4])); // [ 4, 6, 8 ]

如果我們想要對游戲中的目標進行操作以使其獲得的點數(shù)翻倍,該怎么辦?們所要做的就是對我們傳遞給mapdouble函數(shù)進行細微的更改,一切仍然有效:

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 ]

在函數(shù)式編程中,使用仿函數(shù)和高階函數(shù)等抽象來使用通用實用函數(shù)來操作任意數(shù)量的不同數(shù)據(jù)類型的概念非常重要。你可以看到類似的概念以不同的方式應(yīng)用。

“隨著時間的推移,所表達的列表是一個流?!?/p>

聲明與命令

函數(shù)式編程是一種聲明式范例,意味著在沒有明確描述流控制的情況下表達程序邏輯。

命令式程序花費一些代碼來描述用于實現(xiàn)所需結(jié)果的特定步驟 - 流程控制如何 做事。

聲明性程序抽象流程控制過程,而是花費一行代碼來描述數(shù)據(jù)流 :做什么 。如何抽象出來。

例如,這個命令式 映射采用數(shù)組并返回一個新數(shù)組,每個數(shù)字乘以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]

聲明性 映射執(zhí)行相同的操作,但使用Array.prototype.map抽象出流控制,這使您可以更清楚地表達數(shù)據(jù)流:

const doubleMap = numbers => numbers.map(n => n * 2);

console.log(doubleMap([2, 3, 4])); // [4, 6, 8]

命令式 代碼經(jīng)常使用語句。語句 是執(zhí)行某些操作的一段代碼。常用語句的示例包括for,if,switch,throw等...

聲明性 代碼更多地依賴于表達式。表達式 是一段代碼,其值為某個值。表達式通常是函數(shù)調(diào)用,值和運算符的某種組合,它們被計算以產(chǎn)生結(jié)果值。

這些都是表達式的例子:

2 * 2
doubleMap([2, 3, 4])
Math.max(4, 3, 2)

通常在代碼中,將看到表達式被分配給標識符,從函數(shù)返回或傳遞給函數(shù)。在分配,返回或傳遞之前,首先計算表達式,然后使用結(jié)果值。

總結(jié)

  • 純函數(shù)而不是共享狀態(tài)和副作用
  • 使用不可變性而不是可變性
  • 函數(shù)組合而不是命令式流程控制
  • 許多通用的,可重用的,它們使用更高階的函數(shù)來處理許多數(shù)據(jù)類型,而不是只對其處理數(shù)據(jù)進行操作的方法
  • 聲明性而非命令性代碼(做什么,而不是如何做)
  • 表達式而不是語句
  • 宿主和高階函數(shù)優(yōu)于ad-hoc多態(tài)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容