JavaScript基礎(chǔ)專題之深入執(zhí)行上下文(三)

對于ES3每個執(zhí)行上下文,都有三個重要屬性:

  • 變量對象(Variable object,VO)
  • 作用域鏈(Scope chain)
  • this

這篇我們來聊聊這三個重要屬性

image

變量對象

變量對象作為執(zhí)行上下文的一種屬性,每次創(chuàng)建后,根據(jù)執(zhí)行環(huán)境不同上下文下的變量對象也稍有不同,我們比較熟悉的就是全局對象函數(shù)對象,所以我們來聊聊全局上下文下的變量對象和函數(shù)上下文下的變量對象。

全局上下文

我們先了解一個概念,什么叫全局對象。在 W3School 中:

全局對象是預(yù)定義的對象,作為 JavaScript 的全局函數(shù)和全局屬性的占位符。通過使用全局對象,可以訪問所有其他所有預(yù)定義的對象、函數(shù)和屬性。

在頂層 JavaScript 代碼中,可以用關(guān)鍵字 this 引用全局對象。因為全局對象是作用域鏈的頭,這意味著所有非限定性的變量和函數(shù)名都會作為該對象的屬性來查詢。

例如,當(dāng)JavaScript 代碼引用 parseInt() 函數(shù)時,它引用的是全局對象的 parseInt 屬性。全局對象是作用域鏈的頭,還意味著在頂層 JavaScript 代碼中聲明的所有變量都將成為全局對象的屬性。

我們可以根據(jù)代碼理解

  1. 可以通過 this 引用,在客戶端 JavaScript 中,全局對象就是 Window 對象。
console.log(this); //window
  1. 全局對象是由 Object 構(gòu)造函數(shù)實例化的一個對象。
console.log(this instanceof Object);//true
  1. 我們調(diào)用的一些方法都在window下。
console.log(Math.random());
console.log(this.Math.random());

4.作為全局變量的宿主。

var a = 1;
console.log(this.a);

5.客戶端 JavaScript 中,全局對象有 window 屬性指向自身。

var a = 1;
console.log(window.a);//1

this.window.b = 2;
console.log(this.b);//2

我們發(fā)現(xiàn)全局上下文中的變量對象就是全局對象

函數(shù)上下文

在函數(shù)上下文中,不同于全局上下文比較死板,我們用活動對象(activation object, AO)來表示變量對象。

所以活動對象和變量對象其實是一個東西,只是變量對象是規(guī)范上或者說是引擎實現(xiàn)上不可在 JavaScript 環(huán)境中直接訪問,只有到當(dāng)進入一個執(zhí)行上下文中,這個執(zhí)行上下文的變量對象才會被激活,所以稱為activation object,只有在激活狀態(tài)才會對屬性進行訪問。

活動對象是在進入函數(shù)上下文時刻被創(chuàng)建的,它通過函數(shù)的 arguments屬性初始化。arguments 屬性值是 Arguments 對象。

執(zhí)行過程

執(zhí)行上下文的代碼會分成兩個階段進行處理:分析和執(zhí)行,我們也可以叫做:

  1. 進入執(zhí)行上下文
  2. 代碼執(zhí)行

進入執(zhí)行上下文

當(dāng)進入執(zhí)行上下文時,這時候還沒有執(zhí)行代碼,

變量對象會包括:

  1. 函數(shù)的所有形參 (如果是函數(shù)上下文)

    • 由名稱和對應(yīng)值組成的一個變量對象的屬性被創(chuàng)建
    • 沒有實參,屬性值設(shè)為 undefined
  2. 函數(shù)聲明

    • 由名稱和對應(yīng)值(函數(shù)對象(function-object))組成一個變量對象的屬性被創(chuàng)建
    • 如果變量對象已經(jīng)存在相同名稱的屬性,則完全替換這個屬性
  3. 變量聲明

    • 由名稱和對應(yīng)值(undefined)組成一個變量對象的屬性被創(chuàng)建
    • 如果變量名稱跟已經(jīng)聲明的形式參數(shù)或函數(shù)相同,則變量聲明不會干擾已經(jīng)存在的這類屬性

舉個例子:

function foo(a) { 
  var b = 2;
  function c() {}
  var d = function() {};
  b =3;

}

