閉包在 JavaScript 代碼中隨處可見, 但有時由於對它的瞭解不夠深刻, 因此它每每帶有一絲神祕色彩。雖然在寫代碼時隨手就會建立而且使用閉包, 但有時它對咱們來講仍然像一個黑盒,因此有必要探究一下這個黑盒子裏面到底包的什麼餡。閉包
正如引言中所說, 咱們隨手就能建立一個閉包:函數
// eg.1
var variable = 1;
function fn(){
console.log(variable);
}
fn();
複製代碼
這段代碼中的函數 fn 其實就是一個閉包。這可能看起來很詭異, 不過 fn 確實是一個最簡單的閉包,爲何它能被稱爲一個閉包呢? 下面給出閉包的定義以後就能解釋這個問題了。ui
閉包的定義是:在本身的做用域內引用了外層(也稱上層)做用域中的變量的函數稱爲閉包。 這句話中的關鍵詞有兩個: 1. 外層(也稱上層)做用域; 2. 函數。這說明了閉包是個函數,並且這個函數引用了外層做用域裏的變量。this
這樣看來上面 eg.1 中的例子就明朗了: fn 是個函數, 並且 fn 內輸出(引用)了外層做用域中的變量 variable
。也許有的同窗不太清楚 JavaScript 中做用域的概念, 如今來回顧一下。spa
注: MDN英文版中說"閉包是一個函數以及這個函數定義時所在的詞法環境的組合", 這比筆者給出的閉包是個函數的說法更準確, 在讀徹底這篇文章後相信讀者會有清晰的認識。設計
做用域是代碼生存的環境,它限制着代碼的活動範圍。JavaScript 中有三種做用域, 分別是 1. 全局做用域; 2. 函數做用域 3. 塊做用域。後二者也被稱爲局部做用域。3d
做用域之間能夠並列,也能夠互相嵌套。如下面的代碼爲例:code
// 這是全局做用域
function fn1(){
// 這裏面是函數做用域
}
function fn2(){
// 這裏面也是函數做用域
}
複製代碼
上面的代碼中有全局做用域和兩個函數做用域, 全局做用域比較好理解, 函數在定義的時候就有了本身的做用域, 那就是本身的內部。上面的全局做用域包含了兩個函數做用域, 稱爲做用域嵌套, 即做用域裏還套着其餘的做用域, 而全局做用域能夠稱爲外層(或者上層)做用域, 兩個函數做用域稱爲內層(或者下層)做用域。相似於俄羅斯套娃, 大娃套中娃, 中娃套小娃, 一層一層的套。cdn
而嵌套着的做用域有個特色: 內層做用域能夠直接訪問外層做用域裏的變量, 可是外層做用域沒法直接訪問內層做用域內的變量, 例如:blog
// eg.2
// 這是全局做用域, 相對於兩個函數做用域來講是全局做用域就是外層做用域
// 在外層做用域中定義一個變量
var outerVariable = 1;
function fn(){
// 這裏面是函數做用域, 相對說也是內層做用域
// 在內層做用域內定義一個變量
var innerVariable = 2;
// 在內層做用域內訪問外層做用域中的變量 outerVariable
console.log(outerVariable); // 1
}
fn(); // 調用函數, 輸出 1, 說明在函數(內層)做用域裏能夠訪問到全局(外層)做用域裏的變量
// 在外層做用域裏嘗試訪問內層做用域的變量
console.log(innerVariable); // ReferenceError: innerVariable is not defined
// 報了引用錯誤, 說明外層做用域中沒有 innerVariable 這個變量, 也就是說明外層沒法訪問到內層做用域中定義的變量
複製代碼
上面的代碼中的外層做用域爲全局做用域, 內層做用域爲函數做用域。在嘗試在內層做用域中訪問外層做用域時中定義的變量時是能夠的,可是想在外層做用域中直接訪問內層做用域裏定義的變量就不會成功了。
能夠將做用域嵌套進行具象化以便於理解, 以上面的例子 eg.2 爲例:
上圖中能夠看出外層做用域(綠色)內定義了一個函數 fn, 這個函數 fn 有本身的做用域(棕色), 這兩個做用域是嵌套關係, 就產生了內外層做用域這個說法。
上面只介紹了最簡單的閉包例子, 下面介紹更加複雜, 也是更加常見的閉包例子, 以便加深理解。
關於 JavaScript 做用域的更多內容能夠查閱《JavaScript高級程序設計》的 4.2節。
在文章的第 1 部分中, 爲了引入閉包的概念, 只給出了最簡單的閉包例子。 下面以一個稍微複雜的閉包例子來引出關於閉包的更多內容。
一個稍複雜一些的閉包例子:
1 // eg.3
2 function outer(){
3 let outerVariable = 0;
4
5 let inner = function(){
6 console.log('outerVariable === ', outerVariable);
7 }
8
9 return inner;
10 }
11
12 let closure = outer(); // 調用 outer 函數, 獲得 inner 函數, 也就是獲得了一個閉包實例
13
14 // 運行這個閉包, 在這個閉包裏仍然能夠訪問它引用的外層做用域裏的變量 outerVariable
15 closure(); // outerVariable === 0
複製代碼
上面代碼中定義了 outer 和 inner 兩個函數, 其中 inner 函數是定義在 outer 函數中的, 並且引用了 outer 函數中定義的 outerVariable 變量。
同時能夠發現, inner 函數的做用域被嵌套在了 outer 函數的做用域中, 則此時能夠認爲 outer 是 inner 函數的外層做用域。 而 inner 訪問了 外層做用域中的 outerVariable 變量, 此時 inner 就是一個閉包。
又由於函數是一等公民, 能夠做爲其餘函數的返回值, 上面代碼中的 outer 函數就將 inner 函數返回了, 因此調用 outer 函數就是獲得了一個 inner 閉包的一個實例, 上面的代碼將這個實例命名爲 closure。
調用這個閉包, 就運行了 console.log(outerVariable);
這行代碼, 就讀取到了閉包環境中保存的外層做用域中的 outerVariable。
能夠用嵌套的做用域圖片更具象的解釋上面這幾段話:
由上圖可見, 內層做用域的 inner 函數中引用了外層做用域的 outerVariable 變量, 因此 inner 函數成爲了一個閉包。
在實例化一個閉包(例如上面eg.3示例代碼中的第12行)的時候, 會建立一個環境, 這個環境保存了定義這個閉包(例如上面eg.3示例代碼中的第5~7行)時的做用域信息, 這個做用域信息能夠被抽象爲上面圖片中所示的。
請注意的是, 每實例化一個閉包時, 就會建立一個環境, 每一個閉包能夠在本身的環境中操做, 卻不能夠訪問其餘閉包實例對應的環境。並且只要一個閉包實例存在, 它的對應的環境就會存在。
將上面的例子 eg.3 修改爲下面的代碼:
// eg.4
function outer(){
let outerVariable = 0;
let inner = function(){
outerVariable ++; // 增長的代碼, 在輸出 outerVariable 以前, 先讓他自增 1
console.log('outerVariable === ', outerVariable);
}
return inner;
}
let closure1 = outer(); // 修改的代碼, 實例化一個閉包並命名爲 closure1
let closure2 = outer(); // 修改的代碼, 再實例化第二個閉包並命名爲 closure2
// 增長的代碼, 調用三次 closure1, 觀察 closure1 引用的變量 outerVariable 的變化
closure1(); // outerVariable === 1
closure1(); // outerVariable === 2
closure1(); // outerVariable === 3
// 增長的代碼, 調用一次 closure2, 觀察 closure2 引用的變量 outerVariable 的值
closure2(); // outerVariable === 1
/* 發現閉包 closure2 引用的 outerVariable 值沒有受到閉包 closure1 的影響*/
複製代碼
上面的代碼在閉包 inner 輸出 outerVariable 以前使它自增一下, 而後實例化了兩個閉包, 從代碼的運行結果中能夠看出, 這兩個閉包引用的 outerVariable 並非一個, 說明這兩個閉包各自保存了一份 outerVariable, 而且每一個閉包中保存的 outerVariable 的初始值就是定義閉包 inner 時候的值.
這個過程一樣能夠經過圖形表達出來:
上面的圖片能夠說明, 每一個閉包操做的都是本身環境中的 outerVariable, 與其餘的閉包無關, 上面 eg.4 中的代碼的運行結果頁說明了這個結論.
下面舉一個使用了閉包的經典應用, 使用閉包模擬私有變量:
function Person(){
let age = 24;
this.getAge = function(){
return age; // 引用了外層的變量
}
this.grow = function(){
age ++; // 引用了外層的變量
}
}
let xm = new Person(); // 實例化小明 xm
// 沒法經過 xm.age 來訪問 xm 的年齡;
// 獲取 xm 的年齡
let ageThisYear = xm.getAge();
console.log('小明今年的年齡是 ' + ageThisYear); // 小明今年的年齡是 24
// 過了一年, xm 漲了一歲
xm.grow();
// 獲取小明過年以後的年齡
let ageNextYear = xm.getAge();
console.log('小明明年的年齡是', ageNextYear); // 小明明年的年齡是 25
複製代碼
如上面的代碼所示, 在兩個函數 getAge 和 grow 中引用了上層做用域裏的 age 變量, 這兩個函數就是閉包,接着對這兩個閉包進行調用。 就實現了一個簡單版的模擬了私有變量。
MDN對英文版對閉包的定義是: "A closure is the combination of a function and the lexical environment within which that function was declared". 即:"閉包是一個函數以及這個函數定義時所在的詞法環境的組合"。這個定義比本文章內的說法更加準確, 請參考.
完. 若有錯誤, 感謝指出!