javascript函數(shù)式編程基礎(chǔ)
一、引言
函數(shù)式編程的歷史已經(jīng)很悠久了,但是最近幾年卻頻繁的出現(xiàn)在大眾的視野,很多不支持函數(shù)式編程的語(yǔ)言也在積極加入閉包,匿名函數(shù)等非常典型的函數(shù)式編程特性。大量的前端框架也標(biāo)榜自己使用了函數(shù)式編程的特性,好像一旦跟函數(shù)式編程沾邊,就很高大上一樣,而且還有一些專門針對(duì)函數(shù)式編程的框架和庫(kù),比如:RxJS、cycleJS、ramdaJS、lodashJS、underscoreJS 等。函數(shù)式編程變得越來(lái)越流行,掌握這種編程范式對(duì)書寫高質(zhì)量和易于維護(hù)的代碼都大有好處,所以我們有必要掌握它。
二、什么是函數(shù)式編程
維基百科定義:函數(shù)式編程(英語(yǔ):functional programming),又稱泛函編程,是一種編程范式,它將電腦運(yùn)算視為數(shù)學(xué)上的函數(shù)計(jì)算,并且避免使用程序狀態(tài)以及易變對(duì)象。
三、純函數(shù)(函數(shù)式編程的基石,無(wú)副作用的函數(shù))
在初中數(shù)學(xué)里,函數(shù) f 的定義是:對(duì)于輸入 x 產(chǎn)生一個(gè)唯一輸出 y=f(x)。這便是純函數(shù)。它符合兩個(gè)條件:1.此函數(shù)在相同的輸入值時(shí),總是產(chǎn)生相同的輸出。函數(shù)的輸出和當(dāng)前運(yùn)行環(huán)境的上下文狀態(tài)無(wú)關(guān)。2.此函數(shù)運(yùn)行過(guò)程不影響運(yùn)行環(huán)境,也就是無(wú)副作用(如觸發(fā)事件、發(fā)起 http 請(qǐng)求、打印/log 等)。簡(jiǎn)單來(lái)說(shuō),也就是當(dāng)一個(gè)函數(shù)的輸出不受外部環(huán)境影響,同時(shí)也不影響外部環(huán)境時(shí),該函數(shù)就是純函數(shù),也就是它只關(guān)注邏輯運(yùn)算和數(shù)學(xué)運(yùn)算,同一個(gè)輸入總得到同一個(gè)輸出。javascript 內(nèi)置函數(shù)有不少純函數(shù),也有不少非純函數(shù)。
純函數(shù):Array.prototype.sliceArray.prototype.mapString.prototype.toUpperCase
非純函數(shù):Math.randomDate.nowArray.ptototype.splice
這里我們以 slice 和 splice 方法舉例:
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); //=> []
我們看到調(diào)用數(shù)組的 slice 方法每次返回的結(jié)果完全相同,同時(shí) xs 不會(huì)被改變,而調(diào)用 splice 方法每次返回值都不一樣,同時(shí) xs 變得面目全非。這就是我們強(qiáng)調(diào)使用純函數(shù)的原因,因?yàn)榧兒瘮?shù)相對(duì)于非純函數(shù)來(lái)說(shuō),在可緩存性、可移植性、可測(cè)試性以及并行計(jì)算方面都有著巨大的優(yōu)勢(shì)。這里我們以可緩存性舉例:
var squareNumber = memoize(function(x){ return x*x; }); squareNumber(4); //=> 16 squareNumber(4); // 從緩存中讀取輸入值為 4 的結(jié)果 //=> 16
那我們?nèi)绾伟岩粋€(gè)非純函數(shù)變純呢?比如下面這個(gè)函數(shù):
var minimum = 21; var checkAge = function(age) { return age >= minimum; };
這個(gè)函數(shù)的返回值依賴于可變變量 minimum 的值,它依賴于系統(tǒng)狀態(tài)。在大型系統(tǒng)中,這種對(duì)于外部狀態(tài)的依賴是造成系統(tǒng)復(fù)雜性大大提高的主要原因。
var checkAge = function(age) { var minimum = 21; return age >= minimum; };
通過(guò)改造,我們把 checkAge 變成了一個(gè)純函數(shù),它不依賴于系統(tǒng)狀態(tài),但是 minimum 是通過(guò)硬編碼的方式定義的,這限制了函數(shù)的擴(kuò)展性,我們可以在后面的柯里化中看到如何優(yōu)雅的使用函數(shù)式解決這個(gè)問(wèn)題。所以把一個(gè)函數(shù)變純的基本手段是不要依賴系統(tǒng)狀態(tài)。
四、函數(shù)柯里化
curry 的概念很簡(jiǎn)單:將一個(gè)低階函數(shù)轉(zhuǎn)換為高階函數(shù)的過(guò)程就叫柯里化。
用一個(gè)形象的比喻就是:
比如對(duì)于加法操作:var add = (x, y) => x + y,我們可以這樣柯里化:
//es5寫法 var add = function(x) { return function(y) { return x + y; }; }; //es6寫法 var add = x => (y => x + y); //試試看 var increment = add(1); var addTen = add(10); increment(2); // 3 addTen(2); // 12
對(duì)于加法這種極其簡(jiǎn)單的函數(shù)來(lái)說(shuō),柯里化并沒(méi)有什么用。還記得上面的 checkAge 函數(shù)嗎?我們可以這樣柯里化它:
var checkage = min => (age => age > min); var checkage18 = checkage(18); checkage18(20); // =>true
這表明函數(shù)柯里化是一種“預(yù)加載”函數(shù)的能力,通過(guò)傳遞一到兩個(gè)參數(shù)調(diào)用函數(shù),就能得到一個(gè)記住了這些參數(shù)的新函數(shù)。從某種意義上來(lái)講,這是一種對(duì)參數(shù)的緩存,是一種非常高效的編寫函數(shù)的方法:
var curry = require('lodash').curry; //柯里化兩個(gè)純函數(shù) var match = curry((what, str) => str.match(what)); var filter = curry((f, ary) => ary.filter(f)); //判斷字符串里有沒(méi)有空格 var hasSpaces = match(/\s+/g); hasSpaces("hello world"); // [ ' ' ] hasSpaces("spaceless"); // null var findSpaces = filter(hasSpaces); findSpaces(["tori_spelling", "tori amos"]); // ["tori amos"]
五、函數(shù)組合
假設(shè)我們需要對(duì)一個(gè)字符串做一些列操作,如下,為了方便舉例,我們只對(duì)一個(gè)字符串做兩種操作,我們定義了一個(gè)新函數(shù) shout,先調(diào)用 toUpperCase,然后把返回值傳給 exclaim 函數(shù),這樣做有什么不好呢?不優(yōu)雅,如果做得事情一多,嵌套的函數(shù)會(huì)非常深,而且代碼是由內(nèi)往外執(zhí)行,不直觀,我們希望代碼從右往左執(zhí)行,這個(gè)時(shí)候我們就得使用組合。
var toUpperCase = function(x) { return x.toUpperCase(); }; var exclaim = function(x) { return x + '!'; }; var shout = function(x){ return exclaim(toUpperCase(x)); }; shout("send in the clowns"); //=> "SEND IN THE CLOWNS!"
使用組合,我們可以這樣定義我們的 shout 函數(shù):
//定義compose var compose = (...args) => x => args.reduceRight((value, item) => item(value), 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!"
代碼從右往左執(zhí)行,非常清晰明了,一目了然。我們定義的 compose 像 N 面膠一樣,可以將任意多個(gè)純函數(shù)結(jié)合到一起。這種靈活的組合可以讓我們像拼積木一樣來(lái)組合函數(shù)式的代碼:
var head = function(x) { return x[0]; }; var reverse = reduce(function(acc, x){ return [x].concat(acc); }, []); var last = compose(head, reverse); last(['jumpkick', 'roundhouse', 'uppercut']); //=> 'uppercut'
六、聲明式和命令式代碼
命令式代碼:命令“機(jī)器”如何去做事情(how),這樣不管你想要的是什么(what),它都會(huì)按照你的命令實(shí)現(xiàn)。聲明式代碼:告訴“機(jī)器”你想要的是什么(what),讓機(jī)器想出如何去做(how)。與命令式不同,聲明式意味著我們要寫表達(dá)式,而不是一步一步的指示。以 SQL 為例,它就沒(méi)有“先做這個(gè),再做那個(gè)”的命令,有的只是一個(gè)指明我們想要從數(shù)據(jù)庫(kù)取什么數(shù)據(jù)的表達(dá)式。至于如何取數(shù)據(jù)則是由它自己決定的。以后數(shù)據(jù)庫(kù)升級(jí)也好,SQL 引擎優(yōu)化也好,根本不需要更改查詢語(yǔ)句。這是因?yàn)椋卸喾N方式解析一個(gè)表達(dá)式并得到相同的結(jié)果。這里為了方便理解,我們來(lái)看一個(gè)例子:
// 命令式 var makes = []; for (var i = 0; i < cars.length; i++) { makes.push(cars[i].make); } // 聲明式 var makes = cars.map(function(car){ return car.make; });
命令式的循環(huán)要求你必須先實(shí)例化一個(gè)數(shù)組,而且執(zhí)行完這個(gè)實(shí)例化語(yǔ)句之后,解釋器才繼續(xù)執(zhí)行后面的代碼。然后再直接迭代 cars 列表,手動(dòng)增加計(jì)數(shù)器,就像你開了一輛零部件全部暴露在外的汽車一樣。這不是優(yōu)雅的程序員應(yīng)該做的。聲明式的寫法是一個(gè)表達(dá)式,如何進(jìn)行計(jì)數(shù)器迭代,返回的數(shù)組如何收集,這些細(xì)節(jié)都隱藏了起來(lái)。它指明的是做什么,而不是怎么做。除了更加清晰和簡(jiǎn)潔之外,map 函數(shù)還可以進(jìn)一步獨(dú)立優(yōu)化,甚至用解釋器內(nèi)置的速度極快的 map 函數(shù),這么一來(lái)我們主要的業(yè)務(wù)代碼就無(wú)須改動(dòng)了。函數(shù)式編程的一個(gè)明顯的好處就是這種聲明式的代碼,對(duì)于無(wú)副作用的純函數(shù),我們完全可以不考慮函數(shù)內(nèi)部是如何實(shí)現(xiàn)的,專注于編寫業(yè)務(wù)代碼。優(yōu)化代碼時(shí),目光只需要集中在這些穩(wěn)定堅(jiān)固的函數(shù)內(nèi)部即可。相反,不純的不函數(shù)式的代碼會(huì)產(chǎn)生副作用或者依賴外部系統(tǒng)環(huán)境,使用它們的時(shí)候總是要考慮這些不干凈的副作用。在復(fù)雜的系統(tǒng)中,這對(duì)于程序員的心智來(lái)說(shuō)是極大的負(fù)擔(dān)。
七、Point Free
pointfree 模式指的是,永遠(yuǎn)不必說(shuō)出你的數(shù)據(jù)。它的意思是說(shuō),函數(shù)無(wú)須提及將要操作的數(shù)據(jù)是什么樣的。一等公民的函數(shù)、柯里化(curry)以及組合協(xié)作起來(lái)非常有助于實(shí)現(xiàn)這種模式。
// 非 pointfree,因?yàn)樘岬搅藬?shù)據(jù):word var snakeCase = function (word) { return word.toLowerCase().replace(/\s+/ig, '_'); }; // pointfree var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
這種風(fēng)格能夠幫助我們減少不必要的命名,讓代碼保持簡(jiǎn)潔和通用。當(dāng)然,為了在一些函數(shù)中寫出 Point Free 的風(fēng)格,在代碼的其它地方必然是不那么 Point Free 的,這個(gè)地方需要自己取舍。
八、示例應(yīng)用
擁有了以上的知識(shí),我們是時(shí)候該寫一個(gè)示例應(yīng)用了。這里我們使用了 ramda ,沒(méi)有用 lodash 或者其他類庫(kù)。ramda 提供了 compose、curry 等很多函數(shù)。我們的應(yīng)用將做四件事:
1.根據(jù)特定搜索關(guān)鍵字構(gòu)造 url
2.向 flickr 發(fā)送 api 請(qǐng)求
3.把返回的 json 轉(zhuǎn)為 html 圖片
4.把圖片放到屏幕上上面提到了兩個(gè)不純的動(dòng)作,即從 flickr 的 api 獲取數(shù)據(jù)和在屏幕上放置圖片這兩件事。我們先來(lái)定義這兩個(gè)動(dòng)作,這樣就能隔離它們了。這里我們只是簡(jiǎn)單包裝了一下 jQuery 的 getJSON 函數(shù),把它變?yōu)橐粋€(gè) curry 函數(shù),還有就是把參數(shù)位置也調(diào)換了下,我們把它們放在 Impure 命名空間下以用來(lái)隔離,這樣我們就知道它們都是危險(xiǎn)函數(shù)。運(yùn)用函數(shù)柯里化和函數(shù)組合的技巧,我們就可以創(chuàng)建一個(gè)函數(shù)式的實(shí)際應(yīng)用了:
預(yù)覽地址:https://code.h5jun.com/vixe/1/edit?html,js,output 看看,多么美妙的聲明式規(guī)范啊,只說(shuō)做什么,不說(shuō)怎么做。現(xiàn)在我們可以把每一行代碼都視作一個(gè)等式,變量名所代表的屬性就是等式的含義。
九、總結(jié)
我們已經(jīng)見識(shí)到如何在一個(gè)小而不失真實(shí)的應(yīng)用中運(yùn)用新技能了,但是異常處理以及代碼分支呢?如何讓整個(gè)應(yīng)用都是函數(shù)式的,而不僅僅是把破壞性的函數(shù)放到命名空間下?如何讓應(yīng)用更安全更富有表現(xiàn)力?我會(huì)在下一篇文章中介紹函數(shù)式編程的更加高階一些的知識(shí),例如 Functor、Monad、Applicative 等概念。
以上就是javascript函數(shù)式編程基礎(chǔ)的詳細(xì)內(nèi)容,更多關(guān)于javascript函數(shù)式編程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript中的設(shè)計(jì)模式 單例模式
這篇文章主要給大家介紹的是JavaScript中的單例模式,設(shè)計(jì)模式代表了最佳的實(shí)踐,通常被有經(jīng)驗(yàn)的面向?qū)ο蟮能浖_發(fā)人員所采用。設(shè)計(jì)模式是軟件開發(fā)人員在軟件開發(fā)過(guò)程中面臨的一般問(wèn)題的解決方案,需要的朋友可以參考一下2021-09-09徒手實(shí)現(xiàn)關(guān)于JavaScript的24+數(shù)組方法
數(shù)組是我們?nèi)粘9ぷ髦杏玫淖铑l繁的一類數(shù)據(jù)結(jié)構(gòu),能幫助我們解決許多問(wèn)題,而其本身也包含接近33個(gè)之多的方法,做了一個(gè)腦圖分類如下,熟練使用數(shù)組的你,是否想知道他們內(nèi)部的實(shí)現(xiàn)原理呢?接下來(lái)小編就帶大家進(jìn)入主題,希望能幫助到你2021-09-09微信小程序Server端環(huán)境配置詳解(SSL, Nginx HTTPS,TLS 1.2 升級(jí))
這篇文章主要介紹了微信小程序Server端環(huán)境配置詳解(SSL, Nginx HTTPS,TLS 1.2 升級(jí))的相關(guān)資料,需要的朋友可以參考下2017-01-01Moment的feature導(dǎo)致線上bug解決分析
這篇文章主要為大家介紹了Moment的feature導(dǎo)致線上bug解決分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09自定義range?sliders滑塊實(shí)現(xiàn)元素拖動(dòng)方法
這篇文章主要為大家介紹了自定義range?sliders滑塊實(shí)現(xiàn)元素拖動(dòng)方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08微信小程序 基礎(chǔ)知識(shí)css樣式media標(biāo)簽
這篇文章主要介紹了微信小程序 基礎(chǔ)知識(shí)css樣式media標(biāo)簽的相關(guān)資料,需要的朋友可以參考下2017-02-02網(wǎng)頁(yè)里控制圖片大小的相關(guān)代碼
網(wǎng)頁(yè)里控制圖片大小的相關(guān)代碼...2006-06-06umi插件開發(fā)仿dumi項(xiàng)目自動(dòng)生成導(dǎo)航欄實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了umi插件開發(fā)仿dumi項(xiàng)目自動(dòng)生成導(dǎo)航欄實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01微信小程序 簡(jiǎn)單實(shí)例(閱讀器)的實(shí)例開發(fā)
這篇文章主要介紹了微信小程序 簡(jiǎn)單實(shí)例(閱讀器)的實(shí)例開發(fā)的相關(guān)資料,需要的朋友可以參考下2016-09-09