foo(1);

在進入執(zhí)行上下文后,這時候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}

代碼執(zhí)行

在代碼執(zhí)行階段,會順序執(zhí)行代碼,根據(jù)代碼,修改變量對象的值

還是上面的例子,當(dāng)代碼執(zhí)行完后,這時候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

到這里變量對象的創(chuàng)建過程就介紹完了,讓我們簡潔的總結(jié)我們上述所說:

  1. 全局上下文的變量對象初始化是全局對象
  2. 函數(shù)上下文的變量對象初始化只包括 Arguments 對象
  3. 在進入執(zhí)行上下文時會給變量對象添加形參、函數(shù)聲明、變量聲明等初始的屬性值
  4. 在代碼執(zhí)行階段,會再次修改變量對象的屬性值

例子

function foo() {
    console.log(a);
    a = 1;
}

foo(); // ???

function bar() {
    a = 1;
    console.log(a);
}
bar(); // ???

第一段會報錯:Uncaught ReferenceError: a is not defined。

第二段會打?。?code>1。

這是因為函數(shù)中的 "a" 并沒有通過 var 關(guān)鍵字聲明,所有不會被存放在 AO 中。

第一段執(zhí)行 console 的時候, AO 的值是:

AO = {
    arguments: {
        length: 0
    }
}

沒有 a 的值,然后就會到全局去找,全局也沒有,所以會報錯。

當(dāng)?shù)诙螆?zhí)行 console 的時候,全局對象已經(jīng)被賦予了 a 屬性,這時候就可以從全局找到 a 的值,所以會打印 1。

但是這個例子在非嚴格模式下才會成立,因為嚴格模式并不會主動幫你創(chuàng)建一個變量

再看看另一個例子

console.log(foo);

function foo(){
    console.log("foo");
}

var foo = 1;

會打印函數(shù),而不是 undefined 。

這是因為在進入執(zhí)行上下文時,首先會處理函數(shù)聲明,其次會處理變量聲明,如果如果變量名稱跟已經(jīng)聲明的形式參數(shù)或函數(shù)相同,則變量聲明不會干擾已經(jīng)存在的這類屬性。

作用域

在講解作用域鏈之前,先說說作用域

作用域是指程序源代碼中定義變量的區(qū)域。

作用域?qū)θ绾尾檎易兞窟M行了規(guī)定,也就是確定當(dāng)前執(zhí)行代碼對變量的訪問權(quán)限。

JavaScript 采用詞法作用域(lexical scoping),也就是靜態(tài)作用域。

編譯原理

我們都知道JavaScript是一門動態(tài)語言或是解釋性語言,但事實上它是一門編譯語言。

程序中一段源碼在執(zhí)行前虎易經(jīng)理三個步驟,統(tǒng)稱為“編譯”

  1. 分詞/詞法分析(Tokenizing/Lexing)

這個過程會將由字符組成的字符串分解成有意義的代碼塊,這些代碼塊被稱為詞法單元,例如:var = 2;。這段代碼會分解成var、a、=、2、;。如果詞法單元生成器在判斷a是一個獨立的分詞單元還是其他詞法單元的一部分時,調(diào)用的是有狀態(tài)的解析規(guī)則,那么這個過程就稱為詞法分析。

  1. 解析/語法分析(Parsing)

這個過程是將詞法單元流動(數(shù)組)轉(zhuǎn)漢城一個由元素所組成的代表了程序語法結(jié)構(gòu)的書。
這個書稱為“抽象語法樹(AST)”,var a = 2;的抽象語法樹,可能會有一個叫做VariableDeclearation的頂級節(jié)點,接下來是一個叫作Identifier(它的值是 a)的子節(jié)點,以及一個叫作AssignmentExpresstion的子節(jié)點,AssignmentExpresstion節(jié)點有一個叫作NumericLiteral(它的值是2)的子節(jié)點。

  1. 代碼生產(chǎn)

將AST轉(zhuǎn)換為可執(zhí)行代碼的過程為代碼生成

簡單來說,就是有某種方法將var a = 2; 的AST轉(zhuǎn)換為一組機器指令,用來創(chuàng)建一個叫作a的變量(包括分配內(nèi)存),并將一個值儲存在a中。

