Js-函數式編程

前言

JavaScript是一門多範式語言,便可使用OOP(面向對象),也可使用FP(函數式),因爲筆者最近在學習React相關的技術棧,想進一步深刻了解其思想,因此學習了一些FP相關的知識點,本文純屬我的的讀書筆記,若是有錯誤,望輕噴且提點。javascript

什麼是函數式編程

函數式編程(英語:functional programming)或稱函數程序設計、泛函編程,是一種編程範式,它將計算機運算視爲函數運算,而且避免使用程序狀態以及易變對象。即對過程進行抽象,將數據以輸入輸出流的方式封裝進過程內部,從而也下降系統的耦合度。html

爲何Js支持FP

Js支持FP的一個重要緣由在於,在JS中,函數是一等公民。即你能夠像對其餘數據類型同樣對其進行操做,把他們存在數組裏,看成參數傳遞,賦值給變量...等等。以下:前端

const func = () => {}

// 存儲
const a = [func]

// 參數 返回值
const x = (func) => {
    ......
    ......
    return func
}

x(func)
複製代碼

這個特性在編寫語言程序時帶來了極大的便利,下面的知識及例子都創建在此基礎上。java

純函數

概念

純函數是這樣一種函數,即相同的輸入,永遠會獲得相同的輸出,並且沒有任何可觀察的反作用。
反作用包括但不限於:node

  • 打印/log
  • 發送一個http請求
  • 可變數據
  • DOM查詢
    簡單一句話, 即只要是與函數外部環境發生交互的都是反作用。

像Js中, slice就是純函數, 而splice則不是ios

var xs = [1,2,3,4,5];

// 純的
xs.slice(0,3);
//=> [1,2,3]

xs.slice(0,3);
//=> [1,2,3]

xs.slice(0,3);
//=> [1,2,3]


// 不純的
xs.splice(0,3);
//=> [1,2,3]

xs.splice(0,3);
//=> [4,5]

xs.splice(0,3);
//=> []
複製代碼

例子

在React生態中,使用純函數的例子很常見,如React Redner函數,Redux的reducer,Redux-saga的聲明式effects等等。git

React Render
在React中,Render返回了一個JSX表達式,只要輸入相同,便可以保證咱們拿到一樣的輸出(最終結果渲染到DOM上),而內部的封裝細節咱們不須要關心,只要知道它是沒有反作用的,這在咱們開發過程當中帶來了極大的便利。當咱們的程序出問題時(渲染出來與預期不符合),咱們只要關心咱們的入參是否有問題便可。編程

class Component extends React.Component {
    render() {
        return (
            <div />
        )
    }
}
複製代碼

Redux的reducer
Redux的reducer函數要求咱們每一次都要返回一個新的state, 而且在其中不能有任何反作用,只要傳入參數相同,返回計算獲得的下一個 state 就必定相同。沒有特殊狀況、沒有反作用,沒有 API 請求、沒有變量修改,單純執行計算。這樣作可使得咱們很容易的保存了每一次state改變的狀況,對於時間旅行這種需求更是自然的親近。特別是在調試的過程當中,咱們能夠藉助插件,任意達到每個state狀態,可以輕鬆的捕捉到錯誤是在哪個節點出現。redux

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })
    default:
      return state
  }
}
複製代碼

Redux-sage的聲明式effects
許多時候, 咱們會寫這樣的函數axios

const sendRequest = () => {
    return axions.post(...)
}
複製代碼

這是一個不純的函數,由於它包含了反作用,發起了http請求,咱們能夠這樣封裝一下:

const sendRequestReducer = () => {
    return () => {
        return axios.post(...)
    }
}
複製代碼

ok, 如今是一個純函數了,正如Redux-saga中的effects同樣:

import { call } from 'redux-saga/effects'

function* fetchProducts() {
  const products = yield call(Api.fetch, '/products')
  // ...
}
複製代碼

實際上call不當即執行異步調用,相反,call 建立了一條描述結果的信息。那麼這樣作除了增長代碼的複雜度,還能夠給咱們帶來什麼?參考saga的官方文檔就知道了, 答案是測試:

