[譯] 函數式程序員的 JavaScript 簡介 (軟件編寫)(第三部分)

煙霧藝術魔方 — MattysFlicks — (CC BY 2.0)javascript

注意:這是「軟件編寫」系列文章的第三部分,該系列主要闡述如何在 JavaScript ES6+ 中從零開始學習函數式編程和組合化軟件(compositional software)技術(譯註:關於軟件可組合性的概念,參見維基百科 Composability)。後續還有更多精彩內容,敬請期待!
< 上一篇 | <<第一篇 | 下一篇 >前端

對於不熟悉 JavaScript 或 ES6+ 的同窗,這裏作一個簡短的介紹。不管你是 JavaScript 開發新手仍是有經驗的老兵,你均可能學到一些新東西。如下內容僅是淺嘗輒止,吊吊你們的興致。若是想知道更多,還需深刻學習。敬請期待吧。java

學習編程最好的方法就是動手編程。我建議您使用交互式 JavaScript 編程環境(如 CodePenBabel REPL)。react

或者,您也可使用 Node 或瀏覽器控制檯 REPL。android

表達式和值

表達式是能夠求得數據值的代碼塊。ios

下面這些都是 JavaScript 中合法的表達式:git

7;

7 + 1; // 8

7 * 2; // 14

'Hello'; // Hello複製代碼

表達式的值能夠被賦予一個名稱。執行此操做時,表達式首先被計算,取得的結果值被賦值給該名稱。對於這一點咱們將使用 const 關鍵字。這不是惟一的方式,但這將是你使用最多的,因此目前咱們就能夠堅持使用 constgithub

const hello = 'Hello';
hello; // Hello複製代碼

var、let 和 const

JavaScript 支持另外兩種變量聲明關鍵字:var,還有 let。我喜歡根據選擇的順序來考慮它們。默認狀況下,我選擇最嚴格的聲明方式:const。用 const 關鍵字聲明的變量不能被從新賦值。最終值必須在聲明時分配。這可能聽起來很嚴格,但限制是一件好事。這是個標識在提醒你「賦給這個名稱的值將不會改變」。它能夠幫你全面瞭解這個名稱的意義,而無需閱讀整個函數或塊級做用域。web

有時,給變量從新賦值頗有用。好比,若是你正在寫一個手動的強制性迭代,而不是一個更具功能性的方法,你能夠迭代一個用 let 賦值的計數器。npm

由於 var 能告訴你不多關於這個變量的信息,因此它是最無力的聲明標識。自從開始用 ES6,我就再也沒在實際軟件項目中有意使用 var 做聲明瞭。

注意一下,一個變量一旦用 letconst 聲明,任何再次聲明的嘗試都將致使報錯。若是你在 REPL(讀取-求值-輸出循環)環境中更喜歡多一些實驗性和靈活性,那麼建議你使用 var 聲明變量,與 letconst 不一樣,使用 var 從新聲明變量是合法的。

本文將使用 const 來讓您習慣於爲實際程序中用 const,而出於試驗的目的自由切換回 var

數據類型

目前爲止咱們見到了兩種數據類型:數字和字符串。JavaScript 也有布爾值(truefalse)、數組、對象等。稍後咱們再看其餘類型。

數組是一系列值的有序列表。能夠把它比做一個可以裝不少元素的容器。這是一個數組字面量:

[1, 2, 3];複製代碼

固然,它也是一個可被賦予名稱的表達式:

const arr = [1, 2, 3];複製代碼

在 JavaScript 中,對象是一系列鍵值對的集合。它也有字面量:

{
  key: 'value'
}複製代碼

固然,你也能夠給對象賦予名稱:

const foo = {
  bar: 'bar'
}複製代碼

若是你想將現有變量賦值給同名的對象屬性,這有個捷徑。你能夠僅輸入變量名,而不用同時提供一個鍵和一個值:

const a = 'a';
const oldA = { a: a }; // 長而冗餘的寫法
const oA = { a }; // 短小精悍!複製代碼

爲了好玩而已,讓咱們再來一次:

const b = 'b';
const oB = { b };複製代碼

對象能夠輕鬆合併到新的對象中:

const c = {...oA, ...oB}; // { a: 'a', b: 'b' }複製代碼

這些點是對象擴展運算符。它迭代 oA 的屬性並分配到新的對象中,oB 也是同樣,在新對象中已經存在的鍵都會被重寫。在撰寫本文時,對象擴展是一個新的試驗特性,可能尚未被全部主流瀏覽器支持,但若是你那不能用,還能夠用 Object.assign() 替代:

