本文首發(fā)于 vivo互聯(lián)網(wǎng)技術(shù) 微信公眾號(hào)?
鏈接:https://mp.weixin.qq.com/s/ZoXYbjuezOWgNyJKmSQmTw
作者:楊昆襄城網(wǎng)站制作公司哪家好,找創(chuàng)新互聯(lián)!從網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開(kāi)發(fā)、APP開(kāi)發(fā)、響應(yīng)式網(wǎng)站等網(wǎng)站項(xiàng)目制作,到程序開(kāi)發(fā),運(yùn)營(yíng)維護(hù)。創(chuàng)新互聯(lián)于2013年開(kāi)始到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來(lái)保證我們的工作的順利進(jìn)行。專注于網(wǎng)站建設(shè)就選創(chuàng)新互聯(lián)。
?【編寫高質(zhì)量函數(shù)系列】,往期精彩內(nèi)容:
《如何編寫高質(zhì)量的 JS 函數(shù)(1) -- 敲山震虎篇》介紹了函數(shù)的執(zhí)行機(jī)制,此篇將會(huì)從函數(shù)的命名、注釋和魯棒性方面,闡述如何通過(guò) JavaScript 編寫高質(zhì)量的函數(shù)。
?《如何編寫高質(zhì)量的 JS 函數(shù)(2)-- 命名/注釋/魯棒篇》從函數(shù)的命名、注釋和魯棒性方面,闡述如何通過(guò) JavaScript編寫高質(zhì)量的函數(shù)。
《如何 編寫高質(zhì)量的 JS 函數(shù)(3)-- 函數(shù)式編程[理論篇]》通過(guò)背景加提問(wèn)的方式,對(duì)函數(shù)式編程的本質(zhì)、目的、來(lái)龍去脈等方面進(jìn)行一次清晰的闡述。
本文會(huì)從如何用函數(shù)式編程思想編寫高質(zhì)量的函數(shù)、分析源碼里面的技巧,以及實(shí)際工作中如何編寫,來(lái)展示如何打通你的任督二脈。話不多說(shuō),下面就開(kāi)始實(shí)戰(zhàn)吧。
這里我通過(guò)簡(jiǎn)單的 demo 來(lái)說(shuō)明一些技巧。技巧點(diǎn)如下:
這可能是一個(gè)硬編碼,不夠靈活性,你可能需要進(jìn)行處理了,如何處理呢?比如通過(guò)傳參來(lái)干掉值類型的變量,下面舉一個(gè)簡(jiǎn)單的例子。
代碼如下:
document.querySelector('#msg').innerHTML = 'Hello World'
'
我們來(lái)欣賞一下上面的代碼:
第一:硬編碼味道很重,代碼都是寫死的。
第二:擴(kuò)展性很差,復(fù)用性很低,難道我要在其他地方進(jìn)行 crtl c ctrl v 然后再手工改?
第三:如果在 document.querySelector('#msg')獲取對(duì)象后,不想 innerHTML ,我想做一些其他的事情,怎么辦?
OK ,下面我就先向大家展示一下,如何完全重構(gòu)這段代碼。這里我只寫 JS 部分:
代碼如下:// 使用到了組合函數(shù),運(yùn)用了函數(shù)的高階性等
const compose = (...fns) => value => fns.reverse().reduce((acc, fn) => fn(acc), value)
const documentWrite = document.write.bind(document)
const createNode = function(text) {
return '' + text + '
'
}
const setText = msg => msg
const printMessage = compose(
documentWrite,
createNode,
setText
)
printMessage('hi~ godkun')
效果如圖所示:
完整代碼我放在了下面兩個(gè)地址上,小伙伴可自行查看。
codepen:?codepen.io/godkun/pen/…
gist:gist.github.com/godkun/772c…
compose?函數(shù)的執(zhí)行順序是從右向左,也就是數(shù)據(jù)流是從右向左流,可以把
const printMessage = compose(
documentWrite,
createNode,
setText
)
看成是下面這種形式:
documentWrite(createNode(setText(value)))
在 linux 世界里,是遵循 pipe (管道) 的思想,也就是數(shù)據(jù)從左向右流,那怎么把上面的代碼變成 pipe 的形式呢?
很簡(jiǎn)單,只需要把 const compose = (...fns) => value => fns.reverse().reduce((acc, fn) => fn(acc), value)?中的 reverse 去掉就好了,寫成:
const compose = (...fns) => value => fns.reduce((acc, fn) => fn(acc), value)
是不是發(fā)現(xiàn)通過(guò)用函數(shù)式編程進(jìn)行重構(gòu)后,這個(gè)代碼變得非常的靈活,好處大致有如下:
函數(shù)被拆成了一個(gè)個(gè)具有單一功能的小函數(shù)
硬編碼被干掉了,變得更加靈活
使用了組合函數(shù)、高階函數(shù)來(lái)靈活的組合各個(gè)小函數(shù)
思考題:這里我甩貼一張小伙伴在群里分享的圖:
這是我送個(gè)大家的禮物,大家可以嘗試把上面圖片的代碼用函數(shù)式進(jìn)行完全重構(gòu),加油。
代碼 demo 如下:
let arr = [1,3,2,4,5]
function fun(arr) {
let result = arr.sort()
console.log('result', result)
console.log('arr', arr)
}
fun(arr)
結(jié)果如下圖所示:
看上面,你會(huì)發(fā)現(xiàn)數(shù)組 arr 被修改了。由于 fun(arr)?函數(shù)中的參數(shù) arr 是引用類型,如果函數(shù)體內(nèi)對(duì)此引用所指的數(shù)據(jù)進(jìn)行直接操作的話,就會(huì)有潛在的副作用,比如原數(shù)組被修改了,這種情況下,該怎么辦呢?
很簡(jiǎn)單,在函數(shù)體內(nèi)對(duì) arr 這個(gè)引用類型進(jìn)行創(chuàng)建副本。如下面代碼:
let arr = [1,3,2,4,5]
function fun(arr) {
let arrNew = arr.slice()
let result = arrNew.sort()
console.log('result', result)
console.log('arr', arr)
}
fun(arr)
通過(guò) slice 來(lái)創(chuàng)建一個(gè)新的數(shù)組,然后對(duì)新的數(shù)組進(jìn)行操作,這樣就達(dá)到了消除副作用的目的。這里只是舉一個(gè)例子,但是核心思想已經(jīng)闡述出來(lái)了,體現(xiàn)了理論卷中的數(shù)據(jù)不可變的思想了。
如果函數(shù)體內(nèi)引用變量的變化,會(huì)造成超出其作用域的影響,比如上面代碼中對(duì) arr 進(jìn)行操作,影響到了數(shù)組 arr 本身 。這時(shí)就需要思考一下,要不要采用不可變的思想,對(duì)引用類型進(jìn)行處理。
注意函數(shù)里面有沒(méi)有大量的?for?循環(huán)
為什么說(shuō)這個(gè)呢,因?yàn)檫@個(gè)很好判斷。如果有的話,就要思考一下需不需要對(duì) for 循環(huán)進(jìn)行處理,下文有對(duì) for 循環(huán)的專門介紹。
注意函數(shù)里面有沒(méi)有過(guò)多的?if/else
也是一樣的思想,過(guò)多的 if/else 也要根據(jù)情況去做相應(yīng)的處理。
標(biāo)題的意識(shí)其實(shí)可以這樣理解,對(duì)函數(shù)進(jìn)行高階化處理。當(dāng)把函數(shù)當(dāng)成參數(shù)的時(shí)候,也就是把代碼本身當(dāng)成參數(shù)了。
什么情況下要考慮高階化呢。
當(dāng)優(yōu)化到一定地步后,發(fā)現(xiàn)還是不夠復(fù)用性,這時(shí)就要考慮將參數(shù)進(jìn)行函數(shù)化,這樣將參數(shù)變成可以提供更多功能的函數(shù)。
函數(shù)的高階化,往往在其他功能上得以體現(xiàn),比如柯里化,組合。
通過(guò)上面例子的分析,我也向大家展示了如何將函數(shù)最小化。通過(guò)將大函數(shù)拆成多個(gè)具有單一職責(zé)的小函數(shù),來(lái)提高復(fù)用性和靈活性。
函數(shù)式編程?不是萬(wàn)能的,大家不要認(rèn)為它很完美,它也有自己的缺點(diǎn),如下兩點(diǎn):
進(jìn)行?函數(shù)式編程?時(shí), 如果使用不恰當(dāng),會(huì)造成性能問(wèn)題。比如遞歸用的不恰當(dāng),比如柯里化嵌套的過(guò)多。
在進(jìn)行函數(shù)式編程時(shí),不要過(guò)度的抽象,過(guò)度的抽象會(huì)導(dǎo)致可讀性變差。
說(shuō)到函數(shù)式編程,那一定要看看 Ramda.js 的源碼。Ramda.js 的源碼搞懂后,函數(shù)式編程的思想也就基本沒(méi)什么問(wèn)題了。
關(guān)于 Ramda.js 可以看一下阮大的博客:
Ramda 函數(shù)庫(kù)參考教程
看完了,那開(kāi)始執(zhí)行:
git clone git@github.com:ramda/ramda.git
然后我們來(lái)分析源碼,首先按照常規(guī)套路,看一下 source/index.js 文件。
如圖所示:
繼續(xù)分析,看一下 add.js。
import _curry2 from './internal/_curry2';
var add = _curry2(function add(a, b) {
return Number(a) + Number(b);
});
export default add;
看上面代碼,我們發(fā)現(xiàn),add 函數(shù)被包了一個(gè)?_curry2 函數(shù)。下劃線代表這是一個(gè)內(nèi)部方法,不暴露成 API 。這時(shí),再看其他函數(shù),會(huì)發(fā)現(xiàn)都被包了一個(gè)?_curry1/2/3/N 函數(shù)。
如下圖所示:
從代碼中可以知道,1/2/3/N 代表掉參數(shù)個(gè)數(shù)為 1/2/3/N 的函數(shù)的柯里化,而且會(huì)發(fā)現(xiàn),所有的 ramda 函數(shù)都是經(jīng)過(guò)柯里化的。
為什么 ramda.js 要對(duì)函數(shù)全部柯里化?
我們看一下普通的函數(shù) f(a, b, c)?。如果只在調(diào)用的時(shí)候,傳遞 a 。會(huì)發(fā)現(xiàn),JS 在運(yùn)行調(diào)用時(shí),會(huì)將 b 和 c 設(shè)置為 undefined 。
從上面可以知道,JS 語(yǔ)言不能原生支持柯里化。非柯里化函數(shù)會(huì)導(dǎo)致缺少參數(shù)的實(shí)參變成 undefined 。ramda.js 對(duì)函數(shù)全部柯里化的目的,就是為了優(yōu)化上面的場(chǎng)景。
下面,我們看一下?_curry2 代碼,這里為了可讀性,我對(duì)代碼進(jìn)行了改造,我把?_isPlaceholder 去掉了,假設(shè)沒(méi)有占位符,同時(shí)把?_curry1 放在函數(shù)內(nèi),并且對(duì)過(guò)程進(jìn)行了相應(yīng)注釋。
二元參數(shù)的柯里化,代碼如下:
function _curry2(fn) {
return function f2(a, b) {
switch (arguments.length) {
case 0:
return f2;
case 1:
return _curry1(function (_b) {
// 將參數(shù)從右到左依次賦值 1 2
// 第一次執(zhí)行時(shí),是 fn(a, 1)
return fn(a, _b);
});
default:
// 參數(shù)長(zhǎng)度是 2 時(shí) 直接進(jìn)行計(jì)算
return fn(a, b);
}
};
}
function _curry1(fn) {
return function f1(a) {
// 對(duì)參數(shù)長(zhǎng)度進(jìn)行判斷
if (arguments.length === 0) {
return f1;
} else {
// 通過(guò) apply 來(lái)返回函數(shù) fn(a, 1)
return fn.apply(this, arguments);
}
};
}
const add = _curry2(function add(a, b) {
return Number(a) + Number(b);
});
// 第一次調(diào)用是 fn(a, 1)
let r1 = add(1)
// 第二次調(diào)用是 fn(2,1)
let r2 = r1(2)
console.log('sss', r2)
完整代碼地址如下:
gist:gist.github.com/godkun/0d22…
codeopen:codepen.io/godkun/pen/…
看了上面對(duì) ramda.js 源碼中柯里化的分析,是不是有點(diǎn)收獲,就像上面說(shuō)的,柯里化的目的是為了優(yōu)化在 JS 原生下的一些函數(shù)場(chǎng)景。好處如下:
從上面 add 函數(shù)可以知道,通過(guò)柯里化,可以讓函數(shù)在真正需要計(jì)算的時(shí)候進(jìn)行計(jì)算,起到了延遲的作用,也可以說(shuō)體現(xiàn)了惰性思想。
柯里化命名的由來(lái)
本文一開(kāi)始,我就以一個(gè)例子向大家展示了組合函數(shù) compose 和 pipe 的用法。
關(guān)于 ramda 中,compose 和 pipe 的實(shí)現(xiàn)這里就不再分析了,小伙伴自己看著源碼分析一下。這里我就簡(jiǎn)潔說(shuō)一下組合函數(shù)的一些個(gè)人看法。
在我看來(lái),組合是函數(shù)式編程的核心,函數(shù)式編程的思想是要函數(shù)盡可能的小,盡可能的保證職責(zé)單一。這就直接確定了組合函數(shù)在?函數(shù)式編程中的地位,玩好了組合函數(shù),函數(shù)式編程?也就基本上路了。
和前端的組件進(jìn)行對(duì)比來(lái)深刻的理解組合函數(shù)
函數(shù)的組合思想是面向過(guò)程的一種封裝,而前端的組件思想是面對(duì)對(duì)象的一種封裝。
故事的背景
實(shí)際工作中,會(huì)遇到下面這種接收和處理數(shù)據(jù)的場(chǎng)景。
代碼如下:
// 偽代碼
res => {
// name 是字符串,age 是數(shù)字
if (res.data && res.data.name && res.data.age) {
// TODO:
}
}
上面這樣寫,看起來(lái)好像也沒(méi)什么問(wèn)題,但是經(jīng)不起分析。比如 name 是數(shù)字,age 返回的不是數(shù)字。這樣的話, if 中的判斷是能通過(guò)的,但是實(shí)際結(jié)果并不是想要的。
那該怎么辦呢?問(wèn)題不大,跟著我一步步的優(yōu)化就 OK 了。
res => {
if (res.data && typeof res.data.name === 'string' && typeof res.data.age === 'number') {
// TODO:
}
}
看起來(lái)是夠魯棒了,但是這段代碼過(guò)于命令式,無(wú)法復(fù)用到其他地方,在其他的場(chǎng)景中,還要重寫一遍這些代碼。
//?is?是一個(gè)對(duì)象函數(shù)?偽代碼
res => {
if (is.object(res.data) && is.string(res.data.name) && is.number(res.data.age)) {
// TODO:
}
}
將過(guò)程抽象掉的行為也是一種函數(shù)式思想。上面代碼,提高了復(fù)用性,將判斷的過(guò)程抽象成了 is 的對(duì)象函數(shù)中,這樣在其他地方都可以復(fù)用這個(gè) is 。
但是,代碼還是有問(wèn)題,一般來(lái)說(shuō),各個(gè)接口的返回?cái)?shù)據(jù)都是 res.data 這種類型的。所以如果按照上面的代碼,我們會(huì)發(fā)現(xiàn),每次都要寫 is.object(res.data)?這是不能容忍的一件事。我們能不能做到不寫這個(gè)判斷呢?
當(dāng)然可以,你完全可以在 is 里面加一層對(duì) data 的判斷,當(dāng)然這個(gè)需要你把 data 作為參數(shù) 傳給 is 。
//?is?是一個(gè)對(duì)象函數(shù)?偽代碼
res => {
if (is.string(res.data, data.name) && is.number(res.data, data.age)) {
// TODO:
}
}
按照上面的寫法,is 系列函數(shù)會(huì)對(duì)第一個(gè)參數(shù)進(jìn)行 object 類型判斷,會(huì)再次提高復(fù)用性。
好像已經(jīng)很不錯(cuò)了,但其實(shí)還遠(yuǎn)遠(yuǎn)不夠。
有 if 語(yǔ)句存在,可能會(huì)有人說(shuō),if 語(yǔ)句存在有什么的啊?,F(xiàn)在我來(lái)告訴你,這塊有 if 為什么不好。是因?yàn)?if 語(yǔ)句的?()?里面,最終的值都會(huì)表現(xiàn)成布爾值。所以這塊限制的很死,需要解決 if 語(yǔ)句的問(wèn)題。
說(shuō)完這些問(wèn)題,那下面我們來(lái)解決吧。
如果要做到高度抽象和復(fù)用的話,首先把需要的功能羅列一下,大致如下:
第一個(gè)功能:檢查類型
第二個(gè)功能:調(diào)試功能,可以自定義 console 的輸出形式
第三個(gè)功能:處理異常的功能(簡(jiǎn)單版)
看到上面功能后,我們想一下函數(shù)式思想中有哪些武器可以被我們使用到。首先怎么把不同的函數(shù)組合在一起。
現(xiàn)在,如何將小函數(shù)組合成一個(gè)完成特定功能的函數(shù)呢?
想一下,你會(huì)發(fā)現(xiàn),這里需要用到函數(shù)的高階性,要將函數(shù)作為參數(shù)傳入多功能函數(shù)中。ok ,現(xiàn)在我們知道實(shí)現(xiàn)的大致方向了,下面我們來(lái)嘗試一下吧。
這里我直接把我的實(shí)現(xiàn)過(guò)程貼出來(lái)了,有相應(yīng)的注釋,代碼如下:
/** * 多功能函數(shù) * @param {Mixed} value 傳入的數(shù)據(jù) * @param {Function} predicate 謂詞,用來(lái)進(jìn)行斷言 * @param {Mixed} tip 默認(rèn)值是 value */
function tap(value, predicate, tip = value) {
if(predicate(value)) {
log('log', `{type: ${typeof value}, value: ${value} }`, `額外信息:${tip}`)
}
}
const is = {
undef : v => v === null || v === undefined,
notUndef : v => v !== null && v !== undefined,
noString : f => typeof f !== 'string',
noFunc : f => typeof f !== 'function',
noNumber : n => typeof n !== 'number',
noArray : !Array.isArray,
};
function log(level, message, tip) {
console[level].call(console, message, tip)
}
const res1 = {data: {age: '', name: 'godkun'}}
const res2 = {data: {age: 66, name: 'godkun'}}
// 函數(shù)的組合,函數(shù)的高階
tap(res1.data.age, is.noNumber)
tap(res2.data.age, is.noNumber)
結(jié)果圖如下:
會(huì)發(fā)現(xiàn)當(dāng),age 不是 Number 類型的時(shí)候,就會(huì)打印對(duì)應(yīng)的提示信息,當(dāng)是 Number 類型的時(shí)候,就不會(huì)打印信息。
這樣的話,在業(yè)務(wù)中就可以直接寫:
res => {
tap(res.data.age, is.noNumber)
// TODO: 處理 age
}
不用 if 語(yǔ)句,如果有異常,看一下打印信息,會(huì)一目了然的。
當(dāng)然這樣寫肯定不能放到生產(chǎn)上的,因?yàn)?tap 不會(huì)阻止后續(xù)操作,我這樣寫的原因是:這個(gè) tap 函數(shù)主要是用來(lái)開(kāi)發(fā)調(diào)試的。
但是,如果需要保證不符合的數(shù)據(jù)需要直接在 tap 處終止,那可以在 tap 函數(shù)里面加下 return false return true 。然后寫成下面代碼的形式:
res => {
// if 語(yǔ)句中的返回值是布爾值
if (tap(res.data.age, is.noNumber)) {
// TODO: 處理 age
}
}
但是這樣寫,會(huì)有個(gè)不好的地方。那就是用到了 if 語(yǔ)句,用 if 語(yǔ)句也沒(méi)什么不好的。但退一步看 tap 函數(shù),你會(huì)發(fā)現(xiàn),還是不夠復(fù)用,函數(shù)內(nèi),還存在硬編碼的行為。
如下圖所示:
存在兩點(diǎn)問(wèn)題:
第一點(diǎn):把 console 的行為固定死了,導(dǎo)致不能設(shè)置 console.error()?等行為。
第二點(diǎn):不能拋出異常,就算類型不匹配,也阻止不了后續(xù)步驟的執(zhí)行。
怎么解決呢?
簡(jiǎn)單分析一下,這里先采用惰性的思想,讓一個(gè)函數(shù)確定好幾個(gè)參數(shù),然后再讓這個(gè)函數(shù)去調(diào)用其他不固定的參數(shù)。這樣做的好處是減少了相同參數(shù)的多次 coding ,因?yàn)橄嗤膮?shù)已經(jīng)內(nèi)置了,不用再去傳了。
分析到這,你會(huì)發(fā)現(xiàn),這樣的行為其實(shí)就是柯里化,通過(guò)將多元函數(shù)變成可以一元函數(shù)。同時(shí),通過(guò)柯里化,可以靈活設(shè)置好初始化需要提前確定的參數(shù),大大提高了函數(shù)的復(fù)用性和靈活性。
對(duì)于柯里化,由于源碼分析篇,我已經(jīng)分析了 ramda 的柯里化實(shí)現(xiàn)原理,這里我為了節(jié)省代碼,就直接使用 ramda 了。
代碼如下:
const R = require('ramda')
// 其實(shí)這里你可以站在一個(gè)高層去把它們想象成函數(shù)的重載
// 通過(guò)傳參的不同來(lái)實(shí)現(xiàn)不同的功能
const tapThrow = R.curry(_tap)('throw', 'log')
const tapLog = R.curry(_tap)(null, 'log')
function _tap(stop, level, value, predicate, error=value) {
if(predicate(value)) {
if (stop === 'throw') {
log(`${level}`, 'uncaught at check', error)
throw new Error(error)
}
log(`${level}`, `{type: ${typeof value}, value: ${value} }`, `額外信息:${error}`)
}
}
const is = {
undef : v => v === null || v === undefined,
notUndef : v => v !== null && v !== undefined,
noString : f => typeof f !== 'string',
noFunc : f => typeof f !== 'function',
noNumber : n => typeof n !== 'number',
noArray : !Array.isArray,
};
function log(level, message, error) {
console[level].call(console, message, error)
}
const res = {data: {age: '66', name: 'godkun'}}
function main() {
// 不開(kāi)啟異常忽略,使用 console.log 的 tapLog 函數(shù)
// tapLog(res.data.age, is.noNumber)
// 開(kāi)啟異常忽略,使用 console.log 的 tapThrow 函數(shù)
tapThrow(res.data.age, is.noNumber)
console.log('能不能走到這')
}
main()
代碼地址如下:
gist:?gist.github.com/godkun/d394…
關(guān)鍵注釋,我已經(jīng)在代碼中標(biāo)注了。上面代碼在第一次進(jìn)行函數(shù)式優(yōu)化的時(shí)候,在組合和高階的基礎(chǔ)上,加入了柯里化,從而讓函數(shù)變得更有復(fù)用性。
PS: 具有柯里化的函數(shù),在我看來(lái),也是體現(xiàn)了函數(shù)的重載性。
執(zhí)行結(jié)果如下圖所示:
會(huì)發(fā)現(xiàn)使用 tapThrow 函數(shù)時(shí),當(dāng)類型不匹配的時(shí)候,會(huì)阻止后續(xù)步驟的執(zhí)行。
我通過(guò)多次優(yōu)化,向大家展示了,如何一步步的去優(yōu)化一個(gè)函數(shù)。從開(kāi)始的命令式優(yōu)化,到后面的函數(shù)式優(yōu)化,從開(kāi)始的普通函數(shù),到后面的逐步使用了高階、組合、柯里的特性。從開(kāi)始的有 if/else 語(yǔ)句到后面的逐步干掉它,來(lái)獲得更高的復(fù)用性。通過(guò)這個(gè)實(shí)戰(zhàn),大家可以知道,如何循序漸進(jìn)的使用函數(shù)式編程,讓代碼變得更加優(yōu)秀。
之前就有各種干掉 for 循環(huán)的文章。各種討論,這里按照我的看法來(lái)解釋一下,為什么會(huì)存在干掉 for 循環(huán)這一說(shuō)。
代碼如下:
let arr = [1,2,3,4]
for (let i = 0; i < arr.length; i++) {
// TODO: ...
}
我們看上面這段代碼,我來(lái)問(wèn)一個(gè)問(wèn)題:上面這段代碼如何復(fù)用到其他的函數(shù)中?
稍微想一下,大家肯定可以很快的想出來(lái),那就是封裝成函數(shù),然后在其他函數(shù)中進(jìn)行調(diào)用。
因?yàn)?for 循環(huán)是一種命令控制結(jié)構(gòu),它很難被插入到其他操作中,也發(fā)現(xiàn)了 for 循環(huán)很難被復(fù)用的現(xiàn)實(shí)。
當(dāng)你在封裝 for 循環(huán)時(shí),就是在抽象 for 循環(huán),把它隱藏掉。就是在告訴用戶,你只需要調(diào)封裝的函數(shù),而不需要關(guān)心內(nèi)部實(shí)現(xiàn)。
于是乎,JS 就誕生了諸如 map filter reduce 等這種將循環(huán)過(guò)程隱藏掉的函數(shù)。底層本質(zhì)上還是用 for 實(shí)現(xiàn)的,只不過(guò)是把 for 循環(huán)隱藏了,如果按照業(yè)界內(nèi)的說(shuō)話逼格,就是把 for 循環(huán)干掉了。這就是聲明式編程在前端中的應(yīng)用之一。
三種方式:
第一種:傳統(tǒng)的循環(huán)結(jié)構(gòu) - 比如 for 循環(huán)
第二種:鏈?zhǔn)?/p>
第三種:函數(shù)式組合
在編寫函數(shù)時(shí),要考慮緩存是為了避免計(jì)算重復(fù)值。計(jì)算就意味著消耗各種資源,而做重復(fù)的計(jì)算,就是在浪費(fèi)各種資源。
純潔性和緩存有什么關(guān)系?我們想一下可以知道,純函數(shù)總是為給定的輸入返回相同的輸出,那既然如此,我們當(dāng)然要想到可以緩存函數(shù)的輸出。
那如何做函數(shù)的緩存呢?記住一句話:給計(jì)算結(jié)果賦予唯一的鍵值并持久化到緩存中。
大致 demo 代碼:
function mian(key) {
let cache = {}
cache.hasOwnProperty(key) ?
main(key) :
cache[key] = main(key)
}
上面代碼是一種最簡(jiǎn)單的利用純函數(shù)來(lái)做緩存的例子。下面實(shí)現(xiàn)一個(gè)非常完美的緩存函數(shù)。
給原生?JS?函數(shù)加上自動(dòng)記憶化的緩存機(jī)制
代碼如下:
Function.prototype.memorized = () => {
let key = JSON.stringify(arguments)
// 緩存實(shí)現(xiàn)
this._cache = this._cache || {}
this._cache[key] = this._cache[key] || this.apply(this, arguments)
return this._cache[key]
}
Function.prototype.memorize = () => {
let fn = this
// 只記憶一元函數(shù)
if (fn.length === 0 || fn.length > 1) return fn
return () => fn.memorized.apply(fn, arguments)
}
代碼地址如下:
gist:?gist.github.com/godkun/5251…
通過(guò)擴(kuò)展 Function 對(duì)象,我們就可以充分利用函數(shù)的記憶化來(lái)實(shí)現(xiàn)函數(shù)的緩存。
上面函數(shù)緩存實(shí)現(xiàn)的好處有以下兩點(diǎn):
第一:消除了可能存在的全局共享的緩存
第二:將緩存機(jī)制抽象到了函數(shù)的內(nèi)部,使其完全與測(cè)試無(wú)關(guān),只需要關(guān)系函數(shù)的行為即可
實(shí)戰(zhàn)部分,我沒(méi)有提到函子知識(shí),不代表我沒(méi)有實(shí)踐過(guò),正是因?yàn)槲覍?shí)踐過(guò),才決定不提它,因?yàn)閷?duì)于前端來(lái)說(shuō),有時(shí)候你要顧及整個(gè)團(tuán)隊(duì)的技術(shù),組合和柯里還有高階函數(shù)等還是可以很好的滿足基本需求的。
小伙伴們看實(shí)戰(zhàn)篇的時(shí)候,一定要結(jié)合理論篇一起看,這樣才能無(wú)縫連接。
圖解 Monad
monad wiki
What is a monad?-stackoverflow