JavaScript系列之閉包(Closure)

相信不少初學者在學習JavaScript 的時候,一直對閉包(closure) 有所疑惑。由於從字面上來看,徹底看不出它所表明的東西。那麼今天,我想經過這篇文章,儘可能用簡單易懂的話來與各位介紹「閉包」究竟是什麼。javascript

在具體介紹閉包以前,爲了更好的理解本文要介紹的內容,建議先去閱讀前面的文章《JavaScript系列之變量對象》《JavaScript系列之做用域和做用域鏈》,由於它們相互之間都是有關聯的。java

閉包是什麼?

首先,先來看看MDN 對閉包的定義:git

閉包是指那些可以訪問自由變量的函數。github

那什麼是自由變量呢?閉包

自由變量是一個既不是函數的形參,也不是函數的局部變量的變量。函數

由此,咱們能夠看出閉包共有兩部分組成:學習

閉包 = 函數 + 函數可以訪問的自由變量測試

好,若是上面三行就看得懂的話那麼就不用再往下看了,Congratulations!ui

...... 不過若是你是初學者的話,我想應該不會,若是僅用三言兩語就把閉包講通,那還能稱爲Javascript 語言的一個難點嗎?spa

先來舉個例子:

var n = 1;

function f1() {
    console.log(n);  // 1
}

f1() 
複製代碼

f1 函數能夠訪問變量 n,可是 n 既不是 f1 函數的形參,也不是 f1 函數的局部變量,因此這種狀況下的 n 就是自由變量。其實上面代碼中就存在閉包了,即函數 f1 + f1 函數訪問的自由變量 n 就構成了一個閉包

上面代碼中,函數 f1 能夠讀取全局自由變量 n。可是,函數外部沒法讀取函數內部聲明的變量:

function f1() {
    var n = 1;
}

console.log(n)  // Uncaught ReferenceError: n is not defined
複製代碼

若是有時須要獲得函數內的局部變量。正常狀況下,這是辦不到的,只有經過改變形式才能實現。那就是在函數的內部,再定義一個函數。

function f1() {
  var n = 1;
  function f2() {
    console.log(n); // 1
  }
  return f2;
}

var a = f1();
a();
複製代碼

上面代碼中,函數f2就在函數f1內部,這時f1內部的全部局部變量,對f2都是可見的。既然f2能夠讀取f1的局部變量,那麼只要把f2做爲返回值,咱們就能夠在f1外部讀取它的內部變量了。

因此閉包是一個能夠從另外一個函數的做用域訪問變量的函數。這是經過在函數內建立函數來實現的。固然,外部函數沒法訪問內部範圍

在咱們深刻研究閉包以前,有必要先從不使用閉包的狀況切入,瞭解爲何要用閉包。

不使用閉包的狀況

在JavaScript 中,全局變量的錯用可能會使得咱們的代碼出現不可預期的錯誤。

假設咱們如今要作一個計數的函數,一開始咱們想要先寫一個給狗的計數函數:

// 狗的計數函數
var count = 0

function countDogs () {
  count += 1
  console.log(count + ' dog(s)')
}

countDogs()    // 1 dog(s)
countDogs()    // 2 dog(s)
countDogs()    // 3 dog(s)
複製代碼

接着繼續寫代碼的其餘部分,當寫到後面時,我發現我也須要寫貓的計數函數,因而我又開始寫了貓的計數函數:

// 狗的計數函數
var count = 0

function countDogs () {
  count += 1
  console.log(count + ' dog(s)')
}


// 中間的其它代碼...

// 貓的計數函數
var count = 0

function countCats () {
  count += 1
  console.log(count + ' cat(s)')
}

countCats()    // 1 cat(s)
countCats()    // 2 cat(s)
countCats()    // 3 cat(s)
複製代碼