const d = Object.assign({}, oA, oB); // { a: 'a', b: 'b' }複製代碼

這個 Object.assign() 的例子代碼不多,若是你想合併不少對象,它甚至能夠節省一些打字。注意當你使用 Object.assign() 時,你必須傳一個目標對象做爲第一個參數。它就是那個源對象的屬性將被複制過去的對象。若是你忘了傳,第一個參數傳遞的對象將被改變。

以個人經驗,改變一個已經存在的對象而不建立一個新的對象經常引起 bug。至少至少,它很容易出錯。要當心使用 Object.assign()

解構

對象和數組都支持解構,這意味着你能夠從中提取值分配給命過名的變量:

const [t, u] = ['a', 'b'];
t; // 'a'
u; // 'b'

const blep = {
  blop: 'blop'
};

// 下面等同於:
// const blop = blep.blop;
const { blop } = blep;
blop; // 'blop'複製代碼

和上面數組的例子相似,你能夠一次解構屢次分配。下面這行你在大量的 Redux 項目中都能見到。

const { type, payload } = action;複製代碼

下面是它在一個 reducer(後面的話題再詳細說) 的上下文中的使用方法。

const myReducer = (state = {}, action = {}) => {
  const { type, payload } = action;
  switch (type) {
    case 'FOO': return Object.assign({}, state, payload);
    default: return state;
  }
};複製代碼

若是不想爲新綁定使用不一樣的名稱,你能夠分配一個新名稱:

const { blop: bloop } = blep;
bloop; // 'blop'複製代碼

讀做:把 blep.blop 分配給 bloop

比較運算符和三元表達式

你能夠用嚴格的相等操做符(有時稱爲「三等於」)來比較數據值:

3 + 1 === 4; // true複製代碼

還有另一種寬鬆的相等操做符。它正式地被稱爲「等於」運算符。非正式地能夠叫「雙等於」。雙等於有一兩個有效的用例,但大多數時候默認使用 === 操做符是更好的選擇。

其它比較操做符有:

  • > 大於
  • < 小於
  • >= 大於或等於
  • <= 小於或等於
  • != 不等於
  • !== 嚴格不等於
  • && 邏輯與
  • || 邏輯或

三元表達式是一個可讓你使用一個比較器來問問題的表達式,運算出的不一樣答案取決於表達式是否爲真:

14 - 7 === 7 ? 'Yep!' : 'Nope.'; // Yep!複製代碼

函數

JavaScript 支持函數表達式,函數能夠這樣分配名稱:

const double = x => x * 2;複製代碼

這和數學表達式 f(x) = 2x 是一個意思。大聲說出來,這個函數讀做 xf 等於 2x。這個函數只有當你用一個具體的 x 的值應用它的時候纔有意思。在其它方程式裏面你寫 f(2),就等同於 4

換種說話就是 f(2) = 4。您能夠將數學函數視爲從輸入到輸出的映射。這個例子裏 f(x) 是輸入數值 x 到相應的輸出數值的映射,等於輸入數值和 2 的乘積。

在 JavaScript 中,函數表達式的值是函數自己:

double; // [Function: double]複製代碼

你可使用 .toString() 方法看到這個函數的定義。

double.toString(); // 'x => x * 2'複製代碼

若是要將函數應用於某些參數,則必須使用函數調用來調用它。函數調用會接收參數而且計算一個返回值。

你可使用 <functionName>(argument1, argument2, ...rest) 調用一個函數。好比調用咱們的 double 函數,就加一對括號並傳進去一個值:

double(2); // 4複製代碼

和一些函數式語言不一樣,這對括號是有意義的。沒有它們,函數將不會被調用。

double 4; // SyntaxError: Unexpected number複製代碼

簽名

函數的簽名能夠包含如下內容:

  1. 一個 可選的 函數名。
  2. 在括號裏的一組參數。 參數的命名是可選的。
  3. 返回值的類型。

JavaScript 的簽名無需指定類型。JavaScript 引擎將會在運行時判定類型。若是你提供足夠的線索,簽名信息也能夠經過開發工具推斷出來,好比一些 IDE(集成開發環境)和使用數據流分析的 Tern.js

JavaScript 缺乏它本身的函數簽名語法,因此有幾個競爭標準:JSDoc 在歷史上很是流行,但它太過笨拙臃腫,沒有人會不厭其煩地維護更新文檔與代碼同步,因此不少 JS 開發者都棄坑了。

