作用域,作用域鏈,活動對象,執行上下文,靜態作用域等

執行上下文(execution context)

   爲簡單起見,有時也稱爲環境。它定義了變量或函數有權訪問的其他數據,決定了它們各自的行爲。
以下是一個執行上下文的創建過程:

建立階段(發生在當調用一個函數時,但是在執行函數體內的具體代碼以前)
   建立變量,函數,arguments對象,參數
   建立作用域鏈
   確定this的值
代碼執行階段:
   變量賦值,函數引用,執行其它代碼

——–可暫時不讀——–
http://blog.csdn.net/liujie19901217/article/details/52225025 中詳細提及了作用域和執行上下文的區別。

函數的每次調用都有與之緊密相關的作用域和執行環境。從根本上來說,作用域是基於函數的,而執行環境是基於對象的(例如:全局執行環境即window對象)。
換句話說,作用域涉及到所被調用函數中的變量訪問,並且不同的調用場景是不一樣的。執行環境始終是this關鍵字的值,它是擁有當前所執行代碼的對象的引用。每個執行環境都有一個與之關聯的變量對象,環境中定義的所有變量和函數都保存在這個對象中。雖然我們編寫的代碼無法訪問這個對象,但解析器在處理數據時會在後臺使用它。

http://web.jobbole.com/83031/
這篇文章中寫了很多執行上下文到底是如何運作的。

它就像一個容器,用來存儲當前上下文中所有已定義或可獲取的變量、函數等。位於最頂端或最外層的上下文稱爲全局上下文(global context),全局上下文取決於執行環境,如Node中的global和Browser中的window

Js本身是單線程的,每當有function被執行時,就會產生一個新的上下文,這一上下文會被壓入Js的上下文堆棧(context stack)中,function執行結束後則被彈出,因此Js解釋器總是在棧頂上下文中執行。在生成新的上下文時,首先會綁定該上下文的變量對象,其中包括arguments和該函數中定義的變量;之後會創建屬於該上下文的作用域鏈(scope chain),最後將this賦予這一function所屬的Object

就是說執行上下文可以說就是一個裝了已定義或可獲取的變量、函數等等的對象,查了一下執行上下文的建立過程,上面一段說在函數執行之時產生,其實它在函數建立之時就產生了,詳見下文:
http://blog.csdn.net/hi_kevin/article/details/37761919

—可暫時不讀—-

建立階段的第一步,便是建立了一個叫variableObject對象

variableObject對象

活動對象

建立variableObject對象:
建立arguments對象,檢查當前上下文中的參數,建立該對象下的屬性以及屬性值
檢查當前上下文中的函數聲明:
每找到一個函數聲明,就在variableObject下面用函數名建立一個屬性,屬性值就是指向該函數在內存中的地址的一個引用
如果上述函數名已經存在於variableObject下,那麼對應的屬性值會被新的引用所覆蓋。

也就是說這個活動對象就是當前最近的這個被調用的函數或者域中的能訪問的對象和方法。

那這個建立對象過程是如何的呢?
先了解一下編譯和作用域:

編譯

比如一個函數test(),

function test() {
 var a = 0;
 alert(a);
}
test();

在js中,調用它但是還沒有執行時會發生以下過程。

編譯:語法、詞法解析等,確定作用域(非作用域鏈)

該部分主要參閱:https://segmentfault.com/a/1190000007991284

  • a)詞法分析
    類似於人在讀一篇文言文,此時需要先斷句,有時候斷句不同意思也就大不相同。var a = 0; 拆分成 var、a、=、0;

  • b)語法分析

    這個過程是將詞法單元流(數組)轉換成一個由元素逐級嵌套所組成的代表了程序語法結構的樹。這個樹被稱爲“抽象語法樹”(Abstract Syntax Tree,AST)。

  • c)詞法解析,就是將上面語法分析得到的AST轉換成可執行代碼。總而言之可以將上面的var a = 0;轉換成一個機器指令,創建一個變量a ,爲它分配內存,再將數字0存放在裏面。

那以上這些步驟在js中,都是誰來負責完成的呢?

  • 引擎
    從頭到尾負責整個 javascript 程序的編譯及執行過程。

    簡單地說,JavaScript解析引擎就是能夠“讀懂”JavaScript代碼,並準確地給出代碼運行結果的一段程序。

  • 編譯器
    負責語法分析及代碼生成。

接着,函數還沒有被執行,還是在函數執行之前,此時需要明白作用域是什麼,作用域和引擎,編譯器都是“同事”。

作用域(scope):

首先,在js中,一切皆爲對象。
作用域在《你不知道的javascript(上)》被定義爲

負責收集並維護由所有聲明的標識符(變量)組成的一系列查詢,並實施一套非常嚴格的規則,確定當前執行的代碼對這些標識符的訪問權限。

