JavaScript基本系列---實行環境與作用域鏈

題目

本日看筆記發明自身之前記了一個關於同名標識符優先級的內容,詳細是下面如許的:

  • 形參優先級高於當前函數名,低於內部函數名
  • 形參優先級高於arguments
  • 形參優先級高於只聲明卻未賦值的局部變量,然則低於聲明且賦值的局部變量
  • 函數和變量都邑聲明提拔,函數名和變量名同名時,函數名的優先級要高。實行代碼時,同名函數會掩蓋只聲明卻未賦值的變量,然則它不能掩蓋聲明且賦值的變量
  • 局部變量也會聲明提拔,能夠先運用后聲明,不影響外部同名變量

然後我就想,為何會有如許的優先級呢,劃定的?然則彷佛沒有這個劃定,因而最早查閱材料,就有了下文

初識Execution Context

Execution ContextJavascript中一個抽象概念,它定義了變量或函數有權接見的其他數據,決議了它們各自的行動。為了便於明白,我們能夠近似將其等同於實行當前代碼的環境,JavaScript的可實行代碼包括

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

每當實行流轉到這些可實行代碼時,就會“新建”一個Execution Context並進入該Execution Context

《JavaScript基本系列---實行環境與作用域鏈》

在上圖中,共有4個Execution Context,个中有一個是Global Execution Context(有且唯一一個),另有三個Function Execution Context

再識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();

《JavaScript基本系列---實行環境與作用域鏈》

  • 當瀏覽器第一次加載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的狀況以下圖所示:

      《JavaScript基本系列---實行環境與作用域鏈》

親熱打仗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實行環境 + 變量對象 + 作用域鏈 + 閉包

    原文作者:Cshine
    原文地址: https://segmentfault.com/a/1190000014899566
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