JavaScript中範圍和範圍鏈的簡單理解(變數提升)

來源:互聯網
上載者:User

標籤:相互   body   基礎   說明   hang   obj   先來   生命週期   apn   

通過閱讀《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); // haha5     console.log(name2);// xixi6 } 7 changName();8 console.log(name1);//haha9 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);//haha12 console.log(tempName);//xixi
13 }14 swapName();15 console.log(name1);//haha16 console.log(name2);//xixi17 //console.log(tempName);拋出錯誤:Uncaught ReferenceError: tempName is not defined18 }19 changName();20 console.log(name1);21 //console.log(name2); 拋出錯誤:Uncaught ReferenceError: name2 is not defined22 //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();

控制台顯示結果如下:


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

JavaScript中範圍和範圍鏈的簡單理解(變數提升)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.