【進階1-2期】JavaScript深入之執行上下文棧和變數物件

木易楊說發表於2018-12-06

本期的主題是呼叫堆疊,本計劃一共28期,每期重點攻克一個面試重難點,如果你還不瞭解本進階計劃,文末點選檢視全部文章。

如果覺得本系列不錯,歡迎點贊、評論、轉發,您的支援就是我堅持的最大動力。


JS是單執行緒的語言,執行順序肯定是順序執行,但是JS 引擎並不是一行一行地分析和執行程式,而是一段一段地分析執行,會先進行編譯階段然後才是執行階段。

翠花,上程式碼

例子一:變數提升

foo;  // undefined
var foo = function () {
    console.log('foo1');
}

foo();  // foo1,foo賦值

var foo = function () {
    console.log('foo2');
}

foo(); // foo2,foo重新賦值
複製程式碼

例子二:函式提升

foo();  // foo2
function foo() {
    console.log('foo1');
}

foo();  // foo2

function foo() {
    console.log('foo2');
}

foo(); // foo2
複製程式碼

例子三:宣告優先順序,函式 > 變數

foo();  // foo2
var foo = function() {
    console.log('foo1');
}

foo();  // foo1,foo重新賦值

function foo() {
    console.log('foo2');
}

foo(); // foo1
複製程式碼

上面三個例子中,第一個例子是變數提升,第二個例子是函式提升,第三個例子是函式宣告優先順序高於變數宣告。

需要注意的是同一作用域下存在多個同名函式宣告,後面的會替換前面的函式宣告。

執行上下文

執行上下文總共有三種型別

  • 全域性執行上下文:只有一個,瀏覽器中的全域性物件就是 window 物件,this 指向這個全域性物件。
  • 函式執行上下文:存在無數個,只有在函式被呼叫的時候才會被建立,每次呼叫函式都會建立一個新的執行上下文。
  • Eval 函式執行上下文: 指的是執行在 eval 函式中的程式碼,很少用而且不建議使用。

這部分內容在【進階1-1期】中詳細介紹了,點選檢視【進階1-1期】理解JavaScript 中的執行上下文和執行棧

執行上下文棧

因為JS引擎建立了很多的執行上下文,所以JS引擎建立了執行上下文(Execution context stack,ECS)來管理執行上下文。

當 JavaScript 初始化的時候會向執行上下文棧壓入一個全域性執行上下文,我們用 globalContext 表示它,並且只有當整個應用程式結束的時候,執行棧才會被清空,所以程式結束之前, 執行棧最底部永遠有個 globalContext。

ECStack = [		// 使用陣列模擬棧
    globalContext
];
複製程式碼

具體執行過程如下圖所示,這部分內容在【進階1-1期】中詳細介紹了,點選檢視【進階1-1期】理解JavaScript 中的執行上下文和執行棧

【進階1-2期】JavaScript深入之執行上下文棧和變數物件

找不同

有如下兩段程式碼,執行的結果是一樣的,但是兩段程式碼究竟有什麼不同?

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

答案是 執行上下文棧的變化不一樣。

第一段程式碼:

ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
複製程式碼

第二段程式碼:

ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
複製程式碼

函式上下文

在函式上下文中,用活動物件(activation object, AO)來表示變數物件。

活動物件和變數物件的區別在於

  • 1、變數物件(VO)是規範上或者是JS引擎上實現的,並不能在JS環境中直接訪問。
  • 2、當進入到一個執行上下文後,這個變數物件才會被啟用,所以叫活動物件(AO),這時候活動物件上的各種屬性才能被訪問。

呼叫函式時,會為其建立一個Arguments物件,並自動初始化區域性變數arguments,指代該Arguments物件。所有作為引數傳入的值都會成為Arguments物件的陣列元素。

執行過程

執行上下文的程式碼會分成兩個階段進行處理

  • 1、進入執行上下文

  • 2、程式碼執行

進入執行上下文

很明顯,這個時候還沒有執行程式碼

此時的變數物件會包括(如下順序初始化):

  • 1、函式的所有形參 (only函式上下文):沒有實參,屬性值設為undefined。
  • 2、函式宣告:如果變數物件已經存在相同名稱的屬性,則完全替換這個屬性。
  • 3、變數宣告:如果變數名稱跟已經宣告的形參或函式相同,則變數宣告不會干擾已經存在的這類屬性。

上程式碼就直觀了

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

形參arguments這時候已經有賦值了,但是變數還是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、在程式碼執行階段,會再次修改變數物件的屬性值

參考

JavaScript深入之執行上下文棧
JavaScript深入之變數物件

進階系列目錄

  • 【進階1期】 呼叫堆疊
  • 【進階2期】 作用域閉包
  • 【進階3期】 this全面解析
  • 【進階4期】 深淺拷貝原理
  • 【進階5期】 原型Prototype
  • 【進階6期】 高階函式
  • 【進階7期】 事件機制
  • 【進階8期】 Event Loop原理
  • 【進階9期】 Promise原理
  • 【進階10期】Async/Await原理
  • 【進階11期】防抖/節流原理
  • 【進階12期】模組化詳解
  • 【進階13期】ES6重難點
  • 【進階14期】計算機網路概述
  • 【進階15期】瀏覽器渲染原理
  • 【進階16期】webpack配置
  • 【進階17期】webpack原理
  • 【進階18期】前端監控
  • 【進階19期】跨域和安全
  • 【進階20期】效能優化
  • 【進階21期】VirtualDom原理
  • 【進階22期】Diff演算法
  • 【進階23期】MVVM雙向繫結
  • 【進階24期】Vuex原理
  • 【進階25期】Redux原理
  • 【進階26期】路由原理
  • 【進階27期】VueRouter原始碼解析
  • 【進階28期】ReactRouter原始碼解析

交流

進階系列文章彙總:github.com/yygmind/blo…,內有優質前端資料,覺得不錯點個star。

我是木易楊,網易高階前端工程師,跟著我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高階前端的世界,在進階的路上,共勉!

【進階1-2期】JavaScript深入之執行上下文棧和變數物件

相關文章