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

寫在開篇

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

正如前領導說的,本身喝酒吹過的牛皮,跪着都得含着淚去實現。前端

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

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


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

理解執行上下文

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

運行JavaScript代碼時,當代碼執行進入一個環境時,就會爲該環境建立一個執行上下文,它會在你運行代碼前作一些準備工做,如肯定做用域,建立局部變量對象等。編程

具體作了什麼先按下不表,先來看下JavaScript執行環境有哪些?數據結構

JavaScript中執行環境

  1. 全局環境
  2. 函數環境
  3. eval函數環境 (已不推薦使用)

那麼與之對應的執行上下文類型一樣有3種:閉包

執行上下文的類型

  1. 全局執行上下文
  2. 函數執行上下文
  3. eval函數執行上下文

JavaScript運行時首先會進入全局環境,對應會生成全局上下文。程序代碼中基本都會存在函數,那麼調用函數,就會進入函數執行環境,對應就會生成該函數的執行上下文。函數

先插播一個知識點:"JS是單線程"! "單線程"! "單線程"!post

簡單理解下單線程,就是同個時間段只能作一件任務,完成以後才能夠繼續下一個任務。正如女友只有一個,各位面向對象的小夥伴們大家說對不對?有女票的必須說沒毛病。

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

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. 函數調用時就會建立新的上下文,即便調用自身,也會建立不一樣的執行上下文

參考文檔:

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

相關文章
相關標籤/搜索