TypeScript 和 Flow 是目前的大競爭者。這兩者都不能讓我肯定地知道怎麼表達我須要的一切,因此我使用 Rtype,僅僅用於寫文檔。一些人倒退回 Haskell 的 curry-only Hindley–Milner 類型系統。若是僅用於文檔,我很樂意看到 JavaScript 能有一個好的標記系統標準,但目前爲止,我以爲當前的解決方案沒有能勝任這個任務的。如今,怪異的類型標記即便和你在用的不盡相同,也就將就先用着吧。

functionName(param1: Type, param2: Type) => Type複製代碼

double 函數的簽名是:

double(x: n) => n複製代碼

儘管事實上 JavaScript 不須要註釋簽名,知道何爲簽名和它意味着什麼依然很重要,它有助於你高效地交流函數是如何使用和如何構建的。大多數可重複使用的函數構建工具都須要你傳入一樣類型簽名的函數。

默認參數值

JavaScript 支持默認參數值。下面這個函數相似一個恆等函數(以你傳入參數爲返回值的函數),一旦你用 undefined 調用它,或者根本不傳入參數——它就會返回 0,來替代:

const orZero = (n = 0) => n;複製代碼

如上,若想設置默認值,只需在傳入參數時帶上 = 操做符,好比 n = 0。當你用這種方式傳入默認值,像 Tern.js、Flow、或者 TypeScript 這些類型檢測工具能夠自行推斷函數的類型簽名,甚至你不須要刻意聲明類型註解。

結果就是這樣,在你的編輯器或者 IDE 中安裝正確的插件,在你輸入函數調用時,你能夠看見內聯顯示的函數簽名。依據它的調用簽名,函數的使用方法也一目瞭然。不管起不起做用,使用默認值可讓你寫出更具可讀性的代碼。

注意: 使用默認值的參數不會增長函數的 .length 屬性,好比使用依賴 .length 值的自動柯里化會拋出不可用異常。若是你碰上它,一些柯里化工具(好比 lodash/curry)容許你傳入自定義參數來繞開這個限制。

命名參數

JavaScript 函數能夠傳入對象字面量做爲參數,而且使用對象解構來分配參數標識,這樣作能夠達到命名參數的一樣效果。注意,你也可使用默認參數特性傳入默認值。

const createUser = ({
  name = 'Anonymous',
  avatarThumbnail = '/avatars/anonymous.png'
}) => ({
  name,
  avatarThumbnail
});

const george = createUser({
  name: 'George',
  avatarThumbnail: 'avatars/shades-emoji.png'
});

george;
/* { name: 'George', avatarThumbnail: 'avatars/shades-emoji.png' } */複製代碼

剩餘和展開

JavaScript 中函數共有的一個特性是能夠在函數參數中使用剩餘操做符 ... 來將一組剩餘的參數彙集到一塊兒。

例以下面這個函數簡單地丟棄第一個參數,返回其他的參數:

const aTail = (head, ...tail) => tail;
aTail(1, 2, 3); // [2, 3]複製代碼

剩餘參數將各個元素組成一個數組。而展開操做偏偏相反:它將一個數組中的元素擴展爲獨立元素。研究一下這個:

const shiftToLast = (head, ...tail) => [...tail, head];
shiftToLast(1, 2, 3); // [2, 3, 1]複製代碼

JavaScript 數組在使用擴展操做符的時候會調用一個迭代器,對於數組中的每個元素,迭代器都會傳遞一個值。在 [...tail, head] 表達式中,迭代器按順序從 tail 數組中拷貝到一個剛剛建立的新的數組。以前 head 已是一個獨立元素了,咱們只需把它放到數組的末端,就完成了。

柯里化

能夠經過返回另外一個函數來實現柯里化(Curry)和偏應用(partial application):

const highpass = cutoff => n => n >= cutoff;
const gt4 = highpass(4); // highpass() 返回了一個新函數複製代碼

你能夠不使用箭頭函數。JavaScript 也有一個 function 關鍵字。咱們使用箭頭函數是由於 function 關鍵字須要打更多的字。
這種寫法和上面的 highPass() 定義是同樣的:

const highpass = function highpass(cutoff) {
  return function (n) {
    return n >= cutoff;
  };
};複製代碼

JavaScript 中箭頭的大體意義就是「函數」。使用不一樣種的方式聲明,函數行爲會有一些重要的不一樣點(=> 缺乏了它本身的 this ,不能做爲構造函數),但當咱們碰見那就知道不一樣之處了。如今,當你看見 x => x,想到的是 「一個攜帶 x 而且返回 x 的函數」。因此 const highpass = cutoff => n => n >= cutoff; 能夠這樣讀:

highpass 是一個攜帶 cutoff 返回一個攜帶 n 並返回結果 n >= cutoff 的函數的函數」

