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

ourjs發表於2015-04-28

  在這篇文章中,我會試圖講解JavaScript變數的作用域和宣告提升,以及許多隱隱藏的陷阱。為了確保我們不會碰到不可預見的問題,我們必須真正理解這些概念。

  基本定義

  作用範圍是個“木桶”,裡面裝著變數。變數可以是區域性或者全域性性的,但在子範圍中定義的變數是可以訪問父範圍的,這一點可能會造成一些困擾。

  在JavaScript中使用"var"關鍵字宣告變數。一旦在父範圍宣宣告,就會作為各自子範圍的一部分。即在本地範圍內有效,但本地定義的變數不可在全域性範圍內訪問。

  讓我們來看一個例子。執行下面的程式碼,你會發現,你能列印出全域性範圍定義的變數,而全域性範圍無法訪問區域性範圍定義的變數。

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()中它會尋找它父作用域內的變數。

var a = 1;
function someFunction() {
  var a = 2;
  function anotherFunction() {
    console.log(a); // 2
  }
}

  更復雜的情況是,在下面的例子中,一個變數沒有在函式中進行作用域的限定。

  在someFunction()中呼叫了一個沒有在函式範圍內定義的變數 a=2; 這個分配將覆蓋全域性變數的值。

  後續引用將指向全域性變數的值。

var a = 1;
function someFunction() {
  a = 2;
  function anotherFunction() {
    console.log(a); // 2
  }
  anotherFunction();
}
someFunction();
console.log(a); //2

  宣告提升(Hoisting)

  Hoisting會將在函式或全域性範圍內的變數“提升”到頂部宣告的過程。請記住,只有量宣告被提升了,初始化或值分配等等沒有變化,在下面的程式碼的情況下,第一個輸出將不確定...但它不會丟擲任何錯誤。

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

  Window範圍

  在基於瀏覽器的JavaScript中,定義為全域性範圍內的一部分變數實際上是所謂的“Window”物件的屬性。這裡的Window是指“容器”。換句話說,當你想從一個區域性範圍修改全域性定義的變數,你也可以通過修改Window物件的相應的屬性來做到這一點。

var myVariable = 'Global Scope';
function myFunction() {
  window.myVariable = 'Something Else';
}
myFunction();
console.log(myVariable); // Something Else

  可能的陷阱

  如果在函式內部分配一個以前沒有被定義的變數的值,它會自動成為全域性範圍的一部分。

function myFunction() {
  myVariable = 'JavaScript';
}
myFunction();
console.log(myVariable); //JavaScript

  如果你不小心忘記定義了一個區域性變數,你的整個指令碼可能會執行混亂。

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”的例子。

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!"。

  原文地址: codepunker.com

相關文章