JavaScript基礎專題之深刻執行上下文(三)

對於ES3每一個執行上下文,都有三個重要屬性:前端

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

這篇咱們來聊聊這三個重要屬性面試

變量對象

變量對象做爲執行上下文的一種屬性,每次建立後,根據執行環境不一樣上下文下的變量對象也稍有不一樣,咱們比較熟悉的就是全局對象函數對象,因此咱們來聊聊全局上下文下的變量對象和函數上下文下的變量對象。數組

全局上下文

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

全局對象是預約義的對象,做爲 JavaScript 的全局函數和全局屬性的佔位符。經過使用全局對象,能夠訪問全部其餘全部預約義的對象、函數和屬性。dom

在頂層 JavaScript 代碼中,能夠用關鍵字 this 引用全局對象。由於全局對象是做用域鏈的頭,這意味着全部非限定性的變量和函數名都會做爲該對象的屬性來查詢。函數

例如,當JavaScript 代碼引用 parseInt() 函數時,它引用的是全局對象的 parseInt 屬性。全局對象是做用域鏈的頭,還意味着在頂層 JavaScript 代碼中聲明的全部變量都將成爲全局對象的屬性。post

咱們能夠根據代碼理解ui

  1. 能夠經過 this 引用,在客戶端 JavaScript 中,全局對象就是 Window 對象。
console.log(this); //window
複製代碼
  1. 全局對象是由 Object 構造函數實例化的一個對象。
console.log(this instanceof Object);//true
複製代碼
  1. 咱們調用的一些方法都在window下。
console.log(Math.random());
console.log(this.Math.random());
複製代碼

4.做爲全局變量的宿主。this

var a = 1;
console.log(this.a);
複製代碼

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

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

this.window.b = 2;
console.log(this.b);//2
複製代碼

咱們發現全局上下文中的變量對象就是全局對象

函數上下文

在函數上下文中,不一樣於全局上下文比較死板,咱們用活動對象(activation object, AO)來表示變量對象。

因此活動對象和變量對象實際上是一個東西,只是變量對象是規範上或者說是引擎實現上不可在 JavaScript 環境中直接訪問,只有到當進入一個執行上下文中,這個執行上下文的變量對象纔會被激活,因此稱爲activation object,只有在激活狀態纔會對屬性進行訪問。

活動對象是在進入函數上下文時刻被建立的,它經過函數的 arguments屬性初始化。arguments 屬性值是 Arguments 對象。

執行過程

執行上下文的代碼會分紅兩個階段進行處理:分析和執行,咱們也能夠叫作:

  1. 進入執行上下文
  2. 代碼執行

進入執行上下文

當進入執行上下文時,這時候尚未執行代碼,

變量對象會包括:

  1. 函數的全部形參 (若是是函數上下文)

    • 由名稱和對應值組成的一個變量對象的屬性被建立
    • 沒有實參,屬性值設爲 undefined
  2. 函數聲明

    • 由名稱和對應值(函數對象(function-object))組成一個變量對象的屬性被建立
    • 若是變量對象已經存在相同名稱的屬性,則徹底替換這個屬性
  3. 變量聲明

    • 由名稱和對應值(undefined)組成一個變量對象的屬性被建立
    • 若是變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性

舉個例子:

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

}

foo(1);
複製代碼

在進入執行上下文後,這時候的 AO 是:

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

代碼執行

在代碼執行階段,會順序執行代碼,根據代碼,修改變量對象的值

仍是上面的例子,當代碼執行完後,這時候的 AO 是:

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

到這裏變量對象的建立過程就介紹完了,讓咱們簡潔的總結咱們上述所說:

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

例子

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

foo(); // ???

function bar() {
    a = 1;
    console.log(a);
}
bar(); // ???
複製代碼

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

第二段會打印:1

這是由於函數中的 "a" 並無經過 var 關鍵字聲明,全部不會被存放在 AO 中。

第一段執行 console 的時候, AO 的值是:

AO = {
    arguments: {
        length: 0
    }
}
複製代碼

沒有 a 的值,而後就會到全局去找,全局也沒有,因此會報錯。

當第二段執行 console 的時候,全局對象已經被賦予了 a 屬性,這時候就能夠從全局找到 a 的值,因此會打印 1。

可是這個例子在非嚴格模式下才會成立,由於嚴格模式並不會主動幫你建立一個變量

再看看另外一個例子

console.log(foo);

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

var foo = 1;
複製代碼

會打印函數,而不是 undefined 。

