《JavaScript權威指南》中講到:一個變量的做用域是程序源代碼中定義這個變量的區域。數據結構
而實際上來講,做用域就是一套存儲變量的規則,用於肯定在何處以及如何查找變量。閉包
儘管一般將JavaScript稱做爲 動態 或 解釋型 語言,但實際上JavaScript是有編譯過程的。模塊化
當看到 var a = 2
,這段程序時,咱們來看看編譯器和引擎都作了什麼?函數
首先編譯器會在當前做用域中查詢是否已經有一個名爲a的變零存在於同一個做用域的集合中,若是有,則忽略該聲明,繼續進行編譯,若是沒有,則會要求做用域在當前的做用域集合中聲明一個變量a。ui
編譯器將 var a = 2
這個代碼片斷編譯成用於執行的機器指令spa
引擎在運行時會首先在當前做用域中查找是否存在變量a,若是不存在則繼續像上一級查找,若是存在,則引擎會使用這個變量設計
若是引擎最終找到了這個變量a,就會將2賦值給它,若是沒有找到則會拋出一個異常。3d
簡單地說,詞法做用域就是定義在詞法階段的做用域,也就是說,詞法做用域是你在書寫代碼時就已經決定了的。code
看一下如下代碼cdn
function foo(a) {
var b = a * 2;
function bar(c) {
console.log( a, b, c );
}
bar( b * 3 );
}
foo( 2 ); // 2, 4, 12
複製代碼
在這個例子中有三個嵌套的做用域:
① 包含着整個全局做用域,只有一個標識符:foo ② 包含着 foo 函數所建立的做用域,有三個標識符:a、b、bar ③ 包含着 bar 函數所建立的做用域,有一個標識符:c
做用域氣泡由其對應的做用域塊代碼寫在哪裏決定,它們是逐級包含的。
做用域的分類
先看一段代碼:
var a;
a = 2;
console.log( a );
複製代碼
執行結果是 2。
實際上JavaScript代碼在執行時候並非一行一行的執行的,引擎在執行代碼以前會首先對其進行編譯,包括變量和函數在內的全部聲明都會在任何代碼被執行前被‘移動’到最上面,這就叫作變量提高。
再來看一個例子:
foo();
function foo() {
console.log( a ); // undefined
var a = 2;
}
複製代碼
值得注意的是,每一個做用域都會進行提高操做。只有聲明自己會被提高,而賦值或其餘運行邏輯仍會留在原地。
所以上面那段代碼能夠被理解爲下面的形式:
function foo() {
var a;
console.log( a ); // undefined
a = 2;
}
foo();
複製代碼
具體的提高規則以下:
當一個塊或函數嵌套在另外一個塊或函數中時,就發生了做用域的嵌套。所以引擎在查找變量時,會先從當前的執行做用域開始查找,若是找不到,就向上一級繼續查找,當到達最外層的全局做用域時,不管找到仍是沒找到,查找過程就會中止。像這樣由多個執行上下文的變量對象構成的鏈式結構就叫作做用域鏈。
閉包的定義有不少種,可是通常能夠分爲兩類
一種說法是閉包是符合必定條件的函數。好比《JavaScript⾼級程序設計》是這樣定義的:閉包是指有權訪問另外一個函數做用域中的變量的函數。
另外一種說法是閉包是由函數以及和它相關的引用環境組合而成的實體。好比MDN中是這樣定義的:閉包是函數和聲明該函數的詞法環境的組合。
這兩種說法在某種意義上實際上是對立的,一個認爲閉包是函數,另外一個認爲閉包是函數和引用環境組成的總體。但其實第二種的表達看起來更確切點,MDN稍早以前給出的閉包定義是: 閉包是那些可以訪問自由變量(自由變量是指在函數中使用的,但既不是函數參數也不是函數的局部變量的變量。)的函數,這種說法是比較結交接近於第一種說法的,可是後來的MDN定義卻改爲了上面的第二種說法。
函數只是一些可執行的代碼,而閉包的本質來源於兩點:
即便函數已經執行完畢,在執行期間建立的變量也不會被銷燬,所以每運行一次函數就會在內存中留下一組變量。(js固然會有垃圾回收機制,不過若是它發現你正在使用閉包,則不會清理可能會用到的變量)
因此咱們概括一下,就是關於一個函數要成爲一個閉包到底須要滿意幾個條件:
下面代碼就是一個典型的閉包
function foo(){
var a = 2;
function bar(){
console.log(a);//2
}
return bar;
}
var baz = foo();
baz();
baz = null;
複製代碼
因爲閉包占用內存空間,因此要謹慎使用閉包。儘可能在使用完閉包後,及時解除引用,以便更早釋放內存。