
來源 |?https://www.zoo.team/article/vue3-jsx本文介紹一下js中的一個(gè)重要概念——閉包。其實(shí)即便是最初級(jí)的前端開發(fā)人員,應(yīng)該都已經(jīng)接觸過它。一、閉包的概念和特性
function makeFab () { let last = 1, current = 1 return function inner() { [current, last] = [current + last, current] return last }}
let fab = makeFab()console.log(fab()) console.log(fab()) console.log(fab()) console.log(fab())
這是一個(gè)生成斐波那契數(shù)列的例子。makeFab的返回值就是一個(gè)閉包,makeFab像一個(gè)工廠函數(shù),每次調(diào)用都會(huì)創(chuàng)建一個(gè)閉包函數(shù),如例子中的fab。fab每次調(diào)用不需要傳參數(shù),都會(huì)返回不同的值,因?yàn)樵陂]包生成的時(shí)候,它記住了變量last和current,以至于在后續(xù)的調(diào)用中能夠返回不同的值。能記住函數(shù)本身所在作用域的變量,這就是閉包和普通函數(shù)的區(qū)別所在。
MDN中給出的閉包的定義是:函數(shù)與對(duì)其狀態(tài)即詞法環(huán)境的引用共同構(gòu)成閉包。這里的“詞法環(huán)境的引用”,可以簡單理解為“引用了函數(shù)外部的一些變量”,例如上述例子中每次調(diào)用makeFab都會(huì)創(chuàng)建并返回inner函數(shù),引用了last和current兩個(gè)變量。二、閉包——函數(shù)式編程之魂
JavaScript和python這兩門動(dòng)態(tài)語言都強(qiáng)調(diào)一個(gè)概念:萬物皆對(duì)象。自然,函數(shù)也是對(duì)象。
在JavaScript里,我們可以像操作普通變量一樣,把函數(shù)在我們的代碼里拋來拋去,然后在某個(gè)時(shí)刻調(diào)用一下,這就是所謂的函數(shù)式編程。函數(shù)式編程靈活簡潔,而語言對(duì)閉包的支持,讓函數(shù)式編程擁有了靈魂。以實(shí)現(xiàn)一個(gè)可復(fù)用的確認(rèn)框?yàn)槔热缭谟脩暨M(jìn)行一些刪除或者重要操作的時(shí)候,為了防止誤操作,我們可能會(huì)通過彈窗讓用戶再次確認(rèn)操作。因?yàn)榇_認(rèn)框是通用的,所以確認(rèn)框組件的邏輯應(yīng)該足夠抽象,僅僅是負(fù)責(zé)彈窗、觸發(fā)確認(rèn)、觸發(fā)取消事件,而觸發(fā)確認(rèn)/取消事件是異步操作,這時(shí)候我們就需要使用兩個(gè)回調(diào)函數(shù)完成操作,彈窗函數(shù)confirm接收三個(gè)參數(shù):一個(gè)提示語句,一個(gè)確認(rèn)回調(diào)函數(shù),一個(gè)取消回調(diào)函數(shù):function confirm (confirmText, confirmCallback, cancelCallback) { }
這樣我們可以通過向confirm傳遞回調(diào)函數(shù),并且根據(jù)不同結(jié)果完成不同的動(dòng)作,比如我們根據(jù)id刪除一條數(shù)據(jù)可以這樣寫:
function removeItem (id) { confirm('確認(rèn)刪除嗎?', () => { api.removeItem(id).then(xxx) }, () => { console.log('取消刪除') })}
這個(gè)例子中,confirmCallback正是利用了閉包,創(chuàng)建了一個(gè)引用了上下文中id變量的函數(shù),這樣的例子在回調(diào)函數(shù)中比比皆是,并且大多數(shù)時(shí)候引用的變量是很多個(gè)。?試想,如果語言不支持閉包,那這些變量要怎么辦?作為參數(shù)全部傳遞給confirm函數(shù),然后在調(diào)用confirmCallback/cancelCallback時(shí)再作為參數(shù)傳遞給它們?顯然,這里閉包提供了極大便利。三、閉包的一些例子
1. 防抖、節(jié)流函數(shù)
前端很常見的一個(gè)需求是遠(yuǎn)程搜索,根據(jù)用戶輸入框的內(nèi)容自動(dòng)發(fā)送ajax請(qǐng)求,然后從后端把搜索結(jié)果請(qǐng)求回來。為了簡化用戶的操作,有時(shí)候我們并不會(huì)專門放置一個(gè)按鈕來點(diǎn)擊觸發(fā)搜索事件,而是直接監(jiān)聽內(nèi)容的變化來搜索(比如像vue的官網(wǎng)搜索欄)。這時(shí)候?yàn)榱吮苊庹?qǐng)求過于頻繁,我們可能就會(huì)用到“防抖”的技巧,即當(dāng)用戶停止輸入一段時(shí)間(比如500ms)后才執(zhí)行發(fā)送請(qǐng)求。可以寫一個(gè)簡單的防抖函數(shù)實(shí)現(xiàn)這個(gè)功能:function debounce (func, time) { let timer = 0 return function (...args) { timer && clearTimeout(timer) timer = setTimeout(() => { timer = 0 func.apply(this, args) }, time) }}
input.onkeypress = debounce(function () { console.log(input.value) }, 500)
debounce函數(shù)每次調(diào)用時(shí),都會(huì)創(chuàng)建一個(gè)新的閉包函數(shù),該函數(shù)保留了對(duì)事件邏輯處理函數(shù)func以及防抖時(shí)間間隔time以及定時(shí)器標(biāo)志timer的引用。function throttle(func, time) { let timer = 0 return function (...args) { if (timer) return func.apply(this, args) timer = setTimeout(() => timer = 0, time) }}
2. 優(yōu)雅解決按鈕多次連續(xù)點(diǎn)擊問題
用戶點(diǎn)擊一個(gè)表單提交按鈕,前端會(huì)向后臺(tái)發(fā)送一個(gè)異步請(qǐng)求,請(qǐng)求還沒返回,焦急的用戶又多點(diǎn)了幾下按鈕,造成了額外的請(qǐng)求。有時(shí)候多發(fā)幾次請(qǐng)求最多只是多消耗了一些服務(wù)器資源,而另外一些情況是,表單提交本身會(huì)修改后臺(tái)的數(shù)據(jù),那多次提交就會(huì)導(dǎo)致意料之外的后果了。無論是為了減少服務(wù)器資源消耗還是避免多次修改后臺(tái)數(shù)據(jù),給表單提交按鈕添加點(diǎn)擊限制是很有必要的。怎么解決呢?一個(gè)常用的辦法是打個(gè)標(biāo)記,即在響應(yīng)函數(shù)所在作用域聲明一個(gè)布爾變量lock,響應(yīng)函數(shù)被調(diào)用時(shí),先判斷l(xiāng)ock的值,為true則表示上一次請(qǐng)求還未返回,此次點(diǎn)擊無效;為false則將lock設(shè)置為true,然后發(fā)送請(qǐng)求,請(qǐng)求結(jié)束再將lock改為false。?很顯然,這個(gè)lock會(huì)污染函數(shù)所在的作用域,比如在vue組件中,我們可能就要將這個(gè)標(biāo)記記錄在組件屬性上;而當(dāng)有多個(gè)這樣的按鈕,則還需要不同的屬性來標(biāo)記(想想給這些屬性取名都是一件頭疼的事情吧!)。而生成閉包伴隨著新的函數(shù)作用域的創(chuàng)建,利用這一點(diǎn),剛好可以解決這個(gè)問題。下面是一個(gè)簡單的例子:? ? ? ?let clickButton = (function () { let lock = false return function (postParams) { if (lock) return lock = true axios.post('urlxxx', postParams).then( ).catch(error => { console.log(error) }).finally(() => { lock = false }) }})()
button.addEventListener('click', clickButton)
這樣lock變量就會(huì)在一個(gè)單獨(dú)的作用域里,一次點(diǎn)擊的請(qǐng)求發(fā)出以后,必須等請(qǐng)求回來,才會(huì)開始下一次請(qǐng)求。
當(dāng)然,為了避免各個(gè)地方都聲明lock,修改lock,我們可以把上述邏輯抽象一下,實(shí)現(xiàn)一個(gè)裝飾器,就像節(jié)流/防抖函數(shù)一樣。以下是一個(gè)通用的裝飾器函數(shù):function singleClick(func, manuDone = false) { let lock = false return function (...args) { if (lock) return lock = true let done = () => lock = false if (manuDone) return func.call(this, ...args, done) let promise = func.call(this, ...args) promise ? promise.finally(done) : done() return promise }}
默認(rèn)情況下,需要原函數(shù)返回一個(gè)promise以達(dá)到promise決議后將lock重置為false,而如果沒有返回值,lock將會(huì)被立即重置(比如表單驗(yàn)證不通過,響應(yīng)函數(shù)直接返回),調(diào)用示例:
let clickButton = singleClick(function (postParams) { if (!checkForm()) return return axios.post('urlxxx', postParams).then( ).catch(error => { console.log(error) })})button.addEventListener('click', clickButton)
在一些不方便返回promise或者請(qǐng)求結(jié)束還要進(jìn)行其它動(dòng)作之后才能重置lock的地方,singleClick提供了第二個(gè)參數(shù)manuDone,允許你可以手動(dòng)調(diào)用一個(gè)done函數(shù)來重置lock,這個(gè)done函數(shù)會(huì)放在原函數(shù)參數(shù)列表的末尾。使用例子:
let print = singleClick(function (i, done) { console.log('print is called', i) setTimeout(done, 2000)}, true)
function test () { for (let i = 0; i < 10; i++) { setTimeout(() => { print(i) }, i * 1000) }}
print函數(shù)使用singleClick裝飾,每次調(diào)用2秒后重置lock變量,測(cè)試每秒調(diào)用一次print函數(shù),執(zhí)行代碼輸出如下圖:
可以看到,其中一些調(diào)用沒有打印結(jié)果,這正是我們想要的結(jié)果!singleClick裝飾器比每次設(shè)置lock變量要方便許多,這里singleClick函數(shù)的返回值,以及其中的done函數(shù),都是一個(gè)閉包。3. 閉包模擬私有方法或者變量
“封裝”是面向?qū)ο蟮奶匦灾唬^“封裝”,即一個(gè)對(duì)象對(duì)外隱藏了其內(nèi)部的一些屬性或者方法的實(shí)現(xiàn)細(xì)節(jié),外界僅能通過暴露的接口操作該對(duì)象。js是比較“自由”的語言,所以并沒有類似C++語言那樣提供私有變量或成員函數(shù)的定義方式,不過利用閉包,卻可以很好地模擬這個(gè)特性。比如游戲開發(fā)中,玩家對(duì)象身上通常會(huì)有一個(gè)經(jīng)驗(yàn)屬性,假設(shè)為exp,"打怪"、“做任務(wù)”、“使用經(jīng)驗(yàn)書”等都會(huì)增加exp這個(gè)值,而在升級(jí)的時(shí)候又會(huì)減掉exp的值,把exp直接暴露給各處業(yè)務(wù)來操作顯然是很糟糕的。在js里面我們可以用閉包把它隱藏起來,簡單模擬如下:function makePlayer () { let exp = 0 return { getExp () { return exp }, changeExp (delta, sReason = '') { exp += delta } }}
let p = makePlayer()console.log(p.getExp()) p.changeExp(2000)console.log(p.getExp())
這樣我們調(diào)用makePlayer()就會(huì)生成一個(gè)玩家對(duì)象p,p內(nèi)通過方法操作exp這個(gè)變量,但是卻不可以通過p.exp訪問,顯然更符合“封裝”的特性。四、總結(jié)
閉包是js中的強(qiáng)大特性之一,然而至于閉包怎么使用,我覺得不算是一個(gè)問題,甚至我們完全沒必要研究閉包怎么使用。我的觀點(diǎn)是,閉包應(yīng)該是自然而言地出現(xiàn)在你的代碼里,因?yàn)樗墙鉀Q當(dāng)前問題最直截了當(dāng)?shù)霓k法;而當(dāng)你刻意想去使用它的時(shí)候,往往可能已經(jīng)走了彎路。
回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 80+ 篇原創(chuàng)文章