JS執行上下文

1、什麼是執行上下文

執行上下文(Execution Context),簡稱EC。

網上有不少關於執行上下文定義的描述,簡單理解一下,其實就是做用域,也就是運行這段JavaScript代碼的一個環境。數組

2、執行上下文的組成和分類

1. 組成

對於每一個執行上下文EC,都有三個重要的屬性:瀏覽器

  1. 變量對象Variable Object(變量聲明、函數聲明、函數形參)
  2. 做用域鏈 Scope Chain
  3. this指針

2. 分類

執行上下文分爲3類函數

  1. 全局執行上下文
  2. 函數執行上下文
  3. eval執行上下文(幾乎不用,暫時不作解釋)

全局執行上下文

術語理解
代碼開始執行前首先進入的環境。
特色
全局執行上下文有且只有一個。客戶端中通常由瀏覽器建立,也就是 window對象。
注意點

(1)使用var聲明的全局變量,均可以在window對象中訪問到,能夠理解爲windowvar聲明對象的載體。
this

(2)使用let聲明的全局變量,用window對象訪問不到。線程

函數執行上下文

術語理解
函數被調用時,會建立一個函數執行上下文。
特色
函數執行上下文能夠有多個,即便調用自身,也會建立一個新的函數執行上下午呢。

以上是對全局執行上下文和函數執行上下文的區別。指針

下面再來看看執行上下文的生命週期。code

3、執行上下文的生命週期

執行上下文的生命週期能夠分爲3個階段:對象

  1. 建立階段
  2. 執行階段
  3. 回收階段

1. 建立階段

發生在當函數被調用,可是在 未執行內部代碼之前。

建立階段主要作的事情是:生命週期

(1)建立變量對象 Variable Object(建立函數形參、函數聲明、變量聲明)

(2)建立做用域鏈 Scope Chain

(3)肯定this指向 This Binding

咱們先用代碼來更直觀的理解下建立階段的過程:ip

function foo(i){
    var a = 100;
    var b = function(){};
    function c(){}
}
foo(20);

當調用foo(20)的時候,執行上下文的建立狀態以下:

ExecutionContext:{
    scopeChain:{ ... },
    this:{ ... },
    variableObject:{
        arguments:{
            0: 20,
            length: 1
        },
        i: 20,
        c:<function>,
        a:undefined,
        b:undefined
    }
}

2. 執行階段

建立完成後,程序自動進入執行階段,執行階段主要作的事情是:

(1)給變量對象賦值:給VO中的變量賦值,給函數表達式賦值。

(2)調用函數

(3)順序執行代碼

仍是以上面的代碼爲例,執行階段給VO賦值,用僞代碼表示以下:

ExecutionContext:{
    scopeChain:{ ... },
    this:{ ... },
    variableObject:{
        arguments:{
            0: 20,
            length: 1
        },
        i: 20,
        c:<function>,
        a:100,
        b:function
    }
}

3. 回收階段

全部代碼執行完畢,程序關閉,釋放內存。

上下文出棧後,虛擬機進行回收。
全局上下文只有當關閉瀏覽器時纔會出棧。

根據以上內容,咱們瞭解到執行上下文的建立須要建立變量對象,那變量對象究竟是什麼呢?

4、變量對象 VO 和 活動對象 AO

1. VO 概念理解

變量對象 Variable Object,簡稱 VO。簡單理解就是一個對象,這個對象存放的是:全局執行上下文的變量和函數。

VO === this === Global

VO的兩種特殊狀況:

(1)未通過 var聲明的變量,不會存在 VO

(2)函數表達式(與函數聲明相對),也不在 VO

2. AO 概念理解

活動對象Activation Object,也叫激活對象,簡稱AO。
激活對象是在進入函數執行上下文時(函數執行的前一刻)被建立的。

函數執行上下文中,VO是不能直接訪問,因此AO扮演了VO的角色。

VO === AO,而且添加了形參類數組和形參的值

Arguments Object是函數上下文AO的一個對象,它包含的屬性有:

(1)callee:指向當前函數的引用

(2)length:真正傳遞參數的個數

