JavaScript 中的函數式編程

函數式編程(functional programming)或稱函數程序設計,又稱泛函編程,是一種編程範型,比起命令式編程,函數式編程更增強調程序執行的結果而非執行的過程,倡導利用若干簡單的執行單元讓計算結果不斷漸進,逐層推導複雜的運算,而不是設計一個複雜的執行過程。javascript

函數式編程,近年來一直被炒得火熱,國內外的開發者好像都在議論和提倡這種編程範式。在衆多的函數式語言中,Javascript 無疑是最亮眼的一個,愈來愈多的人開始學習和擁抱它,並使用它運用函數式編程來開發實際的大型應用,開源社區也源源不斷的誕生函數式風格的框架和類庫(Angular / React / Redux)。java

做爲 web 平臺惟一的標準通用語言,Javascript 在軟件歷史上掀起了最大的語言熱潮,擁有當下最大的開源包管理工具(npm)的 Javascript 也從 Lisp 手中接過了維持數十年的 「最流行的函數式編程語言」 的名號。在 Javascript 的世界中是自然支持函數式編程的,函數式編程的基本特徵有:web

  • 一等函數
  • 閉包
  • 高階函數
  • 純度

本文會以 Javascript 爲例子,和你們一塊兒來了解和學習函數式編程。npm


一等函數(First Class Functions)

一等函數這個術語最先在20世紀60年代,由英國計算機科學家 Christopher Stracheyfunctions as first-class citizens 一文中提出的。意思是指,函數和其餘一等公民(Number / String...)同樣,擁有和它們同樣的能力和做用:編程

  • 函數儲存爲變量數組

    const foo = () => {...}複製代碼
  • 函數能夠儲存爲數據的一個元素數據結構

    const arr = [1, 2, () => {...}]複製代碼
  • 函數能夠做爲對象的屬性值閉包

    const obj = {name: 'xx', say: () => {}}複製代碼
  • 函數能夠在使用時直接建立出來框架

    1 + (() => { return 2; })()複製代碼
  • 函數能夠做爲變量傳遞給另外一個函數編程語言

    bar (name, fun) { fun(name) }
    bar('xx', (name) => { console.log(name) })複製代碼
  • 函數能夠被另外一個函數返回

    foo() {
      return () => {...}
    }複製代碼

在函數式編程中,函數是做爲基本單元,而且在函數之上創建代碼和數據的封裝,以提升應用的重用和靈活性。支持一等函數的做用是顯而易見的,咱們可使用函數去完成大部分的功能。

閉包(Closure)

歷經了 30年,閉包終於成爲了編程語言的主要特色。可是根據一項調查顯示,有關 Javascript 閉包的問題佔了 23% 左右,對於至關數量的開發者來講閉包仍然模糊而又神祕。對於閉包解釋我仍是更傾向於 Kyle Simpson 的系列書 You Don’t Know JavaScript 中的解釋:

函數在被定義時是能夠訪問當前的詞法做用域,當函數離開做用域以外被執行時,就造成了閉包。

簡而言之,閉包就是一個函數,捕獲了做用域內的外部綁定。來看個例子:

function student (people) {
  return (name) => { return people[name] }
}
var someone = student({xx: {age: 20}, jackson: {age: 21}})
someone('xx') // {age: 20}複製代碼

在執行完 student 函數後,裏面的匿名函數造成了一個閉包,閉包是能夠訪問到 people 對象。閉包爲 Javascript 提供了私有訪問,這讓給開發者創建數據抽象提供了極大地便利,也能夠更好地書寫函數式代碼,創建更增強大的代碼。

來思考一個場景,手頭上擁有一個書本的數組,數組裏麪包含了書本的信息,如今須要作的是找出把書名填充到一個數組中而且返回,咱們通常都會這樣寫:

const books = [{title: '人類簡史', author: 'zz'}, {title: '禪與摩托車維修藝術', author: 'tt'}]

books.map((item) => { return item.title })複製代碼

咱們使用了 Array.prototype.map 方法,傳入了一個匿名函數,函數中 return 了書名 title。假如須要利用閉包來進一步抽象的話,要怎麼寫呢?

function plucker (key) {
  return  (obj) => {
    return (obj && obj[key])
  }
}

books.map(plucker('title'))複製代碼

咱們定義了一個 plucker 函數,它接收一個 key 參數並返回一個匿名函數,匿名函數就是一個閉包並補捕獲了 key 參數。在利用了閉包的狀況下,咱們能夠傳入任意想要的書本信息(好比:plucker('author')),這樣就提升了代碼的重用性和靈活性。當咱們對於閉包認識足夠充分時併合理運用到實際開發中去,將會切身體會到閉包的威力和它給咱們帶來的便利。

高階函數(Higher Order Functions)

在數學和計算機科學中,高階函數式至少知足下列一個條件的函數:

  • 接受一個或多個函數做爲輸入
  • 輸出一個函數

