JavaScript閉包的應用

本文最早發佈於個人我的網站: https://wintc.top/article/33。轉載請註明出處。

  本文介紹一下JS中的一個重要概念——閉包。其實即使是最初級的前端開發人員,應該都已經接觸過它。javascript

1、閉包的概念和特性

  首先看個閉包的例子:前端

function makeFab () {
  let last = 1, current = 1
  return function inner() {
    [current, last] = [current + last, current]
    return last
  }
}

let fab = makeFab()
console.log(fab()) // 1
console.log(fab()) // 2
console.log(fab()) // 3
console.log(fab()) // 5

  這是一個生成斐波那契數列的例子。makeFab的返回值就是一個閉包,makeFab像一個工廠函數,每次調用都會建立一個閉包函數,如例子中的fab。fab每次調用不須要傳參數,都會返回不一樣的值,由於在閉包生成的時候,它記住了變量last和current,以致於在後續的調用中可以返回不一樣的值。能記住函數自己所在做用域的變量,這就是閉包和普通函數的區別所在。vue

  MDN中給出的閉包的定義是:函數與對其狀態即詞法環境的引用共同構成閉包。這裏的「詞法環境的引用」,能夠簡單理解爲「引用了函數外部的一些變量」,例如上述例子中每次調用makeFab都會建立並返回inner函數,引用了last和current兩個變量。java

2、閉包——函數式編程之魂

Javascript和python這兩門動態語言都強調一個概念:萬物皆對象。天然,函數也是對象。
在Javascript裏,咱們能夠像操做普通變量同樣,把函數在咱們的代碼裏拋來拋去,而後在某個時刻調用一下,這就是所謂的函數式編程。函數式編程靈活簡潔,而語言對閉包的支持,讓函數式編程擁有了靈魂。python

以實現一個可複用的確認框爲例,好比在用戶進行一些刪除或者重要操做的時候,爲了防止誤操做,咱們可能會經過彈窗讓用戶再次確認操做。由於確認框是通用的,因此確認框組件的邏輯應該足夠抽象,僅僅是負責彈窗、觸發確認、觸發取消事件,而觸發確認/取消事件是異步操做,這時候咱們就須要使用兩個回調函數完成操做,彈窗函數confirm接收三個參數:一個提示語句,一個確認回調函數,一個取消回調函數:ios

function confirm (confirmText, confirmCallback, cancelCallback) {
  // 插入提示框DOM,包含提示語句、確認按鈕、取消按鈕
  // 添加確認按鈕點擊事件,事件函數中作dom清理工做並調用confirmCallback
  // 添加取消按鈕點擊事件,事件函數中作dom清理工做並調用cancelCallback
}

這樣咱們能夠經過向confirm傳遞迴調函數,而且根據不一樣結果完成不一樣的動做,好比咱們根據id刪除一條數據能夠這樣寫:ajax

function removeItem (id) {
  confirm('確認刪除嗎?', () => {
    // 用戶點擊確認, 發送遠程ajax請求
    api.removeItem(id).then(xxx)
  }, () => {
    // 用戶點擊取消,
    console.log('取消刪除')
  })
}

這個例子中,confirmCallback正是利用了閉包,建立了一個引用了上下文中id變量的函數,這樣的例子在回調函數中比比皆是,而且大多數時候引用的變量是不少個。 試想,若是語言不支持閉包,那這些變量要怎麼辦?做爲參數所有傳遞給confirm函數,而後在調用confirmCallback/cancelCallback時再做爲參數傳遞給它們?顯然,這裏閉包提供了極大便利。編程

3、閉包的一些例子

1. 防抖、節流函數

  前端很常見的一個需求是遠程搜索,根據用戶輸入框的內容自動發送ajax請求,而後從後端把搜索結果請求回來。爲了簡化用戶的操做,有時候咱們並不會專門放置一個按鈕來點擊觸發搜索事件,而是直接監聽內容的變化來搜索(好比像vue的官網搜索欄)。這時候爲了不請求過於頻繁,咱們可能就會用到「防抖」的技巧,即當用戶中止輸入一段時間(好比500ms)後才執行發送請求。能夠寫一個簡單的防抖函數實現這個功能:axios

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函數每次調用時,都會建立一個新的閉包函數,該函數保留了對事件邏輯處理函數func以及防抖時間間隔time以及定時器標誌timer的引用。相似的還有節流函數:後端

function throttle(func, time) {
  let timer = 0 // 定時器標記至關於一個鎖標誌
  return function (...args) {
    if (timer) return
    func.apply(this, args)
    timer = setTimeout(() => timer = 0, time)
  }
}

