JavaScript基礎系列---執行環境與做用域鏈

問題

今天看筆記發現本身以前記了一個關於同名標識符優先級的內容,具體是下面這樣的:javascript

  • 形參優先級高於當前函數名,低於內部函數名
  • 形參優先級高於arguments
  • 形參優先級高於只聲明卻未賦值的局部變量,可是低於聲明且賦值的局部變量
  • 函數和變量都會聲明提高,函數名和變量名同名時,函數名的優先級要高。執行代碼時,同名函數會覆蓋只聲明卻未賦值的變量,可是它不能覆蓋聲明且賦值的變量
  • 局部變量也會聲明提高,能夠先使用後聲明,不影響外部同名變量

而後我就想,爲何會有這樣的優先級呢,規定的?可是好像沒有這個規定,因而開始查閱資料,就有了下文html

初識Execution Context

Execution ContextJavascript中一個抽象概念,它定義了變量或函數有權訪問的其餘數據,決定了它們各自的行爲。爲了便於理解,咱們能夠近似將其等同於執行當前代碼的環境,JavaScript的可執行代碼包括前端

  • 全局代碼
  • 函數代碼
  • eval()代碼

每當執行流轉到這些可執行代碼時,就會「新建」一個Execution Context並進入該Execution Contextjava

clipboard.png

在上圖中,共有4個Execution Context,其中有一個是Global Execution Context(有且僅有一個),還有三個Function Execution Contextsegmentfault

再識Execution Context Stack

瀏覽器中的JavaScript解釋器是單線程的,每次建立並進入一個新的Execution Context時,這個Execution Context就會被推(push)進一個環境棧中,這個棧稱爲Execution Context Stack,噹噹前Execution Context的代碼執行完以後,棧又會將其彈(pop)出,並銷燬這個Execution Context,保存在其中的變量及函數定義也隨之被銷燬,而後把控制權返回給以前的Execution ContextGlobal Execution Context例外,它要等到應用程序退出後 —— 如關閉網頁或瀏覽器 —— 纔會被銷燬)瀏覽器

JavaScript的執行流就是由這個機制控制的,如下面的代碼爲例說明:閉包

var sayHello = 'Hello';
function name(){
    var fisrtName = 'Cao',
        lastName = 'Cshine';
    function getFirstName(){
        return fisrtName;
    }
    function getLatName(){
        return lastName;
    }
    console.log(sayHello + getFirstName() + ' ' + getLastName());
}
name();

clipboard.png

  • 當瀏覽器第一次加載script的時候,默認會進入Global Execution Context,因此Global Execution Context永遠是在棧的最下面。
  • 而後遇到函數調用name(),此時新建並進入Function Execution Context nameFunction Execution Context name入棧;
  • 繼續執行遇到函數調用getFirstName(),因而新建並進入Function Execution Context getFirstNameFunction Execution Context getFirstName入棧,因爲該函數內部不會再新建其餘Execution Context,因此直接執行完畢,而後出棧,控制權交給Function Execution Context name
  • 再往下執行遇到函數調用getLastName(),因而新建並進入Function Execution Context getLastNameFunction Execution Context getLastName入棧,因爲該函數內部不會再新建其餘Execution Context,因此直接執行完畢,而後出棧,控制權交給Function Execution Context name
  • 執行完console後,函數name也執行完畢,因而出棧,控制權交給Function Execution Context name,至此棧中又只有Global Execution Context
  • 關於Execution Context Stack有5個關鍵點:異步

    • 單線程
    • 同步執行(非異步)
    • 1個Global Execution Context
    • 無限制的函數Function Execution Context
    • 每一個函數調用都會建立新的Execution Context,即便是本身調用本身,以下面的代碼:函數

      (function foo(i) {
          if (i === 3) {
              return;
          }
          else {
              foo(++i);
          }
      }(0));

      Execution Context Stack的狀況以下圖所示:ui

      clipboard.png

親密接觸Execution Context

