標籤:相互 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中範圍和範圍鏈的簡單理解(變數提升)