- 前天被問到作用域鏈的知識,感覺有個大概的認識,但是轉化為語言就無法調理清楚地講述出來,回來后決定惡補功課,在此做個筆記。
- 注:筆記有部分內容摘抄自其他技術博客,如覺得有被侵權,我會立馬刪除。
變量對象
- 首先要理解變量對象是什么,代碼在執行時都會經歷先創建再執行的過程,在創建時,就會建立一個變量對象,里面包括arguments、本函數內部聲明的函數、本函數內部聲明的變量、函數形參等。
- 用圖像表示就是:
再舉個例子:
function outerFun (arg1, arg2) {
var outerV1 = 1
var outerV2 = 2
function innerFun1 () {
var innerV1 = 3;
var innerV2 = 4;
console.log('i am innerFun1...')
}
function innerFun2 () {
console.log('i am innerFun2...')
}
function outerV2 () {
return 'i am outerV2'
}
}
outerFun()
Paste_Image.png
活動對象
- 個人覺得就是指的變量對象,只不過變量對象是在創建時候的術語,而活動對象是指在執行狀態的對象。
[[scope]]屬性
[[scope]]是所有外層函數(執行環境)的變量對象的層級鏈。
[[scope]]屬性在函數創建時被存儲,永遠不變,直到函數被銷毀。函數可以不被調用,但該屬性一直存在。
與作用域鏈相比,作用域鏈是活動的執行環境的一個屬性,而[[scope]]是函數的屬性。
舉個例子:
function add(num1,num2) { var sum = num1 + num2; return sum; }
add函數是定義在全局環境里面的,所以在創建時,它的作用域鏈里面的第一個指針會指向一個全局對象,該全局對象包含了所有的全局變量。
作用域鏈
- 剛剛也說了,作用域鏈是執行環境的一個屬性,當函數開始執行時,就要在執行環境中建立作用域鏈,首先通過復制[[scope]]屬性中的對象,來構建起執行環境的初始作用域。然后將當前函數的活動對象(包含本函數內部的變量、this、arguments等對象)等推入作用域鏈的頂端。
- 繼續上面的例子:
- 當開始執行函數
add(5,10)
時,會創建一個稱為“執行上下文(execution context)”的內部對象,也就是add.execution_context,它是屬于add的一個對象。 - 這個對象有一個Scope Chain指針指向作用域鏈,這個作用域鏈根據[[scope]]來進行初始化,然后將自己的活動對象也推入作用域鏈。( 該對象包含了該函數環境中的所有局部變量、函數方法、命名參數以及this,arguments等)
函數每執行一次,它的執行上下文都會被重新創建,到函數運行結束時,又再銷毀。
對于在函數內部作為返回值被返回的閉包函數,在其被返回以后,它的作用域鏈就被初始化了,此時包含外層函數的活動對象、全局變量對象,至于它自己的活動對象,是在調用的時候才建立的。最終它的作用域鏈里面會有3個對象,依次是:自己的活動對象、外層行數的活動對象、全局變量對象。
- 在函數執行過程中,每遇到一個變量,都會經歷一次標識符解析過程以決定從哪里獲取和存儲數據。該過程從作用域鏈頭部,也就是從活動對象開始搜索,查找同名的標識符,如果找到了就使用這個標識符對應的變量,如果沒找到繼續搜索作用域鏈中的下一個對象,如果搜索完所有對象都未找到,則認為該標識符未定義。函數執行過程中,每個標識符都要經歷這樣的搜索過程。
改變作用域鏈
- 函數每次執行時對應的運行期上下文都是獨一無二的,所以多次調用同一個函數就會導致創建多個運行期上下文,當函數執行完畢,執行上下文會被銷毀。每一個運行期上下文都和一個作用域鏈關聯。一般情況下,在運行期上下文運行的過程中,其作用域鏈只會被 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子句中唯一執行的代碼。該函數接收異常對象作為參數,這樣你可以更加靈活和統一的處理錯誤。由于只執行一條語句,且沒有局部變量的訪問,作用域鏈的臨時改變就不會影響代碼性能了。