這些 聲明式調用(declarative calls) 的優點是,咱們能夠經過簡單地遍歷 Generator 並在 yield 後的成功的值上面作一個 deepEqual 測試, 就能測試 Saga 中全部的邏輯。這是一個真正的好處,由於複雜的異步操做都再也不是黑盒,你能夠詳細地測試操做邏輯,無論它有多麼複雜。

import { call } from 'redux-saga/effects'
import Api from '...'

const iterator = fetchProducts()

// expects a call instruction
assert.deepEqual(
  iterator.next().value,
  call(Api.fetch, '/products'),
  "fetchProducts should yield an Effect call(Api.fetch, './products')"
)
複製代碼

總結

純函數有着如下的優勢

可緩存性
首先,純函數總可以根據輸入來作緩存。實現緩存的一種典型方式是 memoize 技術:

var memoize = function(f) {
  var cache = {};

  return function() {
    var arg_str = JSON.stringify(arguments);
    cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
    return cache[arg_str];
  };
};

var squareNumber  = memoize(function(x){ return x*x; });

squareNumber(4);
//=> 16

squareNumber(4); // 從緩存中讀取輸入值爲 4 的結果
//=> 16

squareNumber(5);
//=> 25

squareNumber(5); // 從緩存中讀取輸入值爲 5 的結果
//=> 25
複製代碼

可移植性
純函數由於不依賴外部環境,因此很是便於移植,你能夠在任何地方使用它而不須要附帶着引入其餘不須要的屬性。

可測試性
如上面提到的Redux reducer和Redux-saga同樣, 它對於測試自然親近。

並行代碼
咱們能夠並行運行任意純函數。由於純函數根本不須要訪問共享的內存,並且根據其定義,純函數也不會因反作用而進入競爭態(race condition)。

柯里化

概念

在計算機科學中,柯里化(英語:Currying),又譯爲卡瑞化或加里化,是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數並且返回結果的新函數的技術

var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12
複製代碼

例子

在Lodash類庫中,就有這麼一個curry函數來幫助咱們處理科裏化,關於如何實現一個curry函數,推薦你們參考這篇文章

var abc = function(a, b, c) {
  return [a, b, c];
};
 
var curried = _.curry(abc);
 
curried(1)(2)(3);
// => [1, 2, 3]
 
curried(1, 2)(3);
// => [1, 2, 3]
 
curried(1, 2, 3);
// => [1, 2, 3]
 
// Curried with placeholders.
curried(1)(_, 3)(2);
// => [1, 2, 3]
複製代碼

偏函數應用

偏函數自己與科裏化並不相關, 但在平常的編寫程序中,或許咱們使用更多的是偏函數,因此在這裏簡單的介紹一下偏函數

偏函數應用是找一個函數,固定其中的幾個參數值,從而獲得一個新的函數

有時候,咱們會寫一個專門發送http請求的函數

const sendRequest = (host, fixPath, path) => {
    axios.post(`${host}\${fixPath}\{path}`)
}
複製代碼

可是大多數時候, host和fixPath是固定的, 咱們不想每次都寫一次host和fixPath,但咱們又不能寫死,由於咱們須要sendRequest這個函數是能夠移植的,不受環境的約束,那麼咱們能夠這樣

const sendRequestPart = (path) => {
    const host = '...'
    const fixPath = '...'
    return sendRequest(host, fixPath, path)
}
複製代碼

總結

科裏化和偏函數的主要用途是在組合中,這一小節主要介紹了他們的使用方法和行爲。

組合 compose

組合的功能很是強大, 也是函數式編程的一個核心概念, 所謂的對過程進行封裝很大程度上就是依賴於組合。那麼什麼是組合?

var compose = function(f,g) {
  return function(x) {
    return f(g(x));
  };
};

var toUpperCase = function(x) { return x.toUpperCase(); };
var exclaim = function(x) { return x + '!'; };
var shout = compose(exclaim, toUpperCase);

shout("send in the clowns");
//=> "SEND IN THE CLOWNS!"
複製代碼

上面的compose就是一個最簡單的組合函數, 固然組合函數並不限制於傳入多少個函數參數,它最後只返回一個函數,我我的更喜歡將它認爲像管道同樣,將數據通過不一樣函數的逐漸加工,最後獲得咱們想要的結果

