」JavaScript中的函數運行在它們被定義的做用域裏,而不是它們被執行的做用域裏.」 ——權威指南javascript
在JavaScript中,一切皆對象,包括函數。函數對象和其它對象同樣,擁有能夠經過代碼訪問的屬性和一系列僅供JavaScript引擎訪問的內部屬性。其中一個內部屬性是[[Scope]],由ECMA-262標準第三版定義,該內部屬性包含了函數被建立的做用域中對象的集合,這個集合被稱爲函數的做用域鏈,它決定了哪些數據能被函數訪問。html
在一個函數被定義的時候, 會將它定義時刻的scope chain連接到這個函數對象的[[scope]]屬性.前端
在一個函數對象被調用的時候,會建立一個活動對象(也就是一個對象), 該對象包含了函數的全部局部變量、命名參數、參數集合以及this,而後此對象會被推入做用域鏈的前端,當運行期上下文被銷燬,活動對象也隨之銷燬。java
在每次調用一個函數的時候 ,就會進入一個函數內的做用域,當從函數返回之後,就返回調用前的做用域.git
var sayHello = function(l,s){ var word = "hello world"; } sayHello();
在執行sayHello定義語句的時候, 會建立一個這個函數對象的[[scope]]屬性。github
將這個[[scope]]屬性, 連接到定義它的做用域鏈上。此時由於func定義在全局環境, 因此此時的[[scope]]只是指向全局活動對象window active object.app
在調用sayHello的時候, 會建立一個活動對象(假設爲fObj),並建立arguments屬性。而後會給這個對象添加倆個命名屬性fObj.l, fObj.s; 對於每個在這個函數中申明的局部變量和函數定義, 都做爲該活動對象的同名命名屬性。對於局部變量,變量的值會在真正執行的時候才計算, 此時只是簡單的賦爲undefined.函數
將調用參數賦值給形參,對於缺乏的調用參數,賦值爲undefined。優化
將這個活動對象作爲scope chain的最前端, 並將func的[[scope]]屬性所指向的,定義sayHello時候的頂級活動對象, 加入到scope chain.this
在發生標識符解析的時候, 就會逆向查詢當前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 做用域和做用域鏈
個人博客地址:http://bigdots.github.io、http://www.cnblogs.com/yzg1/