iT邦幫忙

10

JS 執行環境解析

js

JS 執行環境

筆記性質
菜鳥初學,為了避免誤人子弟,請大神留步勘誤 QQ

建議搭配服用: JS Scope / Hoisting

執行環境的基礎

  • 以 Function 做為劃分不同執行環境
  • Function 於呼叫後,會先建立執行環境,才執行內部程式碼
  • 只要有 call function 就會建立執行環境,包括遞迴、whileLoop
  • 只有一個 Global Context (全域環境)

執行環境的種類

  • 全域 Global : 程式開始執行的預設環境
  • 函式 : 開始跑函式內部程式碼的環境
  • Eval
    把一串字串當作指令執行,會先將字串解析成JS,因此效能不佳 (ESLint 不建議使用)

執行環境 堆疊

  • Browser JS 為 單執行緒
    因此每次只能執行一件事,當一個事件在執行時,其他會被丟到執行佇列中。
  • Event Loop 概念 (待整理....)

環境執行流程

  • Call Stack 特性為 後進先出
  1. 會先建立 Global Context (全域執行環境) 於 Call Stack 當中
  2. 執行 a()
  3. 建立 a() 的執行環境,並放入 Call Stack當中
  4. 等 a() 的內容執行完畢後,會從 CallStack 回收掉
  5. 執行 Global Context 的 console.log()
    此段不懂可以去此看精美的圖
function a() {
  var c = 1
}
a()
console.log('123')

執行環境 內容

概念上我們可以把一個 執行環境 想像成一個物件內包含:

  1. Scope Chain [註1]
    負責記錄每個環境之間切換的關聯
    記錄包含 自己的 VO + 所有上層執行環境的 VO 的集合。
  2. VariableObject (VO)
    負責記錄執行環境中定義的變數、函式、參數,
    於建立階段初始化,執行階段給與値
  3. this
  executionContextObject = {
    scopeChain: {},
    variableObject: {},
    this: {}
  }

建立執行環境的背後

  1. 建立 Global 全域環境

執行 Function 前,建立執行該Function的環境 分為兩階段

建立階段

  • 建立階段,初始化這個環境

  • 除 arguments 外都只是先定義變數 & 函式指標,並沒有賦值

  • 進行 Hoisting

  • 初始化 scope chain

  • 判斷決定 this 的值 (因此 this 在函式呼叫時才被決定)

  • 建 variableObject

  • 建 arguments object 並給予值

    • 僅參數的值 會在建立階段就給與
    • 檢查傳入參數,初始化參數的名稱,值以及建立參考
  • 將 function 的宣告 加入 variableObject

    • 函式宣告給予指標
  • 將 變數 的宣告 加入 variableObject

    • 變數 / 函式宣告 初始化值為 undefined (沒有賦值)

執行階段

給與値、設定 Function 的參考、逐行解譯執行程式碼

  function foo(i) {
    var a = 'hello';
    
    // 建立階段 函式表達 不給與 值/指標
    var b = function B() { };
    
    // 建立階段 函式宣告 給與指標
    function c() { } 
  }
  foo(22);

  // 建立階段 / 執行階段
  fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
      // arguments object
      // 0: 表第一個傳入的參數 1: 表2......
      //! 參數的值 會在建立階段就給與。
      arguments: { 0: 22, length: 1 },
      i: 22,
      a: undefined, // 未給與値
      b: undefined,  // 函式運算 不給與値
      c: pointer to function c(), // 函式宣告 給與指標

      // 執行階段才給與値 進行覆寫建立階段建立的變數 / 其他不變
      a: 'hello',
      b: pointer to function B()
    },
    this: { ... }
  }

建立階段 Function 的不同行為

定義函式又分為三種

  • 透過 new Function 關鍵字建立函式 (ESLint不建議,不討論)
  • a 函式運算式 Function Expression
    函式表達式,需要等待至執行階段才會給與值。
var a =  function () {}
  • b 函式宣告式 Function Declaration
    函式宣告會於建立階段,賦予指標指向該 Function
function b() { }

實際範例

  ;(function () {
  
    // 建立階段 a 為 函式運算式 未给值 undefined
    // 建立階段 b 為 函式宣告 給予指標 pointer to function b()
    console.log(typeof a) // undefined
    console.log(typeof b) // function
    
    
    // 執行階段 給與值 pointer to function a()
    var a =  function () {} 
    console.log(typeof a) // function 
    
    // 執行階段 給與值 進行覆寫
    var b = 123 
    console.log(typeof b) // number 
    
    function b() { }
    
    // 執行階段 給值 a : pointer to function a()
    // 執行階段 給與值 b : 123
  }())

建立階段 函數宣告 & 變數宣告 的優先權

  • 建立階段: Hoisting 函數宣告 優先於 變數宣告
  // Hoisting 函數宣告 優先於 變數宣告 (不論 Code相對位置)
  function a() {}
  var a // 並沒有重新給值,因此 執行階段此行不會執行
  console.log(typeof a) // function

  // 於執行階段給予值
  var b = 1
  function b() {}
  console.log(typeof b) // number

練習題

請解釋

  • Why 可在宣告前存取 foo [Hoisting]
  • Foo 被宣告 2 次,為什麼 foo 是 function 而不是 undefined 或 string ?
  • Why bar is undefined ?
  (function () {
    console.log("foo: " + typeof foo)
    var foo = 'hey'
    console.log("foo: " + typeof foo)
    function foo() { return 'hello' }

    console.log("bar: " + typeof bar)
    var bar = function() { return 'world' }
    console.log("bar: " + typeof bar)
  }())
Ans: 1.function / 2.string / 3.undefined / 4.function
// 建立階段: 1.函式宣告 給座標、3.函式運算 不給值
// 執行階段: 2. var foo = 'hey'  foo 給值為 string
// 執行階段: 4. var bar = func~  bar 給值為 function
  • Why 可在宣告前存取 foo
    在建立階段我們就已經將變數建立了 [Hoisting]

  • Foo 被宣告 2 次,為什麼 foo 是 function 而不是 undefined 或 string ?
    第一個 log()

  1. 建立階段
    函式宣告 給與 指標 // foo: pointer to function foo()
  2. 執行階段 var foo = 'hey'
    進行覆寫建立階段的變數,因此 foo: 'Hey'

第二個 log()
因為執行階段會給於值,因此 foo 被改寫成 str 'hey'

  • Why bar is undefined ?
    建立階段還未給與値,因此為 undefined
    執行階段 才會給與値

C() 執行結果為何? Why?

  var c = 1
  function c() {
    console.log(typeof c)
  }
  console.log(c)
  c()
  Ans: c is not a function  
  console.log(c) // 1
  // 建立階段 Hoisting c = function pointer
  // 執行階段給予值 先讀 c = 1 後執行 c() 此時 c 給值為 1
  // 因此 c is not a function

參考資料

andyyou
DavidShariff
PJChENder


0
神Q超人
iT邦新手 1 級 ‧ 2018-06-01 19:31:53

文章很用心,希望可以繼續打下去/images/emoticon/emoticon32.gif

RocMark iT邦新手 5 級 ‧ 2018-06-02 23:00:00 檢舉

感謝支持 !

1

大大的文章很棒,小弟學到很多新知識,不過有個地方覺得困惑,分享一下,
文中有提到 建立階段: Hoisting 函數宣告 優先於 變數宣告
可是在建立階段,執行順序是:

  • 將 function 的宣告 加入 variableObject
  • 將 變數 的宣告 加入 variableObject

如果按照後壓前的邏輯,建立階段 變數宣告 應該是會蓋過 函數宣告 的。

小弟後來查了一下,發現在建立階段處理變數的過程中,
會將變數加入 variableObject,並初始化為 undefined,
不過如果變數名稱已經存在,會略過不處理,

如果是這樣就可以解釋為什麼,Hoisting 函數宣告 優先於 變數宣告
不知道小弟的猜測對不對,還是有其他原因和這個無關呢,哈哈哈。

/images/emoticon/emoticon19.gif

看更多先前的回應...收起先前的回應...

我來串一下門子XD,大大提出的下面這句話是對的!
不過如果變數名稱已經存在,會略過不處理,
所以才會有函數宣告 優先於 變數宣告現象!
一直到執行階段,相同的變數名稱再一次被賦值為止/images/emoticon/emoticon13.gif

RocMark iT邦新手 5 級 ‧ 2018-06-02 22:58:58 檢舉

我是通過測試下列的 Code 得出的 Hoisting函數宣告優先於變數宣告的結論。