const testFunc = compose(func1, func2, func3, func4)  
testFunc(...args) 
複製代碼

在js中, 實現compose函數比較容易

const compose = (...fns) => {
    return (...args) => {
        let res = args
        for (let i = fns.length - 1; i > -1; i--) {
            res = fns[i](res)
        }
        return res
    }
}
複製代碼

例子

React官方推崇組合優於繼承這個概念,這裏選擇兩個比較典型的例子來看

React中的高階組件
在React中,有許多使用高階組件的地方,如React-router的withRouter函數,React-redux的connect函數返回的函數,

// Navbar 和 Comment都是組件
const NavbarWithRouter = withRouter(Navbar);
const ConnectedComment = connect(commentSelector, commentActions)(Comment);
複製代碼

而因爲高階函數的簽名是Component => Component。因此咱們能夠很容易的將他們組合到一塊兒,這也是官方推薦的作法

// 不要這樣作……
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))

// ……你可使用一個函數組合工具
// compose(f, g, h) 和 (...args) => f(g(h(...args)))是同樣的
const enhance = compose(
  // 這些都是單獨一個參數的高階組件
  withRouter,
  connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)
複製代碼

Redux的compose函數
Redux的compose函數實現要比上面提到的簡潔的多

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製代碼

這個實現咋看之下有點懵逼, 因此能夠拆開來看一下

composeFn = compose(fn1, fn2, fn3, fn4)
複製代碼

那麼reduce循環運行時, 第一次a就是fn1, b是fn2, 第二次a是(...args) => fn1(fn2(...args)), b是fn3, 第三次運行的時候則是a是(...args) => fn1(fn2(fn3(...args))), b是fn4, 最後返回了fn1(fn2(fn3(fn4(...args))))

pointfree

它的意思是說,函數無須說起將要操做的數據是什麼樣的。

// 非 pointfree,由於提到了數據:word
var snakeCase = function (word) {
  return word.toLowerCase().replace(/\s+/ig, '_');
};

// pointfree
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
複製代碼

pointfree 模式可以幫助咱們減小沒必要要的命名,讓代碼保持簡潔和通用。對函數式代碼來講,pointfree 是很是好的石蕊試驗,由於它能告訴咱們一個函數是不是接受輸入返回輸出的小函數。好比,while 循環是不能組合的。不過你也要警戒,pointfree 就像是一把雙刃劍,有時候也能混淆視聽。並不是全部的函數式代碼都是 pointfree 的,不過這不要緊。可使用它的時候就使用,不能使用的時候就用普通函數。

總結

有了組合, 配合上面提到的科裏化和偏函數應用, 能夠將程序拆成一個個小函數而後組合起來, 優勢已經很明顯的呈現出來,也很直觀的表達出了函數式編程的封裝過程的核心概念。

範疇學

函數式編程創建在範疇學上,不少時候討論起來不免有點理論化,因此這裏簡單的介紹一下範疇。

有着如下這些組件(component)的蒐集(collection)就構成了一個範疇:

  • 對象的蒐集
  • 態射的蒐集
  • 態射的組合
  • identity 這個獨特的態射

對象的蒐集
對象就是數據類型,例如 String、Boolean、Number 和 Object 等等。一般咱們把數據類型視做全部可能的值的一個集合(set)。像 Boolean 就能夠看做是 [true, false] 的集合,Number 能夠是全部實數的一個集合。把類型看成集合對待是有好處的,由於咱們能夠利用集合論(set theory)處理類型。

態射的蒐集
態射是標準的、普通的純函數。

態射的組合
即上面提到的compose

identity 這個獨特的態射
讓咱們介紹一個名爲 id 的實用函數。這個函數接受隨便什麼輸入而後原封不動地返回它:

var id = function(x){ return x; };
複製代碼

functor

在學習函數式編程的時候,第一次看到functor的時候一臉懵逼, 確實不理解這個東西是什麼, 能夠作什麼,加上一堆術語,頭都大了。在理解functor以前,先認識一個東西

概念

容器

容器爲函數式編程裏普通的變量、對象、函數提供了一層極其強大的外衣,賦予了它們一些很驚豔的特性。

var Container = function(x) {
  this.__value = x;
}
Container.of = x => new Container(x);

