相信不少初學者在學習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
裏面能有一個計算dogs
的count
變數;而countCats
裏面也能有一個計算cats
的count
變量,二者是不會互相干擾的。
爲了達到這樣的效果,咱們就要利用閉包,讓變量保留在該函數中而不會被外在環境干擾。
改爲閉包的寫法會像這樣:
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博客點贊和關注,感激涕零!