乍看之下好像沒啥問題,當我執行countDogs()countCats(),都會讓count增長,然而問題在於當我在不注意的狀況下把count這個變量創建在了全局做用域底下時,不管是執行countDogs()或是countCats()時,都是用到了全局的count變量,這使得當我執行下面的代碼時,它沒有辦法分辨如今究竟是在對狗計數仍是對貓計數,進而致使把貓的數量和狗的數量交錯計算的錯誤狀況:

countCats()    // 1 cat(s)
countCats()    // 2 cat(s)
countCats()    // 3 cat(s)

countDogs()    // 4 dog(s),我但願是 1 dog(s)
countDogs()    // 5 dog(s),我但願是 2 dog(s)

countCats()    // 6 cat(s),我但願是 4 cat(s)
複製代碼

閉包讓函數有私有變量

從上面的例子咱們知道,若是錯誤的使用全局變量,很容易會出現一些莫名其妙的bug ,這時候咱們就能夠利用閉包(closure)的寫法,讓函數有本身私有變量,簡單來講就是countDogs裏面能有一個計算dogscount變數;而countCats裏面也能有一個計算catscount變量,二者是不會互相干擾的。

爲了達到這樣的效果,咱們就要利用閉包,讓變量保留在該函數中而不會被外在環境干擾。

改爲閉包的寫法會像這樣:

function dogHouse () {
  var count = 0
  function countDogs () {
    count += 1
    console.log(count + ' dogs')
  }
  return countDogs
}

const countDogs = dogHouse()
countDogs()    // "1 dogs"
countDogs()    // "2 dogs"
countDogs()    // "3 dogs"
複製代碼

這樣咱們就將專門計算狗的變量count閉包在dogHouse這個函數中,在dogHouse這個函數中裏面的countDogs()纔是咱們真正執行計數的函數,而在dogHouse這個函數中存在count這個變量,因爲JavaScript變量會被縮限在函數的執行上下文中,所以這個count的值只有在dogHouse裏面才能被取用,在dogHouse函數外是取用不到這個值的。

接着由於咱們要可以執行在dogHouse中真正核心countDogs()這個函數,所以咱們會在最後把這個函數給return出來,好讓咱們能夠在外面去調用到dogHouse裏面的這個countDogs()函數。

最後當咱們在使用閉包時,咱們先把存在dogHouse裏面的countDogs拿出來用,並同樣命名爲countDogs(這裏變量名稱能夠本身取),所以當我執行全局中的countDogs時,實際上會執行的是dogHouse裏面的countDogs函數。

上面這是閉包的基本寫法:一個函數裏面包了另外一個函數,同時會 return 裏面的函數讓咱們能夠在外面使用到它

咱們能夠把咱們最一開始的代碼都改爲使用閉包的寫法:

function dogHouse () {
  var count = 0
  function countDogs () {
    count += 1
    console.log(count + ' dogs')
  }
  return countDogs
}

function catHouse () {
  var count = 0
  function countCats () {
    count += 1
    console.log(count + ' cats')
  }
  return countCats
}

const countDogs = dogHouse()
const countCats = catHouse()

countDogs()    // "1 dogs"
countDogs()    // "2 dogs"
countDogs()    // "3 dogs"

countCats()    // "1 cats"
countCats()    // "2 cats"

countDogs()    // "4 dogs"
複製代碼

當咱們正確地使用閉包時,雖然同樣都是使用count來計數,可是是在不一樣執行環境內的count所以也不會相互干擾。

進一步瞭解和使用閉包

另外,甚至在運用的是同一個dogHouse 時,變量間也都是獨立的執行環境不會干擾,好比:

function dogHouse () {
  var count = 0
  function countDogs () {
    count += 1
    console.log(count + ' dogs')
  }
  return countDogs
}

// 雖然都是使用 dogHouse ,可是各是不一樣的執行環境
// 所以彼此的變量不會互相干擾

var countGolden = dogHouse()
var countPug = dogHouse()
var countPuppy = dogHouse()

countGolden()     // 1 dogs
countGolden()     // 2 dogs

countPug()        // 1 dogs
countPuppy()      // 1 dogs

countGolden()     // 3 dogs
countPug()        // 2 dogs
複製代碼

