JavaScript中作用域和作用域鏈的簡單理解(變量提升)


通過閱讀《JS高級程序設計》這本書,對js中的作用域和作用域鏈知識有了初步的了解和認識,准備成筆記供大家參考,筆記中字數比較多,但個人認為敘述的挺詳細的,所以希望讀者耐心看。再者,本人了解的比較基礎,不足的地方希望大家一起交流,共同學習。

1.執行環境(execution context)

  執行環境定義了變量和函數有權訪問的其他數據,決定了他們各自的行為。每個執行環境都有與之對應的變量對象(variable object),保存着該環境中定義的所有變量和函數。我們無法通過代碼來訪問變量對象,但是解析器在處理數據時會在后台使用到它。

  執行環境有全局執行環境(也稱全局環境)和函數執行環境之分。執行環境如其名是在運行和執行代碼的時候才存在的,所以我們運行瀏覽器的時候會創建全局的執行環境,在調用函數時,會創建函數執行環境

1.1 全局執行環境

  全局執行環境是最外圍的一個執行環境,在web瀏覽器中,我們可以認為他是window對象,因此所有的全局變量和函數都是作為window對象的屬性和方法創建的。代碼載入瀏覽器時,全局環境被創建,關閉網頁或者關閉瀏覽時全局環境被銷毀。

1.2 函數執行環境

  每個函數都有自己的執行環境,當執行流進入一個函數時,函數的環境就被推入一個環境棧中,當函數執行完畢后,棧將其環境彈出,把控制權返回給之前的執行環境。

2 作用域、作用域鏈

2.1 全局作用域(globe scope)和局部作用域(local scope)

  全局作用域可以在代碼中的任何地方都能被訪問,例如:

1  var name1="haha";
2 function changName(){
3     var name2="xixi";
4     console.log(name1); // haha
5     console.log(name2);// xixi
6 } 
7 changName();
8 console.log(name1);//haha
9 console.log(name2);//Uncaught ReferenceError: name2 is not defined

  其中,name1具有全局作用域,因此在第4行和第8行都會在控制台上輸出 haha。name2定義在changName()函數內部,具有局部作用域,因此在第9行,解析器找不到變量name2,拋出錯誤。
  另外,在函數中聲明變量時,如果省略 var 操作符,那么聲明的變量就是全局變量,擁有全局作用域,但是不推薦這種做法,因為在局部作用域中很難維護定義的全局變量。

  再者,window對象的內置屬性都擁有全局作用域。

   局部作用域一般只在固定的代碼片段內可以訪問得到,例如上述代碼中的name2,只有在函數內部可以訪問得到。

 2.2 作用域鏈(scope chain)

  全局作用域和局部作用域中變量的訪問權限,其實是由作用域鏈決定的。

  每次進入一個新的執行環境,都會創建一個用於搜索變量和函數的作用域鏈。作用域鏈是函數被創建的作用域中對象的集合。作用域鏈可以保證對執行環境有權訪問的所有變量和函數的有序訪問。

  作用域鏈的最前端始終是當前執行的代碼所在環境的變量對象(如果該環境是函數,則將其活動對象作為變量對象),下一個變量對象來自包含環境(包含當前還行環境的環境),下一個變量對象來自包含環境的包含環境,依次往上,直到全局執行環境的變量對象。全局執行環境的變量對象始終是作用域鏈中的最后一個對象。

  標識符解析是沿着作用域一級一級的向上搜索標識符的過程。搜索過程始終是從作用域的前端逐地向后回溯,直到找到標識符(找不到,就會導致錯誤發生)。

