JavaScript的變量提升(轉自CSDN_blog sunxing007用戶)

下面的程序是什麼結果?

[javascript] view plain copy
  1. var foo = 1;  
  2. function bar() {  
  3.     if (!foo) {  
  4.         var foo = 10;  
  5.     }  
  6.     alert(foo);  
  7. }  
  8. bar();  
結果是10;

那麼下面這個呢?

[javascript] view plain copy
  1. var a = 1;  
  2. function b() {  
  3.     a = 10;  
  4.     return;  
  5.     function a() {}  
  6. }  
  7. b();  
  8. alert(a);  
結果是1.

嚇你一跳吧?發生了什麼事情?這可能是陌生的,危險的,迷惑的,同樣事實上也是非常有用和印象深刻的JavaScript語言特性。對於這種表現行爲,我不知道有沒有一個標準的稱呼,但是我喜歡這個術語:“Hoisting (變量提升)”。這篇文章將對這種機制做一個拋磚引玉式的講解,但是,首先讓我們對javascript的作用域有一些必要的理解。

Javascript的作用域

對於Javascript初學者來說,一個最迷惑的地方就是作用域;事實上,不光是初學者。我就見過一些有經驗的javascript程序員,但他們對scope理解不深。javascript作用域之所以迷惑,是因爲它程序語法本身長的像C家族的語言,像下面的C程序:

  1. #include <stdio.h>  
  2. int main() {  
  3.     int x = 1;  
  4.     printf("%d, ", x); // 1  
  5.     if (1) {  
  6.         int x = 2;  
  7.         printf("%d, ", x); // 2  
  8.     }  
  9.     printf("%d\n", x); // 1  
  10. }  
輸出結果是1 2 1,這是因爲C家族的語言有塊作用域,當程序控制走進一個塊,比如if塊,只作用於該塊的變量可以被聲明,而不會影響塊外面的作用域。但是在Javascript裏面,這樣不行。看看下面的代碼:
[javascript] view plain copy
  1. var x = 1;  
  2. console.log(x); // 1  
  3. if (true) {  
  4.     var x = 2;  
  5.     console.log(x); // 2  
  6. }  
  7. console.log(x); // 2  
結果會是1 2 2。因爲javascript是函數作用域。這是和c家族語言最大的不同。該程序裏面的if並不會創建新的作用域。

對於很多C,c++,Java程序員來說,這不是他們期望和歡迎的。幸運的是,基於javascript函數的靈活性,這裏有可變通的地方。如果你必須創建臨時的作用域,可以像下面這樣:

[javascript] view plain copy
  1. function foo() {  
  2.     var x = 1;  
  3.     if (x) {  
  4.         (function () {  
  5.             var x = 2;  
  6.             // some other code  
  7.         }());  
  8.     }  
  9.     // x is still 1.  
  10. }  
這種方法很靈活,可以用在任何你想創建臨時的作用域的地方。不光是塊內。但是,我強烈推薦你花點時間理解javascript的作用域。它很有用,是我最喜歡的javascript特性之一。如果你理解了作用域,那麼變量提升就對你顯得更有意義。

變量聲明,命名,和提升

在javascript,變量有4種基本方式進入作用域:

  • 1 語言內置:所有的作用域裏都有this和arguments;(譯者注:經過測試arguments在全局作用域是不可見的)
  • 2 形式參數:函數的形式參數會作爲函數體作用域的一部分;
  • 3 函數聲明:像這種形式:function foo(){};
  • 4 變量聲明:像這樣:var foo;

函數聲明和變量聲明總是會被解釋器悄悄地被“提升”到方法體的最頂部。這個意思是,像下面的代碼:

[javascript] view plain copy
  1. function foo() {  
  2.     bar();  
  3.     var x = 1;  
  4. }  
實際上會被解釋成:
[javascript] view plain copy
  1. function foo() {  
  2.     var x;  
  3.     bar();  
  4.     x = 1;  
  5. }  
無論定義該變量的塊是否能被執行。下面的兩個函數實際上是一回事:
[javascript] view plain copy
  1. function foo() {  
  2.     if (false) {  
  3.         var x = 1;  
  4.     }  
  5.     return;  
  6.     var y = 1;  
  7. }  
  8. function foo() {  
  9.     var x, y;  
  10.     if (false) {  
  11.         x = 1;  
  12.     }  
  13.     return;  
  14.     y = 1;  
  15. }  
請注意,變量賦值並沒有被提升,只是聲明被提升了。但是,函數的聲明有點不一樣,函數體也會一同被提升。但是請注意,函數的聲明有兩種方式:
[javascript] view plain copy
  1. function test() {  
  2.     foo(); // TypeError "foo is not a function"  
  3.     bar(); // "this will run!"  
  4.     var foo = function () { // 變量指向函數表達式  
  5.         alert("this won't run!");  
  6.     }  
  7.     function bar() { // 函數聲明 函數名爲bar  
  8.         alert("this will run!");  
  9.     }  
  10. }  
  11. test();  
這個例子裏面,只有函數式的聲明纔會連同函數體一起被提升。foo的聲明會被提升,但是它指向的函數體只會在執行的時候才被賦值。

上面的東西涵蓋了提升的一些基本知識,它們看起來也沒有那麼迷惑。但是,在一些特殊場景,還是有一定的複雜度的。

