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

前端進擊的巨人(一):執行上下文與執行棧,變量對象

推薦 調用函數 context 排隊機 全局環境 argument 面向對象 數量 結構

寫在開篇

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

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

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

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


技術分享圖片

理解執行上下文

執行上下文(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. 函數調用時就會創建新的上下文,即使調用自身,也會創建不同的執行上下文

參考文檔:

  • 執行上下文詳細圖解
  • 理解JavaScript 中的執行上下文和執行棧
  • 這一次,徹底弄懂 JavaScript 執行機制

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

前端進擊的巨人(一):執行上下文與執行棧,變量對象