既然 highpass() 返回一個函數,你可使用它建立一個更獨特的函數:

const gt4 = highpass(4);

gt4(6); // true
gt4(3); // false複製代碼

自動柯里化函數,有利於得到最大的靈活性。好比你有一個函數 add3():

const add3 = curry((a, b, c) => a + b + c);複製代碼

使用自動柯里化,你能夠有不少種不一樣方法使用它,它將根據你傳入多少個參數返回正確結果:

add3(1, 2, 3); // 6
add3(1, 2)(3); // 6
add3(1)(2, 3); // 6
add3(1)(2)(3); // 6複製代碼

令 Haskell 粉遺憾的是,JavaScript 沒有內置自動柯里化機制,但你能夠從 Lodash 引入:

$ npm install --save lodash複製代碼

而後在你的模塊裏:

import curry from 'lodash/curry';複製代碼

或者你可使用下面這個魔性寫法:

// 精簡的遞歸自動柯里化
const curry = (
  f, arr = []
) => (...args) => (
  a => a.length === f.length ?
    f(...a) :
    curry(f, a)
)([...arr, ...args]);複製代碼

函數組合

固然你可以開始組合函數了。組合函數是傳入一個函數的返回值做爲參數給另外一個函數的過程。用數學符號標識:

f . g複製代碼

翻譯成 JavaScript:

f(g(x))複製代碼

這是從內到外地求值:

  1. x 是被求數值
  2. g() 應用給 x
  3. f() 應用給 g(x) 的返回值

例如:

const inc = n => n + 1;
inc(double(2)); // 5複製代碼

數值 2 被傳入 double(),求得 44 被傳入 inc() 求得 5

你能夠給函數傳入任何表達式做爲參數。表達式在函數應用以前被計算:

inc(double(2) * double(2)); // 17複製代碼

既然 double(2) 求得 4,你能夠讀做 inc(4 * 4),而後計算得 inc(16),而後求得 17

函數組合是函數式編程的核心。咱們後面還會介紹不少。

數組

數組有一些內置方法。方法是指對象關聯的函數,一般是這個對象的屬性:

const arr = [1, 2, 3];
arr.map(double); // [2, 4, 6]複製代碼

這個例子裏,arr 是對象,.map() 是一個以函數爲值的對象屬性。當你調用它,這個函數會被應用給參數,和一個特別的參數叫作 thisthis 在方法被調用之時自動設置。這個 this 的存在使 .map() 可以訪問數組的內容。

注意咱們傳遞給 map 的是 double 函數而不是直接調用。由於 map 攜帶一個函數做爲參數並將函數應用給數組的每個元素。它返回一個包含了 double() 返回值的新的數組。

注意原始的 arr 值沒有改變:

arr; // [1, 2, 3]複製代碼

方法鏈

你也能夠鏈式調用方法。方法鏈是指在函數返回值上直接調用方法的過程,在此期間不須要給返回值命名:

const arr = [1, 2, 3];
arr.map(double).map(double); // [4, 8, 12]複製代碼

返回布爾值(truefalse)的函數叫作 斷言(predicate)。.filter() 方法攜帶斷言並返回一個新的數組,新數組中只包含傳入斷言函數(返回 true)的元素:

[2, 4, 6].filter(gt4); // [4, 6]複製代碼

你經常會想要從一個列表選擇一些元素,而後把這些元素序列化到一個新列表中:

[2, 4, 6].filter(gt4).map(double); [8, 12]複製代碼

注意:後面的文章你將看到使用叫作 transducer 東西更高效地同時選擇元素並序列化,不過這以前還有一些其餘東西要了解。

總結

若是你如今有點發懵,沒必要擔憂。咱們僅僅概覽了一下不少事情的表面,它們尚需大量的解釋和總結。很快咱們就會回過頭來,深刻探討其中的一些話題。

繼續閱讀 「高階函數」…

接下來

想要學習更多 JavaScript 函數式編程知識?

和 Eric Elliott 一塊兒學習 JavaScript。 若是你不是其中一員,千萬別錯過!

Eric Elliott「JavaScript 應用程序設計」 (O’Reilly) 以及 「和 Eric Elliott 一塊兒學習 JavaScript」 的做者。 曾就任於 Adobe Systems、Zumba Fitness、The Wall Street Journal、ESPN、BBC and top recording artists including Usher、Frank Ocean、Metallica 等公司,具備豐富的軟件實踐經驗。

他大多數時間在 San Francisco By Area ,和世界上最美麗的姑娘在一塊兒。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃

相關文章
相關標籤/搜索