變量解析順序

最需要牢記在心的是變量解析順序。記得我前面給出的命名進入作用域的4種方式嗎?變量解析的順序就是我列出來的順序。一般來說,如果一個名稱已經被定義,則不會被其他相同名稱的屬性覆蓋。這是說,(譯者沒理解這句,所以先做刪除樣式) 函數的聲明比變量的聲明具有高的優先級這並不是說給那個變量賦值不管用,而是聲明不會被忽略了。 (譯者注: 關於函數的聲明比變量的聲明具有高的優先級,下面的程序能幫助你理解)

[javascript] view plain copy
  1. <script>  
  2. function a(){     
  3. }  
  4. var a;  
  5. alert(a);//打印出a的函數體  
  6. </script>  
  7.   
  8. <script>  
  9.   
  10. var a;  
  11. function a(){     
  12. }  
  13. alert(a);//打印出a的函數體  
  14. </script>  
  15. //但是要注意區分和下面兩個寫法的區別:  
  16. <script>  
  17. var a=1;  
  18. function a(){     
  19. }  
  20. alert(a);//打印出1  
  21. </script>  
  22.   
  23. <script>  
  24. function a(){     
  25. }  
  26.   
  27. var a=1;  
  28.   
  29. alert(a);//打印出1  
  30. </script>  

這裏有3個例外:
1 內置的名稱arguments表現得很奇怪,他看起來應該是聲明在函數形式參數之後,但是卻在函數聲明之前。這是說,如果形參裏面有arguments,它會比內置的那個有優先級。這是很不好的特性,所以要杜絕在形參裏面使用arguments;
2 在任何地方定義this變量都會出語法錯誤,這是個好特性;
3 如果多個形式參數擁有相同的名稱,最後的那個具有優先級,即便實際運行的時候它的值是undefined;

命名函數

你可以給一個函數一個名字。如果這樣的話,它就不是一個函數聲明,同時,函數體定義裏面的指定的函數名( 如果有的話,如下面的spam, 譯者注)將不會被提升, 而是被忽略。這裏一些代碼幫助你理解:

[javascript] view plain copy
  1. foo(); // TypeError "foo is not a function"  
  2. bar(); // valid  
  3. baz(); // TypeError "baz is not a function"  
  4. spam(); // ReferenceError "spam is not defined"  
  5.   
  6. var foo = function () {}; // foo指向匿名函數  
  7. function bar() {}; // 函數聲明  
  8. var baz = function spam() {}; // 命名函數,只有baz被提升,spam不會被提升。  
  9.   
  10. foo(); // valid  
  11. bar(); // valid  
  12. baz(); // valid  
  13. spam(); // ReferenceError "spam is not defined"  

怎麼寫代碼

現在你理解了作用域和變量提升,那麼這對於javascript編碼意味着什麼?最重要的一點是,總是用var定義你的變量。而且我強烈推薦,對於一個名稱,在一個作用域裏面永遠只有一次var聲明。如果你這麼做,你就不會遇到作用域和變量提升問題。

語言規範怎麼說

我發現ECMAScript參考文檔總是很有用。下面是我找到的關於作用域和變量提升的部分:
如果變量在函數體類聲明,則它是函數作用域。否則,它是全局作用域(作爲global的屬性)。變量將會在執行進入作用域的時候被創建。塊不會定義新的作用域,只有函數聲明和程序(譯者以爲,就是全局性質的代碼執行)纔會創造新的作用域。變量在創建的時候會被初始化爲undefined。如果變量聲明語句裏面帶有賦值操作,則賦值操作只有被執行到的時候纔會發生,而不是創建的時候。

我期待這篇文章會對那些對javascript比較迷惑的程序員帶來一絲光明。我自己也盡最大的可能去避免帶來更多的迷惑。如果我說錯了什麼,或者忽略了什麼,請告知。

譯者補充

xu281828044提醒了我發現了IE下全局作用域下命名函數的提升問題:
我翻譯文章的時候是這麼測試的:

  1. <script>  
  2. functiont(){  
  3. spam();  
  4. var baz = function spam() {alert('this is spam')};  
  5. }  
  6. t();  
  7. </script>  
這種寫法, 即非全局作用域下的命名函數的提升,在ie和ff下表現是一致的. 我改成:
  1. <script>  
  2. spam();  
  3. var baz = function spam() {alert('this is spam')};  
  4. </script>  
則ie下是可以執行spam的,ff下不可以. 說明不同瀏覽器在處理這個細節上是有差別的.

這個問題還引導我思考了另2個問題,1:對於全局作用於範圍的變量,var與不var是有區別的. 沒有var的寫法,其變量不會被提升。比如下面兩個程序,第二個會報錯:

  1. <script>  
  2. alert(a);  
  3. var a=1;  
  4. </script>  
  1. <script>  
  2. alert(a);  
  3. a=1;  
  4. </script>  

2: eval中創建的局部變量是不會被提升的(它也沒辦法做到).

  1. <script>  
  2. var a = 1;  
  3. function t(){  
  4.     alert(a);  
  5.     eval('var a = 2');  
  6.     alert(a);  
  7. }  
  8. t();  
  9. alert(a);  
  10. </script> 
發佈了45 篇原創文章 · 獲贊 31 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章