而在《javascript權威指南》第6版中,對於變量:

一個變量的作用域,是程序源代碼中定義該變量的區域

作用域規定了對於變量或是方法的訪問權限的代碼空間。
而它的英語,scope,作爲名詞意思是機會;範圍;眼界;我理解的通俗意義上說就是它是一個對變量或者是方法被訪問的區域空間的大小的描述,控制數據被使用的界限,就是說變量和方法能夠起作用的空間範圍

比如:

     function test() {
        var a = 3;
     //其他代碼 
    }
    console.log(a);

這個時候,在函數test中定義的a,在函數外部就不能被輸出,因爲a變量的作用域的控制之下,外部沒有訪問這個a的能力。而發生的錯誤是ReferenceError,這個錯誤翻譯過來也就是引用錯誤。
因爲作用域,a這個裝值的容器沒有定義就拿過來使用了,我理解的是:就像是你的作文裏面提到了《銀粉世家》說是張恨水先生的經典作品,但其實人家並沒有寫過,你就把它寫在了你的文章裏,此時也就是引用錯誤ReferenceError。

而這樣的一個區域的範圍大小也就是作用域的“大小”,上文提及它是取決於定義該變量或方法的區域。
作用域又分爲全局作用域以及局部作用域。

全局作用域(Global Scope)

全局作用域:顧名思義,全局就是整個範圍,整個空間都能訪問到具有全局作用域的變量和方法。也就是在js代碼內,任何的地方使用這個方法或是變量都是有定義的。

     var a = 1;
    function test() {
        console.log(a);
    }
    test();//1

在函數外面定義了一個變量a,函數內去輸出它,運行函數會發現,這次可以在函數中訪問到a了。
此時的a就是具有全局作用域,變量a的值定義在了最外層的函數。
此時的a也同樣作爲屬性被添加到由瀏覽器內置的對象BOM這個接口提供的window對象上。

如下結果,和上面的片段輸出都是1。

var a = 1;
    function test() {
        console.log(window.a);
    }
    test();//1

所有的全局變量和函數,在瀏覽器中,都是作爲window對象的屬性和方法創建的。寫在最外層的變量和方法,就是全局的。還有不加’var’聲明的變量。

局部作用域 (Local Scope)

局部作用域:局部就是部分範圍,空間才能訪問到,這樣的變量和方法是有着局部作用域的
就像最開始的那個定義在函數中的a,在外部就沒有辦法讀取並輸出它,因爲它的生存範圍在函數test中。
似乎能感覺出來函數好像有個”阻擋外界的屏障 ”,這就是因爲函數也有作用域,稱爲函數作用域,也是局部作用域的一種。
在js的ES6之前,js是沒有塊級作用域的,就是像if這些有花括號的語句就被分爲一個塊,裏面聲明的變量外面訪問不到。而只有函數作用域,變量在聲明它們的函數體以及這個聲明的函數體內嵌套的其他函數體內都是有定義的。
如下:這樣的if就沒辦法像函數一樣拒絕外部對其變量的訪問。

        var a = 1;
        if(a===1) {
            var b = 0;
        }
        console.log(b);//0

作用域,引擎,編譯器相互的作用

*明白了作用域是什麼,那麼作用域是怎麼和引擎它們配合的呢?
比如var a = 0;
  首先第一步,先準備好容器 var a ;編譯器先問作用域,有沒有一個變量名字爲a已經存在在你那邊,如果有,編譯器就繼續編譯,如果沒有,它就讓作用域添加一個,此時a的默認初始是undefined,編譯器執行的查詢是叫RHS。
  第二步,有了容器之後,a=0的操作。編譯器會準備好引擎工作時候的代碼,然後引擎來問作用域,有沒有一個a的變量啊。如果有,引擎就直接使用這個已經存在的變量,給它賦值0,如果沒有就會向這個作用域的父級去找,一直到全局,還是找不到,此時如果不是嚴格模式,引擎就在全局給它加一個變量a,賦值爲0,嚴格模式的話,就會報錯了,而錯誤類型就是上文提過的ReferenceError,此時引擎進行的查詢叫LHS。*

總結來說,變量的賦值會執行兩個操作,首先編譯器會在當前作用域聲明一個變量(如果之前沒有聲明過),然後在運行時引擎會在當前作用域中查找該變量(找不到就向上一級作用域查找),如果能夠找到就會對它賦值。

對變量進行賦值所執行的查詢叫 LHS(就像是表達式左側),試圖找到容器本身。
找到並使用變量值所執行的查詢叫 RHS(就像是表達式右側),試圖找到容器的值。
如果在作用域的操作中沒有找到容器,會報錯,ReferenceError,而如果找到了但是執行的操作不合理無效會報錯,TypeError, 對象用來表示值的類型非預期類型時發生的錯誤。

