(三) js閉包

1. 案例引入

先看一個簡單的需求:瀏覽器

  • 咱們想要實現一個簡單的累加器, 每調用一次累加函數, 變量就加 1

這真的是一個簡單的需求, 咱們能夠立馬寫出來閉包

var counter = 0
function addCounter(){
  counter++
  console.log(counter);
}
addCounter()				// 1
addCounter()				// 2
addCounter()				// 3

可是你有沒有想過: 爲何 counter 能夠一直被累加 ?ide

這裏就牽扯到了js中的 垃圾回收機制函數

所謂的垃圾回收, 就是肯定哪一個變量再也不使用, 而後釋放它佔用的內存, 這是一個週期性的檢測學習

可是對於全局變量而言, 垃圾回收機制並不知道它何時再也不使用, 所以也就沒法釋放其佔用的內存, 全局變量也就會一直被保存在內存當中, 這也就是咱們能夠一直使用它的緣由prototype

其實從執行上下文的角度來看, 全局變量的聲明是在全局上下文中進行的, 被一直保存在GO中, 而全局上下文是在咱們關閉瀏覽器時纔會被銷燬, 所以其餘函數就能夠經過其保存的上下文環境 (也就是做用域鏈), 到GO中引用countercode

這樣, 第一個問題咱們就解決了blog

好, 咱們再來思考另一個問題, 若是另一個函數也用到了 counter, 或者另一個開發者聲明瞭一個一樣的全局變量 counter,那麼這勢必會影響上面累加器的運做, 由於 counter 的值被其餘函數 / 變量聲明 改變了, 這顯然是咱們所不但願的ip

那既然不想counter被其餘因素所改變, 那咱們把counter做爲累加器的內部變量不就好了麼, 這樣只有函數本身能用, 行嗎? 顯然不行內存

由於函數在執行完畢後, 會銷燬其所在的上下文環境, 保存在AO中的變量會被垃圾回收機制釋放, 對於沒法一直保存在內存中的變量, 咱們顯然沒法一直對他進行操做

那有沒有辦法能夠解決這個需求: 咱們既想把 counter 聲明爲局部變量, 又讓他一直保留在內存中呢 ?

答案是能夠的, 這就是咱們須要閉包的緣由

2. 什麼是閉包

我的理解:

  • 咱們知道: 執行上下文是一個運行時環境, 當函數執行完畢, 函數上下文會被釋放, 局部變量天然也就沒法一直保留

  • 而閉包的存在, 就是幫助咱們把這些會被釋放的上下文保存到內存中,使其一直存在

MDN中給出閉包的以下定義:

  • 一個函數和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一塊兒(或者說函數被引用包圍),這樣的組合就是閉包closure)。

示例

function fn() {
  var count = 0
  function addCount() {
    count++
  }
  addCount()
}

fn()

上面有沒有造成閉包呢? 咱們根據MDN的定義來分析一下:

  • addCount 是否是一個函數? 是
  • addCount 對其周圍狀態有沒有引用 ? 有, 引用了所處上下文環境中的 count

既然造成了閉包, 那能不能完成咱們上面的需求呢: **咱們既想把 counter 聲明爲局部變量, 又讓他一直保留在內存中 **

咱們來接着調用addCount函數, 看三次調用之後, count的值是否是3

注意看 watch中count的變化

經過演示咱們發現, count 並無一直被保留在內存中, 而是每次調用都被初始化爲了0

這顯然沒法完成咱們的需求


ECMAScript中, 給出以下定義:

  • 從理論角度:全部的函數都是閉包。由於它們都在建立的時候就將上層上下文的數據保存起來了。哪怕是簡單的全局變量也是如此,由於函數中訪問全局變量就至關因而在訪問自由變量,這個時候使用最外層的做用域。

  • 從實踐角度:如下函數纔算是閉包:

    • 即便建立它的上下文已經銷燬,它仍然存在(好比,內部函數從父函數中返回)
    • 在代碼中引用了自由變量

根據ECMAScript給出的定義, 咱們再來對比MDN給出的定義, 咱們發現, MDN中對閉包的定義其實就是ECMAScript中的閉包的理論角度,

但這對咱們的需求並無實際的做用, 那咱們再來看 從實際角度出發給出的定義

示例

function fn() {
  var count = 0
  function addCount() {
    count++
  }
  return addCount
}

var add = fn()
add()
add()
add()

注意: 與上面代碼不一樣的是, 咱們此次將addCount函數做爲返回值傳遞到外部

注意此時watch中count的變化

咱們能夠清晰的看到, 內部變量count, 在函數外部被一直引用, 並且實現了值的累加 !

也就是說, count被一直保留在了內存之中, 這也真正實現了咱們上面所一直所提的需求:

  • 咱們既想把 counter 聲明爲局部變量, 又讓他一直保留在內存中

其實說了這麼多, 可能仍是比較抽象, 咱們先回顧如下閉包的理解:

  • 咱們知道: 執行上下文是一個運行時環境, 當函數執行完畢, 函數上下文會被釋放, 局部變量天然也就沒法一直保留

  • 而閉包的存在, 就是幫助咱們把這些會被釋放的上下文保存到內存中,使其一直存在

咱們舉個例子:

​ 假設我家院子 (內存)裏有一個大水缸 (不能釋放的內存), 水缸裏面有滿滿的水 (全局變量), 這個水過路口渴的人 (其餘函數等)均可以取來喝,可是不一樣的人都來喝, 不就把我家的水給 污染 了麼, 何況這是我家, 大家怎麼能想喝就喝呢, 因而我想出了這樣一個辦法:

​ 我先把水(此時變成局部變量)都移到紙箱(外層函數)裏, 可是紙箱存不住水 (函數被釋放)呀, 因而我又立刻把這些水都灌進水瓶 (內層函數)裏, 而後把這些水瓶放在紙箱裏 (函數嵌套), 這樣咱們的水得以保存, 可是別人也想喝怎麼辦, 那就按照個人說明去紙箱裏拿水瓶 (暴露給外部的引用, 到此造成閉包), 而且你只能經過暴露出的水瓶來喝水

​ 後來家家裝了自來水, 你們也都不須要來我家喝水了, 因而我就把全部的水瓶丟掉(清除引用, 釋放閉包的內存),


綜上:

怎麼理解閉包:

  • 咱們知道: 執行上下文是一個運行時環境, 當函數執行完畢, 函數上下文會被釋放, 局部變量天然也就沒法一直保留

  • 而閉包的存在, 就是幫助咱們把這些會被釋放的上下文保存到內存中,使其一直存在

什麼纔算閉包:

從實踐角度:如下函數纔算是閉包:

  • 即便建立它的上下文已經銷燬,它仍然存在(好比,內部函數從父函數中返回)
  • 在代碼中引用了自由變量

3. 閉包的應用

1. 訪問函數內部的變量, 也就是上面一直提的需求

function fn() {
  var count = 0
  function addCount() {
    count++
  }
  return addCount
}

var add = fn()
add()

2. 定時器傳值, 本質與上面一致

function func(param) {
  return function () {
    alert(param)
  }
}

var f1 = func(1)

setTimeout(f1, 1000)

3. 利用閉包判斷數據類型

function isType(type) {
  return function (target) {
    return `[object ${type}]` === Object.prototype.toString.call(target)
  }
}

const isArray = isType('Array')
console.log(isArray([1, 2, 3]));				// true
console.log(isArray({}));
僅記錄本身的學習總結,若有錯誤,還請評論指正~
相關文章
相關標籤/搜索