本文是篇譯文,原文連接An Introduction to Reasonably Pure Functional Programming,不當之處還請指正。javascript
一個好的程序員應該有能力掌控你寫的代碼,可以以最簡單的方法使你的代碼正確而且可讀。做爲一名優秀的程序員,你會編寫儘可能短小的函數,使代碼更好的被複用;你會編寫測試代碼,使本身有足夠的信心相信代碼會按本來的意圖正確運行。沒有人喜歡解bug,因此一名優秀的程序員也要會避免一些錯誤,這些要靠經驗得到,也能夠遵循一些最佳實踐,好比Douglas Crockford 最著名的JavaScript:The good partshtml
函數式編程可以下降程序的複雜程度:函數看起來就像是一個數學公式。學習函數編程可以幫助你編寫簡單而且更少bug的代碼。java
純函數能夠理解爲一種 相同的輸入一定有相同的輸出的函數,沒有任何能夠觀察到反作用node
//pure function add(a + b) { return a + b; }
上面是一個純函數,它不依賴也不改變任何函數之外的變量狀態,對於相同的輸入總能返回相同的輸出。react
//impure var minimum = 21; var checkAge = function(age) { return age >= minimum; // 若是minimum改變,函數結果也會改變 }
這個函數不是純函數,由於它依賴外部可變的狀態jquery
若是咱們將變量移到函數內部,那麼它就變成了純函數,這樣咱們就可以保證函數每次都能正確的比較年齡。git
var checkAge = function(age) { var minimum = 21; return age >= minimum; };
純函數沒有反作用,一些你要記住的是,它不會:程序員
訪問函數之外的系統狀態es6
修改以參數形式傳遞過來的對象github
發起http請求
保留用戶輸入
查詢DOM
你須要留意一些會改變數組和對象的增變方法,舉例來講你要知道splice和slice之間的差別。
//impure, splice 改變了原數組 var firstThree = function(arr) { return arr.splice(0,3); } //pure, slice 返回了一個新數組 var firstThree = function(arr) { return arr.slice(0,3); }
若是咱們避免使用傳入函數的對象的增變方法,咱們的程序將更容易理解,咱們也有理由指望咱們的函數不會改變任何函數以外的東西。
let items = ['a', 'b', 'c']; let newItems = pure(items); //對於純函數items始終應該是['a', 'b', 'c']
相比於不純的函數,純函數有以下優勢:
更加容易被測試,由於它們惟一的職責就是根據輸入計算輸出
結果能夠被緩存,由於相同的輸入總會得到相同的輸出
自我文檔化,由於函數的依賴關係很清晰
更容易被調用,由於你不用擔憂函數會有什麼反作用
由於純函數的結果能夠被緩存,咱們能夠記住他們,這樣以來複雜昂貴的操做只須要在被調用時執行一次。例如,緩存一個大的查詢索引的結果能夠極大的改善程序的性能。
使用純函數可以極大的下降程序的複雜度。可是,若是咱們使用過多的函數式編程的抽象概念,咱們的函數式編程也會很是難以理解。
import _ from 'ramda'; import $ from 'jquery'; var Impure = { getJSON: _.curry(function(callback, url) { $.getJSON(url, callback); }), setHtml: _.curry(function(sel, html) { $(sel).html(html); }) }; var img = function (url) { return $('<img />', { src: url }); }; var url = function (t) { return 'http://api.flickr.com/services/feeds/photos_public.gne?tags=' + t + '&format=json&jsoncallback=?'; }; var mediaUrl = _.compose(_.prop('m'), _.prop('media')); var mediaToImg = _.compose(img, mediaUrl); var images = _.compose(_.map(mediaToImg), _.prop('items')); var renderImages = _.compose(Impure.setHtml("body"), images); var app = _.compose(Impure.getJSON(renderImages), url); app("cats");
花一分鐘理解上面的代碼。
除非你接觸過函數式編程的這些概念(柯里化,組合和prop),不然很難理解上述代碼。相比於純函數式的方法,下面的代碼則更加容易理解和修改,它更加清晰的描述程序而且更少的代碼。
app函數的參數是一個標籤字符串
從Flickr獲取JSON數據
從返回的數據裏抽出urls
建立<img>節點數組
將他們插入文檔
var app = (tags) => { let url = `http://api.flickr.com/services/feeds/photos_public.gne?tags=${tags}&format=json&jsoncallback=?`; $.getJSON(url, (data) => { let urls = data.items.map((item) => item.media.m) let images = urls.map(url) => $('<img />', {src:url}) ); $(document.body).html(images); }) } app("cats");
或者可使用fetch和Promise來更好的進行異步操做。
let flickr = (tags)=> { let url = `http://api.flickr.com/services/feeds/photos_public.gne?tags=${tags}&format=json&jsoncallback=?` return fetch(url) .then((resp)=> resp.json()) .then((data)=> { let urls = data.items.map((item)=> item.media.m ) let images = urls.map((url)=> $('<img />', { src: url }) ) return images }) } flickr("cats").then((images)=> { $(document.body).html(images) })
Ajax請求和DOM操做都不是純的,可是咱們能夠將餘下的操做組成純函數,將返回的JSON數據轉換成圖片節點數組。
let responseToImages = (resp) => { let urls = resp.items.map((item) => item.media.m) let images = urls.map((url) => $('<img />', {src:url})) return images }
咱們的函數作了2件事情:
將返回的數據轉換成urls
將urls轉換成圖片節點
函數式的方法是將上述2個任務拆開,而後使用compose將一個函數的結果做爲參數傳給另外一個參數。
let urls = (data) => { return data.items.map((item) => item.media.m) } let images = (urls) => { return urls.map((url) => $('<img />', {src: url})) } let responseToImages = _.compose(images, urls)
compose 返回一系列函數的組合,每一個函數都會將後一個函數的結果做爲本身的入參
這裏compose作的事情,就是將urls的結果傳入images函數
let responseToImages = (data) => { return images(urls(data)) }
經過將代碼變成純函數,讓咱們在之後有機會複用他們,他們更加容易被測試和自文檔化。很差的是當咱們過分的使用這些函數抽象(像第一個例子那樣), 就會使事情變得複雜,這不是咱們想要的。當咱們重構代碼的時候最重要的是要問一下本身:
這是否讓代碼更加容易閱讀和理解?
我並非要詆譭函數式編程。每一個程序員都應該齊心合力去學習基礎函數,這些函數讓你在編程過程當中使用一些抽象出的通常模式,寫出更加簡潔明瞭的代碼,或者像Marijn Haverbeke說的
一個程序員可以用常規的基礎函數武裝本身,更重要的是知道如何使用它們,要比那些苦思冥想的人高效的多。-- Eloquent JavaScript, Marijn Haverbeke
這裏列出了一些JavaScript開發者應該掌握的基礎函數
Arrays
-forEach
-map
-filter
-reduce
Functions
-debounce
-compose
-partial
-curry
讓咱們來經過實踐看一下函數式編程能如何改善下面的代碼
let items = ['a', 'b', 'c']; let upperCaseItems = () => { let arr = []; for (let i=0, ii= items.length; i<ii; i++) { let item = items[i]; arr.push(item.toUpperCase()); } items = arr; }
共享狀態來簡化函數
這看起來很明顯且微不足道,可是我仍是讓函數訪問和修改了外部的狀態,這讓函數難以測試且容易出錯。
//pure let upperCaseItems = (items) => { let arr = []; for (let i =0, ii= items.length; i< ii; i++) { let item = items[i]; arr.push(item.toUpperCase()); } return arr; }
使用更加可讀的語言抽象forEach來迭代
let upperCaseItems = (items) => { let arr = []; items.forEach((item) => { arr.push(item.toUpperCase()); }) return arr; }
使用map進一步簡化代碼
let upperCaseItems = (items) => { return items.map((item) => item.toUpperCase()) }
進一步簡化代碼
let upperCase = (item) => item.toUpperCase() let upperCaseItems = (item) => items.map(upperCase)
刪除代碼直到它不能工做
咱們不須要爲這種簡單的任務編寫函數,語言自己就提供了足夠的抽象來完成功能
let items = ['a', 'b', 'c'] let upperCaseItems = item.map((item) => item.toUpperCase())
純函數的一個關鍵優勢是易於測試,因此在這一節我會爲咱們以前的Flicker模塊編寫測試。
咱們會使用Mocha來運行測試,使用Babel來編譯ES6代碼。
mkdir test-harness cd test-harness npm init -y npm install mocha babel-register babel-preset-es2015 --save-dev echo '{ "presets": ["es2015"] }' > .babelrc mkdir test touch test/example.js
Mocha提供了一些好用的函數如describe和it來拆分測試和鉤子(例如before和after這種用來組裝和拆分任務的鉤子)。assert是用來進行相等測試的斷言庫,assert和assert.deepEqual是頗有用且值得注意的函數。
讓咱們來編寫第一個測試test/example.js
import assert from 'assert'; describe('Math', () => { describe('.floor', () => { it('rounds down to the nearest whole number', () => { let value = Math.floor(4.24) assert(value === 4) }) }) })
打開package.json文件,將"test"腳本修改以下
mocha --compilers js:babel-register --recursive
而後你就能夠在命令行運行npm test
Math .floor ✓ rounds down to the nearest whole number 1 passing (32ms)
Note:若是你想讓mocha監視改變,而且自動運行測試,能夠在上述命令後面加上-w選項。
mocha --compilers js:babel-register --recursive -w
咱們的模塊文件是lib/flickr.js
import $ from 'jquery'; import { compose } from 'underscore'; let urls = (data) => { return data.items.map((item) => item.media.m) } let images = (urls) => { return urls.map((url) => $('<img />', {src: url})[0] ) } let responseToImages = compose(images, urls) let flickr = (tags) => { let url = `http://api.flickr.com/services/feeds/photos_public.gne?tags=${tags}&format=json&jsoncallback=?` return fetch(url) .then((response) => reponse.json()) .then(responseToImages) } export default { _responseToImages: responseToImages, flickr: flickr }
咱們的模塊暴露了2個方法:一個公有flickr和一個私有函數_responseToImages,這樣就能夠獨立的測試他們。
咱們使用了一組依賴:jquery,underscore和polyfill函數fetch和Promise。爲了測試他們,咱們使用jsdom來模擬DOM對象window和document,使用sinon包來測試fetch api。
npm install jquery underscore whatwg-fetch es6-promise jsdom sinon --save-dev touch test/_setup.js
打開test/_setup.js,使用全局對象來配置jsdom
global.document = require('jsdom').jsdom('<html></html>'); global.window = document.defaultView; global.$ = require('jquery')(window); global.fetch = require('whatwg-fetch').fetch;
咱們的測試代碼在test/flickr.js,咱們將爲函數的輸出設置斷言。咱們"stub"或者覆蓋全局的fetch方法,來阻斷和模擬HTTP請求,這樣咱們就能夠在不直接訪問Flickr api的狀況下運行咱們的測試。
import assert from 'assert'; import Flickr from '../lib/flickr'; import sinon from 'sinon'; import { Promise } from 'es6-promise'; import { Response } from 'whatwg-fetch'; let sampleResponse = { items: [{ media: { m: 'lolcat.jpg' } }, { media: {m: 'dancing_pug.gif'} }] } //實際項目中咱們會將這個test helper移到一個模塊裏 let jsonResponse = (obj) => { let json = JSON.stringify(obj); var response = new Response(json, { status: 200, headers: {'Content-type': 'application/json'} }); return Promise.resolve(response); } describe('Flickr', () => { describe('._responseToImages', () => { it("maps response JSON to a NodeList of <img>", () => { let images = Flickr._responseToImages(sampleResponse); assert(images.length === 2); assert(images[0].nodeName === 'IMG'); assert(images[0].src === 'lolcat.jpg'); }) }) describe('.flickr', () => { //截斷fetch 請求,返回一個Promise對象 before(() => { sinon.stub(global, 'fetch', (url) => { return jsonResponse(sampleResponse) }) }) after(() => { global.fetch.restore(); }) it("returns a Promise that resolve with a NodeList of <img>", (done) => { Flickr.flickr('cats').then((images) => { assert(images.length === 2); assert(images[1].nodeName === 'IMG'); assert(images[1].src === 'dancing_pug.gif'); done(); }) }) }) })
運行npm test,會獲得以下結果:
Math .floor ✓ rounds down to the nearest whole number Flickr ._responseToImages ✓ maps response JSON to a NodeList of <img> .flickr ✓ returns a Promise that resolves with a NodeList of <img> 3 passing (67ms)
到這裏,咱們已經成功的測試了咱們的模塊以及組成它的函數,學習到了純函數以及如何使用函數組合。咱們知道了純函數與不純函數的區別,知道純函數更可讀,由小函數組成,更容易測試。相比於不太合理的純函數式編程,咱們的代碼更加可讀、理解和修改,這也是咱們重構代碼的目的。
Professor Frisby’s Mostly Adequate Guide to Functional Programming – @drboolean-這是一本很優秀的介紹函數式編程的書,本文的不少內容和例子出自這本書
Eloquent Javascript – Functional Programming @marijnjh-介紹編程的好書,一樣有一章介紹函數式編程的內容很棒
Underscore-深刻的挖掘像Underscore,lodash,Ramda這樣的工具庫是成爲成熟開發者的重要一步。理解如何使用這些函數將極大下降你代碼的長度,讓你的程序更加聲明式的。
以上就是本文的所有!很是感謝閱讀,我但願這篇文章很好的向你介紹了函數式編程,重構以及測試你的JavaScript。因爲目前特別火熱的庫如React,Redux,Elm,Cycle和ReactiveX都在鼓勵和使用這種模式,因此這個時候寫這樣一篇有趣的範例也算是推波助流吧。