函數(shù)式編程入門實(shí)踐(一)
在文章之前,先和大家講一下對(duì)于函數(shù)式編程(Functional Programming, aka. FP)的理解(下文我會(huì)用FP指代函數(shù)式編程):
- FP需要保證函數(shù)都是純凈的,既不依賴外部的狀態(tài)變量,也不產(chǎn)生副作用?;诖饲疤嵯?,那么純函數(shù)的組合與調(diào)用,在時(shí)間順序上就不會(huì)產(chǎn)生依賴,改變多個(gè)函數(shù)的調(diào)用順序也不必?fù)?dān)心產(chǎn)生問題,因此也會(huì)消滅許多潛在的bug。
- 函數(shù)必須有輸入輸出。如果一個(gè)函數(shù)缺乏輸入或輸出,那么它其實(shí)是一段處理程序procedure而已。
- 函數(shù)盡可能的保持功能的單一,如果一個(gè)函數(shù)做了多件事情,那么它理論上應(yīng)當(dāng)被拆分為多個(gè)函數(shù)。
- FP的意義之一就是,在適當(dāng)?shù)臅r(shí)機(jī)使用聲明式編程,抽象了程序流的控制與表現(xiàn),從理解和維護(hù)的角度上會(huì)勝于命令式編程。
- FP是一種范式,但并不意味這和OOP(面向?qū)ο缶幊蹋_突,兩者當(dāng)然是可以和諧共存的。個(gè)人認(rèn)為 React 其實(shí)就是一個(gè)很好的栗子~
- Javascript的函數(shù)一等公民以及閉包的特性,決定了Javascript的確是適合施展FP的舞臺(tái)
理解閉包
閉包對(duì)于 Javascript 來說,當(dāng)然十分重要。然而對(duì)于函數(shù)式編程來說,這更加是必不可少的,必須掌握的概念,閉包的定義如下:
Closure is when a function remembers and accesses variables from outside of its own scope, even when that function is executed in a different scope.
相信大部分同學(xué)都對(duì)閉包有不錯(cuò)的理解,但是由于對(duì)FP的學(xué)習(xí)十分重要。接下來我還是會(huì)啰嗦的帶大家過一遍。閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)
簡單示例如下
// Closure demo function cube(x) { let z = 1; return function larger(y) { return x * y * z++; }; } const makeCube = cube(10); console.log(makeCube(5)); // 50 console.log(makeCube(5)); // 100
那么有沒有想過在函數(shù)makeCube,或者也可以說是函數(shù)larger是怎么記住原本不屬于自己作用域的變量x和z的呢?在控制臺(tái)查看makeCube.prototype,點(diǎn)開會(huì)發(fā)現(xiàn)原來是有個(gè)[[Scopes]]這個(gè)內(nèi)置屬性里的Closure(cube)記住了函數(shù)larger返回時(shí)記住的變量x和z。如果多嵌套幾層函數(shù),也會(huì)發(fā)現(xiàn)多幾個(gè)Closure(name)在[[Scopes]]的Scopes[]數(shù)組里,按序查找變量。
再看下圖測(cè)試代碼:
function cube(x) { return function wrapper(y) { let z = 1; return function larger() { return x * y * z++; }; } } const makeCubeY = cube(10); const makeCube = makeCubeY(5); const $__VAR1__ = '1. This var is just for test.'; let $__VAR2__ = '2. This var is just for test.'; var $__VAR3__ = '3. This var is just for test.'; console.log(makeCubeY.prototype, makeCube.prototype); console.log(makeCube()); // 50 console.log(makeCube()); // 100
打印makeCubeY.prototype:
打印makeCube.prototype:
通過這幾個(gè)實(shí)驗(yàn)可以從另一個(gè)角度去理解Javascript中閉包,一個(gè)閉包是怎么去查找不是自己作用域的變量呢?makeCube函數(shù)分別從[[Scopes]]中的Closure(wrapper)里找到變量y、z,Closure(cube)里找到變量x。至于全局let、const聲明的變量放在了Script里,全局var聲明的變量放在了Global里。
在學(xué)習(xí)FP前,理解閉包是尤為重要的~ 因?yàn)槭聦?shí)上大量的FP工具函數(shù)都使用了閉包這個(gè)特性。
工具函數(shù)
unary
const unary = fn => arg => fn(arg);
一元函數(shù),應(yīng)用于當(dāng)只想在某個(gè)函數(shù)上傳遞一個(gè)參數(shù)情況下使用。嘗試考慮以下場(chǎng)景:
console.log(['1', '2', '3'].map(parseInt)); // [1, NaN, NaN] console.log(['1', '2', '3'].map(unary(parseInt))); // [1, 2, 3]
parseInt(string, radix)接收兩個(gè)參數(shù),而map函數(shù)中接收的回調(diào)函數(shù)callback(currentValue[, index[, array]]),第二個(gè)參數(shù)是index,此時(shí)如果parseInt的使用就是錯(cuò)誤的。當(dāng)然除了Array.prototype.map,大量內(nèi)置的數(shù)組方法中的回調(diào)函數(shù)中都不止傳遞一個(gè)參數(shù),如果存在適用的只需要第一個(gè)參數(shù)的場(chǎng)景,unary函數(shù)就發(fā)揮了它的價(jià)值,無需修改函數(shù),優(yōu)雅簡潔地就接入了。(對(duì)于unary函數(shù),fn就是閉包記憶的變量數(shù)據(jù))
identity
const identity = v => v;
有同學(xué)會(huì)看到identity函數(shù)會(huì)覺得莫名其妙?是干嘛的?我第一眼看到也很迷惑?但是考慮以下場(chǎng)景:
console.log([false, 1, 2, 0, '5', true].filter( identity )); // [1, 2, "5", true] console.log([false, 0].some( identity )); // false console.log([-2, 1, '3'].every( identity )); // true
怎么樣?眼前一亮吧,沒想到identity函數(shù)原來深藏不露,事實(shí)上雖然identity返回了原值,但是在這些函數(shù)中Javascript會(huì)對(duì)返回的值進(jìn)行類型裝換,變成了布爾值。比如filter函數(shù)。我們可以看MDN定義filter描述如下(看標(biāo)粗的那一句)。
filter() calls a provided callback function once for each element in an array, and constructs a new array of all the values for which callback returns a value that coerces to true.
constant
const constant = v => () => v;
同樣,這個(gè)函數(shù)...乍一看,也不知道具體有什么用。但是考慮場(chǎng)景如下:
onst p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve('Hello!'); }, 200); }); p1.then(() => 'Hi').then(console.log); // Hi! p1.then(constant('Hi')).then(console.log); // Hi! p1.then('Hi').then(console.log); // Hello!
由于Promise.prototype.then只接受函數(shù),如果我僅僅只需要傳遞一個(gè)值時(shí),那么constant便會(huì)提供這種便利。當(dāng)然這個(gè)并沒有什么功能上的提升,但是的確提高了可閱讀性,也是函數(shù)式編程的一個(gè)優(yōu)點(diǎn)。
spreadArgs & gatherArgs
const spreadArgs = fn => argsArr => fn( ...argsArr ); const gatherArgs = fn => (...argsArr) => fn( argsArr );
嗯這兩個(gè)函數(shù)見名知義。分別用于展開一個(gè)函數(shù)的所有參數(shù)和收集一個(gè)函數(shù)所有參數(shù),這兩個(gè)函數(shù)明顯對(duì)立,那么它們的應(yīng)用場(chǎng)景又是什么呢?
spreadArgs函數(shù)示例如下:
function cube(x, y, z) { return x * y * z; } function make(fn, points) { return fn(points); } console.log(make(cube, [3, 4, 5])); // NaN console.log(make(spreadArgs(cube), [3, 4, 5])); // 60
gatherArgs函數(shù)示例如下:
function combineFirstTwo([v1, v2]) { return v1 + v2; } console.log([1, 2, 3, 4, 5].reduce(combineFirstTwo)); // Uncaught TypeError console.log([1, 2, 3, 4, 5].reduce(gatherArgs(combineFirstTwo))); // 15
看完以上代碼,簡單的兩個(gè)工具函數(shù),輕易的做到了對(duì)一個(gè)函數(shù)的轉(zhuǎn)換,從而使其適用于另一個(gè)場(chǎng)景。如果從此應(yīng)該可以瞥見函數(shù)式編程的一點(diǎn)點(diǎn)魅力,那么下面的兩個(gè)函數(shù)將給大家?guī)砀嗟捏@喜。
partial & curry
const partial = (fn, ...presetArgs) => (...laterArgs) => fn(...presetArgs, ...laterArgs); const curry = (fn, arity = fn.length, nextCurried) => (nextCurried = prevArgs => nextArg => { const args = [...prevArgs, nextArg]; if (args.length >= arity) { return fn(...args); } else { return nextCurried(args); } })([]);
相信大家對(duì)函數(shù)柯里化應(yīng)該或多或少有點(diǎn)了解。維基百科定義:
在計(jì)算機(jī)科學(xué)中,柯里化(英語:Currying),又譯為卡瑞化或加里化,是把接受多個(gè)參數(shù)的函數(shù)變換成接受一個(gè)單一參數(shù)(最初函數(shù)的第一個(gè)參數(shù))的函數(shù),并且返回接受余下的參數(shù)而且返回結(jié)果的新函數(shù)的技術(shù)。
當(dāng)然得益于閉包的強(qiáng)大威力,柯里化這個(gè)武器得以誕生于Javascript世界。請(qǐng)大家先精讀以上關(guān)于partiel、curry函數(shù)的代碼。
喝一杯咖啡~
先模擬一個(gè)ajax函數(shù)如下:
function ajax(url, params, callback) { setTimeout(() => { callback( `GET ${url} \nparams: ${params} \ndata: Hello! ${params} ` ); }); }
考慮partial使用場(chǎng)景如下:
const fetchPerson = partial( ajax, "http://some.api/person" ); fetchPerson('Teddy Bear', console.log); /* GET http://some.api/person params: Teddy Bear data: Hello! Teddy Bear */
考慮curry使用場(chǎng)景如下:
const fetchPerson = curry(ajax)('http://some.api/person'); const fetchUncleBarney = fetchPerson('Uncle Barney'); fetchUncleBarney(console.log); /* GET http://some.api/person params: Uncle Barney data: Hello! Uncle Barney */
partial和curry函數(shù)功能相似,但又有具體的不同應(yīng)用場(chǎng)景,但總體來說curry會(huì)比partial更自動(dòng)化一點(diǎn)。
但是!相信看完示例的同學(xué)又會(huì)有一連串問號(hào)?為什么好好地參數(shù)不一次性傳入,而非要分開多次傳入這么麻煩?原因如下:
- 最首要的原因是partial和curry函數(shù)都允許我們通過參數(shù)控制將一個(gè)函數(shù)的調(diào)用在時(shí)間和空間上分開了。傳統(tǒng)函數(shù)需要一次性將參數(shù)湊齊才能調(diào)用,但是有時(shí)候我們可以提前預(yù)置部分參數(shù),在最終需要觸發(fā)此函數(shù)時(shí),才將剩余參數(shù)傳入。這時(shí)候partial和curry就會(huì)變得十分有用。
- partial和curry的存在讓函數(shù)組合(compose)會(huì)更加便利。(函數(shù)組合也計(jì)劃之后和大家分享,這里就不詳細(xì)說了)。
- 當(dāng)然最重要是也提升了可閱讀性!一開始可能不這么以為,但是如果你實(shí)踐操作感受之后,也許會(huì)改觀。
P.S. 關(guān)于函數(shù)式編程的實(shí)踐,大家可以使用lodash/fp模塊進(jìn)行入門實(shí)踐。
一些思考
因?yàn)槲乙彩呛瘮?shù)式編程的初學(xué)者,如有不正確的地方,歡迎大家糾正~
接下來還是會(huì)繼續(xù)整理FP的學(xué)習(xí)資料,學(xué)習(xí)實(shí)踐,連載一些我對(duì)于函數(shù)式編程的學(xué)習(xí)與思考,希望和大家一起進(jìn)步~
謝謝大家(●´∀`●)~
以上所述是小編給大家介紹的Javascript函數(shù)式編程詳解整合,希望對(duì)大家有所幫助,如果大家有任何疑問請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
解決微信小程序云開發(fā)中獲取數(shù)據(jù)庫的內(nèi)容為空的方法
這篇文章主要介紹了解決微信小程序云開發(fā)中獲取數(shù)據(jù)庫的內(nèi)容為空的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-05-05用Javascript實(shí)現(xiàn)錨點(diǎn)(Anchor)間平滑跳轉(zhuǎn)
本文介紹的方法,實(shí)現(xiàn)了錨點(diǎn)(Anchor)間平滑跳轉(zhuǎn),效果非常不錯(cuò)。2009-09-09inquirer.js一個(gè)用戶與命令行交互的工具詳解
這篇文章主要介紹了inquirer.js一個(gè)用戶與命令行交互的工具詳解,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-05-05純javascript實(shí)現(xiàn)簡單下拉刷新功能
這篇文章主要介紹了純javascript實(shí)現(xiàn)簡單下拉刷新功能,沒有借助任何的框架,十分簡單實(shí)用,有需要的小伙伴來參考下吧。2015-03-03JavaScript監(jiān)聽一個(gè)DOM元素大小變化
這篇文章主要介紹了JavaScript監(jiān)聽一個(gè)DOM元素大小變化,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04JS數(shù)組索引檢測(cè)中的數(shù)據(jù)類型問題詳解
這篇文章主要給大家介紹了關(guān)于JS數(shù)組索引檢測(cè)中的數(shù)據(jù)類型問題的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01