從柯里化分析JavaScript重要的高階函數(shù)實(shí)例
前情回顧
我們?cè)谇捌?《?從歷史講起,JavaScript 基因里寫(xiě)著函數(shù)式編程》 講到了 JavaScript 的函數(shù)式基因最早可追溯到 1930 年的 lambda 運(yùn)算,這個(gè)時(shí)間比第一臺(tái)計(jì)算機(jī)誕生的時(shí)間都還要早十幾年。JavaScript 閉包的概念也來(lái)源于 lambda 運(yùn)算中變量的被綁定關(guān)系。
因?yàn)樵?lambda 演算的設(shè)定中,參數(shù)只能是一個(gè),所以通過(guò)柯里化的天才想法來(lái)實(shí)現(xiàn)接收多個(gè)參數(shù):
lambda x. ( lambda y. plus x y )
說(shuō)這個(gè)想法是“天才”一點(diǎn)不為過(guò),把函數(shù)自身作為輸入?yún)?shù)或輸出返回值,至今受用,也就是【高階函數(shù)】的定義。
將上述 lambda 演算柯里化寫(xiě)法轉(zhuǎn)變到 JavaScript 中,就變成了:
function add(a) { return function (b) { return a + b } } add(1)(2)
所以,剖析閉包從柯里化開(kāi)始,柯里化是閉包的“孿生子”。
讀完本篇,你會(huì)發(fā)現(xiàn) JavaScript 高階函數(shù)中處處是閉包、處處是柯里化~
百變柯里化
最開(kāi)始,本瓜理解 柯里化 == 閉包 + 遞歸,得出的柯里化寫(xiě)法是這樣的:
let arr = [] function addCurry() { let arg = Array.prototype.slice.call(arguments); // 遞歸獲取后續(xù)參數(shù) arr = arr.concat(arg); if (arg.length === 0) { // 如果參數(shù)為空,則判斷遞歸結(jié)束 return arr.reduce((a,b)=>{return a+b}) // 求和 } else { return addCurry; } } addCurry(1)(2)(3)()
但這樣的寫(xiě)法, addCurry
函數(shù)會(huì)引用一個(gè)外部變量 arr
,不符合純函數(shù)的特性,于是就優(yōu)化為:
function addCurry() { let arr = [...arguments] let fn = function () { if(arguments.length === 0) { return arr.reduce((a, b) => a + b) } else { arr.push(...arguments) return fn } } return fn }
上述寫(xiě)法,又總是要以 ‘( )’ 空括號(hào)結(jié)尾,于是再改進(jìn)為隱式轉(zhuǎn)換 .toString
寫(xiě)法:
function addCurry() { let arr = [...arguments] // 利用閉包的特性收集所有參數(shù)值 var fn = function() { arr.push(...arguments); return fn; }; // 利用 toString 隱式轉(zhuǎn)換 fn.toString = function () { return arr.reduce(function (a, b) { return a + b; }); } return fn; }
- 注意一些舊版本的瀏覽器隱式轉(zhuǎn)換會(huì)默認(rèn)執(zhí)行
好了,到這一步,如果你把上述三種柯里化寫(xiě)法都會(huì)手寫(xiě)了,那面試中考柯里化的基礎(chǔ)一關(guān)算是過(guò)了。
然而,不止于此,柯里化實(shí)際存在很多變體, 只有深刻吃透它的思想,而非停留在一種寫(xiě)法上,才能算得上“高級(jí)”、“優(yōu)雅”。
接下來(lái),讓我們看看它怎么變?!
緩存?zhèn)鲄?/h3>
柯里化最基礎(chǔ)的用法是緩存?zhèn)鲄ⅰ?/p>
我們經(jīng)常遇到這樣的場(chǎng)景:
已知一個(gè) ajax
函數(shù),它有 3 個(gè)參數(shù) url、data、callback
function ajax(url, data, callback) { // ... }
不用柯里化是怎樣減少傳參的呢?通常是以下這樣,寫(xiě)死參數(shù)位置的方式來(lái)減少傳參:
function ajaxTest1(data, callback) { ajax('http://www.test.com/test1', data, callback); }
而通過(guò)柯里化,則是這樣:
function ajax(url, data, callback) { // ... } let ajaxTest2 = partial(ajax,'http://www.test.com/test2') ajaxTest2(data,callback)
其中 partial
函數(shù)是這樣寫(xiě)的:
function partial(fn, ...presetArgs) { // presetArgs 是需要先被綁定下來(lái)的參數(shù) return function partiallyApplied(...laterArgs) { // ...laterArgs 是后續(xù)參數(shù) let allArgs =presetArgs.concat(laterArgs) // 收集到一起 return fn.apply(this, allArgs) // 傳給回調(diào)函數(shù) fn } }
柯里化固定參數(shù)的好處在:復(fù)用了原本的 ajax 函數(shù),并在原有基礎(chǔ)上做了修改,取其精華,棄其糟粕,封裝原有函數(shù)之后,就能為我所用。
并且 partial
函數(shù)不止對(duì) ajax
函數(shù)有作用,對(duì)于其它想減少傳參的函數(shù)同樣適用。
緩存判斷
我們可以設(shè)想一個(gè)通用場(chǎng)景,假設(shè)有一個(gè) handleOption 函數(shù),當(dāng)符合條件 'A',執(zhí)行語(yǔ)句:console.log('A')
;不符合時(shí),則執(zhí)行語(yǔ)句:console.log('others')
轉(zhuǎn)為代碼即:
const handleOption = (param) =>{ if(param === 'A'){ console.log('A') }else{ console.log('others') } }
現(xiàn)在的問(wèn)題是:我們每次調(diào)用 handleOption('A')
,都必須要走完 if...else... 的判斷流程。比如:
const handleOption = (param) =>{ console.log('每次調(diào)用 handleOption 都要執(zhí)行 if...else...') if(param === 'A'){ console.log('A') }else{ console.log('others') } } handleOption('A') handleOption('A') handleOption('A')
控制臺(tái)打印:
有沒(méi)有什么辦法,多次調(diào)用 handleOption('A')
,卻只走一次 if...else...?
答案是:柯里化。
const handleOption = ((param) =>{ console.log('從始至終只用執(zhí)行一次 if...else...') if(param === 'A'){ return ()=>console.log('A') }else{ return ()=>console.log('others') } }) const tmp = handleOption('A') tmp() tmp() tmp()
控制臺(tái)打?。?/p>
這樣的場(chǎng)景是有實(shí)戰(zhàn)意義的,當(dāng)我們做前端兼容時(shí),經(jīng)常要先判斷是來(lái)源于哪個(gè)環(huán)境,再執(zhí)行某個(gè)方法。比如說(shuō)在 firefox 和 chrome 環(huán)境下,添加事件監(jiān)聽(tīng)是 addEventListener
方法,而在 IE 下,添加事件是 attachEvent
方法;如果每次綁定這個(gè)監(jiān)聽(tīng),都要判斷是來(lái)自于哪個(gè)環(huán)境,那肯定是很費(fèi)勁。我們通過(guò)上述封裝的方法,可以做到 一處判斷,多次使用。
肯定有小伙伴會(huì)問(wèn)了:這也是柯里化?
嗯。。。怎么不算呢?
把 'A' 條件先固定下來(lái),也可叫“緩存下來(lái)”,后續(xù)的函數(shù)執(zhí)行將不再傳 'A' 這個(gè)參數(shù),實(shí)打?qū)嵉模喊讯鄥?shù)轉(zhuǎn)化為單參數(shù),逐個(gè)傳遞。
緩存計(jì)算
我們?cè)僭O(shè)想這樣一個(gè)場(chǎng)景,現(xiàn)在有一個(gè)函數(shù)是來(lái)做大數(shù)計(jì)算的:
const calculateFn = (num)=>{ const startTime = new Date() for(let i=0;i<num;i++){} // 大數(shù)計(jì)算 const endTime = new Date() console.log(endTime - startTime) return "Calculate big numbers" } calculateFn(10_000_000_000)
這是一個(gè)非常耗時(shí)的函數(shù),在控制臺(tái)看看,需要 8s+
如果業(yè)務(wù)代碼中需要多次用到這個(gè)大數(shù)計(jì)算結(jié)果,多次調(diào)用 calculateFn(10_000_000_000)
肯定是不明智的,太費(fèi)時(shí)。
一般的做法就是聲明一個(gè)全局變量,把運(yùn)算結(jié)果保存下來(lái):
比如 const resNums = calculateFn(10_000_000_000)
如果有多個(gè)大數(shù)運(yùn)算呢?沿著這個(gè)思路,即聲名多個(gè)變量:
const resNumsA = calculateFn(10_000_000_000) const resNumsB = calculateFn(20_000_000_000) const resNumsC = calculateFn(30_000_000_000)
我們講就是說(shuō):奧卡姆剃刀原則 —— 如無(wú)必要、勿增實(shí)體。
申明這么多全局變量,先不談?wù)純?nèi)存、占命名空間這事,就把 calculateFn()
函數(shù)的參數(shù)和聲名的常量名一一對(duì)應(yīng),都是一個(gè)麻煩事。
有沒(méi)有什么辦法?只用函數(shù),不增加多個(gè)全局常量,就實(shí)現(xiàn)多次調(diào)用,只計(jì)算一次?
答案是:柯里化。
代碼如下:
function cached(fn){ const cacheObj = Object.create(null); // 創(chuàng)建一個(gè)對(duì)象 return function cachedFn (str) { // 返回回調(diào)函數(shù) if ( !cacheObj [str] ) { // 在對(duì)象里面查詢(xún),函數(shù)結(jié)果是否被計(jì)算過(guò) let result = fn(str); cacheObj [str] = result; // 沒(méi)有則要執(zhí)行原函數(shù),并把計(jì)算結(jié)果緩存起來(lái) } return cacheObj [str] // 被緩存過(guò),直接返回 } } const calculateFn = (num)=>{ console.log("計(jì)算即緩存") const startTime = new Date() for(let i=0;i<num;i++){} // 大數(shù)計(jì)算 const endTime = new Date() console.log(endTime - startTime) // 耗時(shí) return "Calculate big numbers" } let cashedCalculate = cached(calculateFn) console.log(cashedCalculate(10_000_000_000)) // 計(jì)算即緩存 // 9944 // Calculate big numbers console.log(cashedCalculate(10_000_000_000)) // Calculate big numbers console.log(cashedCalculate(20_000_000_000)) // 計(jì)算即緩存 // 22126 // Calculate big numbers console.log(cashedCalculate(20_000_000_000)) // Calculate big numbers
這樣只用通過(guò)一個(gè) cached
緩存函數(shù)的處理,所有的大數(shù)計(jì)算都能保證:輸入?yún)?shù)相同的情況下,全局只用計(jì)算一次,后續(xù)可直接使用更加語(yǔ)義話(huà)的函數(shù)調(diào)用來(lái)得到之前計(jì)算的結(jié)果。
此處也是柯里化的應(yīng)用,在 cached
函數(shù)中先傳需要處理的函數(shù)參數(shù),后續(xù)再傳入具體需要操作得值,將多參轉(zhuǎn)化為單個(gè)參數(shù)逐一傳入。
緩存函數(shù)
柯里化的思想不僅可以緩存判斷條件,緩存計(jì)算結(jié)果、緩存?zhèn)鲄?,還能緩存“函數(shù)”。
設(shè)想,我們有一個(gè)數(shù)字 7 要經(jīng)過(guò)兩個(gè)函數(shù)的計(jì)算,先乘以 10 ,再加 100,寫(xiě)法如下:
const multi10 = function(x) { return x * 10; } const add100 = function(x) { return x + 100; } add100(multi10(7))
用柯里化處理后,即變成:
const multi10 = function(x) { return x * 10; } const add100 = function(x) { return x + 100; } const compose = function(f,g) { return function(x) { return f(g(x)) } } compose(add100, multi10)(7)
前者寫(xiě)法有兩個(gè)傳參是寫(xiě)在一起的,而后者則逐一傳參。把最后的執(zhí)行函數(shù)改寫(xiě):
let compute = compose(add100, multi10) compute(7)
所以,這里的柯里化直接把函數(shù)處理給緩存了,當(dāng)聲明 compute 變量時(shí),并沒(méi)有執(zhí)行操作,只是為了拿到 ()=> f(g(x)),最后執(zhí)行 compute(7),才會(huì)執(zhí)行整個(gè)運(yùn)算;
怎么樣?柯里化確實(shí)百變吧?柯里化的起源和閉包的定義是同宗同源。正如前文最開(kāi)始所說(shuō),柯里化是閉包的一對(duì)“孿生子”。
我們對(duì)閉包的解釋?zhuān)?ldquo;閉包是一個(gè)函數(shù)內(nèi)有另外一個(gè)函數(shù),內(nèi)部的函數(shù)可以訪問(wèn)外部函數(shù)的變量,這樣的語(yǔ)法結(jié)構(gòu)是閉包。”與我們對(duì)柯里化的解釋“把接受多個(gè)參數(shù)的函數(shù)變換成接受一個(gè)單一參數(shù)(或部分)的函數(shù),并且返回接受余下的參數(shù)和返回結(jié)果的新函數(shù)的技術(shù)”,這兩種說(shuō)法幾乎是“等效的”,只是從不同角度對(duì) 同一問(wèn)題 作出的解釋?zhuān)拖?lambda 演算和圖靈機(jī)對(duì)希爾伯特第十問(wèn)題的解釋一樣。
同一問(wèn)題:指的是在 lambda 演算誕生之時(shí),提出的:怎樣用 lambda 演算實(shí)現(xiàn)接收多個(gè)參數(shù)?
防抖與節(jié)流
好了,我們?cè)賮?lái)看看除了其它高階函數(shù)中閉包思想(柯里化思想)的應(yīng)用。首先是最最常用的防抖與節(jié)流函數(shù)。
防抖:就像英雄聯(lián)盟的回城鍵,按了之后,間隔一定秒數(shù)才會(huì)執(zhí)行生效。
function debounce(fn, delay) { delay = delay || 200; let timer = null; return function() { let arg = arguments; // 每次操作時(shí),清除上次的定時(shí)器 clearTimeout(timer); timer = null; // 定義新的定時(shí)器,一段時(shí)間后進(jìn)行操作 timer = setTimeout(function() { fn.apply(this, arg); }, delay); } }; var count = 0; window.onscroll = debounce(function(e) { console.log(e.type, ++count); // scroll }, 500);
節(jié)流函數(shù):就像英雄聯(lián)盟的技能鍵,是有 CD 的,一段時(shí)間內(nèi)只能按一次,按了之后就要等 CD;
// 函數(shù)節(jié)流,頻繁操作中間隔 delay 的時(shí)間才處理一次 function throttle(fn, delay) { delay = delay || 200; let timer = null; // 每次滾動(dòng)初始的標(biāo)識(shí) let timestamp = 0; return function() { let arg = arguments; let now = Date.now(); // 設(shè)置開(kāi)始時(shí)間 if (timestamp === 0) { timestamp = now; } clearTimeout(timer); timer = null; // 已經(jīng)到了delay的一段時(shí)間,進(jìn)行處理 if (now - timestamp >= delay) { fn.apply(this, arg); timestamp = now; } // 添加定時(shí)器,確保最后一次的操作也能處理 else { timer = setTimeout(function() { fn.apply(this, arg); // 恢復(fù)標(biāo)識(shí) timestamp = 0; }, delay); } } }; var count = 0; window.onscroll = throttle(function(e) { console.log(e.type, ++count); // scroll }, 500);
代碼均可復(fù)制到控制臺(tái)中測(cè)試。在防抖和節(jié)流的場(chǎng)景下,被預(yù)先固定住的變量是 timer
。
lodash 高階函數(shù)
lodash 大家肯定不陌生,它是最流行的 JavaScript 庫(kù)之一,透過(guò)函數(shù)式編程模式為開(kāi)發(fā)者提供常用的函數(shù)。
其中有一些封裝的高階函數(shù),讓一些平平無(wú)奇的普通函數(shù)也能有相應(yīng)的高階功能。
舉幾個(gè)例子:
// 防抖動(dòng) _.debounce(func, [wait=0], [options={}]) // 節(jié)流 _.throttle(func, [wait=0], [options={}]) // 將一個(gè)斷言函數(shù)結(jié)果取反 _.negate(predicate) // 柯里化函數(shù) _.curry(func, [arity=func.length]) // 部分應(yīng)用 _.partial(func, [partials]) // 返回一個(gè)帶記憶的函數(shù) _.memoize(func, [resolver]) // 包裝函數(shù) _.wrap(value, [wrapper=identity])
研究源碼你就會(huì)發(fā)現(xiàn),_.debounce 防抖、_.throttle 節(jié)流上面說(shuō)過(guò),_.curry 柯里化上面說(shuō)過(guò)、_.partial 在“緩存?zhèn)鲄?rdquo;里說(shuō)過(guò)、_.memoize 在“緩存計(jì)算”里也說(shuō)過(guò)......
再舉一個(gè)例子:
現(xiàn)在要求一個(gè)函數(shù)在達(dá)到 n 次之前,每次都正常執(zhí)行,第 n 次不執(zhí)行。
也是非常常見(jiàn)的業(yè)務(wù)場(chǎng)景!JavaScript 實(shí)現(xiàn):
function before(n, func) { let result, count = n; return function(...args) { count = count - 1 if (count > 0) result = func.apply(this, args) if (count <= 1) func = undefined return result } } const fn= before(3,(x)=>console.log(x)) fn(1) // 1 fn(2) // 2 fn(3) // 不執(zhí)行
反過(guò)來(lái):函數(shù)只有到 n 次的時(shí)候才執(zhí)行,n 之前的都不執(zhí)行。
function after(n, func) { let count = n || 0 return function(...args) { count = count - 1 if (count < 1) return func.apply(this, args) } } const fn= after(3,(x)=>console.log(x)) fn(1) // 不執(zhí)行 fn(2) // 不執(zhí)行 fn(3) // 3
全是“閉包”、全是把參數(shù)“柯里化”。
細(xì)細(xì)體會(huì),在控制臺(tái)上敲一敲、改一改、跑一跑,下次或許你就可以自己寫(xiě)出這些有特定功能的高階函數(shù)了。
結(jié)語(yǔ)
綜合以上,可見(jiàn)由函數(shù)式啟發(fā)的“閉包”、“柯里化”思想對(duì) JavaScript 有多重要。幾乎所有的高階函數(shù)都離不開(kāi)閉包、參數(shù)由多轉(zhuǎn)逐一的柯里化傳參思想。所在在很多面試中,都會(huì)問(wèn)閉包,不管是一兩年、還是三五年經(jīng)驗(yàn)的前端程序員。定義一個(gè)前端的 JavaScript 技能是初級(jí),還是中高級(jí),這是其中很重要的一個(gè)判斷點(diǎn)。
對(duì)閉包概念模糊不清的、或者只會(huì)背概念的 => 初級(jí)
會(huì)寫(xiě)防抖、節(jié)流、或柯里化等高階函數(shù)的 => 中級(jí)
深刻理解高階函數(shù)封裝思想、能自主用閉包封裝高階函數(shù) => 高級(jí)
以上就是從柯里化分析JavaScript重要的高階函數(shù)實(shí)例的詳細(xì)內(nèi)容,更多關(guān)于JavaScript 柯里化高階函數(shù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
javascript中的深復(fù)制詳解及實(shí)例分析
這篇文章主要介紹了javascript中的深復(fù)制詳解及實(shí)例分析的相關(guān)資料,需要的朋友可以參考下2016-12-12JavaScript中的一些隱式轉(zhuǎn)換和總結(jié)(推薦)
這篇文章主要介紹了JavaScript中的一些隱式轉(zhuǎn)換和總結(jié),非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-12-12js實(shí)現(xiàn)簡(jiǎn)單的無(wú)縫輪播效果
這篇文章主要為大家詳細(xì)介紹了js實(shí)現(xiàn)簡(jiǎn)單的無(wú)縫輪播效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-09-09Iframe 自動(dòng)適應(yīng)頁(yè)面的高度示例代碼
這篇文章主要介紹了Iframe如何自動(dòng)適應(yīng)頁(yè)面的高度,需要的朋友可以參考下2014-02-02