深刻理解JavaScript執行上下文、函數堆棧、提高的概念

首先明確幾個概念:javascript

  • EC:函數執行環境(或執行上下文),Execution Contextjava

  • ECS:執行環境棧,Execution Context Stacksegmentfault

  • VO:變量對象,Variable Object瀏覽器

  • AO:活動對象,Active Objectide

  • scope chain:做用域鏈函數

想當初本身看到這幾個概念的時候是一(m)臉(d)懵(z)逼(z),可是不得不說這幾個概念對之後深刻學習JS有很大的幫助。來不及解釋了,趕忙上車~學習

EC(執行上下文)

每次當控制器轉到ECMAScript可執行代碼的時候,就會進入到一個執行上下文。ui

那什麼是可執行代碼呢?this

可執行代碼的類型

全局代碼(Global code
  • 這種類型的代碼是在"程序"級處理的:例如加載外部的js文件或者本地<script></script>標籤內的代碼。全局代碼不包括任何function體內的代碼。 這個是默認的代碼運行環境,一旦代碼被載入,引擎最早進入的就是這個環境。spa

函數代碼(Function code
  • 任何一個函數體內的代碼,可是須要注意的是,具體的函數體內的代碼是不包括內部函數的代碼

Eval代碼(Eval code
  • eval內部的代碼

這裏僅僅引入EC這個概念,後面還有關於EC創建細節的介紹。

ECS(執行環境棧)

咱們用MDN上的一個例子來引入函數執行棧的概念

function foo(i) {
  if (i < 0) return;
  console.log('begin:' + i);
  foo(i - 1);
  console.log('end:' + i);
}
foo(2);

// 輸出:

// begin:2
// begin:1
// begin:0
// end:0
// end:1
// end:2

這裏先不關心執行結果。磨刀不誤砍柴功,先了解一下函數執行上下文堆棧的概念。相信弄明白了下面的概念,一切也就水落石出了

咱們都知道,瀏覽器中的JS解釋器被實現爲單線程,這也就意味着同一時間只能發生一件事情,其餘的行爲或事件將會被放在叫作執行棧裏面排隊。下面的圖是單線程棧的抽象視圖:

執行堆棧

當瀏覽器首次載入你的腳本,它將默認進入全局執行上下文。若是,你在你的全局代碼中調用一個函數,你程序的時序將進入被調用的函數,並建立一個新的執行上下文,並將新建立的上下文壓入執行棧的頂部。

若是你調用當前函數內部的其餘函數,相同的事情會在此上演。代碼的執行流程進入內部函數,建立一個新的執行上下文並把它壓入執行棧的頂部。瀏覽器總會執行位於棧頂的執行上下文,一旦當前上下文函數執行結束,它將被從棧頂彈出,並將上下文控制權交給當前的棧。這樣,堆棧中的上下文就會被依次執行而且彈出堆棧,直到回到全局的上下文。

看到這裏,想必你們都已經深諳上述例子輸出結果的緣由了,這裏我大概繪了一個流程圖來幫助理解。

![Alt text](./屏幕快照 2017-04-11 下午7.29.55.png)

VO(變量對象)/AO(活動對象)

這裏爲何要用一個/呢?按照字面理解,AO其實就是被激活的VO,兩個實際上是一個東西。下面引用知乎上的一段話,幫助理解一下。原文連接

變量對象(Variable object)是說JS的執行上下文中都有個對象用來存放執行上下文中可被訪問可是不能被delete函數標示符形參變量聲明等。它們會被掛在這個對象上,對象的屬性對應它們的名字對象屬性的值對應它們的值但這個對象是規範上或者說是引擎實現上的不可在JS環境中訪問到活動對象

激活對象(Activation object)有了變量對象存每一個上下文中的東西,可是它何時能被訪問到呢?就是每進入一個執行上下文時,這個執行上下文兒中的變量對象就被激活,也就是該上下文中的函數標示符、形參、變量聲明等就能夠被訪問到了

EC創建的細節

一、建立階段【當函數被調用,但未執行任何其內部代碼以前】
  • 建立做用域鏈(Scope Chain)

  • 建立變量,函數和參數。

  • 求」this「的值

二、執行階段
  • 初始化變量的值和函數的引用,解釋/執行代碼。

咱們能夠將每一個執行上下文抽象爲一個對象,這個對象具備三個屬性

ECObj: {
    scopeChain: { /* 變量對象(variableObject)+ 全部父級執行上下文的變量對象*/ }, 
    variableObject: { /*函數 arguments/參數,內部變量和函數聲明 */ }, 
    this: {} 
}

解釋器執行代碼的僞邏輯

一、查找調用函數的代碼。

二、執行代碼以前,先進入建立上下文階段:

  • 初始化做用域鏈

  • 建立變量對象:

    • 建立arguments對象,檢查上下文,初始化參數名稱和值並建立引用的複製。

    • 掃描上下文的函數聲明(而非函數表達式)

      • 爲發現的每個函數,在變量對象上建立一個屬性——確切的說是函數的名字——其有一個指向函數在內存中的引用。

      • 若是函數的名字已經存在,引用指針將被重寫。

    • 掃描上下文的變量聲明

      • 爲發現的每一個變量聲明,在變量對象上建立一個屬性——就是變量的名字,而且將變量的值初始化爲undefined

      • 若是變量的名字已經在變量對象裏存在,將不會進行任何操做並繼續掃描。

    • 求出上下文內部「this」的值

三、激活/代碼執行階段:

  • 在當前上下文上運行/解釋函數代碼,並隨着代碼一行行執行指派變量的值。

VO --- 對應上述第二個階段
function foo(i){
            var a = 'hello'
            var b = function(){}
            function c(){}
        }
        foo(22)

當咱們調用foo(22)時,整個建立階段是下面這樣的

ECObj = {
        scopChain: {...},
         variableObject: {
            arguments: {
                0: 22,
                length: 1
            },
            i: 22,
            c: pointer to function c()
            a: undefined,
            b: undefined
        },
        this: { ... }
    }

正如咱們看到的,在上下文建立階段,VO的初始化過程以下(該過程是有前後順序的:函數的形參==>>函數聲明==>>變量聲明):

  • 函數的形參(當進入函數執行上下文時) —— 變量對象的一個屬性,其屬性名就是形參的名字,其值就是實參的值;對於沒有傳遞的參數,其值爲undefined

  • 函數聲明(FunctionDeclaration, FD) —— 變量對象的一個屬性,其屬性名和值都是函數對象建立出來的;若是變量對象已經包含了相同名字的屬性,則替換它的值

  • 變量聲明(var,VariableDeclaration) —— 變量對象的一個屬性,其屬性名即爲變量名,其值爲undefined;若是變量名和已經聲明的函數名或者函數的參數名相同,則不會影響已經存在的屬性。

對於函數的形參沒有什麼可說的,主要看一下函數的聲明以及變量的聲明兩個部分。

一、如何理解函數聲明過程當中若是變量對象已經包含了相同名字的屬性,則替換它的值這句話?

看以下這段代碼:

function foo1(a){
    console.log(a)
    function a(){} 
}
foo1(20)//'function a(){}'

根據上面的介紹,咱們知道VO建立過程當中,函數形參的優先級是高於函數的聲明的,結果是函數體內部聲明的function a(){}覆蓋了函數形參a的聲明,所以最後輸出a是一個function

二、如何理解變量聲明過程當中若是變量名和已經聲明的函數名或者函數的參數名相同,則不會影響已經存在的屬性這句話?

//情景一:與參數名相同
function foo2(a){
    console.log(a)
    var a = 10
}
foo2(20) //'20'

//情景二:與函數名相同

function foo2(){
    console.log(a)
    var a = 10
    function a(){}
}
foo2() //'function a(){}'

下面是幾個比較有趣的例子,當作加餐小菜,你們細細品味。這裏給出一句話當作參考:

函數的聲明比變量優先級要高,而且定義過程不會被變量覆蓋,除非是賦值

function foo3(a){
    var a = 10
    function a(){}
    console.log(a)
}
foo3(20) //'10'

function foo3(a){
    var a 
    function a(){}
    console.log(a)
}
foo3(20) //'function a(){}'
AO --- 對應第三個階段

正如咱們看到的,建立的過程僅負責處理定義屬性的名字,而並不爲他們指派具體的值,固然還有對形參/實參的處理。一旦建立階段完成,執行流進入函數而且激活/代碼執行階段,看下函數執行完成後的樣子:

ECObj = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: 'hello',
        b: pointer to function privateB()
    },
    this: { ... }
}

提高(Hoisting)

對於下面的代碼,相信不少人都能一眼看出輸出結果,可是卻不多有人能給出爲何會產生這種輸出結果的解釋。

(function() {
    console.log(typeof foo); // 函數指針
    console.log(typeof bar); // undefined

    var foo = 'hello',
        bar = function() {
            return 'world';
        };
        
    function foo() {
        return 'hello';
    }
}());

一、爲何咱們能在foo聲明以前訪問它?
回想在VO的建立階段,咱們知道函數在該階段就已經被建立在變量對象中。因此在函數開始執行以前,foo已經被定義了。

二、Foo被聲明瞭兩次,爲何foo顯示爲函數而不是undefined或字符串?
咱們知道,在建立階段,函數聲明是優先於變量被建立的。並且在變量的建立過程當中,若是發現VO中已經存在相同名稱的屬性,則不會影響已經存在的屬性。
所以,對foo()函數的引用首先被建立在活動對象裏,而且當咱們解釋到var foo時,咱們看見foo屬性名已經存在,因此代碼什麼都不作並繼續執行。

三、爲何bar的值是undefined?
bar採用的是函數表達式的方式來定義的,因此bar其實是一個變量,但變量的值是函數,而且咱們知道變量在建立階段被建立但他們被初始化爲undefined,這也是爲何函數表達式不會被提高的緣由。

總結:

一、EC分爲兩個階段,建立執行上下文和執行代碼。
二、每一個EC能夠抽象爲一個對象,這個對象具備三個屬性,分別爲:做用域鏈ScopeVO|AOAOVO只能有一個)以及this
三、函數EC中的AO在進入函數EC時,肯定了Arguments對象的屬性;在執行函數EC時,其它變量屬性具體化。
四、EC建立的過程是由前後順序的:參數聲明 > 函數聲明 > 變量聲明

參考

javascript 執行環境,變量對象,做用域鏈
What is the Execution Context & Stack in JavaScript?
函數MDN

相關文章
相關標籤/搜索