將參數代入閉包中

可是這麼作的話你可能以爲還不夠清楚,由於都是叫作dogs,這時候咱們同樣能夠把外面的變量經過函數的參數代入閉包中,像是下面這樣,返回的結果就清楚多了:

// 經過函數的參數將值代入閉包中
function dogHouse (name) {
  var count = 0
  function countDogs () {
    count += 1
    console.log(count + ' ' + name)
  }
  return countDogs
}

// 一樣是使用 dogHouse 可是使用不一樣的參數
var countGolden = dogHouse('Golden')
var countPug = dogHouse('Pug')
var countPuppy = dogHouse('Puppy')

// 結果看起來更清楚了
countGolden()     // 1 Golden
countGolden()     // 2 Golden

countPug()        // 1 Pug
countPuppy()      // 1 Puppy

countGolden()     // 3 Golden
countPug()        // 2 Pug
複製代碼

爲了進一步簡化代碼,咱們能夠在閉包中直接return一個函數出來,咱們就能夠沒必要爲裏面的函數命名了,而是用匿名函數的方式直接把它返回出來。

所以寫法能夠簡化成這樣:

function dogHouse () {
  var count = 0
  // 把本來 countDogs 函數改爲匿名函數直接放進來
  return function () {
    count += 1
    console.log(count + ' dogs')
  }
}

function catHouse () {
  var count = 0
  // 把本來 countCats 函數改爲匿名函數直接放進來
  return function () {
    count += 1
    console.log(count + ' cats')
  }
}
複製代碼

而後咱們剛剛有提到,能夠透過函數參數的方式把值代入閉包當中,所以實際上咱們只須要一個counter ,在不一樣的時間點給它參數區分就好。這樣子無論你是要記錄哪種動物都很方便了,並且代碼也至關簡潔:

function createCounter (name) {
  var count = 0
  return function () {
    count++
    console.log(count + ' ' + name)
  }
}

const dogCounter = createCounter('dogs')
const catCounter = createCounter('cats')
const pigCounter = createCounter('pigs')

dogCounter()     // 1 dogs
dogCounter()     // 2 dogs
catCounter()     // 1 cats
catCounter()     // 2 cats
pigCounter()     // 1 pigs
dogCounter()     // 3 dogs
catCounter()     // 3 cats
複製代碼

閉包的實際應用

咱們要實現這樣的一個需求:點擊某個按鈕,提示點擊的是"第n個"按鈕,此處咱們先不用事件代理:

.....
<button>測試1</button>
<button>測試2</button>
<button>測試3</button>
<script type="text/javascript">
   var buttons = document.getElementsByTagName('button')
    for (var i = 0; i < buttons.length; i++) {
      buttons[i].onclick = function () {
        console.log('第' + (i + 1) + '個')
      }
    }
</script>  
複製代碼

這時候可能會預期點選不一樣的按鈕時,會根據每一個button 點擊順序的不一樣而獲得不一樣的結果。可是實際執行後,你會發現返回的結果都是「第四個」。這是由於i是全局變量,執行到點擊事件時,此時i的值爲3。

若是要強制返回預期的結果,那該如何修改呢?最簡單的是用let聲明i

for (let i = 0; i < buttons.length; i++) {
    buttons[i].onclick = function () {
        console.log('第' + (i + 1) + '個')
    }
}
複製代碼

簡單來講,經過let能夠幫咱們把所定義的變量縮限在塊級做用域中,也就是變量的做用域只有在{ }內,來避免 i 這個變量跑到全局變量被重複覆蓋。

另外咱們能夠經過閉包的方式來修改:

for (var i = 0; i < buttons.length; i++) {
    (function (j) {
        buttons[j].onclick = function () {
          console.log('第' + (j + 1) + '個')
        }
    })(i)
}
複製代碼

但願看完這篇文章後,你能對於閉包有更清楚的認識。

若是以爲文章對你有些許幫助,歡迎在個人GitHub博客點贊和關注,感激涕零!

相關文章
相關標籤/搜索