函數式編程瞭解一下(JavaScript描述)

面向對象編程和麪向過程編程都是編程範式,函數式編程也是一種編程範式,意味着它們都是軟件構建的思惟方式。與命令式或面向對象代碼相比,函數式代碼傾向於更簡潔、更可預測以及更易於測試。javascript

什麼是函數式編程 (一般簡稱爲 FP)

是指經過複合純函數來構建軟件的過程,它避免了共享的狀態易變的數據、以及反作用。函數式編程是聲明式而不是命令式,而且應用程序狀態經過純函數流轉。java

理解函數式編程中核心概念

  • 純函數(Pure functions)
  • 函數複合(Function composition)
  • 避免共享狀態(Avoid shared state)
  • 避免改變狀態(Avoid mutating state)
  • 避免反作用(Avoid side effects)
  • 聲明式與命令式(Declarative and Imperative)

純函數

純函數是知足以下條件的函數:git

  • 相同輸入老是會返回相同的輸出
  • 不產生反作用
  • 不依賴於外部狀態

JS中純函數的例子:github

String.prototype.toUpperCase  
Array.prototype.map
Function.prototype.bind
複製代碼

JS中非純函數的例子:編程

Date.now
Math.random
Array.prototype.sort
document.body.appendChild
複製代碼

純函數的好處:數組

  1. 易於測試(上下文無關)
  2. 可並行計算(時序無關)
  3. bug 自限性(問題不會擴散)

函數複合

函數複合是結合兩個或多個函數,從而產生一個新函數或進行某些計算的過程。markdown

在 JavaScript 中至關於執行 f(g(x))。網絡

共享狀態

共享狀態 的意思是:任意變量、對象或者內存空間存在於共享做用域(包括全局做用域和閉包做用域)下,或者做爲對象的屬性在各個做用域之間被傳遞。數據結構

一般,在面向對象編程中,對象以添加屬性到其餘對象上的方式在做用域之間共享。與面向對象編程不一樣,函數式編程避免共享狀態,它依賴於不可變數據結構和純粹的計算過程來從已存在的數據中派生出新的數據。
共享狀態的一個常見問題是改變函數調用次序函數調用的次序可能會改變函數調用的結果,進而可能致使一連串的錯誤:閉包

const x = {
  val: 2
};
const x1 = () => x.val += 1;
const x2 = () => x.val *= 2;
x1(); // -> 3
x2(); // -> 6
複製代碼

下面的例子與上面的相同,除了函數調用的次序不一樣:

const x = {
  val: 2
};
const x1 = () => x.val += 1;
const x2 = () => x.val *= 2;
x2(); // -> 4
x1(); // -> 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});

x1(x); // -> 3
x2(x); // -> 4

/** x2(x); // -> 4 x1(x); // -> 3 */
複製代碼

不修改狀態

在其餘類型的語言中,變量每每用來保存"狀態"。而函數式編程只是返回新的值,不修改系統變量,便是無破壞性的數據轉換。

反作用

反作用是指除了函數返回值之外,任何在函數調用以外觀察到的應用程序狀態改變。

反作用包括:

  • 改變了任何外部變量或對象屬性
  • 寫文件
  • 髮網絡請求
  • 在屏幕輸出
  • 調用另外一個有反作用的函數

在函數式編程中,反作用被儘量避免。

聲明式與命令式

  • 命令式:程序花費大量代碼來描述用來達成指望結果的特定步驟,即"How to do"
  • 聲明式:程序抽象了控制流過程,花費大量代碼描述的是數據流,即"What to do"

函數式編程是一個聲明式範式,意思是說程序邏輯不須要經過明確描述控制流程來表達。
命令式:

let list = [1, 2, 3, 4];
let map1 = [];
for (let i = 0; i < list.length; i++) {
  map1.push(list[i] * 2);
}
複製代碼

聲明式:

let list = [1, 2, 3, 4];
let map2 = list.map(x => 2 * x);
複製代碼

再來看聲明式例子中引出的兩個重要概念:


在講容器前不得不提什麼是範疇:

彼此之間存在某種關係的概念、事物、對象等等,都構成"範疇"。

範疇的數學模型簡單理解就是:"集合 + 函數"。

  • 容器(Container):能夠把"範疇"想象成是一個容器,裏面包含:值和值的變形(函數)
  • 函子(Functor):是一個有接口的容器,可以遍歷其中的值。可以將容器裏面的每個值,映射到另外一個容器。

函數式編程的應用

在函數式編程中,一般使用functors以及使用高階函數抽象來建立通用功能函數,以處理任意數值或不一樣類型的數據。

高階函數

高階函數指的是一個函數以函數爲參數,或以函數爲返回值,或者既以函數爲參數又以函數爲返回值。

高階函數經常使用於:

  • 部分應用於函數參數(偏函數應用)或建立一個柯里化的函數,用於複用或函數複合
  • 接受一個函數列表並返回一些由這個列表中的函數組成的複合函數。
    面向對象編程傾向於把方法和數據集中到對象上。那些被集中的方法一般只能用來操做包含在特定對象實例上的數據。而函數式編程傾向於複用一組通用的函數功能來處理數據。

偏函數

經過指定部分參數來產生一個新定製函數的形式就是偏函數。

const isType = function (type) {
  return function (obj) {
    return toString.call(obj) == '[object' + type + ']';
  };
};

const isString = isType('string');
const isFunction = isType('function');
複製代碼

柯里化

柯里化是將一個多參數函數轉換成多個單參數函數。

// 柯里化以前
function add(x, y) {
  return x + y;
}

add(1, 2) // 3

// 柯里化以後
function addX(y) {
  return function (x) {
    return x + y;
  };
}

addX(2)(1) // 3
複製代碼

函數的複合

若是一個值要通過多個函數,才能變成另一個值,就能夠把全部中間步驟合併成一個函數,這叫作"函數的複合"。

一個簡單的函數的複合例子:

const compose = function (f, g) {
  return function (x) {
    return f(g(x));
  };
}
複製代碼

實現一個高階函數用來減小非純函數:

function batch (fn) {
  return function (target, ...args) {
    if (target.length >= 0) {
      return Array.from(target).map(item => fn.apply(this, [item, ...args]));
    } else {
      return fn.apply(this, [target, ...args]);
    }
  }
}
複製代碼

例如:兩個非純函數 -> batch(fn) -> 一個非純函數

結論

函數式編程偏好:

  • 使用表達式替代語句
  • 讓可變數據成爲不可變的
  • 用函數複合替代命令控制流
  • 使用聲明式而不是命令式代碼
  • 使用純函數而不是使用共享狀態和反作用
  • 使用容器與高階函數替代多態
  • 使用高階函數來操做許多數據類型,建立通用、可複用功能取代只是操做集中的數據的方法
相關文章
相關標籤/搜索