例如:

 1 var name1 = "haha";
 2 function changeName(){
 3     var name2="xixi";
 4     function swapName(){
 5         console.log(name1);//haha
 6         console.log(name2);//xixi
 7         var tempName=name2;
 8         name2=name1;
 9         name1=tempName;
10         console.log(name1);//xixi
11 console.log(name2);//haha 12 console.log(tempName);//xixi
13 } 14 swapName(); 15 console.log(name1);//haha 16 console.log(name2);//xixi 17 //console.log(tempName);拋出錯誤:Uncaught ReferenceError: tempName is not defined 18 } 19 changName(); 20 console.log(name1); 21 //console.log(name2); 拋出錯誤:Uncaught ReferenceError: name2 is not defined 22 //console.log(tempName);拋出錯誤:Uncaught ReferenceError: tempName is not defined

 運行結果如下:

上述代碼中,一共有三個執行環境:全局環境、changeName()的局部環境和 swapName() 的局部環境。所以,

 1.函數 swapName()的作用域鏈包含三個對象:自己的變量對象----->changeName()局部環境的變量對象 ----->全局環境的變量對象

 2.函數changeName()的作用域包含兩個對象:自己的變量對象----->全局環境的變量對象。

就上述程序中出現的變量和函數來講(不考慮隱形變量):

 1.swapName() 局部環境的變量對象中存放變量 tempName;

 2.changeName() 局部環境的變量對象中存放變量 name2 和 函數swapName();

 3.全局環境的變量對象中存放變量 name1 、函數changeName();

 

  在swapName()的執行環境中,在執行第5句代碼時,解析器沿着函數 swapName()的作用域鏈一級級向后回溯查找變量 name1,直到在全局環境中找到變量 name1.並輸出在控制台上。同樣,在執行第6句代碼時,解析器沿着函數 swapName()的作用域鏈一級級向后回溯,在函數changeName()的變量對象中發現變量 name2.通過代碼對 name1 和 name2進行交換,並輸出在控制台上,根據結果我們發現,這兩個變量的值確實交換了。因此我們可以得出結論,函數的局部環境可以訪問函數作用域中的變量,也可以訪問和操作父環境(包含環境)乃至全局環境中的變量。

  在changeName() 的執行環境中,執行第15行和第16行代碼時,可以正確地輸出  name1 和 name2 和兩個變量的值(調用了函數swapName(),所以倆變量的值已相互交換),那是因為 name1 在changName()的父環境(全局環境)中, name2 在他自己的局部環境中,即 name1 和 name2 都在其作用域鏈上。但當執行第17行代碼是發生錯誤 tempName is not defined。因為解析器沿着 函數changeName()的作用域鏈一級級的查找 變量 tempName時,並不能找到該變量的存在(變量 tempName不在其作用域鏈上),所以拋出錯誤。因此,我們可以得出結論:父環境只能訪問其包含環境和自己環境中的變量和函數,不能訪問其子環境中的變量和函數。

  同理,在全局環境中,其變量對象中只存放變量 name1 、函數changeName();  解析器只能訪問變量 name1 和函數 changeName(), 而不能訪問和操作 函數 changeName() 和函數 swapName() 中定義的變量或者函數。因此,在執行第21行和第22行代碼時拋出變量沒有定義的錯誤。所以說,全局環境只能訪問全局環境中的變量和函數,不能直接訪問局部環境中的任何數據。

  其實,我們可以把作用域鏈想象成這樣(里面的能訪問外面的,外面的不能訪問里面的,圖為參考): 

  

作用域鏈相關知識的總結:

1.執行環境決定了變量的生命周期,以及哪部分代碼可以訪問其中變量

2,執行環境有全局執行環境(全局環境)和局部執行環境之分。

3.每次進入一個新的執行環境,都會創建一個用於搜索變量和函數的作用域鏈

4.函數的局部環境可以訪問函數作用域中的變量和函數,也可以訪問其父環境,乃至全局環境中的變量和環境。

5.全局環境只能訪問全局環境中定義的變量和函數,不能直接訪問局部環境中的任何數據。

6.變量的執行環境有助於確定應該合適釋放內存。

 

3.提升(hoisting)

提升有變量提升和函數提升之分,下面我們依次介紹他們。

3.1 變量提升(variable hoisting)

請看一下代碼:

1  var name="haha";
2  function changeName(){
3      console.log(name);
4      var name="xixi";
5  }
6  changeName();
7  console.log(name);

大家認為第6行和第7行代碼輸出的結果應該是什么?好了,答案是:輸出結果結果分別是 undefined  haha。為什么是undefined? 按照作用域鏈的思路思考,輸出的結果應該是 haha或者xixi啊? (當然大家都知道 xixi是不可能的,因為解析器在解析第3行代碼時,還不知道第4行中的賦值內容)。

  那我們先來分析一下代碼 函數changeName() 的作用域鏈: 自己的變量對象 -----> 全局變量對象。解析器在函數執行環境中發現變量 name,因此不會再向全局環境的變量對象中尋找。但是大家要注意的是,解析器在解析第3句代碼時,還不知道變量name的值,也就是說只知道有變量name,但是不知道它具體的值(因為還沒有執行第4句代碼),因此輸出是 undefined,第7行輸出haha大家應該都理解把(作用域問題)。所以上述代碼可以寫成下面的形式:

1   var name="haha"; 2 function changeName(){ 3 var name; 4  console.log(name); 5 name="xixi"; 6  } 7  changeName(); 8 console.log(name);

這個現象就是變量提升

  變量提升,就是把變量提升到函數的頂部,需要注意的是,變量提升只是提升變量的聲明,不會吧變量的值也提升上來!見上述代碼,最常見的代碼如下,函數example1()和函數example2()是等價的:

 1 function example1(){
 2     var a="haha";
 3     var b="xixi";
 4     var c="heihei";
 5 }
 6 
 7 
 8 function example2(){
 9     var a,b,c;
10     a="haha";
11     b="xixi";
12     c="heihei";
13 }


3.2 函數提升()

  函數提升就是把函數提升到前面。

  在JavaScript中函數的創建方式有三種:函數聲明(靜態的,像函數example1()的形式)、函數表達式(函數字面量)、函數構造法(動態的,匿名的)。函數表達式的形式如下:

1 var func1 = function(n1,n2){
2     //function body;
3 };

函數構造法構造函數的形式如下:

 var func2 = new Function("para1","para2",...,"function body");    

在這里需要說明的是:只有函數聲明形式才能被提升!例如:

//函數聲明
function myTest1(){ 
    func(); 
    function func(){ 
        console.log("我可以被提升"); 
    } 
} 
myTest1();

//函數表達式
function myTest2(){ 
    func(); 
    var func = function(){ 
        console.log("我不能被提升"); 
    } 
} 
myTest2();

控制台顯示結果如下:


以上就是該文章所講述的東西,歡迎大家批評指正!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM