做用域是JavaScript最重要的概念之一,想要學好JavaScript就須要理解JavaScript做用域和做用域鏈的工做原理。今天這篇文章對JavaScript做用域和做用域鏈做簡單的介紹,但願能幫助你們更好的學習JavaScript。前端
51CTO推薦專題:JavaScript入門函數
JavaScript做用域性能
任何程序設計語言都有做用域的概念,簡單的說,做用域就是變量與函數的可訪問範圍,即做用域控制着變量與函數的可見性和生命週期。在JavaScript中,變量的做用域有全局做用域和局部做用域兩種。學習
1. 全局做用域(Global Scope)優化
在代碼中任何地方都能訪問到的對象擁有全局做用域,通常來講一下幾種情形擁有全局做用域:this
(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在函數外部沒法訪問到。code
(3)全部window對象的屬性擁有全局做用域htm
通常狀況下,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子句中惟一執行的代碼。該函數接收異常對象做爲參數,這樣你能夠更加靈活和統一的處理錯誤。因爲只執行一條語句,且沒有局部變量的訪問,做用域鏈的臨時改變就不會影響代碼性能了。