//試試看
Container.of(1);
//=> Container(1)

Container.of('abcd');
//=> Container('abcd')
複製代碼

Container.of 把東西裝進容器裏以後,因爲這一層外殼的阻擋,普通的函數就對他們再也不起做用了,因此咱們須要加一個接口來讓外部的函數也能做用到容器裏面的值(像Array也是一個容器):

Container.prototype.fmap = function(f){
  return Container.of(f(this.__value))
}
複製代碼

咱們能夠這樣使用它:

Container.of(3)
    .fmap(x => x + 1)                //=> Container(4)
    .fmap(x => 'Result is ' + x);    //=> Container('Result is 4')
複製代碼

咱們經過簡單的代碼就實現了一個鏈式調用,而且這也是一個functor

Functor(函子)是實現了 fmap 並遵照一些特定規則的容器類型。

這樣子看仍是有點很差理解, 那麼參考下面這句話可能會好一點:

a functor is nothing more than a data structure you can map functions over with the purpose of lifting values from a container, modifying them, and then putting them back into a container. 都是些簡單的單詞,意會比起本人翻譯會更容易理解。

加上一張圖:

image

ok, 如今大概知道functor是一個什麼樣的東西了。

做用

那麼functor有什麼做用呢?

鏈式調用
首先它能夠鏈式調用,正如上面提到的同樣。

Immutable
能夠看到, 咱們每次都是返回了一個新的Container.of, 因此數據是Immutable的, 而Immutable的做用就不在這裏贅述了。

將控制權交給Container
將控制權交給Container, 這樣他就能夠決定什麼時候何地怎麼去調用咱們傳給fmap的function,這個做用很是強大,能夠爲咱們作空值判斷、異步處理、惰性求值等一系列麻煩的事。

例子

上面做用的第三點可能直觀上有點難以理解, 下面舉三個簡單的例子

Maybe Container
定義一個Maybe Container來幫咱們處理空值的判斷

var Maybe = function(x) {
  this.__value = x;
}

Maybe.of = function(x) {
  return new Maybe(x);
}

Maybe.prototype.fmap = function(f) {
  return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
}

Maybe.prototype.isNothing = function() {
  return (this.__value === null || this.__value === undefined);
}

//試試看
import _ from 'lodash';
var add = _.curry(_.add);

Maybe.of({name: "Stark"})
    .fmap(_.prop("age"))
    .fmap(add(10));
//=> Maybe(null)

Maybe.of({name: "Stark", age: 21})
    .fmap(_.prop("age"))
    .fmap(add(10));
//=> Maybe(31)
複製代碼

固然, 這裏能夠利用上面提到的科裏化函數來簡化掉一堆fmap的狀況

import _ from 'lodash';
var compose = _.flowRight;
var add = _.curry(_.add);

// 創造一個柯里化的 map
var map = _.curry((f, functor) => functor.fmap(f));

var doEverything = map(compose(add(10), _.property("age")));

var functor = Maybe.of({name: "Stark", age: 21});
doEverything(functor);
//=> Maybe(31)
複製代碼

Task Container
咱們能夠編寫一個Task Container來幫咱們處理異步的狀況

var fs = require('fs');

//  readFile :: String -> Task(Error, JSON)
var readFile = function(filename) {
  return new Task(function(reject, result) {
    fs.readFile(filename, 'utf-8', function(err, data) {
      err ? reject(err) : result(data);
    });
  });
};

readFile("metamorphosis").fmap(split('\n')).fmap(head);
複製代碼

例子中的 reject 和 result 函數分別是失敗和成功的回調。正如你看到的,咱們只是簡單地調用 Task 的 map 函數,就能操做未來的值,好像這個值就在那兒似的。(這看起來有點像Promise)

Io Container
咱們能夠利用Io Container來作惰性求值

import _ from 'lodash';
var compose = _.flowRight;

var IO = function(f) {
    this.__value = f;
}

IO.of = x => new IO(_ => x);

IO.prototype.map = function(f) {
    return new IO(compose(f, this.__value))
};

var io_document = new IO(_ => window.document);

io_document.map(function(doc){ return doc.title });
//=> IO(document.title)
複製代碼