下列 a,b情況 都沒有進行賦値,所以都不會在執行階段進行覆寫變數。
因此所有宣告 a,b 變數的動作應該都是在建立階段執行的。
不論下面 a 情況或 b情況,變數、函式程式碼的位置順序不同,回傳結果都是 Function。
而得出 Hoisting函數宣告優先於變數宣告這個結論。

var a
function a() {}
console.log(typeof a) // function

function b() {}
var b
console.log(typeof b) // function

以上是個人見解沒有文件 Support,
可以的話提供一下 "如果變數名稱已經存在,會略過不處理" 的出處。
我蠻想再去深入了解的 ~
我只是個菜鳥 大家互相交流交流 !

RocMark iT邦新手 5 級 ‧ 2018-06-02 23:33:58 檢舉

補個資料
詳細解說 Variable object,最上方有翻譯版本可以選擇。
我也來研究一下..

不好意思,文章中的程式碼借我用一下XD

  (function () {
    console.log("foo: " + typeof foo) //(1)
    var foo = 'hey'
    console.log("foo: " + typeof foo) //(2)
    function foo() { return 'hello' }
    console.log("foo: " + typeof foo) //(3)
  }())

以下環境建立的時候,會先看有沒有帶入參數,再來偵測function的宣告找到function foo(){},所以在(1)的console.log會印出function類型,然後會再去尋找有沒有宣告變數,這時後發現有var foo = 'hey',不過會因為foo這個名稱已經被指標建立function所以他就會略過這個變數,因此foo不會在(1)的時候被這行蓋過變成印出undefined,環境就在這時候建立完畢了!
進入執行階段foo被賦值為'hey'所以才在(2)的時候會印出string!
可以再加碼看個(3),因為再執行階段的時候foo已經被賦值成'hey',所以雖然再下方還是有function foo(){}宣告,但這時候(3)依然會印出string,因為執行階段在建立階段之後。

所以RocMark大大文章說的沒有錯,再建立階段函數宣告優先於變數宣告,而fysh711426大說的也對,因為指標再function身上,所以會忽略後面再宣告的var foo,就是變數名稱已經存在,會略過不處理

啊因為我也還再學習,所以以上如果有問題也可以提出討論!我也不一定是對的,因為我也很菜XD

RocMark 我偷看了參考資料 andyyou /images/emoticon/emoticon25.gif
https://ithelp.ithome.com.tw/upload/images/20180603/201068652e6JTT9AiA.jpg

發現 補充資料 裡面也有寫
https://ithelp.ithome.com.tw/upload/images/20180603/20106865cEcZ0rFFjH.jpg

原來是測試出來的,大大真的很用心,我也是好奇有沒有文件 Support,
不過現在有上面這兩段就可以解釋,為什麼 函數宣告優先於變數宣告建立階段的執行順序 不衝突的原因了。
/images/emoticon/emoticon42.gif

神Q超人 大大講的好詳細,建立階段的邏輯我今天才知道,原來您是這方面的專家。
/images/emoticon/emoticon32.gif

哈哈哈,也不算專家,只是很喜歡JS所以之前買一堆書回來看/images/emoticon/emoticon33.gif
不過樓主真的超級猛,居然自己測試出那麼多東西!/images/emoticon/emoticon12.gif

RocMark iT邦新手 5 級 ‧ 2018-06-03 22:21:34 檢舉

感謝兩位的支援 XD 又多學了一點東西/images/emoticon/emoticon12.gif

1
fillano
iT邦超人 1 級 ‧ 2018-06-05 15:49:51

執行環境是指Execution Context?如果是的話,除了Function,記得還有Global跟Eval這兩種。Global你好像有提到一些就是了。

文章寫的很詳細,加油阿!!!

RocMark iT邦新手 5 級 ‧ 2018-06-05 16:02:44 檢舉

執行環境是指Execution Context 沒錯

Eval 我本身有用ESLint在檢查代碼,看了一下ESLint的 Rule,是說不建議使用,因為效能不佳,目前我都只在自學,不是很清楚在實務上會不會用到,所以就先略過了。

至於 Global 環境,運行原理應該與 Function 相同,如有誤歡迎大大幫補充 XD

RocMark iT邦新手 5 級 ‧ 2018-06-05 16:25:16 檢舉

補充個Global的資料以便日後複習

我要留言

立即登入留言