Funktionale Programmierung kann die Komplexität des Programms reduzieren: Eine Funktion sieht aus wie eine mathematische Formel. Das Erlernen der funktionalen Programmierung kann Ihnen helfen, einfacheren Code mit weniger Fehlern zu schreiben.
Reine Funktion
Eine reine Funktion kann als eine Funktion verstanden werden, die die gleiche Eingabe hat und die gleiche Ausgabe haben muss, ohne beobachtbare Nebenwirkungen
//pure function add(a + b) { return a + b; }
Die Oben ist eine reine Funktion, die nicht von anderen Variablen als der Funktion abhängt oder deren Status ändert und immer die gleiche Ausgabe für die gleiche Eingabe zurückgeben kann.
//impure var minimum = 21; var checkAge = function(age) { return age >= minimum; // 如果minimum改变,函数结果也会改变 }
Diese Funktion ist keine reine Funktion, da sie auf einem externen veränderlichen Zustand basiert
Wenn wir die Variable innerhalb der Funktion verschieben, wird sie zu einer reinen Funktion, also stellen wir sicher, dass Die Funktion kann das Alter jedes Mal korrekt vergleichen.
var checkAge = function(age) { var minimum = 21; return age >= minimum; };
Eine reine Funktion hat keine Nebenwirkungen. Einige Dinge, die Sie beachten müssen, sind, dass sie nicht auf den Systemstatus außerhalb der Funktion zugreift.
Änderungen werden übergeben in als Parameter Objekt
HTTP-Anfrage initiieren
Benutzereingaben beibehalten
DOM abfragen
Kontrollierte Mutation
Sie müssen einiges beachten Mutator-Methoden mutieren Arrays und Objekte. Sie müssen beispielsweise den Unterschied zwischen Splice und Slice kennen.
Wenn wir die Verwendung von Mutatormethoden für die an die Funktion übergebenen Objekte vermeiden, werden unsere Programme leichter zu verstehen und wir können vernünftigerweise davon ausgehen, dass unsere Funktionen nichts außerhalb der Funktion ändern.//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']
Im Vergleich zu unreinen Funktionen haben reine Funktionen die folgenden Vorteile:
Sie sind einfacher zu testen, da ihre einzige Verantwortung auf der Berechnungsausgabe basiert bei Eingabe
Ergebnisse können zwischengespeichert werden, da die gleiche Eingabe immer die gleiche Ausgabe erhält
Selbstdokumentierend, da Funktionsabhängigkeiten klar sind
Einfacher aufzurufen, da Sie nicht Wir müssen uns keine Gedanken über die Nebenwirkungen der Funktion machen
Da die Ergebnisse reiner Funktionen zwischengespeichert werden können, können wir sie uns merken, sodass komplexe und teure Vorgänge beim Aufruf nur einmal ausgeführt werden müssen. Beispielsweise kann das Zwischenspeichern der Ergebnisse eines großen Abfrageindex die Programmleistung erheblich verbessern.
Unvernünftige Programmierung reiner Funktionen
Die Verwendung reiner Funktionen kann die Komplexität des Programms erheblich reduzieren. Wenn wir jedoch zu viele abstrakte Konzepte der funktionalen Programmierung verwenden, wird auch unsere funktionale Programmierung sehr schwer zu verstehen sein.
Nehmen Sie sich eine Minute, um den obigen Code zu verstehen.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");
Solange Sie nicht mit diesen Konzepten der funktionalen Programmierung (Currying, Komposition und Requisiten) vertraut sind, ist es schwierig, den obigen Code zu verstehen. Im Vergleich zum rein funktionalen Ansatz ist der folgende Code einfacher zu verstehen und zu ändern, er beschreibt das Programm klarer und erfordert weniger Code.
Der Parameter der App-Funktion ist eine Tag-Zeichenfolge
JSON-Daten von Flickr abrufen
URLs aus den zurückgegebenen Daten extrahieren
Erstellen Sie Knotenarray
fügen Sie sie in das Dokument ein
oder Sie können Fetch und Promise für bessere asynchrone Vorgänge verwenden.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");
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) })
let responseToImages = (resp) => { let urls = resp.items.map((item) => item.media.m) let images = urls.map((url) => $('<img />', {src:url})) return images }
Konvertiert die zurückgegebenen Daten in URLs
Konvertiert URLs in Bildknoten
Funktion Die traditionelle Methode besteht darin, Trennen Sie die beiden oben genannten Aufgaben und übergeben Sie dann mit Compose das Ergebnis einer Funktion als Parameter an einen anderen Parameter.
compose gibt eine Kombination von Funktionen zurück. Jede Funktion verwendet das Ergebnis der letzteren Funktion als eigenen Eingabeparameter.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)
Was Compose hier tut, besteht darin, die Ergebnisse von URLs einschließlich der zu übergeben Die Bilderfunktion
gibt uns die Möglichkeit, sie in Zukunft wiederzuverwenden, indem wir den Code in reine Funktionen umwandeln, wodurch sie einfacher zu testen und selbstdokumentierbar sind. Das Schlimme ist, dass die übermäßige Verwendung dieser funktionalen Abstraktionen (wie im ersten Beispiel) die Dinge komplizierter macht, was nicht das ist, was wir wollen. Die wichtigste Frage, die Sie sich bei der Umgestaltung von Code stellen sollten, ist:let responseToImages = (data) => { return images(urls(data)) }
Wird der Code dadurch leichter lesbar und verständlicher?
基本功能函数
我并不是要诋毁函数式编程。每个程序员都应该齐心协力去学习基础函数,这些函数让你在编程过程中使用一些抽象出的一般模式,写出更加简洁明了的代码,或者像Marijn Haverbeke说的
一个程序员能够用常规的基础函数武装自己,更重要的是知道如何使用它们,要比那些苦思冥想的人高效的多。--Eloquent JavaScript, Marijn Haverbeke
这里列出了一些JavaScript开发者应该掌握的基础函数
Arrays
-forEach
-map
-filter
-reduce
Functions
-debounce
-compose
-partial
-curry
Less is More
让我们来通过实践看一下函数式编程能如何改善下面的代码
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
测试我们的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个方法:一个公有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)
到这里,我们已经成功的测试了我们的模块以及组成它的函数,学习到了纯函数以及如何使用函数组合。我们知道了纯函数与不纯函数的区别,知道纯函数更可读,由小函数组成,更容易测试。相比于不太合理的纯函数式编程,我们的代码更加可读、理解和修改,这也是我们重构代码的目的。