1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 5612字,需瀏覽 12分鐘

         ·

        2020-10-10 01:15


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

        本文介紹一下js中的一個重要概念——閉包。其實即便是最初級的前端開發(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()) // 1console.log(fab()) // 2console.log(fab()) // 3console.log(fab()) // 5
        這是一個生成斐波那契數(shù)列的例子。makeFab的返回值就是一個閉包,makeFab像一個工廠函數(shù),每次調(diào)用都會創(chuàng)建一個閉包函數(shù),如例子中的fab。
        fab每次調(diào)用不需要傳參數(shù),都會返回不同的值,因為在閉包生成的時候,它記住了變量last和current,以至于在后續(xù)的調(diào)用中能夠返回不同的值。
        能記住函數(shù)本身所在作用域的變量,這就是閉包和普通函數(shù)的區(qū)別所在。
        MDN中給出的閉包的定義是:函數(shù)與對其狀態(tài)即詞法環(huán)境的引用共同構(gòu)成閉包。
        這里的“詞法環(huán)境的引用”,可以簡單理解為“引用了函數(shù)外部的一些變量”,例如上述例子中每次調(diào)用makeFab都會創(chuàng)建并返回inner函數(shù),引用了last和current兩個變量。

        二、閉包——函數(shù)式編程之魂

        JavaScript和python這兩門動態(tài)語言都強調(diào)一個概念:萬物皆對象。自然,函數(shù)也是對象。
        JavaScript里,我們可以像操作普通變量一樣,把函數(shù)在我們的代碼里拋來拋去,然后在某個時刻調(diào)用一下,這就是所謂的函數(shù)式編程。
        函數(shù)式編程靈活簡潔,而語言對閉包的支持,讓函數(shù)式編程擁有了靈魂。
        以實現(xiàn)一個可復用的確認框為例,比如在用戶進行一些刪除或者重要操作的時候,為了防止誤操作,我們可能會通過彈窗讓用戶再次確認操作。
        因為確認框是通用的,所以確認框組件的邏輯應(yīng)該足夠抽象,僅僅是負責彈窗、觸發(fā)確認、觸發(fā)取消事件,而觸發(fā)確認/取消事件是異步操作,這時候我們就需要使用兩個回調(diào)函數(shù)完成操作,彈窗函數(shù)confirm接收三個參數(shù):一個提示語句,一個確認回調(diào)函數(shù),一個取消回調(diào)函數(shù):
        function confirm (confirmText, confirmCallback, cancelCallback) { // 插入提示框DOM,包含提示語句、確認按鈕、取消按鈕 // 添加確認按鈕點擊事件,事件函數(shù)中做dom清理工作并調(diào)用confirmCallback // 添加取消按鈕點擊事件,事件函數(shù)中做dom清理工作并調(diào)用cancelCallback}
        這樣我們可以通過向confirm傳遞回調(diào)函數(shù),并且根據(jù)不同結(jié)果完成不同的動作,比如我們根據(jù)id刪除一條數(shù)據(jù)可以這樣寫:
        function removeItem (id) { confirm('確認刪除嗎?', () => { // 用戶點擊確認, 發(fā)送遠程ajax請求 api.removeItem(id).then(xxx) }, () => { // 用戶點擊取消, console.log('取消刪除') })}
        這個例子中,confirmCallback正是利用了閉包,創(chuàng)建了一個引用了上下文中id變量的函數(shù),這樣的例子在回調(diào)函數(shù)中比比皆是,并且大多數(shù)時候引用的變量是很多個。?
        試想,如果語言不支持閉包,那這些變量要怎么辦?作為參數(shù)全部傳遞給confirm函數(shù),然后在調(diào)用confirmCallback/cancelCallback時再作為參數(shù)傳遞給它們?顯然,這里閉包提供了極大便利。

        三、閉包的一些例子

        1. 防抖、節(jié)流函數(shù)

        前端很常見的一個需求是遠程搜索,根據(jù)用戶輸入框的內(nèi)容自動發(fā)送ajax請求,然后從后端把搜索結(jié)果請求回來。
        為了簡化用戶的操作,有時候我們并不會專門放置一個按鈕來點擊觸發(fā)搜索事件,而是直接監(jiān)聽內(nèi)容的變化來搜索(比如像vue的官網(wǎng)搜索欄)。
        這時候為了避免請求過于頻繁,我們可能就會用到“防抖”的技巧,即當用戶停止輸入一段時間(比如500ms)后才執(zhí)行發(fā)送請求。
        可以寫一個簡單的防抖函數(shù)實現(xiàn)這個功能:
        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)用時,都會創(chuàng)建一個新的閉包函數(shù),該函數(shù)保留了對事件邏輯處理函數(shù)func以及防抖時間間隔time以及定時器標志timer的引用。
        類似的還有節(jié)流函數(shù):
        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ù)點擊問題

        用戶點擊一個表單提交按鈕,前端會向后臺發(fā)送一個異步請求,請求還沒返回,焦急的用戶又多點了幾下按鈕,造成了額外的請求。
        有時候多發(fā)幾次請求最多只是多消耗了一些服務(wù)器資源,而另外一些情況是,表單提交本身會修改后臺的數(shù)據(jù),那多次提交就會導致意料之外的后果了。
        無論是為了減少服務(wù)器資源消耗還是避免多次修改后臺數(shù)據(jù),給表單提交按鈕添加點擊限制是很有必要的。
        怎么解決呢?一個常用的辦法是打個標記,即在響應(yīng)函數(shù)所在作用域聲明一個布爾變量lock,響應(yīng)函數(shù)被調(diào)用時,先判斷l(xiāng)ock的值,為true則表示上一次請求還未返回,此次點擊無效;為false則將lock設(shè)置為true,然后發(fā)送請求,請求結(jié)束再將lock改為false。
        ?很顯然,這個lock會污染函數(shù)所在的作用域,比如在vue組件中,我們可能就要將這個標記記錄在組件屬性上;而當有多個這樣的按鈕,則還需要不同的屬性來標記(想想給這些屬性取名都是一件頭疼的事情吧?。?。
        而生成閉包伴隨著新的函數(shù)作用域的創(chuàng)建,利用這一點,剛好可以解決這個問題。下面是一個簡單的例子:? ? ? ?
        let clickButton = (function () { let lock = false return function (postParams) { if (lock) return lock = true // 使用axios發(fā)送請求 axios.post('urlxxx', postParams).then( // 表單提交成功 ).catch(error => { // 表單提交出錯 console.log(error) }).finally(() => { // 不管成功失敗 都解鎖 lock = false }) }})()
        button.addEventListener('click', clickButton)
        這樣lock變量就會在一個單獨的作用域里,一次點擊的請求發(fā)出以后,必須等請求回來,才會開始下一次請求。
        當然,為了避免各個地方都聲明lock,修改lock,我們可以把上述邏輯抽象一下,實現(xiàn)一個裝飾器,就像節(jié)流/防抖函數(shù)一樣。以下是一個通用的裝飾器函數(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 }}
        默認情況下,需要原函數(shù)返回一個promise以達到promise決議后將lock重置為false,而如果沒有返回值,lock將會被立即重置(比如表單驗證不通過,響應(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或者請求結(jié)束還要進行其它動作之后才能重置lock的地方,singleClick提供了第二個參數(shù)manuDone,允許你可以手動調(diào)用一個done函數(shù)來重置lock,這個done函數(shù)會放在原函數(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變量,測試每秒調(diào)用一次print函數(shù),執(zhí)行代碼輸出如下圖:

        可以看到,其中一些調(diào)用沒有打印結(jié)果,這正是我們想要的結(jié)果!singleClick裝飾器比每次設(shè)置lock變量要方便許多,這里singleClick函數(shù)的返回值,以及其中的done函數(shù),都是一個閉包。

        3. 閉包模擬私有方法或者變量

        “封裝”是面向?qū)ο蟮奶匦灾唬^“封裝”,即一個對象對外隱藏了其內(nèi)部的一些屬性或者方法的實現(xiàn)細節(jié),外界僅能通過暴露的接口操作該對象。
        js是比較“自由”的語言,所以并沒有類似C++語言那樣提供私有變量或成員函數(shù)的定義方式,不過利用閉包,卻可以很好地模擬這個特性。
        比如游戲開發(fā)中,玩家對象身上通常會有一個經(jīng)驗屬性,假設(shè)為exp,"打怪"、“做任務(wù)”、“使用經(jīng)驗書”等都會增加exp這個值,而在升級的時候又會減掉exp的值,把exp直接暴露給各處業(yè)務(wù)來操作顯然是很糟糕的。
        js里面我們可以用閉包把它隱藏起來,簡單模擬如下:
        function makePlayer () { let exp = 0 // 經(jīng)驗值 return { getExp () { return exp }, changeExp (delta, sReason = '') { // log(xxx),記錄變動日志 exp += delta } }}
        let p = makePlayer()console.log(p.getExp()) // 0p.changeExp(2000)console.log(p.getExp()) // 2000
        這樣我們調(diào)用makePlayer()就會生成一個玩家對象p,p內(nèi)通過方法操作exp這個變量,但是卻不可以通過p.exp訪問,顯然更符合“封裝”的特性。

        四、總結(jié)

        閉包是js中的強大特性之一,然而至于閉包怎么使用,我覺得不算是一個問題,甚至我們完全沒必要研究閉包怎么使用。
        我的觀點是,閉包應(yīng)該是自然而言地出現(xiàn)在你的代碼里,因為它是解決當前問題最直截了當?shù)霓k法;而當你刻意想去使用它的時候,往往可能已經(jīng)走了彎路。
        ?本文完~

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

        回復“加群”與大佬們一起交流學習~

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

        瀏覽 50
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            性爱网站在线观看 | 久久久久久91香蕉国产 | 日bi黄片视频 | 日本少妇裸体做爰高潮片 | 国产男女激情视频无遮挡免费观看 | 男人插女人下面的视频 | 一级A片处破外女视频 | 丁香美女| 操B电影毛片成人网站 | 中国一级片免费看 |