賦值操作

JavaScript在引擎中,變量的賦值操作會執(zhí)行兩個動作,首先編譯器會在當(dāng)前作用域中聲明一個變量(如果之前沒聲明過),然后在運行時引擎會在作用域中查找該變量,如果能夠找到就會給它賦值

在編譯器中的過程

先引入兩個名詞

RHS:負責(zé)查找某個變量的值

LHS:找到變量的容器本身,從而對其賦值

現(xiàn)在我們以console.log(a)為例,其中對a的引用進行是一個RHS引用,因為這里a并沒有賦予任何值。響應(yīng)地,需要查找并取得a的值,這樣值就傳遞給console.log()。

相比之下,例如:

a = 2;

這里對a的引用則是LHS的引用,因為實際上我們并不關(guān)心當(dāng)前的值是什么,只是想為= 2這個值操作找個一個目標或是容器

一個例子:

function foo(a){
  console.log(a + b)
}
var b = 2
foo(2)

首先會對b進行RHS查詢,無法在函數(shù)內(nèi)部獲得值,就會在上一級作用域查找,找到b之后再進行RHS查詢。就是說,如果該變量如果在該作用域沒有找到對應(yīng)的賦值,就會向上查找,直到找到對應(yīng)的賦值。

靜態(tài)作用域與動態(tài)作用域

我們大多使用的作用域是詞法作用域, 而函數(shù)的作用域在函數(shù)定義的時候就決定了。

而與詞法作用域相對的是動態(tài)作用域,函數(shù)的作用域是在函數(shù)調(diào)用的時候才決定的。

讓我們認真看個例子就能明白之間的區(qū)別:

var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar();

假設(shè)JavaScript采用靜態(tài)作用域,讓我們分析下執(zhí)行過程:

執(zhí)行 foo 函數(shù),先從 foo 函數(shù)內(nèi)部查找是否有局部變量 value,如果沒有,就根據(jù)書寫的位置,查找上面一層的代碼,也就是 value 等于 1,所以結(jié)果會打印 1。

假設(shè)JavaScript采用動態(tài)作用域,讓我們分析下執(zhí)行過程:

執(zhí)行 foo 函數(shù),依然是從 foo 函數(shù)內(nèi)部查找是否有局部變量 value。如果沒有,就從調(diào)用函數(shù)的作用域,也就是 bar 函數(shù)內(nèi)部查找 value 變量,所以結(jié)果會打印 2。

前面我們已經(jīng)說了,JavaScript采用的是靜態(tài)作用域,所以這個例子的結(jié)果是 1。

動態(tài)作用域

bash 就是動態(tài)作用域
例如:

value=1
function foo () {
    echo $value;
}
function bar () {
    local value=2;
    foo;
}
bar

作用域鏈

說完了作用域,終于到作用域鏈了。當(dāng)查找變量的時候,會先從當(dāng)前上下文的變量對象中查找,如果沒有找到,就會從父級(詞法層面上的父級)執(zhí)行上下文的變量對象中查找,一直找到全局上下文的變量對象,也就是全局對象。這樣由多個執(zhí)行上下文的變量對象構(gòu)成的鏈表就叫做作用域鏈。

下面,讓我們以一個函數(shù)的創(chuàng)建和激活兩個時期來講解作用域鏈是如何創(chuàng)建和變化的。

函數(shù)創(chuàng)建

函數(shù)的作用域在函數(shù)定義的時候就決定了。

這是因為函數(shù)有一個內(nèi)部屬性 [[scope]],當(dāng)函數(shù)創(chuàng)建的時候,就會保存所有父變量對象到其中,你可以理解 [[scope]] 就是所有父變量對象的層級鏈,但是需要注意:[[scope]] 并不代表完整的作用域鏈

舉個例子:


function foo() {
    function bar() {
        ...
    }
}

函數(shù)創(chuàng)建時,各自的[[scope]]為:

foo.[[scope]] = [
  globalContext.VO
];

bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
];

函數(shù)激活

當(dāng)函數(shù)激活時,進入函數(shù)上下文,創(chuàng)建 VO/AO后,就會將活動對象添加到作用鏈的前端。

這時候執(zhí)行上下文的作用域鏈,我們命名為Scope

Scope = [AO].concat([[Scope]]);

這樣我們就創(chuàng)建了一個作用域鏈。

重新思考

以下面的例子為例,結(jié)合著之前講的變量對象和執(zhí)行上下文棧,我們來總結(jié)一下函數(shù)執(zhí)行上下文中作用域鏈和變量對象的創(chuàng)建過程:

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();

執(zhí)行過程如下:

  1. checkscope 函數(shù)被創(chuàng)建,保存作用域鏈到內(nèi)部屬性[[scope]]
checkscope.[[scope]] = [
    globalContext.VO
];
  1. 執(zhí)行 checkscope 函數(shù),創(chuàng)建 checkscope 函數(shù)執(zhí)行上下文,checkscope 函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文棧
ECStack = [
    checkscopeContext,
    globalContext
];
  1. checkscope 函數(shù)并不立刻執(zhí)行,開始做準備工作,第一步:復(fù)制函數(shù)[[scope]]屬性創(chuàng)建作用域鏈
checkscopeContext = {
    Scope: checkscope.[[scope]],
}
  1. 第二步:用 arguments 創(chuàng)建活動對象,隨后初始化活動對象,加入形參、函數(shù)聲明、變量聲明
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: checkscope.[[scope]],
}
  1. 第三步:將活動對象壓入 checkscope 作用域鏈頂端
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}
  1. 準備工作做完,開始執(zhí)行函數(shù),隨著函數(shù)的執(zhí)行,修改 AO 的屬性值
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, [[Scope]]]
}
  1. 查找到 scope2 的值,返回后函數(shù)執(zhí)行完畢,函數(shù)上下文從執(zhí)行上下文棧中彈出
ECStack = [
    globalContext
];

this

好吧,現(xiàn)在在說說this的問題,總結(jié)性的東西,面試題都會刷到,我就不多說了,下面我講講面試不考的知識,說說this到底是什么

先看一段代碼

function foo() {
  var a = 2;
  this.bar();
}
function bar() {
  console.log( this.a );
}
foo(); 

聰明的同學(xué)肯定會發(fā)現(xiàn)會發(fā)現(xiàn)結(jié)果是undefined,在嚴格模式下會報錯,首先,這段代碼試圖通過 this.bar() 來引用 bar() 函數(shù)。但是調(diào)用 bar() 最自然的方法是省略前面的 this,直接使用詞法引用標識符。
此外,我們發(fā)現(xiàn)我們試圖通過內(nèi)部調(diào)用函數(shù)來改變詞法作用域,從而讓bar() 可以訪問 foo() 作用域里的變量 a。這是不可能實現(xiàn)的。this 是在運行時進行綁定的,并不是在編寫時綁定,它的上下文取決于函數(shù)調(diào)用時的各種條件。this 的綁定和函數(shù)聲明的位置沒有任何關(guān)系,只取決于函數(shù)的調(diào)用方式。
當(dāng)一個函數(shù)被調(diào)用時,會創(chuàng)建一個活動對象。這個對象會包含函數(shù)在哪里被調(diào)用、函數(shù)的調(diào)用方法、傳入的參數(shù)等信息。this 就是記錄的其中一個屬性,會在函數(shù)執(zhí)行的過程中用到。也就是說this在函數(shù)創(chuàng)建的時候,已經(jīng)形成了。

這樣執(zhí)行上下文的三個屬性就講完了,大概過程如圖所示:


image

回顧

上面我們把三大屬性就講解了一遍,下面說說以前做過的例子:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

兩段代碼都會打印'local scope'。雖然兩段代碼執(zhí)行的結(jié)果一樣,但是兩段代碼究竟有哪些不同呢?

具體執(zhí)行分析

我們分析第一段代碼:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

執(zhí)行過程如下:

  1. 執(zhí)行全局代碼,創(chuàng)建全局執(zhí)行上下文,全局上下文被壓入執(zhí)行上下文棧
    ECStack = [
        globalContext
    ];
  1. 全局上下文初始化
    globalContext = {
        VO: [global],
        Scope: [globalContext.VO],
        this: globalContext.VO
    }
  1. 初始化的同時,checkscope 函數(shù)被創(chuàng)建,保存作用域鏈到函數(shù)的內(nèi)部屬性[[scope]]
    checkscope.[[scope]] = [
      globalContext.VO
    ];
  1. 執(zhí)行 checkscope 函數(shù),創(chuàng)建 checkscope 函數(shù)執(zhí)行上下文,checkscope 函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文棧
    ECStack = [
        checkscopeContext,
        globalContext
    ];
  1. checkscope 函數(shù)執(zhí)行上下文初始化:
  • 復(fù)制函數(shù) [[scope]] 屬性創(chuàng)建作用域鏈,
  • 用 arguments 創(chuàng)建活動對象,
  • 初始化活動對象,即加入形參、函數(shù)聲明、變量聲明,
  • 將活動對象壓入 checkscope 作用域鏈頂端。

同時 f 函數(shù)被創(chuàng)建,保存作用域鏈到 f 函數(shù)的內(nèi)部屬性[[scope]]

    checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: reference to function f(){}
        },
        Scope: [AO, globalContext.VO],
        this: undefined
    }
  1. 執(zhí)行 f 函數(shù),創(chuàng)建 f 函數(shù)執(zhí)行上下文,f 函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文棧
    ECStack = [
        fContext,
        checkscopeContext,
        globalContext
    ];
  1. f 函數(shù)執(zhí)行上下文初始化, 以下跟第 4 步相同:
  • 復(fù)制函數(shù) [[scope]] 屬性創(chuàng)建作用域鏈
  • 用 arguments 創(chuàng)建活動對象
  • 初始化活動對象,即加入形參、函數(shù)聲明、變量聲明
  • 將活動對象壓入 f 作用域鏈頂端
    fContext = {
        AO: {
            arguments: {
                length: 0
            }
        },
        Scope: [AO, checkscopeContext.AO, globalContext.VO],
        this: undefined
    }
  1. f 函數(shù)執(zhí)行,沿著作用域鏈查找 scope 值,返回 scope 值

  2. f 函數(shù)執(zhí)行完畢,f 函數(shù)上下文從執(zhí)行上下文棧中彈出

    ECStack = [
        checkscopeContext,
        globalContext
    ];
  1. checkscope 函數(shù)執(zhí)行完畢,checkscope 執(zhí)行上下文從執(zhí)行上下文棧中彈出
    ECStack = [
        globalContext
    ];

ES5標準

ES5中在 我們改進了命名方式

  • 詞法環(huán)境(lexical environment)
  • 變量環(huán)境(variable environment)
  • this (this value)

所以執(zhí)行上下文在概念上表示如下:

ExecutionContext = {
  ThisBinding = <this value>,
  LexicalEnvironment = { ... },
  VariableEnvironment = { ... },
}

詞法環(huán)境

官方的 ES5 文檔把詞法環(huán)境定義為

詞法環(huán)境是一種規(guī)范類型,基于 ECMAScript 代碼的詞法嵌套結(jié)構(gòu)來定義標識符和具體變量和函數(shù)的關(guān)聯(lián)。一個詞法環(huán)境由環(huán)境記錄器和一個可能的引用外部詞法環(huán)境的空值組成。

簡單來說詞法環(huán)境是一種持有標識符—變量映射的結(jié)構(gòu)。(這里的標識符指的是變量/函數(shù)的名字,而變量是對實際對象[包含函數(shù)類型對象]或原始數(shù)據(jù)的引用)。
現(xiàn)在,在詞法環(huán)境的內(nèi)部有兩個組件:(1) 環(huán)境記錄器和 (2) 一個外部環(huán)境的引用。

環(huán)境記錄器是存儲變量和函數(shù)聲明的實際位置。
外部環(huán)境的引用意味著它可以訪問其父級詞法環(huán)境(作用域)。

詞法環(huán)境有兩種類型:

全局環(huán)境(在全局執(zhí)行上下文中)是沒有外部環(huán)境引用的詞法環(huán)境。全局環(huán)境的外部環(huán)境引用是 null。它擁有內(nèi)建的 Object/Array/等、在環(huán)境記錄器內(nèi)的原型函數(shù)(關(guān)聯(lián)全局對象,比如 window 對象)還有任何用戶定義的全局變量,并且 this的值指向全局對象。
在函數(shù)環(huán)境中,函數(shù)內(nèi)部用戶定義的變量存儲在環(huán)境記錄器中。并且引用的外部環(huán)境可能是全局環(huán)境,或者任何包含此內(nèi)部函數(shù)的外部函數(shù)。