(3)properties-indexes:函數的參數值(按照參數列表從左到右排列)

3. VO 的初始化過程

(1)根據函數參數,建立並初始化arguments

變量聲明var、函數形參、函數聲明

(2)掃描函數聲明

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

(3)掃描變量聲明

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

注:函數聲明優先級高於變量聲明優先級

5、示例分析

1. 如何理解函數聲明中「若變量對象已經包含了相同名字的屬性,則替換它的值」

用代碼來理解一下:

function fun(a){
    console.log(a); // function a(){}
    function a(){}
}
fun(100);

咱們調用了fun(100),傳入a的值是100,爲何執行console語句後結果卻不是100呢?別急,咱們接着分析~

建立階段:

步驟 1-1:根據形參建立arguments,用實參賦值給對應的形參,沒有實參的賦值爲undefined
AO_Step1:{
    arguments:{
        0: 100,
        length:1
    },
    a: 100
}

步驟 1-2:掃描函數聲明,此時發現名稱爲a的函數聲明,將其添加到AO上,替換掉已經存在的相同屬性名稱a,也就是替換掉形參爲a的值。
AO_Step2:{
    arguments:{
        0: 100,
        length:1
    },
    a: 指向function a(){}
}

步驟 1-3:掃描變量聲明,未發現有變量。

執行階段:

步驟 2-1:沒有賦值語句,第一行執行console命令,而此時a指向的是funciton,因此輸出function a(){}

2. 如何理解變量聲明中「若是變量名和已經聲明的函數名或者函數的參數名相同,則不影響已經存在的屬性」

用代碼來理解一下

情景1:變量與參數名相同

function fun2(a){
    console.log(a); // 100
    var a = 10;
    console.log(a) // 10
}

fun2(100);

// 分析步驟:

建立階段:
步驟 1-1:根據arguments建立並初始化AO
AO = {
    arguments:{
        0: 100,
        length:1
    },
    a:100
}

步驟 1-2:掃描函數聲明,此時沒有額外的函數聲明,因此AO仍是和上次一致
AO = {
    arguments:{
        0: 100,
        length:1
    },
    a:100
}

步驟 1-3:掃描變量聲明,發現AO中已經存在了a屬性,因此不修改已存在的屬性。
AO = {
    arguments:{
        0: 100,
        length:1
    },
    a:100
}

執行階段:
步驟 2-1:按順序執行console語句,此時AO中的a是100,因此輸出100.
步驟 2-2:執行到賦值語句,對AO中的a進行賦值,此時a是10。
步驟 2-3:按順序執行,執行console語句,此時a是10,因此輸出10。

情景2:變量與函數名相同

function fun3(){
    console.log(a); // function a(){}
    var a = 10;
    function a(){}
    console.log(a) // 10
}
fun3();

// 分析步驟:

建立階段:
步驟 1-1:根據arguments建立並初始化AO
AO={
    arguments:{
        length:0
    }
}

步驟 1-2:掃描函數聲明,此時a指向函數聲明(Function Declaration)
AO={
   arguments:{
        length:0
   }, 
   a: FD
}

步驟 1-3:掃描變量聲明,發現AO中已經存在了a屬性,則跳過,不影響已存在的屬性。
AO={
   arguments:{
        length:0
   }, 
   a: FD
}


執行階段:
步驟 2-1:執行第一行語句console,此時a指向的是函數聲明,因此輸出函數聲明。
AO={
   arguments:{
        length:0
   }, 
   a: FD
}

步驟 2-2:執行第二句對AO中的變量對象進行賦值,因此a的值改成10。
AO={
   arguments:{
        length:0
   }, 
   a: 10
}

步驟 2-3:執行第三句,是函數聲明,在執行階段不會再將其添加到AO中,直接跳過。因此AO仍是上次的狀態。
AO={
   arguments:{
        length:0
   }, 
   a: 10
}

步驟 2-4:執行第四句,此時a的值是10,因此輸出10。
AO={
   arguments:{
        length:0
   }, 
   a: 10
}

根據以上的示例,咱們已經大體明白了EC以及EC的生命週期。

