我的博客地址:http://hovertree.com/
”JavaScript中的函數運行在它們被定義的作用域裡,而不是它們被執行的作用域裡.” ——權威指南
在JavaScript中,一切皆對象,包括函數。函數對象和其它對象一樣,擁有可以通過代碼訪問的屬性和一系列僅供JavaScript引擎訪問的內部屬性。其中一個內部屬性是[[Scope]],由ECMA-262標准第三版定義,該內部屬性包含了函數被創建的作用域中對象的集合,這個集合被稱為函數的作用域鏈,它決定了哪些數據能被函數訪問。
在一個函數被定義的時候, 會將它定義時刻的scope chain鏈接到這個函數對象的[[scope]]屬性.
在一個函數對象被調用的時候,會創建一個活動對象(也就是一個對象), 該對象包含了函數的所有局部變量、命名參數、參數集合以及this,然後此對象會被推入作用域鏈的前端,當運行期上下文被銷毀,活動對象也隨之銷毀。
在每次調用一個函數的時候 ,就會進入一個函數內的作用域,當從函數返回以後,就返回調用前的作用域.
var sayHello = function(l,s){
var word = "hello world";
}
sayHello();
在執行sayHello定義語句的時候, 會創建一個這個函數對象的[[scope]]屬性。
將這個[[scope]]屬性, 鏈接到定義它的作用域鏈上。此時因為func定義在全局環境, 所以此時的[[scope]]只是指向全局活動對象window active object.
在調用sayHello的時候, 會創建一個活動對象(假設為fObj),並創建arguments屬性。然後會給這個對象添加倆個命名屬性fObj.l, fObj.s; 對於每一個在這個函數中申明的局部變量和函數定義, 都作為該活動對象的同名命名屬性。對於局部變量,變量的值會在真正執行的時候才計算, 此時只是簡單的賦為undefined.
將調用參數賦值給形參,對於缺少的調用參數,賦值為undefined。
將這個活動對象做為scope chain的最前端, 並將func的[[scope]]屬性所指向的,定義sayHello時候的頂級活動對象, 加入到scope chain.
在發生標識符解析的時候, 就會逆向查詢當前scope chain列表的每一個活動對象的屬性,如果找到同名的就返回。找不到,那就是這個標識符沒有被定義。
作用域鏈全過程解析:
function factory() {
var name = 'laruence';
var intro = function(){
alert('I am ' + name);
}
return intro;
}
function app(para){
var name = para;
var func = factory();
func();
}
app('eve');
首先當調用app的時候, scope chain是由: {window活動對象(全局)}->{app的活動對象} 組成。此時的scope chain如下:(對於局部變量,變量的值會在真正執行的時候才計算, 此時只是簡單的賦為undefined.
)
[[scope chain]] = [
{
para : 'eve',
name : undefined,
func : undefined,
arguments : []
}, {
window call object
}
]
當調用進入factory的函數體的時候, 此時的factory的scope chain為:
[[scope chain]] = [
{
name : undefined,
intor : undefined
}, {
window call object
}
]
注意: 此時的作用域鏈中, 並不包含app的活動對象.(JavaScript中的函數運行在它們被定義的作用域裡,而不是它們被執行的作用域裡.)
在定義intro函數的時候, intro函數的[[scope]]為:
[[scope chain]] = [
{
name : 'laruence',
intor : undefined
}, {
window call object
}
]
從factory函數返回以後,在app體內調用intor的時候, 發生了標識符解析, 而此時的sope chain是:
[[scope chain]] = [
{
intro call object
}, {
name : 'laruence',
intor : undefined
}, {
window call object
}
]
所以, name標識符解析的結果(在上面的作用域鏈中一層層往上匹配)應該是factory活動對象中的name屬性, 也就是’laruence’。
標識符解析過程:
該過程從作用域鏈頭部,也就是從活動對象開始搜索,查找同名的標識符,如果找到了就使用這個標識符對應的變量,如果沒找到繼續搜索作用域鏈中的下一個對象,如果搜索完所有對象都未找到,則認為該標識符未定義。函數執行過程中,每個標識符都要經歷這樣的搜索過程。
從作用域鏈的結構可以看出,在運行期上下文的作用域鏈中,標識符所在的位置越深,讀寫速度就會越慢。全局變量總是存在於運行期上下文作用域鏈的最末端,因此在標識符解析的時候,查找全局變量是最慢的。所以,在編寫代碼的時候應盡量少使用全局變量,盡可能使用局部變量。一個好的經驗法則是:如果一個跨作用域的對象被引用了一次以上,則先把它存儲到局部變量裡再使用。例如下面的代碼:
function changeColor(){
var doc=document;
doc.getElementById("btnChange").onclick=function(){
doc.getElementById("targetCanvas").style.backgroundColor="red";
};
}
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();
};
}
}
with 語句的作用是將代碼的作用域設置到一個特定的對象中
當代碼運行到with語句時,運行期上下文的作用域鏈臨時被改變了。一個新的可變對象被創建,它包含了參數指定的對象的所有屬性。這個對象將被推入作用域鏈的頭部,這意味著函數的所有局部變量現在處於第二個作用域鏈對象中,因此訪問代價更高了。
參考文章:Javascript作用域原理、理解 JavaScript 作用域和作用域鏈