你不知道的JavaScript:閉包

前言

在瞭解閉包的概念時,我但願你可以有JavaScript詞法做用域的知識,由於它會讓你更容易讀懂這篇文章。程序員

感觸

對於那些使用過JavaScript但卻徹底不理解閉包概念的人來講,理解閉包能夠看作是某種意義上的重生,可是你須要付出大量的努力和犧牲才能理解這個概念。
回憶我一年前,雖然使用過不少JavaScript,但卻徹底不理解閉包是什麼。當我瞭解到模塊模式的時候,我才激動地發現了原來這就是閉包?閉包

JavaScript中閉包無處不在,你只須要可以識別並擁抱它。

開始

直接上定義
當函數能夠記住並訪問所在的做用域時,就產生了閉包。即函數是在當前詞法做用域以外執行函數

function foo () {
    const a = 2
    function bar () {
        console.log(a)
    }
    return bar
}
const baz = foo()
baz() // 2  ---  媽媽呀!這就是閉包?太簡單了吧!

函數foo()使用它的內部方法 bar()做爲返回值,而bar()內部有着對foo()做用域的引用(即a),在執行foo()事後,內部函數bar()賦值給baz,調用baz()顯然能夠執行bar()。
能夠看到bar()在自身做用域以外執行了,一般在foo()執行事後,咱們會以爲foo()會被JS引擎的垃圾回收機制銷燬,實際上並不會,由於baz有着對bar()的引用,而bar()內部有着foo()做用域的引用,所以foo()並不會被銷燬,以供bar()在任什麼時候間被引用,所以bar()記住了並訪問了自身所在的foo()做用域
固然,這兒還有另一個例子:code

function foo () {
    const a = 2
    function baz () {
        console.log(a)
    }
    bar(baz)
}
function bar (fn) {
    fn() // 這就是閉包
}

本例中,baz()在foo()以外調用,而且baz()自身有着涵蓋foo()做用域的引用,所以baz()能夠記住foo()的做用域,保證其不會被垃圾回收機制銷燬ip

如今我懂了

上一節的代碼過於死板,咱們來看看更實用的代碼。作用域

function wait (message) {
    setTimeout(function timer () {
        console.log(message)
    }, 1000)
}
wait('hello')

很明顯,內部函數timer()持有對wait()的閉包
或者在jQuery中回調函數

function setupBot (name, selector) {
    $(selector).click(function activator () {
        console.log(name)
    })
}
setupBot ('hello', '#bot')

能夠看到,閉包在你寫的代碼中無處不在,特別是回調函數,全是閉包it

循環與閉包

給一個經典的案例io

for(var i = 1 ; i <= 5 i ++) {
    setTimeout(function timer () {
        console.log(i)
    }, i * 1000)
}

你可能會天真的覺得它會輸出:1,2,3,4,5?
事實上,它會以每秒一次的頻率輸出5次6
爲何?
由於延遲函數會在循環結束時才執行。就算你setTimeout(...,0),也會在循環完成時,輸出5次6
固然,不要覺得主要的緣由是延遲函數會在循環結束時才執行,否則我爲何會在閉包這一節用使用這個例子,哈哈。
那麼真正致使這個與預期不符的是閉包
首先內部函數timer()有着涵蓋for循環的閉包,這5次調用timer()都是封閉在同一個做用域中,他們共享同一個i,只有一個i
那麼咱們如何讓它按照咱們的預期,輸出1,2,3,4,5呢?
固然是讓每一個timer(),都有一個屬於本身的i,這裏的解決方案有不少:console

  1. IIFE當即執行函數能夠造成一個塊做用域,咱們只須要把每次迭代的i,保存在timer()的塊做用域中,經過這個保存的值打印出來就ok了

    for(var i = 1 ; i <= 5; i ++) {
          (function() {
            var j = i
            setTimeout(function timer () {
              console.log(j)
            }, i * 1000)
          }
          )(i)
      }
  2. ES6中的const或者let,它們均可以構造一個塊級做用域(PS:const 定義常量,沒法被修改

    for(var i = 1 ; i <= 5; i ++) {
        const j = i
        setTimeout(function timer () {
            console.log(j)
        }, j * 1000)
    }
  3. 咱們能夠用let稍微改進一下(爲何在for循環中使用let,不用const,上面已經說得很清楚了

    for(let i = 1 ; i <= 5; i ++) {
        setTimeout(function timer () {
            console.log(i)
        }, i * 1000)
    }

不知道你怎麼想,反正塊級做用域閉包的使用,讓我成爲了一隻快樂的JavaScript程序員

模塊

這是閉包運用得最廣的地方了吧
看看下面的代碼

function Module(){
    const something = 'Do A'
    const another = 'Do B'
    function doA(){
      console.log(something)
    }

    function doB(){
      console.log(another)
    }
    return {
      doA,
      doB
    }
  }
  const foo = Module()
  foo.doA()
  foo.doB()

這種模式,在JavaScript中被稱爲模塊,其中包含的閉包,相信你們一眼就看出來了吧。
Module()中的 doA() 與 doB() 都包含了對Module()的閉包
那麼模塊模式須要具有的條件是:

  • 必須有外部的封閉函數,且至少被調用一次(每次調用都會產生一個新的模塊)
  • 封閉函數必須返回至少一個內部函數,造成閉包,而且能夠修改和訪問私有狀態。

因爲調用一次就會產生一個模塊,那麼是否有單例模式呢?

const foo = (function Module(another){
    const something = 'Do A'
    function doA(){
      console.log(something)
    }

    function doB(another){
      console.log(another)
    }
    return {
      doA,
      doB
    }
  })()
  foo.doA()
  foo.doB('Do B')

經過IIFE,當即調用這個模塊,只暴露foo,那麼這個模塊只有foo這一個實例。

如今的模塊機制

// bar.js
function hello(who) {
    return `hello ${who}`
}
export hello
// foo.js
// 僅導入hello()
import hello from 'bar'

const name = 'jack'
function awesome () {
    console.log(hello(name))
}
export awesome
// baz.js
// 導入完整模塊
module foo from 'foo'
module bar from 'bar'

console.log(bar.hello('john'))
foo.awesome()

這裏模塊文件中的內容一樣被當作好像包含在做用域中的閉包同樣處理

小結

閉包就好像是JavaScript中,充滿神奇色彩的一部分,可是當咱們揭開她的面紗,才發現她居然這麼美,她一直陪在你身邊,可是你卻一直逃避她,此次我不想你再錯過她了。

相關文章
相關標籤/搜索