這是由於在進入執行上下文時,首先會處理函數聲明,其次會處理變量聲明,若是若是變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性。

做用域

在講解做用域鏈以前,先說說做用域

做用域是指程序源代碼中定義變量的區域。

做用域對如何查找變量進行了規定,也就是肯定當前執行代碼對變量的訪問權限。

JavaScript 採用詞法做用域(lexical scoping),也就是靜態做用域。

編譯原理

咱們都知道JavaScript是一門動態語言或是解釋性語言,但事實上它是一門編譯語言。

程序中一段源碼在執行前虎易經理三個步驟,統稱爲「編譯」

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

這個過程會將由字符組成的字符串分解成有意義的代碼塊,這些代碼塊被稱爲詞法單元,例如:var = 2;。這段代碼會分解成var、a、=、二、;。若是詞法單元生成器在判斷a是一個獨立的分詞單元仍是其餘詞法單元的一部分時,調用的是有狀態的解析規則,那麼這個過程就稱爲詞法分析。

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

這個過程是將詞法單元流動(數組)轉漢城一個由元素所組成的表明了程序語法結構的書。 這個書稱爲「抽象語法樹(AST)」,var a = 2;的抽象語法樹,可能會有一個叫作VariableDeclearation的頂級節點,接下來是一個叫做Identifier(它的值是 a)的子節點,以及一個叫做AssignmentExpresstion的子節點,AssignmentExpresstion節點有一個叫做NumericLiteral(它的值是2)的子節點。

  1. 代碼生產

將AST轉換爲可執行代碼的過程爲代碼生成

簡單來講,就是有某種方法將var a = 2; 的AST轉換爲一組機器指令,用來建立一個叫做a的變量(包括分配內存),並將一個值儲存在a中。

賦值操做

JavaScript在引擎中,變量的賦值操做會執行兩個動做,首先編譯器會在當前做用域中聲明一個變量(若是以前沒聲明過),而後在運行時引擎會在做用域中查找該變量,若是可以找到就會給它賦值

在編譯器中的過程

先引入兩個名詞

RHS:負責查找某個變量的值

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

如今咱們以console.log(a)爲例,其中對a的引用進行是一個RHS引用,由於這裏a並無賦予任何值。響應地,須要查找並取得a的值,這樣值就傳遞給console.log()。

相比之下,例如:

a = 2;

這裏對a的引用則是LHS的引用,由於實際上咱們並不關心當前的值是什麼,只是想爲= 2這個值操做找個一個目標或是容器

一個例子:

function foo(a){
  console.log(a + b)
}
var b = 2
foo(2)
複製代碼

首先會對b進行RHS查詢,沒法在函數內部得到值,就會在上一級做用域查找,找到b以後再進行RHS查詢。就是說,若是該變量若是在該做用域沒有找到對應的賦值,就會向上查找,直到找到對應的賦值。

靜態做用域與動態做用域

咱們大多使用的做用域是詞法做用域, 而函數的做用域在函數定義的時候就決定了。

而與詞法做用域相對的是動態做用域,函數的做用域是在函數調用的時候才決定的。

讓咱們認真看個例子就能明白之間的區別:

var value = 1;

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

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

bar();

複製代碼

假設JavaScript採用靜態做用域,讓咱們分析下執行過程:

執行 foo 函數,先從 foo 函數內部查找是否有局部變量 value,若是沒有,就根據書寫的位置,查找上面一層的代碼,也就是 value 等於 1,因此結果會打印 1。

假設JavaScript採用動態做用域,讓咱們分析下執行過程:

執行 foo 函數,依然是從 foo 函數內部查找是否有局部變量 value。若是沒有,就從調用函數的做用域,也就是 bar 函數內部查找 value 變量,因此結果會打印 2。

前面咱們已經說了,JavaScript採用的是靜態做用域,因此這個例子的結果是 1。

動態做用域

bash 就是動態做用域 例如:

value=1
function foo () {
    echo $value;
}
function bar () {
    local value=2;
    foo;
}
bar
複製代碼

做用域鏈

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

下面,讓咱們以一個函數的建立和激活兩個時期來說解做用域鏈是如何建立和變化的。

函數建立

函數的做用域在函數定義的時候就決定了。

這是由於函數有一個內部屬性 [[scope]],當函數建立的時候,就會保存全部父變量對象到其中,你能夠理解 [[scope]] 就是全部父變量對象的層級鏈,可是須要注意:[[scope]] 並不表明完整的做用域鏈

舉個例子:

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

複製代碼

函數建立時,各自的[[scope]]爲:

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

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

複製代碼

