函數(shù)式程式設(shè)計(jì)能夠降低程式的複雜程度:函數(shù)看起來(lái)就像是一個(gè)數(shù)學(xué)公式。學(xué)習(xí)函數(shù)程式設(shè)計(jì)能夠幫助你寫(xiě)簡(jiǎn)單且更少bug的程式碼。
純函數(shù)
純函數(shù)可以理解為一種相同的輸入必定有相同的輸出的函數(shù),沒(méi)有任何可以觀察到副作用
//pure function add(a + b) { return a + b; }
上面是一個(gè)純函數(shù),它不依賴也不改變?nèi)魏魏瘮?shù)以外的變數(shù)狀態(tài),對(duì)於相同的輸入總是能傳回相同的輸出。
//impure var minimum = 21; var checkAge = function(age) { return age >= minimum; // 如果minimum改變,函數(shù)結(jié)果也會(huì)改變 }
這個(gè)函數(shù)不是純函數(shù),因?yàn)樗蕾囃獠靠勺兊臓顟B(tài)
如果我們將變數(shù)移到函數(shù)內(nèi)部,那麼它就變成了純函數(shù),這樣我們就能夠保證函數(shù)每次都能正確的比較年齡。
var checkAge = function(age) { var minimum = 21; return age >= minimum; };
純函數(shù)沒(méi)有副作用,一些你要記住的是,它不會(huì):
訪問(wèn)函數(shù)以外的系統(tǒng)狀態(tài)
修改以參數(shù)形式傳遞過(guò)來(lái)的對(duì)象
發(fā)起http請(qǐng)求
保留用戶輸入
查詢DOM
控制增變(controlled mutation)
你需要留意一些會(huì)改變數(shù)組和對(duì)象的增變方法,舉例來(lái)說(shuō)你要知道splice和slice之間的差異。
//impure, splice 改變了原數(shù)組 var firstThree = function(arr) { return arr.splice(0,3); } //pure, slice 返回了一個(gè)新數(shù)組 var firstThree = function(arr) { return arr.slice(0,3); }
如果我們避免使用傳入函數(shù)的物件的增變方法,我們的程式將更容易理解,我們也有理由期望我們的函數(shù)不會(huì)改變?nèi)魏魏瘮?shù)之外的東西。
let items = ['a', 'b', 'c']; let newItems = pure(items); //對(duì)于純函數(shù)items始終應(yīng)該是['a', 'b', 'c']
純函數(shù)的優(yōu)點(diǎn)
相比於不純的函數(shù),純函數(shù)有以下優(yōu)點(diǎn):
更容易被測(cè)試,因?yàn)樗鼈兾ㄒ坏穆氊?zé)就是根據(jù)輸入計(jì)算輸出
結(jié)果可以被緩存,因?yàn)橄嗤妮斎肟偸菚?huì)獲得相同的輸出
自我文檔化,因?yàn)楹瘮?shù)的依賴關(guān)係很清晰
更容易被調(diào)用,因?yàn)槟悴挥脫?dān)心函數(shù)會(huì)有什麼副作用
因?yàn)榧兒瘮?shù)的結(jié)果可以被緩存,我們可以記住他們,這樣以來(lái)複雜昂貴的操作只需要在被調(diào)用時(shí)執(zhí)行一次。例如,快取一個(gè)大的查詢索引的結(jié)果可以極大的改善程序的效能。
不合理的純函數(shù)程式設(shè)計(jì)
使用純函數(shù)能夠極大的降低程式的複雜度。但是,如果我們使用過(guò)多的函數(shù)式程式設(shè)計(jì)的抽象概念,我們的函數(shù)式程式設(shè)計(jì)也會(huì)非常難以理解。
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");
花一分鐘理解上面的程式碼。
除非你接觸過(guò)函數(shù)式程式設(shè)計(jì)的這些概念(柯里化,組合和prop),否則很難理解上述程式碼。相較於純函數(shù)式的方法,下面的程式碼則更容易理解和修改,它更清晰的描述程式並且更少的程式碼。
app函數(shù)的參數(shù)是一個(gè)標(biāo)籤字串
從Flickr獲取JSON資料
從返回的資料中抽出urls
創(chuàng)建節(jié)點(diǎn)數(shù)組
將他們插入文檔來(lái)更好的進(jìn)行非同步操作。
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");
Ajax請(qǐng)求和DOM操作都不是純的,但是我們可以將餘下的操作組成純函數(shù),將傳回的JSON資料轉(zhuǎn)換成圖片節(jié)點(diǎn)數(shù)組。
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) })
我們的函數(shù)做了2件事:
將傳回的資料轉(zhuǎn)換成urls
??將urls轉(zhuǎn)換成圖片節(jié)點(diǎn)????函數(shù)式的方法是將上述2個(gè)任務(wù)拆開(kāi),然後使用compose將一個(gè)函數(shù)的結(jié)果作為參數(shù)傳給另一個(gè)參數(shù)。 ??let responseToImages = (resp) => { let urls = resp.items.map((item) => item.media.m) let images = urls.map((url) => $('<img />', {src:url})) return images }??compose 傳回一系列函數(shù)的組合,每個(gè)函數(shù)都會(huì)將後一個(gè)函數(shù)的結(jié)果作為自己的入?yún)????這裡compose做的事情,就是將urls的結(jié)果傳入images函數(shù)??
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)??透過(guò)將程式碼變成純函數(shù),讓我們?cè)谝葬嵊袡C(jī)會(huì)重複使用他們,他們更容易被測(cè)試和自我文件化。不好的是當(dāng)我們過(guò)度的使用這些函數(shù)抽象(像第一個(gè)例子), 就會(huì)使事情變得複雜,這不是我們想要的。當(dāng)我們重構(gòu)程式碼的時(shí)候最重要的是要問(wèn)自己:????這是否讓程式碼更容易閱讀和理解? ??
基本功能函數(shù)
我并不是要詆毀函數(shù)式編程。每個(gè)程序員都應(yīng)該齊心協(xié)力去學(xué)習(xí)基礎(chǔ)函數(shù),這些函數(shù)讓你在編程過(guò)程中使用一些抽象出的一般模式,寫(xiě)出更加簡(jiǎn)潔明了的代碼,或者像Marijn Haverbeke說(shuō)的
一個(gè)程序員能夠用常規(guī)的基礎(chǔ)函數(shù)武裝自己,更重要的是知道如何使用它們,要比那些苦思冥想的人高效的多。--Eloquent JavaScript, Marijn Haverbeke
這里列出了一些JavaScript開(kāi)發(fā)者應(yīng)該掌握的基礎(chǔ)函數(shù)
Arrays
-forEach
-map
-filter
-reduce
Functions
-debounce
-compose
-partial
-curry
Less is More
讓我們來(lái)通過(guò)實(shí)踐看一下函數(shù)式編程能如何改善下面的代碼
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; }
共享狀態(tài)來(lái)簡(jiǎn)化函數(shù)
這看起來(lái)很明顯且微不足道,但是我還是讓函數(shù)訪問(wèn)和修改了外部的狀態(tài),這讓函數(shù)難以測(cè)試且容易出錯(cuò)。
//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; }
使用更加可讀的語(yǔ)言抽象forEach來(lái)迭代
let upperCaseItems = (items) => { let arr = []; items.forEach((item) => { arr.push(item.toUpperCase()); }) return arr; }
使用map進(jìn)一步簡(jiǎn)化代碼
let upperCaseItems = (items) => { return items.map((item) => item.toUpperCase()) }
進(jìn)一步簡(jiǎn)化代碼
let upperCase = (item) => item.toUpperCase() let upperCaseItems = (item) => items.map(upperCase)
刪除代碼直到它不能工作
我們不需要為這種簡(jiǎn)單的任務(wù)編寫(xiě)函數(shù),語(yǔ)言本身就提供了足夠的抽象來(lái)完成功能
let items = ['a', 'b', 'c'] let upperCaseItems = item.map((item) => item.toUpperCase())
測(cè)試
純函數(shù)的一個(gè)關(guān)鍵優(yōu)點(diǎn)是易于測(cè)試,所以在這一節(jié)我會(huì)為我們之前的Flicker模塊編寫(xiě)測(cè)試。
我們會(huì)使用Mocha來(lái)運(yùn)行測(cè)試,使用Babel來(lái)編譯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提供了一些好用的函數(shù)如describe和it來(lái)拆分測(cè)試和鉤子(例如before和after這種用來(lái)組裝和拆分任務(wù)的鉤子)。assert是用來(lái)進(jìn)行相等測(cè)試的斷言庫(kù),assert和assert.deepEqual是很有用且值得注意的函數(shù)。
讓我們來(lái)編寫(xiě)第一個(gè)測(cè)試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) }) }) })
打開(kāi)package.json文件,將"test"腳本修改如下
mocha --compilers js:babel-register --recursive
然后你就可以在命令行運(yùn)行npm test
Math .floor ? rounds down to the nearest whole number 1 passing (32ms)
Note:如果你想讓mocha監(jiān)視改變,并且自動(dòng)運(yùn)行測(cè)試,可以在上述命令后面加上-w選項(xiàng)。
mocha --compilers js:babel-register --recursive -w
測(cè)試我們的Flicker模塊
我們的模塊文件是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個(gè)方法:一個(gè)公有flickr和一個(gè)私有函數(shù)_responseToImages,這樣就可以獨(dú)立的測(cè)試他們。
我們使用了一組依賴:jquery,underscore和polyfill函數(shù)fetch和Promise。為了測(cè)試他們,我們使用jsdom來(lái)模擬DOM對(duì)象window和document,使用sinon包來(lái)測(cè)試fetch api。
npm install jquery underscore whatwg-fetch es6-promise jsdom sinon --save-dev touch test/_setup.js
打開(kāi)test/_setup.js,使用全局對(duì)象來(lái)配置jsdom
global.document = require('jsdom').jsdom('<html></html>'); global.window = document.defaultView; global.$ = require('jquery')(window); global.fetch = require('whatwg-fetch').fetch;
我們的測(cè)試代碼在test/flickr.js,我們將為函數(shù)的輸出設(shè)置斷言。我們"stub"或者覆蓋全局的fetch方法,來(lái)阻斷和模擬HTTP請(qǐng)求,這樣我們就可以在不直接訪問(wèn)Flickr api的情況下運(yùn)行我們的測(cè)試。
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'} }] } //實(shí)際項(xiàng)目中我們會(huì)將這個(gè)test helper移到一個(gè)模塊里 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', () => { //截?cái)鄁etch 請(qǐng)求,返回一個(gè)Promise對(duì)象 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(); }) }) }) })
運(yùn)行npm test,會(huì)得到如下結(jié)果:
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)
到這里,我們已經(jīng)成功的測(cè)試了我們的模塊以及組成它的函數(shù),學(xué)習(xí)到了純函數(shù)以及如何使用函數(shù)組合。我們知道了純函數(shù)與不純函數(shù)的區(qū)別,知道純函數(shù)更可讀,由小函數(shù)組成,更容易測(cè)試。相比于不太合理的純函數(shù)式編程,我們的代碼更加可讀、理解和修改,這也是我們重構(gòu)代碼的目的。

熱AI工具

Undress AI Tool
免費(fèi)脫衣圖片

Undresser.AI Undress
人工智慧驅(qū)動(dòng)的應(yīng)用程序,用於創(chuàng)建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費(fèi)的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費(fèi)的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強(qiáng)大的PHP整合開(kāi)發(fā)環(huán)境

Dreamweaver CS6
視覺(jué)化網(wǎng)頁(yè)開(kāi)發(fā)工具

SublimeText3 Mac版
神級(jí)程式碼編輯軟體(SublimeText3)
