作用域是JavaScript最重要的概念之一,想要學好JavaScript就需要理解JavaScript作用域和作用域鏈的工作原理。今天這篇文章對JavaScript作用域和作用域鏈作簡單的介紹,希望能幫助大家更好的學習JavaScript。
JavaScript作用域
任何程序設計語言都有作用域的概念,簡單的說,作用域就是變量與函數的可訪問范圍,即作用域控制著變量與函數的可見性和生命周期。在JavaScript中,變量的作用域有全局作用域和局部作用域兩種。
1. 全局作用域(Global Scope)
在代碼中任何地方都能訪問到的對象擁有全局作用域,一般來說一下幾種情形擁有全局作用域:
(1)最外層函數和在最外層函數外面定義的變量擁有全局作用域,例如:
(3)所有window對象的屬性擁有全局作用域
一般情況下,window對象的內置屬性都都擁有全局作用域,例如window.name、window.location、window.top等等。
2. 局部作用域(Local Scope)
和全局作用域相反,局部作用域一般只在固定的代碼片段內可訪問到,最常見的例如函數內部,所有在一些地方也會看到有人把這種作用域成為函數作用域,例如下列代碼中的blogName和函數innerSay都只擁有局部作用域。
在JavaScript中,函數也是對象,實際上,JavaScript里一切都是對象。函數對象和其它對象一樣,擁有可以通過代碼訪問的屬性和一系列僅供JavaScript引擎訪問的內部屬性。其中一個內部屬性是[[Scope]],由ECMA-262標準第三版定義,該內部屬性包含了函數被創建的作用域中對象的集合,這個集合被稱為函數的作用域鏈,它決定了哪些數據能被函數訪問。
當一個函數創建后,它的作用域鏈會被創建此函數的作用域中可訪問的數據對象填充。例如定義下面這樣一個函數:
函數add的作用域將會在執行時用到。例如執行如下代碼:
這些值按照它們出現在函數中的順序被復制到運行期上下文的作用域鏈中。它們共同組成了一個新的對象,叫“活動對象(activation object)”,該對象包含了函數的所有局部變量、命名參數、參數集合以及this,然后此對象會被推入作用域鏈的前端,當運行期上下文被銷毀,活動對象也隨之銷毀。新的作用域鏈如下圖所示:
在函數執行過程中,沒遇到一個變量,都會經歷一次標識符解析過程以決定從哪里獲取和存儲數據。該過程從作用域鏈頭部,也就是從活動對象開始搜索,查找同名的標識符,如果找到了就使用這個標識符對應的變量,如果沒找到繼續搜索作用域鏈中的下一個對象,如果搜索完所有對象都未找到,則認為該標識符未定義。函數執行過程中,每個標識符都要經歷這樣的搜索過程。
作用域鏈和代碼優化
從作用域鏈的結構可以看出,在運行期上下文的作用域鏈中,標識符所在的位置越深,讀寫速度就會越慢。如上圖所示,因為全局變量總是存在于運行期上下文作用域鏈的最末端,因此在標識符解析的時候,查找全局變量是最慢的。所以,在編寫代碼的時候應盡量少使用全局變量,盡可能使用局部變量。一個好的經驗法則是:如果一個跨作用域的對象被引用了一次以上,則先把它存儲到局部變量里再使用。例如下面的代碼:
document.getElementById("btnChange").onclick=function(){
document.getElementById("targetCanvas").style.backgroundColor="red";
};
}
var doc=document;
doc.getElementById("btnChange").onclick=function(){
doc.getElementById("targetCanvas").style.backgroundColor="red";
};
}
改變作用域鏈
函數每次執行時對應的運行期上下文都是獨一無二的,所以多次調用同一個函數就會導致創建多個運行期上下文,當函數執行完畢,執行上下文會被銷毀。每一個運行期上下文都和一個作用域鏈關聯。一般情況下,在運行期上下文運行的過程中,其作用域鏈只會被 with 語句和 catch 語句影響。
with語句是對象的快捷應用方式,用來避免書寫重復代碼。例如:
with(document){
var bd=body,
links=getElementsByTagName("a"),
i=0,
len=links.length;
while(i < len){
update(links[i++]);
}
getElementById("btnInit").onclick=function(){
doSomething();
};
}
}
當代碼運行到with語句時,運行期上下文的作用域鏈臨時被改變了。一個新的可變對象被創建,它包含了參數指定的對象的所有屬性。這個對象將被推入作用域鏈的頭部,這意味著函數的所有局部變量現在處于第二個作用域鏈對象中,因此訪問代價更高了。如下圖所示:
因此在程序中應避免使用with語句,在這個例子中,只要簡單的把document存儲在一個局部變量中就可以提升性能。
另外一個會改變作用域鏈的是try-catch語句中的catch語句。當try代碼塊中發生錯誤時,執行過程會跳轉到catch語句,然后把異常對象推入一個可變對象并置于作用域的頭部。在catch代碼塊內部,函數的所有局部變量將會被放在第二個作用域鏈對象中。示例代碼:
doSomething();
}catch(ex){
alert(ex.message); //作用域鏈在此處改變
}
doSomething();
}catch(ex){
handleError(ex); //委托給處理器方法
}
新聞熱點
疑難解答