函數激活

當函數激活時,進入函數上下文,建立 VO/AO後,就會將活動對象添加到做用鏈的前端。

這時候執行上下文的做用域鏈,咱們命名爲Scope

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

複製代碼

這樣咱們就建立了一個做用域鏈。

從新思考

如下面的例子爲例,結合着以前講的變量對象和執行上下文棧,咱們來總結一下函數執行上下文中做用域鏈和變量對象的建立過程:

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

執行過程以下:

  1. checkscope 函數被建立,保存做用域鏈到內部屬性[[scope]]
checkscope.[[scope]] = [
    globalContext.VO
];
複製代碼
  1. 執行 checkscope 函數,建立 checkscope 函數執行上下文,checkscope 函數執行上下文被壓入執行上下文棧
ECStack = [
    checkscopeContext,
    globalContext
];
複製代碼
  1. checkscope 函數並不馬上執行,開始作準備工做,第一步:複製函數[[scope]]屬性建立做用域鏈
checkscopeContext = {
    Scope: checkscope.[[scope]],
}
複製代碼
  1. 第二步:用 arguments 建立活動對象,隨後初始化活動對象,加入形參、函數聲明、變量聲明
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: checkscope.[[scope]],
}
複製代碼
  1. 第三步:將活動對象壓入 checkscope 做用域鏈頂端
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}
複製代碼
  1. 準備工做作完,開始執行函數,隨着函數的執行,修改 AO 的屬性值
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, [[Scope]]]
}
複製代碼
  1. 查找到 scope2 的值,返回後函數執行完畢,函數上下文從執行上下文棧中彈出
ECStack = [
    globalContext
];
複製代碼

this

好吧,如今在說說this的問題,總結性的東西,面試題都會刷到,我就很少說了,下面我講講面試不考的知識,說說this究竟是什麼

先看一段代碼

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

聰明的同窗確定會發現會發現結果是undefined,在嚴格模式下會報錯,首先,這段代碼試圖經過 this.bar() 來引用 bar() 函數。可是調用 bar() 最天然的方法是省略前面的 this,直接使用詞法引用標識符。 此外,咱們發現咱們試圖經過內部調用函數來改變詞法做用域,從而讓bar() 能夠訪問 foo() 做用域裏的變量 a。這是不可能實現的。this 是在運行時進行綁定的,並非在編寫時綁定,它的上下文取決於函數調用時的各類條件。this 的綁定和函數聲明的位置沒有任何關係,只取決於函數的調用方式。 當一個函數被調用時,會建立一個活動對象。這個對象會包含函數在哪裏被調用、函數的調用方法、傳入的參數等信息。this 就是記錄的其中一個屬性,會在函數執行的過程當中用到。也就是說this在函數建立的時候,已經造成了。

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

回顧

上面咱們把三大屬性就講解了一遍,下面說說之前作過的例子:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
複製代碼

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

具體執行分析

咱們分析第一段代碼:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
複製代碼

執行過程以下:

  1. 執行全局代碼,建立全局執行上下文,全局上下文被壓入執行上下文棧
ECStack = [
        globalContext
    ];
複製代碼
  1. 全局上下文初始化
globalContext = {
        VO: [global],
        Scope: [globalContext.VO],
        this: globalContext.VO
    }
複製代碼
  1. 初始化的同時,checkscope 函數被建立,保存做用域鏈到函數的內部屬性[[scope]]
checkscope.[[scope]] = [
      globalContext.VO
    ];
複製代碼
  1. 執行 checkscope 函數,建立 checkscope 函數執行上下文,checkscope 函數執行上下文被壓入執行上下文棧
ECStack = [
        checkscopeContext,
        globalContext
    ];
複製代碼
  1. checkscope 函數執行上下文初始化:
  • 複製函數 [[scope]] 屬性建立做用域鏈,
  • 用 arguments 建立活動對象,
  • 初始化活動對象,即加入形參、函數聲明、變量聲明,
  • 將活動對象壓入 checkscope 做用域鏈頂端。

同時 f 函數被建立,保存做用域鏈到 f 函數的內部屬性[[scope]]

checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: reference to function f(){}
        },
        Scope: [AO, globalContext.VO],
        this: undefined
    }
複製代碼
  1. 執行 f 函數,建立 f 函數執行上下文,f 函數執行上下文被壓入執行上下文棧
ECStack = [
        fContext,
        checkscopeContext,
        globalContext
    ];
