在以前的文章中連續介紹了做用域的知識,有了這些知識儲備,咱們就來學習本節的內容做用域閉包
。回憶工做這幾年,大量使用JavaScript
或多或少也在運用閉包,如今咱們試着從理論角度來討論下閉包。閉包
遇到這種問題,第一時間看mdn文檔。官方文檔以下解釋:函數
閉包是由函數以及建立該函數的詞法環境組合而成。學習
這個描述過於抽象不利於理解先放一邊,咱們先來看一段代碼:ui
function foo() {
var a = 1;
function bar() {
console.log( a ); // 1
}
bar();
}
foo();
複製代碼
這是一段嵌套函數代碼,foo
函數中聲名了一個bar
函數。根據咱們以前學習做用域相關的知識。詞法做用域是由代碼聲明的位置決定的,foo
函數其中有兩個標識符a
、bar
,bar
函數能夠訪問在其外部聲明的變量也就是a
。spa
再來看另外一段代碼:設計
function foo() {
var a = 1;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 1
複製代碼
這段代碼直觀的展現了閉包的效果。咱們將bar
函數自己看成一個函數對象返回。在運行foo
函數後,其返回值(也就是bar
函數的引用)賦值給變量baz
並調用baz()
,實際上就是經過不一樣的標識符引用調用了內部函數bar
。3d
在foo()
執行後,一般會期待foo()
的整個內部做用域都被銷燬,由於咱們知道引擎有垃圾回收器用來釋放再也不使用的內存空間。因爲看上去foo()
的內容不會再被使用,因此很天然地會考慮對其進行回收。code
而閉包的「神奇」之處正是能夠阻止這件事情的發生。事實上內部做用域依然存在,所以沒有被回收。誰在使用這個內部做用域?原來是bar()
自己在使用。cdn
bar()
所聲明的位置,它擁有涵蓋foo()
內部做用域的閉包,使得該做用域可以一直存活,以供bar()
在以後任什麼時候間進行引用。對象
bar() 依然持有對該做用域的引用,而這個引用就叫做閉包。
當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,即便函數是在當前詞法做用 域以外執行。
for
循環是最多見的例子:
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000 );
}
複製代碼
咱們對這段代碼的預期結果分別輸出數字1-5,每秒一次,每次一個。但這段代碼在運行時會以每秒一次的頻率輸出五次6。延遲函數的回調會在循環結束時才執行。這顯然不是咱們想要的結果。
咱們試圖假設循環中的每一個迭代在運行時都會給本身「捕獲」一個i
的副本。可是根據做用域的工做原理,實際狀況是儘管循環中的五個函數是在各個迭代中分別定義的,可是它們都被封閉在一個共享的全局做用域中,所以實際上只有一個i
。
咱們須要更多的閉包做用域,特別是在循環的過程當中每一個迭代都須要一個閉包做用域,例如:
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})( i );
}
複製代碼
在迭代內使用IIFE
會爲每一個迭代都生成一個新的做用域,使得延遲函數的回調能夠將新的做用域封閉在每一個迭代內部,每一個迭代中都會含有一個具備正確值的變量供咱們訪問。
ES6中let
聲明,能夠用來劫持塊做用域,而且在這個塊做用域中聲明一個變量,例如:
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000 );
}
複製代碼
咱們利用閉包的強大功能,能夠實現一個JavaScript
模塊,例如:
function module() {
var something = "module";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = module();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
複製代碼
首先,module
只是一個函數,必需要經過調用它來建立一個模塊實例。若是不執行外部函數,內部做用域和閉包都沒法被建立。
而後module
函數返回一個對象。咱們保持內部數據變量是隱藏且私有的狀態。能夠將這個對象類型的返回值看做本質上是模塊的公共API。
這個對象類型的返回值最終被賦值給外部的變量foo
,而後就能夠經過它來訪問API中的屬性方法,好比foo.doSomething()
。
能夠對這個模式進行簡單的 改進來實現單例模式:
var foo = (function module() {
var something = "module";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
複製代碼
閉包本質也是做用域的產物,閉包的規律也是做用域的規律。本章也是簡單的介紹了一下閉包,更多更深刻的內容仍是來源咱們項目中的代碼。