JavaScript變量做用域(Variable Scope)和閉包(closure)的基礎知識

  在這篇文章中,我會試圖講解JavaScript變量的做用域和聲明提高,以及許多隱隱藏的陷阱。爲了確保咱們不會碰到不可預見的問題,咱們必須真正理解這些概念。web

  基本定義

  做用範圍是個「木桶」,裏面裝着變量。變量能夠是局部或者全局性的,但在子範圍中定義的變量是能夠訪問父範圍的,這一點可能會形成一些困擾。編程

  在JavaScript中使用"var"關鍵字聲明變量。一旦在父範圍宣聲明,就會做爲各自子範圍的一部分。即在本地範圍內有效,但本地定義的變量不可在全局範圍內訪問。數組

  讓咱們來看一個例子。執行下面的代碼,你會發現,你能打印出全局範圍定義的變量,而全局範圍沒法訪問局部範圍定義的變量。瀏覽器

1
2
3
4
5
6
7
var agloballydefinedvariable = 'Global' ;
function someFunction() {
   var alocallydefinedvariable = 'Local' ;
   console.log(agloballydefinedvariable); // Global
}
console.log(alocallydefinedvariable);
// Uncaught ReferenceError: alocallydefinedvariable is not defined

  做用域鏈(Scope Chain)

  若是你忘記使用「var」的關鍵字來定義局部變量,事情可能會變得很是糟糕。爲何會這樣呢?由於JavaScript會首先在父做用域內搜索一個未定義的變量,而後再到全局範圍進行搜索。在下面的例子中,JavaScript知道變量「a」是someFunction()的一個局部變量,在anotherFunction()中它會尋找它父做用域內的變量。閉包

1
2
3
4
5
6
7
var a = 1;
function someFunction() {
   var a = 2;
   function anotherFunction() {
     console.log(a); // 2
   }
}

  更復雜的狀況是,在下面的例子中,一個變量沒有在函數中進行做用域的限定。編程語言

  在someFunction()中調用了一個沒有在函數範圍內定義的變量 a=2; 這個分配將覆蓋全局變量的值。函數式編程

  後續引用將指向全局變量的值。函數

1
2
3
4
5
6
7
8
9
10
var a = 1;
function someFunction() {
   a = 2;
   function anotherFunction() {
     console.log(a); // 2
   }
   anotherFunction();
}
someFunction();
console.log(a); //2

  聲明提高(Hoisting)

  Hoisting會將在函數或全局範圍內的變量「提高」到頂部聲明的過程。請記住,只有量聲明被提高了,初始化或值分配等等沒有變化,在下面的代碼的狀況下,第一個輸出將不肯定...但它不會拋出任何錯誤。spa

1
2
3
console.log(a); //undefined
var a = 1;
console.log(a); //1

  Window範圍

  在基於瀏覽器的JavaScript中,定義爲全局範圍內的一部分變量其實是所謂的「Window」對象的屬性。這裏的Window是指「容器」。換句話說,當你想從一個局部範圍修改全局定義的變量,你也能夠經過修改Window對象的相應的屬性來作到這一點。code

1
2
3
4
5
6
var myVariable = 'Global Scope' ;
function myFunction() {
   window.myVariable = 'Something Else' ;
}
myFunction();
console.log(myVariable); // Something Else

  可能的陷阱

  若是在函數內部分配一個之前沒有被定義的變量的值,它會自動成爲全局範圍的一部分。

1
2
3
4
5
function myFunction() {
   myVariable = 'JavaScript' ;
}
myFunction();
console.log(myVariable); //JavaScript

  若是你不當心忘記定義了一個局部變量,你的整個腳本可能會運行混亂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var city= "LA" ;
var team= "Lakers" ;
function showTeam () {
     console.log (city + " " + team);
}
function showCity () {
     city = "Moscow" ;
     console.log (city);
}
showTeam(); // LA Lakers
showCity(); // Moscow
/*
由於上面的 showCity 中定義的變量 "city" 沒有使用 "var" 聲明,全局範圍內的變量被覆蓋了。所以會致使下面的問題 :)
*/
showTeam(); // Moscow Lakers

  內部函數依然會存儲局部變量即便它的外部函數已經執行完畢

  這聽起來可能有點怪異,看一個例子,就會更容易理解。解釋這一點的最好辦法是使用一個簡單的「Hello World」的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
function greet(who) {
     var iterations = 0;
     return function () {
         console.log(++iterations);
         return 'Hello ' + who + '!' ;
     };
}
var greeting = greet( 'World' );
console.log( typeof greeting); //function
console.log( typeof greeting()); //string & iterations=1
console.log(greeting()); //Hello World! & iterations=2
console.log(greeting( "Universe" )); //Hello World! & iterations=3
//輸出不是 Hello Universe. world 被閉包封閉保存了起來

  注*  在計算機科學中,閉包(Closure)是詞法閉包(Lexical Closure)的簡稱,是引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即便已經離開了創造它的環境也不例外。因此,有另外一種說法認爲閉包是由函數和與其相關的引用環境組合而成的實體。閉包在運行時能夠有多個實例,不一樣的引用環境和相同的函數組合能夠產生不一樣的實例。

  閉包的概念出現於60年代,最先實現閉包的程序語言是Scheme。以後,閉包被普遍使用於函數式編程語言如ML語言和LISP。不少命令式程序語言也開始支持閉包。  引自:Wiki

  正如你上面看到的那樣,greet() 返回一個被稱爲「閉包」的內部函數。閉包除了會儲存他們本身本地做用域內部的封閉起來的函數和變量外,還會存儲外部引用的參數。參看咱們的具體例子,參數 who 和 iterations 就是被閉包封閉起來的局部變量。

  這意味着,greeting已成爲一個包含who和iterations在內的函數(直接返回的匿名函數)。- 它不會再次執行greet,它只會執行閉包並且返回結果永遠是 "Hello World!"。

相關文章
相關標籤/搜索