注意咱們這裏雖然感受上返回了一個實際的值 IO(document.title),但事實上只是一個對象:{ __value: [Function] },它並無執行,而是簡單地把咱們想要的操做存了起來,只有當咱們在真的須要這個值得時候,IO 纔會真的開始求值,

functor 範疇

functor 的概念來自於範疇學,並知足一些定律。 即functor 接受一個範疇的對象和態射(morphism),而後把它們映射(map)到另外一個範疇裏去

Js中的functor

Js中也有一些實現了functor, 如map、filter

map    :: (A -> B)   -> Array(A) -> Array(B)
filter :: (A -> Boolean) -> Array(A) -> Array(A)
複製代碼

Monad

普通functor的問題

咱們來寫一個函數 cat,這個函數的做用和 Linux 命令行下的 cat 同樣,讀取一個文件,而後打出這個文件的內容

import fs from 'fs';
import _ from 'lodash';

var map = _.curry((f, x) => x.map(f));
var compose = _.flowRight;

var readFile = function(filename) {
    return new IO(_ => fs.readFileSync(filename, 'utf-8'));
};

var print = function(x) {
    return new IO(_ => {
        console.log(x);
        return x;
    });
}

var cat = compose(map(print), readFile);

cat("file")
//=> IO(IO("file的內容"))
複製代碼

ok, 咱們最後獲得的是兩層嵌套的IO, 要獲取其中的值

cat("file").__value().__value()
複製代碼

問題很明顯的出來了, 咱們須要連續調用兩次_value才能獲取, 那麼假如咱們嵌套了更多呢, 難道每次都要調用一大堆__value嗎, 那固然是不可能的。

概念

咱們可使用一個join函數, 來將Container裏面的東西拿出來, 像這樣

var join = x => x.join();
IO.prototype.join = function() {
  return this.__value ? IO.of(null) : this.__value();
}

// 試試看
var foo = IO.of(IO.of('123'));

foo.join();
複製代碼

彷佛這樣也有點麻煩, 每次都要使用一個join來剖析

var doSomething = compose(join, map(f), join, map(g), join, map(h));
複製代碼

咱們可使用一個chain函數, 來幫助咱們作這些事

var chain = _.curry((f, functor) => functor.chain(f));
IO.prototype.chain = function(f) {
  return this.map(f).join();
}

// 如今能夠這樣調用了
var doSomething = compose(chain(f), chain(g), chain(h));

// 固然,也能夠這樣
someMonad.chain(f).chain(g).chain(h)

// 寫成這樣是否是很熟悉呢?
readFile('file')
    .chain(x => new IO(_ => {
        console.log(x);
        return x;
    }))
    .chain(x => new IO(_ => {
        // 對x作一些事情,而後返回
    }))
複製代碼

ok, 事實上這就是一個Monad, 並且你也會很熟悉, 這就像一個Promise的then, 那麼什麼是Monad呢?
Monad有一個bind方法, 就是上面講到的chain(同一個東西不一樣叫法),

function bind<T, U>(instance: M<T>, transform: (value: T) => M<U>): M<U> {
    // ...
}
複製代碼

其實,Monad 的做用跟 Functor 相似,也是應用一個函數到一個上下文中的值。不一樣之處在於,Functor 應用的是一個接收一個普通值而且返回一個普通值的函數,而 Monad 應用的是一個接收一個普通值可是返回一個在上下文中的值的函數。上下文即一個Container。

Promise是Monad

須要被認爲是Monad須要具有如下三個條件

  • 擁有容器, 即Maybe、IO之類。
  • 一個能夠將普通類型轉換爲具備上下文的值的函數, 即Contanier.of
  • 擁有bind函數(即上面提到的bind, 而不是ES5的bind)

那麼Promise具有了什麼條件?

  • 擁有容器 Promise, 即上面第一點
  • Promise.resolve(value)將值轉換爲一個具備上下文的值, 即上面第二點。
  • Promise.prototype.then(onFullfill: value => Promise) 擁有一個bind(then)函數, 接受一個函數做爲參數, 該函數接受一個普通值並返回一個含有上下文的值。 即上面第三點

