1. 程式人生 > >前端進擊的巨人(一):執行上下文與執行棧,變數物件

前端進擊的巨人(一):執行上下文與執行棧,變數物件

寫在開篇

已經不敢自稱前端小白,曾經吹過的牛逼總要一點點去實現。

正如前領導說的,自己喝酒吹過的牛皮,跪著都得含著淚去實現。

那麼沒有年終完美總結,來個新年莽撞開始可好。

進擊巨人系列開篇,不忘初心,砥礪前行。


前端進擊的巨人(一):執行上下文與執行棧,變數物件

理解執行上下文

執行上下文(Execution Context): 函式執行前進行的準備工作(也稱執行上下文環境)

執行JavaScript程式碼時,當代碼執行進入一個環境時,就會為該環境建立一個執行上下文,它會在你執行程式碼前做一些準備工作,如確定作用域,建立區域性變數物件等。

具體做了什麼先按下不表,先來看下JavaScript執行環境有哪些?

JavaScript中執行環境

  1. 全域性環境
  2. 函式環境
  3. eval函式環境 (已不推薦使用)

那麼與之對應的執行上下文型別同樣有3種:

執行上下文的型別

  1. 全域性執行上下文
  2. 函式執行上下文
  3. eval函式執行上下文

JavaScript執行時首先會進入全域性環境,對應會生成全域性上下文。程式程式碼中基本都會存在函式,那麼呼叫函式,就會進入函式執行環境,對應就會生成該函式的執行上下文。

先插播一個知識點:"JS是單執行緒"! "單執行緒"! "單執行緒"!

簡單理解下單執行緒,就是同個時間段只能做一件任務,完成之後才可以繼續下一個任務。正如女朋友只有一個,各位面向物件的小夥伴們你們說對不對?有女票的必須說沒毛病。

既然是這樣,必須要有一個排隊機制,不然就會出現幾個流氓霸著車道不讓過,"還有王法麼?"

JS中管理多個執行上下文

函式程式設計中,程式碼中會宣告多個函式,對應的執行上下文也會存在多個。在JavaScript中,通過棧的存取方式來管理執行上下文,我們可稱其為執行棧,或函式呼叫棧(Call Stack)。

在說明執行棧前,先來補下"棧資料結構"知識點。

棧資料結構

乒乓球盒子理解棧儲存
藉助前端大神的例子,用乒乓球盒子來理解棧的存取方式。(這個例子讓我徹底記住了棧資料結構)

棧遵循"先進後出,後進先出"的規則,或稱LIFO ("Last In First Out") 規則。

如圖所示,我們只能從棧頂取出或放入乒乓球,最先放進盒子的總是最後才能取出。
棧中"放入/取出"

,也可稱為"入棧/出棧"

總結棧資料結構的特點:

  1. 後進先出,先進後出
  2. 出口在頂部,且僅有一個

執行棧(函式呼叫棧)

理解完棧的存取方式,我們接著分析JavaScript中如何通過棧來管理多個執行上下文。

程式執行進入一個執行環境時,它的執行上下文就會被建立,並被推入執行棧中(入棧);
程式執行完成時,它的執行上下文就會被銷燬,並從棧頂被推出(出棧),控制權交由下一個執行上下文。

因為JS執行中最先進入全域性環境,所以處於"棧底的永遠是全域性環境的執行上下文"。而處於"棧頂的是當前正在執行函式的執行上下文",當函式呼叫完成後,它就會從棧頂被推出(理想的情況下,閉包會阻止該操作,閉包後續文章深入詳解)。

"全域性環境只有一個,對應的全域性執行上下文也只有一個,只有當頁面被關閉之後它才會從執行棧中被推出,否則一直存在於棧底"

文字太多不如上程式碼系列 ——》程式碼 + 圖,一覽無遺:

function foo () {
    function bar () {
        return 'I am bar';
    }
    return bar();
}
foo();

出入棧過程

執行上下文的生命週期

執行上下文的生命週期有兩個階段:

  1. 建立階段(進入執行上下文)

  2. 執行階段(程式碼執行

建立階段:函式被呼叫時,進入函式環境,為其建立一個執行上下文,此時進入建立階段

執行階段:執行函式中程式碼時,此時執行上下文進入執行階段

建立階段的操作

  1. 建立變數物件
    • 函式環境會初始化建立Arguments物件(並賦值
    • 函式宣告(並賦值
    • 變數宣告,函式表示式宣告(未賦值
  2. 確定this指向(this由呼叫者確定
  3. 確定作用域(詞法環境決定,哪裡宣告定義,就在哪裡確定

執行階段的操作

  1. 變數物件賦值
    • 變數賦值
    • 函式表示式賦值
  2. 呼叫函式
  3. 順序執行其它程式碼

看到這裡,我們不經會問變數物件是什麼鬼,它與程式碼中常見的函式宣告,變數宣告有神馬關係???

變數物件和活動物件的區別:

當進入到一個執行上下文後,這個變數物件才會被啟用,所以叫活動物件(AO),這時候活動物件上的各種屬性才能被訪問。

"建立階段對函式宣告做賦值,變數及函式表示式僅做宣告,真正的賦值操作要等到執行上下文程式碼執行階段"

程式碼例子1:變數提升

function foo() {
  console.log(a);         // 輸出undefined
  var a = 'I am here';    // 賦值
}
foo();

// 實際執行過程
function foo() {
  var a;                // 變數宣告,var初始化undefined
  console.log(a); 
  a = 'I am here';     // 變數重新賦值
}

程式碼例子2:函式宣告優先順序

function foo() {
    console.log(bar);
    var bar = 20;
    function bar() {
      return 10;
    }
    var bar = function() {
        return 30;
    }
}
foo();  // 輸出bar()整個函式宣告

函式宣告,變數宣告,函式表示式的優先順序

  1. 函式宣告,如果有同名屬性,會替換掉
  2. 變數,函式表示式
  3. 函式宣告優先 > 變數,函式表示式

執行上下文的數量限制(堆疊溢位)

執行上下文可存在多個,雖然沒有明確的數量限制,但如果超出棧分配的空間,會造成堆疊溢位。常見於遞迴呼叫,沒有終止條件造成死迴圈的場景。

// 遞迴呼叫自身
function foo() {
  foo();
}
foo();

// 報錯: Uncaught RangeError: Maximum call stack size exceeded

文末總結

  1. JavaScript是單執行緒
  2. 棧頂的執行上下文處於執行中,其它需要排隊
  3. 全域性上下文只有一個處於棧底,頁面關閉時出棧
  4. 函式執行上下文可存在多個,但應避免遞迴時堆疊溢位
  5. 函式呼叫時就會建立新的上下文,即使呼叫自身,也會建立不同的執行上下文

參考文件:

作者:以樂之名
本文原創,有不當的地方歡迎指出。轉載請指明出處。