同時,咱們知道函數每次調用都會產生一個新的函數執行上下文。

那麼,若是有若干個執行上下文呢,JavaScript是怎樣執行的?

這就涉及到 執行上下文棧 的相關知識。

6、執行上下文棧

1. 術語理解

執行上下文棧(Execution context stack,ECS),簡稱ECS。

簡單理解就是若干個執行上下文組成了執行上下文棧。也稱爲執行棧、調用棧。

2. 做用

用來存儲代碼執行期間的全部上下文。

3. 特色

咱們知道棧的特色是先進後出。能夠理解爲瓶子,先進來的東西永遠在最底部。

因此

執行上下文棧的特色就是LIFO(Last In First Out)
也就是後進先出。

4. 存儲機制

  1. JS首次執行時,會將全局執行上下文存入棧底,因此全局執行上下文永遠在最底部。
  2. 當有函數調用時,會建立一個新的函數執行上下文存入執行棧。
  3. 永遠是棧頂處於當前正在執行狀態,執行完成後出棧,開始執行下一個。

    5. 示例分析

    咱們用代碼簡單理解一下

示例1:

function f1(){
    f2();
    console.log(1)
}
function f2(){
    f3();
    console.log(2)
}
function f3(){
    console.log(3)
}
f1(); // 3 2 1

根據執行棧的特色進行分析:

(1)咱們假設執行上下文棧是數組ECStack,則ECStack=[globalContext],存入全局執行上下文(咱們暫且叫它globalStack

(2)調用f1()函數,進入f1函數開始執行,建立f1的函數執行上下文,存入執行棧,即ECStack.push('f1 context')

(3)f1函數內部調用了f2()函數,則建立f2的函數執行上下文,存入執行棧,即ECStack.push('f2 context')f2執行完成以前,f1沒法執行console語句

(4)f2函數內部調用了f3()函數,則建立f3的函數執行上下文,存入執行棧,即ECStack.push('f3 context')f3執行完成以前,f2沒法執行console語句

(5)f3執行完成,輸出3,並出棧,ECStack.pop()

(6)f2執行完成,輸出2,並出棧ECStack.pop()

(7)f1執行完成,輸出1,並出棧ECStack.pop()

(8)最後ECStack只剩[globalContext]全局執行上下文

示例2:

function foo(i){
    if(i == 3){
        return 
    }
    foo(i+1);
    console.log(i) 
}
foo(0); // 2,1,0

分析:

(1)調用foo函數,建立foo函數的函數執行上下文,存入EC,傳0i=0if條件不知足不執行,

(2)執行到foo(1),再次調用foo函數,建立一個新的函數執行上下文,存入EC,此時傳入的i1if條件不知足不執行,

(3)又執行到foo(2),又建立新的函數執行上下文,存入EC,此時i2if條件不知足不執行

(3)又執行到foo(3),再次建立新的函數執行上下文,存入EC,此時i3if知足直接退出,EC彈出foo(3)

(4)EC彈出foo(3)後執行foo(2)剩下的代碼,輸出2foo(2)執行完成,EC彈出foo(2)

(5)EC彈出foo(2)後執行foo(1)剩下的代碼,輸出1foo(1)執行完成,EC彈出foo(1)

(6)EC彈出foo(1)後執行foo(0)剩下的代碼,輸出0foo(0)執行完成,EC彈出foo(0),此時EC只剩下全局執行上下文。

7、總結

  1. 全局執行上下文只有一個,而且在棧底。
  2. 當瀏覽器關閉時,全局執行上下文才會出棧。
  3. 函數執行上下文能夠有多個,而且函數每調用執行一次(即便是調用自身),就會生成一個新的函數執行上下文。
  4. Js是單線程,因此是同步執行,執行上下文棧中,永遠是處於棧頂的是執行狀態。
  5. VO或是AO只有一個,建立過程的順序是:參數聲明>函數聲明>變量聲明
  6. 每一個EC能夠抽象爲一個對象,這個對象包含三個屬性:做用域鏈、VO/AO、this
相關文章
相關標籤/搜索