2. 優雅解決按鈕屢次連續點擊問題

        用戶點擊一個表單提交按鈕,前端會向後臺發送一個異步請求,請求還沒返回,焦急的用戶又多點了幾下按鈕,形成了額外的請求。有時候多發幾回請求最多隻是多消耗了一些服務器資源,而另一些狀況是,表單提交自己會修改後臺的數據,那屢次提交就會致使意料以外的後果了。不管是爲了減小服務器資源消耗仍是避免屢次修改後臺數據,給表單提交按鈕添加點擊限制是頗有必要的。

        怎麼解決呢?一個經常使用的辦法是打個標記,即在響應函數所在做用域聲明一個布爾變量lock,響應函數被調用時,先判斷lock的值,爲true則表示上一次請求還未返回,這次點擊無效;爲false則將lock設置爲true,而後發送請求,請求結束再將lock改成false。

        很顯然,這個lock會污染函數所在的做用域,好比在Vue組件中,咱們可能就要將這個標記記錄在組件屬性上;而當有多個這樣的按鈕,則還須要不一樣的屬性來標記(想一想給這些屬性取名都是一件頭疼的事情吧!)。而生成閉包伴隨着新的函數做用域的建立,利用這一點,恰好能夠解決這個問題。下面是一個簡單的例子:       

let clickButton = (function () {
  let lock = false
  return function (postParams) {
    if (lock) return
    lock = true
    // 使用axios發送請求
    axios.post('urlxxx', postParams).then(
      // 表單提交成功
    ).catch(error => {
      // 表單提交出錯
      console.log(error)
    }).finally(() => {
      // 無論成功失敗 都解鎖
      lock = false
    })
  }
})()

button.addEventListener('click', clickButton)

        這樣lock變量就會在一個單獨的做用域裏,一次點擊的請求發出之後,必須等請求回來,纔會開始下一次請求。

        固然,爲了不各個地方都聲明lock,修改lock,咱們能夠把上述邏輯抽象一下,實現一個裝飾器,就像節流/防抖函數同樣。如下是一個通用的裝飾器函數:

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
  }
}

        默認狀況下,須要原函數返回一個promise以達到promise決議後將lock重置爲false,而若是沒有返回值,lock將會被當即重置(好比表單驗證不經過,響應函數直接返回),調用示例:

let clickButton = singleClick(function (postParams) {
  if (!checkForm()) return
  return axios.post('urlxxx', postParams).then(
    // 表單提交成功
  ).catch(error => {
    // 表單提交出錯
    console.log(error)
  })
})
button.addEventListener('click', clickButton)

        在一些不方便返回promise或者請求結束還要進行其它動做以後才能重置lock的地方,singleClick提供了第二個參數manuDone,容許你能夠手動調用一個done函數來重置lock,這個done函數會放在原函數參數列表的末尾。使用例子:

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函數使用singleClick裝飾,每次調用2秒後重置lock變量,測試每秒調用一次print函數,執行代碼輸出以下圖:

       能夠看到,其中一些調用沒有打印結果,這正是咱們想要的結果!singleClick裝飾器比每次設置lock變量要方便許多,這裏singleClick函數的返回值,以及其中的done函數,都是一個閉包。

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

  「封裝」是面向對象的特性之一,所謂「封裝」,即一個對象對外隱藏了其內部的一些屬性或者方法的實現細節,外界僅能經過暴露的接口操做該對象。JS是比較「自由」的語言,因此並無相似C++語言那樣提供私有變量或成員函數的定義方式,不過利用閉包,卻能夠很好地模擬這個特性。

  好比遊戲開發中,玩家對象身上一般會有一個經驗屬性,假設爲exp,"打怪"、「作任務」、「使用經驗書」等都會增長exp這個值,而在升級的時候又會減掉exp的值,把exp直接暴露給各處業務來操做顯然是很糟糕的。在JS裏面咱們能夠用閉包把它隱藏起來,簡單模擬以下:

function makePlayer () {
  let exp = 0 // 經驗值
  return {
    getExp () {
      return exp
    },
    changeExp (delta, sReason = '') {
      // log(xxx),記錄變更日誌
      exp += delta
    }
  }
}

let p = makePlayer()
console.log(p.getExp()) // 0
p.changeExp(2000)
console.log(p.getExp()) // 2000

  這樣咱們調用makePlayer()就會生成一個玩家對象p,p內經過方法操做exp這個變量,可是卻不能夠經過p.exp訪問,顯然更符合「封裝」的特性。

4、總結

        閉包是JS中的強大特性之一,然而至於閉包怎麼使用,我以爲不算是一個問題,甚至咱們徹底不必研究閉包怎麼使用。個人觀點是,閉包應該是天然而言地出如今你的代碼裏,由於它是解決當前問題最直截了當的辦法;而當你刻意想去使用它的時候,每每可能已經走了彎路。

相關文章
相關標籤/搜索