不過Promise比Monad擁有更多的功能。

  • 若是then返回了一個正常的value, Promise會調用Promise.resolve將其轉換爲Promise
  • 普通的Monad只能提供在計算的時候傳遞一個值, 而Promise有兩個不一樣的值 - 一個用於成功值,一個用於錯誤(相似於Either monad)。可使用then方法的第二個回調或使用特殊的.catch方法捕獲錯誤

Applicative Functor

提到了Functor和Monad而不提Applicative Functor就不完整了。

概念

Applicative Functor就是讓不一樣 functor 能夠相互應用(apply)的能力
舉一個簡單的例子, 假設有兩個同類型的 functor,咱們想把這二者做爲一個函數的兩個參數傳遞過去來調用這個函數。

// 這樣是行不通的,由於 2 和 3 都藏在瓶子裏。
add(Container.of(2), Container.of(3));
//NaN

// 使用可靠的 map 函數試試
var container_of_add_2 = map(add, Container.of(2));
// Container(add(2))
複製代碼

這時候咱們建立了一個 Container,它內部的值是一個局部調用的(partially applied)的函數。確切點講就是,咱們想讓 Container(add(2)) 中的 add(2) 應用到 Container(3) 中的 3 上來完成調用。也就是說,咱們想把一個 functor 應用到另外一個上。 巧的是,完成這種任務的工具已經存在了,即 chain 函數。咱們能夠先 chain 而後再 map 那個局部調用的 add(2),就像這樣:

Container.of(2).chain(function(two) {
  return Container.of(3).map(add(two));
});
複製代碼

然而這樣咱們須要延遲Container.of(3)的創建, 這對咱們來講是很不方便的也是沒有必要的, 咱們能夠經過創建一個ap函數來達成咱們想要的效果

Container.prototype.ap = function(other_container) {
  return other_container.map(this.__value);
}

Container.of(2).map(add).ap(Container.of(3));
// Container(5)
複製代碼

注意上面的add是科裏化函數, this.__value是一個純函數。

因爲這種先 map 再 ap 的操做很廣泛,咱們能夠抽象出一個工具函數 liftA2:

const liftA2 = (f, m1, m2) => m1.map(f).ap(m2)
liftA2(add, Container.of(2), Container.of(3))
複製代碼

應用

正如咱們上面所說, 咱們能夠獨立建立兩個Container, 那麼在Task中也能夠同時發起兩個http請求,而沒必要等到第一個返回再執行第二個

// Http.get :: String -> Task Error HTML

var renderPage = curry(function(destinations, events) { /* render page */  });

Task.of(renderPage).ap(Http.get('/destinations')).ap(Http.get('/events'))
// Task("<div>some page with dest and events</div>")
複製代碼

Functor\Monad\Applicative Functor的數學規律

Functor

// identity
map(id) === id;

// composition
compose(map(f), map(g)) === map(compose(f, g));
複製代碼

Monad

bind(unit(x), f) ≡ f(x)
bind(m, unit) ≡ m
bind(bind(m, f), g) ≡ bind(m, x ⇒ bind(f(x), g))
複製代碼

Applicative Functor

Identity: A.of(x => x).ap(v) === v
Homomorphism: A.of(f).ap(A.of(x)) === A.of(f(x))
Interchange: u.ap(A.of(y)) === A.of(f => f(y)).ap(u)

複製代碼

js 與 函數式和麪向對象

如下引用自文章漫談 JS 函數式編程(一)

面向對象對數據進行抽象,將行爲以對象方法的方式封裝到數據實體內部,從而下降系統的耦合度。而函數式編程,選擇對過程進行抽象,將數據以輸入輸出流的方式封裝進過程內部,從而也下降系統的耦合度。二者雖是大相徑庭,然而在系統設計的目標上能夠說是異曲同工的。

面向對象思想和函數式編程思想也是不矛盾的,由於一個龐大的系統,可能既要對數據進行抽象,又要對過程進行抽象,或者一個局部適合進行數據抽象,另外一個局部適合進行過程抽象,這都是可能的。數據抽象不必定以對象實體爲形式,一樣過程抽象也不是說形式上必然是 functional 的,好比流式對象(InputStream、OutputStream)、Express 的 middleware,就帶有明顯的過程抽象的特徵。可是在一般狀況下,OOP更適合用來作數據抽象,FP更適合用來作過程抽象。