環(huán)境記錄器也有兩種類型:

聲明式環(huán)境記錄器存儲變量、函數(shù)和參數(shù)。
對象環(huán)境記錄器用來定義出現(xiàn)在全局上下文中的變量和函數(shù)的關(guān)系。

簡而言之,

在全局環(huán)境中,環(huán)境記錄器是對象環(huán)境記錄器。
在函數(shù)環(huán)境中,環(huán)境記錄器是聲明式環(huán)境記錄器。

對于函數(shù)環(huán)境,聲明式環(huán)境記錄器還包含了一個傳遞給函數(shù)的 arguments 對象(此對象存儲索引和參數(shù)的映射)和傳遞給函數(shù)的參數(shù)的 length。
抽象地講,詞法環(huán)境在偽代碼中看起來像這樣:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在這里綁定標識符
    }
    outer: <null>
  }
}

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在這里綁定標識符
    }
    outer: <Global or outer function environment reference>
  }
}

變量環(huán)境

它同樣是一個詞法環(huán)境,其環(huán)境記錄器持有變量聲明語句在執(zhí)行上下文中創(chuàng)建的綁定關(guān)系。
如上所述,變量環(huán)境也是一個詞法環(huán)境,所以它有著上面定義的詞法環(huán)境的所有屬性。
在 ES6 中,詞法環(huán)境組件和變量環(huán)境的一個不同就是前者被用來存儲函數(shù)聲明和變量(let 和 const)綁定,而后者只用來存儲 var 變量綁定。
我們看點樣例代碼來理解上面的概念:

let a = 20;
const b = 30;
var c;

function multiply(e, f) {
 var g = 20;
 return e * f * g;
}

c = multiply(20, 30);
執(zhí)行上下文看起來像這樣:

GlobalExectionContext = {

  ThisBinding: <Global Object>,

  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在這里綁定標識符
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>
  },

  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在這里綁定標識符
      c: undefined,
    }
    outer: <null>
  }
}

FunctionExectionContext = {
  ThisBinding: <Global Object>,

  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在這里綁定標識符
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在這里綁定標識符
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>
  }
}

只有遇到調(diào)用函數(shù) multiply 時,函數(shù)執(zhí)行上下文才會被創(chuàng)建。
可能你已經(jīng)注意到 let 和 const 定義的變量并沒有關(guān)聯(lián)任何值,但 var 定義的變量被設(shè)成了 undefined。
這是因為在創(chuàng)建階段時,引擎檢查代碼找出變量和函數(shù)聲明,雖然函數(shù)聲明完全存儲在環(huán)境中,但是變量最初設(shè)置為 undefined(var 情況下),或者未初始化(let 和 const 情況下)。
這就是為什么你可以在聲明之前訪問 var 定義的變量(雖然是 undefined),但是在聲明之前訪問 let 和 const 的變量會得到一個引用錯誤。
這就是我們說的變量聲明提升。
執(zhí)行階段
這是整篇文章中最簡單的部分。在此階段,完成對所有這些變量的分配,最后執(zhí)行代碼。
注意 — 在執(zhí)行階段,如果 JavaScript 引擎不能在源碼中聲明的實際位置找到 let 變量的值,它會被賦值為 undefined。

總結(jié)

本篇文章對執(zhí)行上下文進行了深入的討論,也對不同的標準進行了大致的分析,意義在于略懂一些底層知識。說了那么多也寫不好代碼,知道個大概就好了。

JavaScript基礎(chǔ)專題系列

JavaScript基礎(chǔ)系列目錄地址:

JavaScript基礎(chǔ)專題之原型與原型鏈(一)

JavaScript基礎(chǔ)專題之執(zhí)行上下文和執(zhí)行棧(二)

新手寫作,如果有錯誤或者不嚴謹?shù)牡胤?,請大伙給予指正。如果這片文章對你有所幫助或者有所啟發(fā),還請給一個贊,鼓勵一下作者,在此謝過。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容