每一個Execution Context在概念上能夠當作由下面三者組成:

  • 變量對象(Variable object,簡稱VO
  • 做用域鏈(Scope Chain
  • this

變量對象(Variable object

該對象與Execution Context相關聯,保存着Execution Context中定義的全部變量、函數聲明以及函數形參,這個對象咱們沒法訪問,可是解析器在後臺處理數據是用到它(注意函數表達式以及沒用var/let/const聲明的變量不在VO中)

Global Execution Context中的變量對象VO根據宿主環境的不一樣而不一樣,在瀏覽器中爲window對象,所以全部的全局變量和函數都是做爲window對象的屬性和方法建立的。

對於Function Execution Context,變量對象VO爲函數的活動對象,活動對象是在進入Function Execution Context時建立的,它經過函數的arguments屬性初始化,也就是最初只包含arguments這一個屬性。

JavaScript解釋器內部,每次調用Execution Context都會經歷下面兩個階段:

  • 建立階段(發生在函數調用時,可是內部代碼執行前,這將解釋聲明提高現象)

    • 建立做用域鏈(做用域鏈見下文)
    • 建立變量對象VO
    • 肯定this的值
  • 激活/代碼執行階段

    • 變量賦值、執行代碼

其中建立階段的第二步建立變量對象VO的過程能夠理解成下面這樣:

  • Global Execution Context中沒有這一步) 建立arguments對象,掃描函數的全部形參,並將形參名稱 和對應值組成的鍵值對做爲變量對象VO的屬性。若是沒有傳遞對應的實參,將undefined做爲對應值。若是形參名爲arguments,將覆蓋arguments對象
  • 掃描Execution Context中全部的函數聲明(注意是函數聲明,函數表達式不算)

    • 將函數名和對應值(指向內存中該函數的引用指針)組成組成的鍵值對做爲變量對象VO的屬性
    • 若是變量對象VO已經存在同名的屬性,則覆蓋這個屬性
  • 掃描Execution Context中全部的變量聲明

    • 由變量名和對應值(此時爲undefined) 組成,做爲變量對象的屬性
    • 若是變量名與已經聲明的形參或函數相同,此時什麼都不會發生,變量聲明不會干擾已經存在的這個同名屬性。

好~~如今咱們來看代碼捋一遍:

function foo(num) {
    console.log(num);// 66
    console.log(a);// undefined
    console.log(b);// undefined
    console.log(fc);// f function fc() {}
    var a = 'hello';
    var b = function fb() {};
    function fc() {}
}
foo(66);
  • 當調用foo(66)時,建立階段時,Execution Context能夠理解成下面這個樣子

    fooExecutionContext = {
        scopeChain: { ... },
        variableObject: {
            arguments: {
                0: 66,
                length: 1
            },
            num: 66,
            fc: pointer to function fc()
            a: undefined,
            b: undefined
        },
        this: { ... }
    }
  • 當建立階段完成之後,執行流進入函數內部,激活執行階段,而後代碼完成執行,Execution Context能夠理解成下面這個樣子:

    fooExecutionContext = {
        scopeChain: { ... },
        variableObject: {
            arguments: {
                0: 66,
                length: 1
            },
            num: 66,
            fc: pointer to function fc()
            a: 'hello',
            b: pointer to function fb()
        },
        this: { ... }
    }

做用域鏈(Scope Chain

當代碼在一個Execution Context中執行時,就會建立變量對象的一個做用域鏈,做用域鏈的用途是保證對執行環境有權訪問的全部變量和函數的有序訪問

Global Execution Context中的做用域鏈只有Global Execution Context的變量對象(也就是window對象),而Function Execution Context中的做用域鏈還會有「父」Execution Context的變量對象,這裏就會要牽扯到[[Scopes]]屬性,能夠將函數做用域鏈理解爲---- 當前Function Execution Context的變量對象VO(也就是該函數的活動對象AO) + [[Scopes]],怎麼理解呢,咱們繼續往下看

[[Scopes]]屬性

[[Scopes]]這個屬性與函數的做用域鏈有着密不可分的關係,JavaScript中每一個函數都表示爲一個函數對象,[[Scopes]]是函數對象的一個內部屬性,只有JavaScript引擎能夠訪問。

結合函數的生命週期:

  • 函數定義

    • [[Scopes]]屬性在函數定義時被存儲,保持不變,直至函數被銷燬
    • [[Scopes]]屬性連接到定義該函數的做用域鏈上,因此他保存的是全部包含該函數的 「父/祖父/曾祖父...」 Execution Context的變量對象(OV),咱們將其稱爲全部父變量對象(All POV
    • !!!特別注意 [[Scopes]]是在定義一個函數的時候決定的
  • 函數調用

    • 函數調用時,會建立並進入一個新的Function Execution Context,根據前面討論過的調用Function Execution Context的兩個階段可知:先建立做用域鏈,這個建立過程會將該函數對象的[[Scopes]]屬性加入到其中
    • 而後會建立該函數的活動對象AO(做爲該Function Execution Context的變量對象VO),並將建立的這個活動對象AO加到做用域鏈的最前端
    • 而後肯定this的值
    • 正式執行函數內的代碼

經過上面的過程咱們大概能夠理解:做用域鏈 = 當前Function Execution Context的變量對象VO(也就是該函數的活動對象AO) + [[Scopes]],有了這個做用域鏈, 在發生標識符解析的時候, 就會沿着做用域鏈一級一級地搜索標識符,最開始是搜索當前Function Execution Context的變量對象VO,若是沒有找到,就會根據[[Scopes]]找到父變量對象,而後繼續搜索該父變量對象中是否有該標識符;若是仍沒有找到,便會找到祖父變量對象並搜索其中是否有該標識符;如此一級級的搜索,直至找到標識符爲止(若是直到最後也找不到,通常會報未定義的錯誤);注意:對於thisarguments,只會搜到其自己的變量(活動)對象爲止,而不會繼續按着做用域鏈搜素。

如今再結合例子來捋一遍:

var a = 10;
function foo(d) {
    var b = 20;
    function bar() {
        var c = 30;
        console.log(a +  b + c + d); // 110
        //這裏能夠訪問a,b,c,d
    }
    //這裏能夠訪問a,b,d 可是不能訪問c
    bar();
}
//這裏只能訪問a
foo(50);
  • 當瀏覽器第一次加載script的時候,默認會進入Global Execution Context的建立階段

    • 建立Scope Chain(做用域鏈)
    • 建立變量對象,此處爲window對象。而後會掃描全部的全局函數聲明,再掃描全局變量聲明。以後該變量對象會加到Scope Chain
    • 肯定this的值
    • 此時Global Execution Context能夠表示爲:

      globalEC = {
          scopeChain: {
              pointer to globalEC.VO
          },
          VO: {
              a: undefined,
              foo: pointer to function foo(),
              (其餘window屬性)
          },
          this: { ... }
      }
  • 接着進入Global Execution Context的執行階段

    • 遇到賦值語句var a = 10,因而globalEC.VO.a = 10

      globalEC = {
          scopeChain: {
              pointer to globalEC.VO
          },
          VO: {
              a: 10,
              foo: pointer to function foo(),
              (其餘window屬性)
          },
          this: { ... }
      }
    • 遇到foo函數定義語句,進入foo函數的定義階段,foo[[Scopes]]屬性被肯定

      foo.[[Scopes]] = {
          pointer to globalEC.VO
      }
    • 遇到foo(50)調用語句,進入foo函數調用階段,此時進入Function Execution Context foo的建立階段

      • 建立Scope Chain(做用域鏈)
      • 建立變量對象,此處爲foo的活動對象。先建立arguments對象,而後掃描函數的全部形參,以後會掃描foo函數內全部的函數聲明,再掃描foo函數內的變量聲明。以後該變量對象會加到Scope Chain
      • 肯定this的值
      • 此時Function Execution Context foo能夠表示爲

        fooEC = {
            scopeChain: {
                pointer to fooEC.VO,
                foo.[[Scopes]]
            },
            VO: {
                arguments: {
                    0: 66,
                    length: 1
                },
                b: undefined,
                d: 50,
                bar: pointer to function bar(),
            },
            this: { ... }
        }
    • 接着進入Function Execution Context foo的執行階段

      • 遇到賦值語句var b = 20;,因而fooEC .VO.b = 20

        fooEC = {
            scopeChain: {
                pointer to fooEC.VO,
                foo.[[Scopes]]
            },
            VO: {
                arguments: {
                    0: 66,
                    length: 1
                },
                b: 20,
                d: 50,
                bar: pointer to function bar(),
            },
            this: { ... }
        }
      • 遇到bar函數定義語句,進入bar函數的定義階段,bar[[Scopes]]`屬性被肯定

        bar.[[Scopes]] = {
            pointer to fooEC.VO,
            pointer to globalEC.VO
        }
      • 遇到bar()調用語句,進入bar函數調用階段,此時進入Function Execution Context bar的建立階段

        • 建立Scope Chain(做用域鏈)
        • 建立變量對象,此處爲bar的活動對象。先建立arguments對象,而後掃描函數的全部形參,以後會掃描foo函數內全部的函數聲明,再掃描bar函數內的變量聲明。以後該變量對象會加到Scope Chain
        • 肯定this的值
        • 此時Function Execution Context bar能夠表示爲

          barEC = {
             scopeChain: {
                 pointer to barEC.VO,
                 bar.[[Scopes]]
             },
             VO: {
                 arguments: {
                     length: 0
                 },
                 c: undefined
             },
             this: { ... }
          }
      • 接着進入Function Execution Context bar的執行階段

        • 遇到賦值語句var c = 30,因而barEC.VO.c = 30

          barEC = {
              scopeChain: {
                  pointer to barEC.VO,
                  bar.[[Scopes]]
              },
              VO: {
                  arguments: {
                      length: 0
                  },
                  c: 30
              },
              this: { ... }
          }
        • 遇到打印語句console.log(a + b + c + d);,須要訪問變量a,b,c,d

          • 經過bar.[[Scopes]].globalEC.VO.a訪問獲得a=10
          • 經過bar.[[Scopes]].fooEC.VO.b,bar.[[Scopes]].fooEC.VO.d訪問獲得b=20,d=50
          • 經過barEC.VO.c訪問獲得c=30
          • 經過運算得出結果110
      • bar函數執行完畢,Function Execution Context bar銷燬,變量c也隨之銷燬
    • foo函數執行完畢,Function Execution Context foo銷燬,b,d,bar也隨之銷燬
  • 全部代碼執行完畢,等到該網頁被關閉或者瀏覽器被關閉,Global Execution Context才銷燬,a,foo纔會銷燬

經過上面的例子,相信對Execution Context和做用域鏈的理解也更清楚了,下面簡單總結一下做用域鏈:

  • 做用域鏈的前端始終是當前執行的代碼所在Execution Context的變量對象;
  • 下一個變量對象來自其包含Execution Context,以此類推;
  • 最後一個變量對象始終是Global Execution Context的變量對象;
  • 內部Execution Context可經過做用域鏈訪問外部Execution Context反之不能夠
  • 標識符解析是沿着做用域鏈一級一級地搜索標識符的過程。搜索過程始終從做用域鏈的前端開始,而後逐級的向後回溯,直到找到標識符爲止(若是找不到,一般會致使錯誤);
  • 做用域鏈的本質是一個指向變量對象的指針列表,只引用而不實際包含變量對象。

延長做用域鏈

下面兩種語句能夠在做用域鏈的前端臨時增長一個變量對象以延長做用域鏈,該變量對象會在代碼執行後被移除

  • try-catch語句的catch
    建立一個新的變量對象,其中包含的是被拋出的錯誤對象的聲明
  • with語句
    將指定的對象添加到做用域鏈中

    function buildUrl(){
        var qs = "?debug=true";
        with(location){
            var url = href + qs;
        }
        //console.log(href) 將會報href is not defined的錯誤,由於with語句執行完with建立的變量對象就被移除了
        return url;
    }

    with語句接收window.location對象,所以其變量對象就包含了window.location對象的全部屬性,而這個變量對象被添加到做用域鏈的前端。因此在with語句裏面使用href至關於window.location.href

解答問題

如今咱們來解答最開始的優先級問題

  • 形參優先級高於當前函數名,低於內部函數名

    function fn(fn){
        console.log(fn);// cc
    }
    fn('cc');

    函數fn屬於Global Execution Context,而形參fn屬於Function Execution Context fn,此時做用域的前端是Function Execution Context fn的變量對象,因此console.log(fn)爲形參的值

    function fa(fb){
        console.log(fb);// ƒ fb(){}
        function fb(){}
        console.log(fb);// ƒ fb(){}
    }
    fa('aaa');

    調用fa函數時,進入Function Execution Context fa的建立階段,根據前面所說的變量對象建立過程:

    先建立arguments對象,而後掃描函數的全部形參,以後會掃描函數內全部的函數聲明,再掃描函數內的變量聲明;
    掃描函數聲明時,若是變量對象 VO中已經存在同名的屬性,則覆蓋這個屬性

    咱們能夠獲得fa的變量對象表示爲:

    fa.VO = {
        arguments: {
            0:'aaa',
            length: 1
        },
        fb: pointer to function fb(),
    }

    因此console.log(fb)獲得的是fa.VO.fb的值ƒ fb(){}

  • 形參優先級高於arguments

    function fn(aa){
        console.log(arguments);// Arguments ["hello world"]
    }
    fn('hello world');
    
    function fn(arguments){
        console.log(arguments);// hello world
    }
    fn('hello world');

    調用fn函數時,進入Function Execution Context fn的建立階段,根據前面所說的變量對象建立過程:

    先建立arguments對象,而後掃描函數的全部形參,以後會掃描函數內全部的函數聲明,再掃描函數內的變量聲明;
    先建立arguments對象,後掃描函數形參,若是形參名爲arguments,將會覆蓋arguments對象

    因此當形參名爲arguments時,console.log(arguments)爲形參的值hello world

  • 形參優先級高於只聲明卻未賦值的局部變量,可是低於聲明且賦值的局部變量

    function fa(aa){
        console.log(aa);//aaaaa
        var aa;
        console.log(aa);//aaaaa
    }
    fa('aaaaa');

    調用fa函數時,進入Function Execution Context fa的建立階段,根據前面所說的變量對象建立過程:

    先建立arguments對象,而後掃描函數的全部形參,以後會掃描函數內全部的函數聲明,再掃描函數內的變量聲明;
    掃描函數內的變量聲明時,若是變量名與已經聲明的形參或函數相同,此時什麼都不會發生,變量聲明不會干擾已經存在的這個同名屬性

    因此建立階段以後Function Execution Context fa的變量對象表示爲:

    fa.VO = {
        arguments: {
            0:'aaaaa',
            length: 1
        },
        aa:'aaaaa',
    }

    以後進入Function Execution Context fa的執行階段:console.log(aa);打印出fa.VO.aa(形參aa)的值aaaaa;因爲var aa;僅聲明而未賦值,因此不會改變fa.VO.aa的值,因此下一個console.log(aa);打印出的仍然是fa.VO.aa(形參aa)的值aaaaa

    function fb(bb){
        console.log(bb);//bbbbb
        var bb = 'BBBBB';
        console.log(bb);//BBBBB
    }
    fb('bbbbb');

    調用fb函數時,進入Function Execution Context fb的建立階段,根據前面所說的變量對象建立過程:

    先建立arguments對象,而後掃描函數的全部形參,以後會掃描函數內全部的函數聲明,再掃描函數內的變量聲明;
    掃描函數內的變量聲明時,若是變量名與已經聲明的形參或函數相同,此時什麼都不會發生,變量聲明不會干擾已經存在的這個同名屬性

    因此建立階段以後Function Execution Context fb的變量對象表示爲:

    fb.VO = {
        arguments: {
            0:'bbbbb',
            length: 1
        },
        bb:'bbbbb',
    }

    以後進入Function Execution Context fb的執行階段:console.log(bb);打印出fb.VO.bb(形參bb)的值'bbbbb';遇到var bb = 'BBBBB';fb.VO.bb的值將被賦爲BBBBB,因此下一個console.log(bb);打印出fb.VO.bb(局部變量bb)的值BBBBB

  • 函數和變量都會聲明提高,函數名和變量名同名時,函數名的優先級要高。

    console.log(cc);//ƒ cc(){}
    var cc = 1;
    function cc(){}

    根據Global Execution Context的建立階段中建立變量對象的過程:是先掃描函數聲明,再掃描變量聲明,且變量聲明不會影響已存在的同名屬性。因此在遇到var cc = 1;這個聲明語句以前,global.VO.ccƒ cc(){}

  • 執行代碼時,同名函數會覆蓋只聲明卻未賦值的變量,可是它不能覆蓋聲明且賦值的變量

    var cc = 1;
    var dd;
    function cc(){}
    function dd(){}
    console.log(cc);//1
    console.log(dd);//ƒ dd(){}

    Global Execution Context的建立階段以後,Global Execution Context的變量對象能夠表示爲:

    global.VO = {
        cc:pointer to function cc(),
        dd:pointer to function dd()
    }

    而後進入Global Execution Context的執行階段,遇到var cc = 1;這個聲明賦值語句後, global.VO.cc將被賦值爲1;而後再遇到var dd這個聲明語句,因爲僅聲明未賦值,因此不改變global.VO.dd的值;因此console.log(cc);打印出1console.log(dd);打印出ƒ dd(){}

  • 局部變量也會聲明提高,能夠先使用後聲明,不影響外部同名變量

每一個Execution Context都會有變量建立這個過程,因此會有聲明提高;根據做用域鏈,若是局部變量與外部變量同名,那麼最早找到的是局部變量,影響不到外部同名變量

相關資料

JavaScript基礎系列---變量及其值類型
Understanding Scope in JavaScript
What is the Execution Context & Stack in JavaScript?
深刻探討JavaScript的執行環境和棧
做用域原理
JavaScript執行環境 + 變量對象 + 做用域鏈 + 閉包

相關文章
相關標籤/搜索