固然因爲Javascript自己是多範式語言, 因此能夠在合適的地方使用合適的編程方式。總而言之, 二者互不排斥,是可共存的。

尾遞歸優化

因爲函數式編程,若是尾遞歸不作優化,很容易爆棧, 這個知識點有不少文章提出來了, 這裏推薦一篇文章

聲明式編程

聲明式主要表如今於只關心結果而不關心過程, 這裏推薦一篇輕鬆易懂的文章
或者舉個例子:
在JQ時代的時候, 假如咱們須要渲染一個DOM, 並改變其文字顏色, 咱們須要這樣的步驟:

  • 找到DOM的class或者id
  • 根據class或者id找到DOM
  • 從新賦值DOM的style屬性的color屬性

而在React中, 咱們能夠直接告訴JSX咱們想要DOM的顏色變成紅色便可。

const textColor = 'red'
const comp = () => {
    return (
        <div style={{
            color: textColor
        }} />
    )
}
複製代碼

而關於聲明式和函數式, 我我的認爲函數式和聲明式同樣, 也是屬於關心結果, 可是函數式最重要的特色是「函數第一位」,即函數能夠出如今任何地方。 二者其實不該該作比較。

函數式編程在JS中的實踐

  • Undescore/Lodash/Ramda庫 特別是Lodash, 打開node_modules基本都能看到
  • Immutable-js 數據不可變
  • React
  • Redux
  • ES6 尾遞歸優化

函數式編程在前端開發中的優點

如下引用自知乎答案

優化綁定

說白了前端和後端不同的關鍵點是後端HTTP較多,前端渲染多,前端真正的剛需是數據綁定機制。後端一次對話,計算好Response發回就完成任務了,因此後端吃了二十年年MVC老本仍是挺好用的。前端處理的是連續的時間軸,並不是一次對話,像後端那樣賦值簡單傳遞就容易斷檔,致使狀態不一致,帶來大量額外複雜度和Bug。無論是標準FRP仍是Mobx這種命令式API的TFRP,內部都是基於函數式設計的。函數式從新發明的Return和分號是要比裸命令式好得多的(前端狀態能夠同步,後端線程安全等等,想怎麼封裝就怎麼封裝)。

封裝做用

接上條,大幅簡化異步,IO,渲染等做用/反作用相關代碼。和不少人想象的不同,函數式很擅長處理做用,只是多一層抽象,若是應用稍微複雜一點,這點成本很快就能找回來(Redux Saga是個例子,特別是你寫測試的狀況下)。渲染如今你們均可以理解冪等渲染地好處了,其實函數式編程各類做用和狀態也是冪等的,對於複雜應用很是有幫助。

複用

引用透明,無反作用,代數設計讓函數式代碼能夠正確優雅地複用。前端不像後端業務固定,作好業務分析和DDD就能夠搭個靜態結構,高枕無憂了。前端的好代碼必定是活的,每處均可能亂改。可組合性其實很重要。經過高階函數來組合效果和效率都要高於繼承,試着多用ramda,你就能夠發現絕大部分東西都能一行寫完,最後給個實參就變成一個UI,來需求改兩筆就變成另一個。

總結

函數式編程在JS的將來是大放異彩仍是泯然衆人,都不影響咱們學習它的思想。本文裏面有許多引用沒有特別指出,但都會在底部放上連接(如介意請留言), 望見諒。

參考&引用

聲明式編程和命令式編程有什麼區別?
用 JS 代碼完整解釋 Monad
怎麼理解「聲明式渲染」?
JavaScript函數式編程(二)
JavaScript Functors Explained
前端開發js函數式編程真實用途體如今哪裏?
js 是更傾向於函數式編程了仍是更傾向於面向對象?或者沒有傾向?只是簡單的提供了更多的語法糖?
漫談 JS 函數式編程(一)
有哪些函數式編程在前端的實踐經驗?
前端使用面向對象式編程 仍是 函數式編程 針對什麼問題用什麼方式 分別有什麼具體案例?
什麼是 Monad (Functional Programming)?
Monads In Javascript
Functor、Applicative 和 Monad
JavaScript 讓 Monad 更簡單
函數式編程

相關文章
相關標籤/搜索