摘要:每一個運行期上下文都和一個作用域鏈關聯。這個對象將被推入作用域鏈的頭部,這意味著函數的所有局部變量現在處于第二個作用域鏈對象中,因此訪問代價更高了。在代碼塊內部,函數的所有局部變量將會被放在第二個作用域鏈對象中。
參考:
Javascript作用域原理
理解 JavaScript 作用域和作用域鏈
JavaScript 作用域作用域就是變量與函數的可訪問范圍,即作用域控制著變量與函數的可見性和生命周期。
在JavaScript中,變量的作用域有 全局作用域和 局部作用域兩種。
全局作用域(Global Scope)在 代碼中任何地方都能訪問到的對象擁有全局作用域,一般來說以下幾種情形擁有全局作用域:
(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); } doSomething(); //山邊小溪 alert(blogName); //夢想天空 alert(authorName); //腳本錯誤
變量blogName擁有全局作用域,而authorName在函數外部無法訪問到。
局部作用域(Local Scope)和全局作用域相反,局部作用域一般只在固定的代碼片段內可訪問到,最常見的例如函數內部,所有在一些地方也會看到有人把這種作用域稱為 函數作用域,例如下列代碼中的blogName和函數innerSay都只擁有局部作用域。
function doSomething(){ var blogName="夢想天空"; function innerSay(){ alert(blogName); } innerSay(); } alert(blogName); //腳本錯誤 innerSay(); //腳本錯誤JavaScript 的作用域鏈(Scope Chain) [[scope]] 屬性
函數對象其中一個內部屬性是[[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,然后此對象會被推入作用域鏈的前端,當運行期上下文被銷毀,活動對象也隨之銷毀。
新的作用域鏈如下圖所示:
在函數執行過程中,每遇到一個變量,都會經歷一次標識符解析過程以決定從哪里獲取和存儲數據。
該過程從作用域鏈頭部,也就是從活動對象開始搜索,查找同名的標識符,如果找到了就使用這個標識符對應的變量,如果沒找到繼續搜索作用域鏈中的下一個對象;
如果搜索完所有對象都未找到,則認為該標識符未定義。
函數執行過程中,每個標識符都要經歷這樣的搜索過程。
函數運行在它們被定義的作用域里JS權威指南 中有一句很精辟的描述:
JavaScript中的函數運行在它們被定義的作用域里,而不是它們被執行的作用域里.
在JS中,作用域的概念和其他語言差不多, 在每次調用一個函數的時候 ,就會進入一個函數內的作用域,當從函數返回以后,就返回調用前的作用域.
JS的作用域的實現具體過程如下(ECMA262中所述):
任何執行上下文時刻的作用域, 都是由作用域鏈(scope chain, 后面介紹)來實現.
在一個函數被定義的時候, 會將它定義時刻的scope chain鏈接到這個函數對象的[[scope]]屬性.
在一個函數對象被調用的時候,會創建一個活動對象(也就是一個對象), 然后對于每一個函數的形參,都命名為該活動對象的命名屬性, 然后將這個活動對象做為此時的作用域鏈(scope chain)最前端, 并將這個函數對象的[[scope]]加入到scope chain中.
看個例子:
函數對象的[[scope]]屬性是在定義一個函數的時候決定的, 而非調用的時候, 所以如下面的例子:
var name = "laruence"; function echo() { alert(name); } function env() { var name = "eve"; echo();markdown previewmarkdown previewmarkdown previewmarkdown preview } env(); // 運行結果是: laruence
結合上面的知識, 我們來看看下面這個例子:
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的活動對象} 組成.
在剛進入app函數體時, app的活動對象有一個arguments屬性, 倆個值為undefined的屬性: name和func. 和一個值為’eve’的屬性para;
此時的scope chain如下:
[[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的活動對象.
在定義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 } ]
因為scope chain中,并不包含factory活動對象. 所以, name標識符解析的結果應該是factory活動對象中的name屬性, 也就是’laruence’.
所以運行結果是:
I am laruence作用域鏈和代碼優化
從作用域鏈的結構可以看出,在運行期上下文的作用域鏈中,標識符所在的位置越深,讀寫速度就會越慢。
全局變量總是存在于運行期上下文作用域鏈的最末端,因此在標識符解析的時候,查找全局變量是最慢的。
所以,在編寫代碼的時候應盡量少使用全局變量,盡可能使用局部變量。
一個好的經驗法則是:如果一個跨作用域的對象被引用了一次以上,則先把它存儲到局部變量里再使用。
例如下面的代碼:
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 語句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(); }; } }
這里使用with語句來避免多次書寫document,看上去更高效,實際上產生了性能問題。
當代碼運行到with語句時,運行期上下文的作用域鏈臨時被改變了。
一個新的可變對象被創建,它包含了參數指定的對象的所有屬性。
這個對象將被推入作用域鏈的頭部,這意味著函數的所有局部變量現在處于第二個作用域鏈對象中,因此訪問代價更高了。
如下圖所示:
因此在程序中應避免使用with語句,在這個例子中,只要簡單的把document存儲在一個局部變量中就可以提升性能。
catch語句另外一個會改變作用域鏈的是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子句中唯一執行的代碼。
該函數接收異常對象作為參數,這樣你可以更加靈活和統一的處理錯誤。
由于只執行一條語句,且沒有局部變量的訪問,作用域鏈的臨時改變就不會影響代碼性能了。
Javascript 的預編譯在JS中, 是有預編譯的過程的, JS在執行每一段JS代碼之前, 都會首先處理var關鍵字和function定義式(函數定義式和函數表達式).
如上文所說, 在調用函數執行之前, 會首先創建一個活動對象, 然后搜尋這個函數中的局部變量定義,和函數定義, 將變量名和函數名都做為這個活動對象的同名屬性, 對于局部變量定義,變量的值會在真正執行的時候才計算, 此時只是簡單的賦為undefined.
而對于函數的定義,是一個要注意的地方:
這就是函數定義式和函數表達式的不同, 對于函數定義式, 會將函數定義提前. 而函數表達式, 會在執行過程中才計算.
var name = "laruence"; age = 26;
我們都知道不使用var關鍵字定義的變量, 相當于是全局變量, 聯系到我們剛才的知識:
在對age做標識符解析的時候, 因為是寫操作, 所以當找到到全局的window活動對象的時候都沒有找到這個標識符的時候, 會在window活動對象的基礎上, 返回一個值為undefined的age屬性.
現在, 也許你注意到了我剛才說的: JS在執行每一段JS代碼.
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/78873.html
摘要:關于作用域實現的描述任何執行上下文時刻的作用域,都是由作用域鏈來實現的。在一個函數被定義的時候,會將它此時的作用域鏈鏈接到這個函數對象的屬性。參考資料鳥哥作用域原理理解作用域和作用域鏈阮一峰老師微博上的關于作用域的一道題 javascript作用域原理學習 在每次調用一個函數的時候,就會進入一個函數內的作用域,當從函數返回 以后,就會返回調用前的作用域。 ECMA262關于作...
摘要:函數的作用域也可被分為全局作用域和局部作用域函數作用域被定義在指定函數內部的函數被稱為局部函數或內部函數。局部變量在函數內部聲明的變量被成為局部變量,它只能在函數的內部進行訪問。 作用域 概述 變量和函數都具有作用域 作用域就是變量和函數的可被訪問的范圍 控制著變量和函數的可見性和生命周期。變量的作用域可被分為全局作用域和局部作用域(函數作用域) 如果變量是被定義在全局作用域的話 在J...
摘要:閉包是怎么通過作用域鏈霸占更多內存的本文是作者學習高級程序設計第一小節的一點個人理解,詳細教程請參考原教材。函數執行過程創建了一個函數的活動對象,作用域鏈的最前端指向這個對象。函數執行完畢返回值后執行環境作用域鏈和活動對象一并銷毀。 JavaScript 閉包是怎么通過作用域鏈霸占更多內存的? 本文是作者學習《JavaScript 高級程序設計》7.2第一小節的一點個人理解,詳細教程請...
摘要:一前言這個周末,注意力都在學習基礎知識上面,剛好看到了閉包這個神圣的東西,所以打算把這兩天學到的總結下來,算是鞏固自己所學。因此要注意閉包的使用,否則會導致性能問題。五總結閉包的作用能夠讀取其他函數內部變量。 # 一、前言 這個周末,注意力都在學習基礎Js知識上面,剛好看到了閉包這個神圣的東西,所以打算把這兩天學到的總結下來,算是鞏固自己所學。也可能有些不正確的地方,也請大家看到了,麻...
摘要:全局執行環境的變量對象始終是作用域鏈中的最后一個變量對象。綜上,每個函數對應一個執行環境,每個執行環境對應一個變量對象,而多個變量對象構成了作用域鏈,如果當前執行環境是函數,那么其活動對象在作用域鏈的前端。 1.幾個概念 先說幾個概念:函數、執行環境、變量對象、作用域鏈、活動對象。這幾個東東之間有什么關系呢,往下看~ 函數 函數大家都知道,我想說的是,js中,在函數內部有兩個特殊...
摘要:我們再來看一下第一段代碼小紅小黑腳本出錯腳本出錯在這段代碼中變量與函數,都擁有局部作用域。作用域鏈的最前端,始終都是當前執行代碼所在的作用域的變量對象。 個人博客原址 無論什么語言中,作用域都是一個十分重要的概念,在JavaScript中也不例外,作用域定義了變量或者函數有權訪問的范圍,決定了它們各自的行為。要理解JavaScript中的作用域首先就要知道:在let出現之前,JS中變...
閱讀 1368·2021-09-13 10:25
閱讀 552·2019-08-30 15:53
閱讀 2265·2019-08-30 15:44
閱讀 2026·2019-08-29 17:20
閱讀 1594·2019-08-29 16:36
閱讀 1795·2019-08-29 14:10
閱讀 1785·2019-08-29 12:44
閱讀 1166·2019-08-23 14:13