範圍是JavaScript最重要的概念之一,想要學好JavaScript就需要理解JavaScript範圍和範圍鏈的工作原理。今天這篇文章對JavaScript範圍和範圍鏈作簡單的介紹,希望能協助大家更好的學習JavaScript。
JavaScript範圍
任何程式設計語言都有範圍的概念,簡單的說,範圍就是變數與函數的可存取範圍,即範圍控制著變數與函數的可見度和生命週期。在JavaScript中,變數的範圍有全域範圍和局部範圍兩種。
1. 全域範圍(Global Scope)
在代碼中任何地方都能訪問到的對象擁有全域範圍,一般來說一下幾種情形擁有全域範圍:
(1)最外層函數和在最外層函數外面定義的變數擁有全域範圍,例如: 複製代碼 代碼如下:var authorName="山邊小溪";
function doSomething(){
var blogName="夢想天空";
function innerSay(){
alert(blogName);
}
innerSay();
}
alert(authorName); //山邊小溪
alert(blogName); //指令碼錯誤
doSomething(); //夢想天空
innerSay() //指令碼錯誤
(2)所有末定義直接賦值的變數自動聲明為擁有全域範圍,例如: 複製代碼 代碼如下:function doSomething(){
var authorName="山邊小溪";
blogName="夢想天空";
alert(authorName);
}
alert(blogName); //夢想天空
alert(authorName); //指令碼錯誤
變數blogName擁有全域範圍,而authorName在函數外部無法訪問到。
(3)所有window對象的屬性擁有全域範圍
一般情況下,window對象的內建屬性都都擁有全域範圍,例如window.name、window.location、window.top等等。
1. 局部範圍(Local Scope)
和全域範圍相反,局部範圍一般只在固定的程式碼片段內可訪問到,最常見的例如函數內部,所有在一些地方也會看到有人把這種範圍成為函數範圍,例如下列代碼中的blogName和函數innerSay都只擁有局部範圍。 複製代碼 代碼如下:function doSomething(){
var blogName="夢想天空";
function innerSay(){
alert(blogName);
}
innerSay();
}
alert(blogName); //指令碼錯誤
innerSay(); //指令碼錯誤
範圍鏈(Scope Chain)
在JavaScript中,函數也是對象,實際上,JavaScript裡一切都是對象。函數對象和其它對象一樣,擁有可以通過代碼訪問的屬性和一系列僅供JavaScript引擎訪問的內部屬性。其中一個內部屬性是[[Scope]],由ECMA-262標準第三版定義,該內部屬性包含了函數被建立的範圍中對象的集合,這個集合被稱為函數的範圍鏈,它決定了哪些資料能被函數訪問。
當一個函數建立後,它的範圍鏈會被建立此函數的範圍中可訪問的資料對象填充。例如定義下面這樣一個函數: 複製代碼 代碼如下:function add(num1,num2) {
var sum = num1 + num2;
return sum;
}
在函數add建立時,它的範圍鏈中會填入一個全域對象,該全域對象包含了所有全域變數,如所示(注意:圖片只例舉了全部變數中的一部分):
函數add的範圍將會在執行時用到。例如執行如下代碼:
複製代碼 代碼如下:var total = add(5,10);
執行此函數時會建立一個稱為“運行期上下文(execution context)”的內部對象,運行期上下文定義了函數執行時的環境。每個運行期上下文都有自己的範圍鏈,用於標識符解析,當運行期上下文被建立時,而它的範圍鏈初始化為當前運行函數的[[Scope]]所包含的對象。
這些值按照它們出現在函數中的順序被複製到運行期內容相關的範圍鏈中。它們共同組成了一個新的對象,叫“使用中的物件(activation object)”,該對象包含了函數的所有局部變數、具名引數、參數集合以及this,然後此對象會被推入範圍鏈的前端,當運行期上下文被銷毀,使用中的物件也隨之銷毀。新的範圍鏈如所示:
在函數執行過程中,沒遇到一個變數,都會經曆一次標識符解析過程以決定從哪裡擷取和儲存資料。該過程從範圍鏈頭部,也就是從使用中的物件開始搜尋,尋找同名的標識符,如果找到了就使用這個標識符對應的變數,如果沒找到繼續搜尋範圍鏈中的下一個對象,如果搜尋完所有對象都未找到,則認為該標識符未定義。函數執行過程中,每個標識符都要經曆這樣的搜尋過程。
範圍鏈和代碼最佳化
從範圍鏈的結構可以看出,在運行期內容相關的範圍鏈中,標識符所在的位置越深,讀寫速度就會越慢。如所示,因為全域變數總是存在於運行期上下文範圍鏈的最末端,因此在標識符解析的時候,尋找全域變數是最慢的。所以,在編寫代碼的時候應盡量少使用全域變數,儘可能使用局部變數。一個好的經驗法則是:如果一個跨範圍的對象被引用了一次以上,則先把它儲存到局部變數裡再使用。例如下面的代碼: 複製代碼 代碼如下:function changeColor(){
document.getElementById("btnChange").onclick=function(){
document.getElementById("targetCanvas").style.backgroundColor="red";
};
}
這個函數引用了兩次全域變數document,尋找該變數必須遍曆整個範圍鏈,直到最後在全域對象中才能找到。這段代碼可以重寫如下: 複製代碼 代碼如下:function changeColor(){
var doc=document;
doc.getElementById("btnChange").onclick=function(){
doc.getElementById("targetCanvas").style.backgroundColor="red";
};
}
這段代碼比較簡單,重寫後不會顯示出巨大的效能提升,但是如果程式中有大量的全域變數被從反覆訪問,那麼重寫後的代碼效能會有顯著改善。
改變範圍鏈
函數每次執行時對應的運行期上下文都是獨一無二的,所以多次調用同一個函數就會導致建立多個運行期上下文,當函數執行完畢,執行內容會被銷毀。每一個運行期上下文都和一個範圍鏈關聯。一般情況下,在運行期上下文啟動並執行過程中,其範圍鏈只會被 with 語句和 catch 語句影響。
with語句是對象的快捷應用方式,用來避免書寫重複代碼。例如: 複製代碼 代碼如下:function initUI(){
with(document){
var bd=body,
links=getElementsByTagName("a"),
i=0,
len=links.length;
while(i < len){
update(links[i++]);
}
getElementById("btnInit").onclick=function(){
doSomething();
};
}
}
這裡使用width語句來避免多次書寫document,看上去更高效,實際上產生了效能問題。
當代碼運行到with語句時,運行期內容相關的範圍鏈臨時被改變了。一個新的可變對象被建立,它包含了參數指定的對象的所有屬性。這個對象將被推入範圍鏈的頭部,這意味著函數的所有局部變數現在處於第二個範圍鏈對象中,因此訪問代價更高了。如所示:
因此在程式中應避免使用with語句,在這個例子中,只要簡單的把document儲存在一個局部變數中就可以提升效能。
另外一個會改變範圍鏈的是try-catch語句中的catch語句。當try代碼塊中發生錯誤時,執行過程會跳轉到catch語句,然後把異常對象推入一個可變對象共置於範圍的頭部。在catch代碼塊內部,函數的所有局部變數將會被放在第二個範圍鏈對象中。範例程式碼: 複製代碼 代碼如下:try{
doSomething();
}catch(ex){
alert(ex.message); //範圍鏈在此處改變
}
請注意,一旦catch語句執行完畢,範圍鏈機會返回到之前的狀態。try-catch語句在代碼調試和異常處理中非常有用,因此不建議完全避免。你可以通過最佳化代碼來減少catch語句對效能的影響。一個很好的模式是將錯誤委託給一個函數處理,例如: 複製代碼 代碼如下:try{
doSomething();
}catch(ex){
handleError(ex); //委託給處理器方法
}
最佳化後的代碼,handleError方法是catch子句中唯一執行的代碼。該函數接收異常對象作為參數,這樣你可以更加靈活和統一的處理錯誤。由於只執行一條語句,且沒有局部變數的訪問,範圍鏈的臨時改變就不會影響代碼效能了。