變量提升與預編譯

在上面提到創建活動對象的時候,會需要檢查當前上下文的參數,找到函數聲明等,這兩個過程就是在進行預編譯。
js的代碼並不是像我們人眼讀的時候一樣一行一行往下去第一次讀取並執行。
比如下面:

 function test() {
        alert(a);
    }
    test();

此時會發生錯誤,也就是上文提到過的ReferenceError。

 function test() {
        alert(a);
        var a = 0;
    }
    test();//undefined

此時未定義,大家都知道,js在給聲明瞭的變量,函數,但是沒有賦初始值的時候,是默認填入了一個undefined作爲初始值的。
也就是說讀取test函數的時候,一開始就已經有了a這樣一個變量。

變量提升也就是這個意思,在活動對象填充的過程中,先把變量和函數“認識”,對於var a = 0;其實是var a和a=0,它拆分成了兩個這樣的步驟。而函數test(),也是讀取了這個函數的名字,賦值爲undefined,而函數名又其實是對內存地址的引用,我理解這個過程就是劃了一塊內存空間給這個函數,用這個函數名test指向這樣一個地址,這個內存空間是用棧的方式,而這個空間的描述就是作用域。

在執行上下文建立中的第一個環節,此時是函數還沒有運行之前,通過引擎,作用域,編譯器的相互配合,完成了預編譯,把函數的變量和方法全部都“檢驗”,並且填入了合適的值,存放在了活動對象中。
接來下需要初始化作用域鏈:

作用域鏈(scope chain)

   簡單按字面來說,作用域鏈就是由作用域組成的鏈,它是一個單向的鏈表結構
  在js中,函數也是對象,它是很多屬性的集合。其中ECMA-262標準第三版定義了這樣一個屬性[[Scope]],FireFox的幾個引擎(SpiderMonkey和Rhino)提供了私有屬性_ parent _來訪問它,這個屬性包含了函數被創建時的作用域中對象的集合。而作用域鏈便是這個集合的一個指針列表。儘管是所有對象都有這個屬性,但是隻有函數,這個屬性纔有用。

作用域鏈只是一個指向變量對象的指針列表。它只是引用實際並不包含變量對象。

在一個函數被定義的時候, 會將它定義時刻的scope chain鏈接到這個函數對象的[[scope]]屬性.

  就是說比如在全局下創建一個函數test,它存在一個屬性[[scope]],這個屬性這時就鏈接指向了一個集合名字叫作用域鏈,英文是scope chain。在函數被創建的時候因爲是在全局下,這個集合這時候就加入了對所有的全局變量和方法的指向,如window等。
這裏寫圖片描述

當函數調用,但是還沒有執行的時候,test的執行環境通過複製這個函數的[[scope]]屬性中的對象,然後加上這個函數的活動對象作爲這個scope chain鏈的頂端,這樣就構成了一個執行環境的作用域鏈。

當調用這個函數時,通過複製test函數中預先生成的[[scope]]屬性的對象,也就是scope chain,然後:
上面分析了半天的活動對象,此時就發揮了它的作用,對它的引用將被加入到執行環境作用域鏈的頂端,也就是圖中的scope chain下有兩個格子,從上往下數第一個格子指向活動對象,第二個格子纔是原來的全局對象們。此時text()的執行環境的作用域鏈就生成了。
也就是說在函數聲明的階段,此時scope chain已經有了公共的部分內容,而等到被執行前,新創建的執行環境的作用域鏈複製了公共的部分,然後才加上這個函數自己的部分,也就是活動對象

this設置後,代碼就開始執行了。

此時可回過頭往上將執行上下文的(暫時可不讀)重看一遍。
當執行後,Js的上下文堆棧又將執行上下文彈出,把控制權交給之前的執行上下文,被彈出的這個執行上下文被銷燬。活動對象也被銷燬,默認情況下當函數返回時會銷燬它的活動對象和作用域鏈。

詞法作用域

詞法作用域又叫靜態作用域,相對應的是動態作用域。而大部分語言都是採用靜態作用域。

   var a = 1;
    function add() {
        a = a + 9;
        alert(a);
    }
    function test() {
        var a = 0;
        add();//10
    }
    test();

比如上面的例子,如果採用的是動態作用域,那麼add就應該是輸出9。而靜態作用域就是指在聲明的時候,作用域就已經確定了。上面這個test,作用域鏈中應該是有底層window,上一個就是add的自身的一堆對象,也就是活動對象,那麼a只會在這個線上去尋找,而動態作用域,就會去test裏面找a,誰調用這個函數add,它的作用域鏈會把調用的test加入它的搜索計劃。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章