<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【JS】741- JavaScript 閉包應(yīng)用介紹

          共 5612字,需瀏覽 12分鐘

           ·

          2020-10-10 01:15


          來源 |?https://www.zoo.team/article/vue3-jsx

          本文介紹一下js中的一個(gè)重要概念——閉包。其實(shí)即便是最初級(jí)的前端開發(fā)人員,應(yīng)該都已經(jīng)接觸過它。

          一、閉包的概念和特性

          首先看個(gè)閉包的例子:
          function makeFab () { let last = 1, current = 1 return function inner() { [current, last] = [current + last, current] return last }}
          let fab = makeFab()console.log(fab()) // 1console.log(fab()) // 2console.log(fab()) // 3console.log(fab()) // 5
          這是一個(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) { // 插入提示框DOM,包含提示語句、確認(rèn)按鈕、取消按鈕 // 添加確認(rèn)按鈕點(diǎn)擊事件,事件函數(shù)中做dom清理工作并調(diào)用confirmCallback // 添加取消按鈕點(diǎn)擊事件,事件函數(shù)中做dom清理工作并調(diào)用cancelCallback}
          這樣我們可以通過向confirm傳遞回調(diào)函數(shù),并且根據(jù)不同結(jié)果完成不同的動(dòng)作,比如我們根據(jù)id刪除一條數(shù)據(jù)可以這樣寫:
          function removeItem (id) { confirm('確認(rèn)刪除嗎?', () => { // 用戶點(diǎn)擊確認(rèn), 發(fā)送遠(yuǎn)程ajax請(qǐng)求 api.removeItem(id).then(xxx) }, () => { // 用戶點(diǎn)擊取消, 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的引用。
          類似的還有節(jié)流函數(shù):
          function throttle(func, time) { let timer = 0 // 定時(shí)器標(biāo)記相當(dāng)于一個(gè)鎖標(biāo)志 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發(fā)送請(qǐng)求 axios.post('urlxxx', postParams).then( // 表單提交成功 ).catch(error => { // 表單提交出錯(cuò) 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 => { // 表單提交出錯(cuò) 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 // 經(jīng)驗(yàn)值 return { getExp () { return exp }, changeExp (delta, sReason = '') { // log(xxx),記錄變動(dòng)日志 exp += delta } }}
          let p = makePlayer()console.log(p.getExp()) // 0p.changeExp(2000)console.log(p.getExp()) // 2000
          這樣我們調(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)走了彎路。
          ?本文完~

          1. JavaScript 重溫系列(22篇全)
          2. ECMAScript 重溫系列(10篇全)
          3. JavaScript設(shè)計(jì)模式 重溫系列(9篇全)
          4.?正則 / 框架 / 算法等 重溫系列(16篇全)
          5.?Webpack4 入門(上)||?Webpack4 入門(下)
          6.?MobX 入門(上)?||??MobX 入門(下)
          7. 80+篇原創(chuàng)系列匯總

          回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~

          點(diǎn)擊“閱讀原文”查看 80+ 篇原創(chuàng)文章

          瀏覽 50
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本级婬乱片A片AAA毛片地址 | 第一色网站| 色国产综合免费视频在线播放 | 成人无码天堂 | 青娱乐cao |