在上述的 plucker 函數就是一個例子,還有咱們熟知的 Array.prototype 相關的方法,好比 .map、.sort 等等都是高階函數,由於它們知足接受一個函數做爲參數的條件。
那麼先來看一個一階函數的例子,定義一個函數,它會將數組中4個字母的單詞給過濾掉:

const words = ['foo', 'bar', 'test', 'some']; 
const filter = words => {
  let arr = [];
  for(let i = 0, { length } = words; i < length; i++) {
    const word = words[i];
    if(word.length !== 4) {
      arr.push(word);
    }
  }
  return arr;
}

filter(words); // ['foo', 'bar']複製代碼

假如如今又須要過濾數組中,以 ‘b’ 字母開頭的單詞?那麼再定義一個函數:

const startWith = words => {
  let arr = [];
  for(let i = 0, { length } = words; i < length; i++) {
    const word = words[i];
    if(word.indexOf('b') !== 0) {
      arr.push(word);
    }
  }
  return arr;
}
filter(words); // ['foo', 'test', 'some']複製代碼

根據上面兩個函數的對比來看,其實主要代碼的邏輯都是類似的,先遍歷數組再進行條件判斷,最後 push 到數組中。其實,遍歷和過濾均可以抽象出來,能夠方便其餘的相似函數去調用,畢竟在數組中根據條件過濾是很常見的需求。

const reduce = (reducer, init, arr) => {
  let acc = init;
  for(let i = 0,{ length } = arr; i < length; i++) {
    acc = reducer(acc, arr[i]);
  }
  return acc;
}
reduce((acc, curr) => acc + curr, 0, [1, 2, 3]);    // 6複製代碼

若是使用過 Underscore 庫的話,就會發現 reduce 和 _.reduce 做用是同樣的,實現的是累計的功能。reduce 接受了 3 個參數:ruducer 函數、累計的初始值和一個數組,遍歷時將每一個數組元素做爲 reducer 的參數傳入,返回值又賦值給累計變量 init,遍歷完成時也就完成了累計的功能。

如今若是將 rudece 應用到第一個需求上(過濾四個字母的單詞):

const func = (fn ,arr) => {
  return reduce((acc, curr) => fn(curr) ? acc.concat([curr]) : acc, [], arr)
}
console.log(func(word => word.length !== 4, words)); // ["foo", "bar"]複製代碼

能夠發現,將公共代碼抽象出來以後,filter 的函數實現很是簡潔,只需傳入不一樣的條件函數,就能爲咱們去處理符合各類條件的數據。高階函數能夠用來實現函數的多態性,而且相對於一階函數,高階函數的複用性和靈活性更好。

純度(Purity)

函數式編程不只僅只關心函數,也是思考如何儘可能地下降軟件複雜性的一種方式。在一些函數式編程語言中,純度是被強制執行的,不容許使用有反作用的表達式。可是在 Javascript 中,純度必須經過管理區實現,而且很是容易在偶然間建立和使用非純函數。

一個純函數須要知足如下三個條件:

  • 函數結果只能經過參數來計算得出
  • 不能依賴於能被外部操做改變的數據
  • 不能改變外部狀態

根據這上述條件來看,在 Javascript 的世界中去維持絕對純淨是不可能的,由於缺乏了大多數函數式語言中使用的高效、不變的數據結構。咱們知道在 Javascript 擁有能力去freeze()對象,可是隻能對接對象的頂級屬性,這就意味着一個嵌套對象下的屬性是仍然可以被更改的。

var obj = Object.freeze({
    foo: 'hello',
    bar: {
        text: 'world'
    }
})

obj.foo = 'goodbye';
console.log(obj.foo); // hello

obj.bar.text = 'goobye';
console.log(obj.bar.text); // goodbye複製代碼

在 ES6 中新增的 const 關鍵字,使用 const 能夠定義一個不可以被從新賦值爲不一樣的值,可是一個 const 對象的屬性仍是可變的。

const obj = 'hello';
obj = 'goodbye';    // Uncaught TypeError: Assignment to constant variable.

const obj = {
    foo: 'hello',
    bar: 'world'
}

obj.foo = 'goodbye';
console.log(obj);     // {foo: 'goodbye', bar: 'world'}複製代碼

在 Javascrpt 中實現綜合不變性還有很長的路要走。換句話來講,雖然不可以保證絕對的純淨,可是咱們能夠將純淨的部分抽離出來,將變化的影響降到最低,使得代碼變得更加通用和容易測試。

總結:

  • 函數式編程是支持一等函數的,函數具備其餘數據類型相同的功能
  • 函數式編程中使用閉包來進行數據的封裝
  • 使用高階函數來創建代碼的抽象,使代碼更加靈活通用
  • 儘可能抽離純函數來保持代碼的可測性和通用性

ps: 若是文中有出現錯誤的地方,歡迎你們指正,我會盡快修正,很是感謝 :)

參考文獻:

相關文章
相關標籤/搜索