JavaScript 做用域學習筆記

參考:javascript

Javascript做用域原理html

理解 JavaScript 做用域和做用域鏈前端

JavaScript 做用域

做用域就是變量與函數的可訪問範圍,即做用域控制着變量與函數的可見性和生命週期。java

在JavaScript中,變量的做用域有 全局做用域局部做用域兩種。markdown

全局做用域(Global Scope)

代碼中任何地方都能訪問到的對象擁有全局做用域,通常來講如下幾種情形擁有全局做用域:app

(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都只擁有局部做用域。this

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的屬性: namefunc. 和一個值爲’eve’的屬性para;

此時的scope chain以下:

[[scope chain]] = [
{
     para : 'eve',
     name : undefined,
     func : undefined,
     arguments : []
}, {
     window call object
}
]

當調用進入factory的函數體的時候, 此時的factoryscope 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活動對象的基礎上, 返回一個值爲undefinedage屬性.

如今, 也許你注意到了我剛纔說的: JS在執行每一段JS代碼.

<script>
     alert(typeof eve); //結果:undefined
</script>
<script>
     function eve() {
          alert('I am Laruence');
     }
</script>
相關文章
相關標籤/搜索