複製代碼
  1. f 函數執行上下文初始化, 如下跟第 4 步相同:
  • 複製函數 [[scope]] 屬性建立做用域鏈
  • 用 arguments 建立活動對象
  • 初始化活動對象,即加入形參、函數聲明、變量聲明
  • 將活動對象壓入 f 做用域鏈頂端
fContext = {
        AO: {
            arguments: {
                length: 0
            }
        },
        Scope: [AO, checkscopeContext.AO, globalContext.VO],
        this: undefined
    }
複製代碼
  1. f 函數執行,沿着做用域鏈查找 scope 值,返回 scope 值

  2. f 函數執行完畢,f 函數上下文從執行上下文棧中彈出

ECStack = [
        checkscopeContext,
        globalContext
    ];
複製代碼
  1. checkscope 函數執行完畢,checkscope 執行上下文從執行上下文棧中彈出
ECStack = [
        globalContext
    ];
複製代碼

ES5標準

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

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

因此執行上下文在概念上表示以下:

ExecutionContext = {
  ThisBinding = <this value>,
  LexicalEnvironment = { ... },
  VariableEnvironment = { ... },
}
複製代碼

詞法環境

官方的 ES5 文檔把詞法環境定義爲

詞法環境是一種規範類型,基於 ECMAScript 代碼的詞法嵌套結構來定義標識符和具體變量和函數的關聯。一個詞法環境由環境記錄器和一個可能的引用外部詞法環境的空值組成。

簡單來講詞法環境是一種持有標識符—變量映射的結構。(這裏的標識符指的是變量/函數的名字,而變量是對實際對象[包含函數類型對象]或原始數據的引用)。 如今,在詞法環境的內部有兩個組件:(1) 環境記錄器和 (2) 一個外部環境的引用。

環境記錄器是存儲變量和函數聲明的實際位置。 外部環境的引用意味着它能夠訪問其父級詞法環境(做用域)。

詞法環境有兩種類型:

全局環境(在全局執行上下文中)是沒有外部環境引用的詞法環境。全局環境的外部環境引用是 null。它擁有內建的 Object/Array/等、在環境記錄器內的原型函數(關聯全局對象,好比 window 對象)還有任何用戶定義的全局變量,而且 this的值指向全局對象。 在函數環境中,函數內部用戶定義的變量存儲在環境記錄器中。而且引用的外部環境多是全局環境,或者任何包含此內部函數的外部函數。

環境記錄器也有兩種類型:

聲明式環境記錄器存儲變量、函數和參數。 對象環境記錄器用來定義出如今全局上下文中的變量和函數的關係。

簡而言之,

在全局環境中,環境記錄器是對象環境記錄器。 在函數環境中,環境記錄器是聲明式環境記錄器。

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

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

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

變量環境

它一樣是一個詞法環境,其環境記錄器持有變量聲明語句在執行上下文中建立的綁定關係。 如上所述,變量環境也是一個詞法環境,因此它有着上面定義的詞法環境的全部屬性。 在 ES6 中,詞法環境組件和變量環境的一個不一樣就是前者被用來存儲函數聲明和變量(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);
執行上下文看起來像這樣:

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>
  }
}
複製代碼

只有遇到調用函數 multiply 時,函數執行上下文才會被建立。 可能你已經注意到 let 和 const 定義的變量並無關聯任何值,但 var 定義的變量被設成了 undefined。 這是由於在建立階段時,引擎檢查代碼找出變量和函數聲明,雖然函數聲明徹底存儲在環境中,可是變量最初設置爲 undefined(var 狀況下),或者未初始化(let 和 const 狀況下)。 這就是爲何你能夠在聲明以前訪問 var 定義的變量(雖然是 undefined),可是在聲明以前訪問 let 和 const 的變量會獲得一個引用錯誤。 這就是咱們說的變量聲明提高。 執行階段 這是整篇文章中最簡單的部分。在此階段,完成對全部這些變量的分配,最後執行代碼。 注意 — 在執行階段,若是 JavaScript 引擎不能在源碼中聲明的實際位置找到 let 變量的值,它會被賦值爲 undefined。

總結

本篇文章對執行上下文進行了深刻的討論,也對不一樣的標準進行了大體的分析,意義在於略懂一些底層知識。說了那麼多也寫很差代碼,知道個大概就行了。

JavaScript基礎專題系列

JavaScript基礎系列目錄地址:

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

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

新手寫做,若是有錯誤或者不嚴謹的地方,請大夥給予指正。若是這片文章對你有所幫助或者有所啓發,還請給一個贊,鼓勵一下做者,在此謝過。

相關文章
相關標籤/搜索