夯實基礎上篇-圖解 JavaScript 執行機制

我不是大熊哦發表於2022-04-09

前言

講基礎不容易,本文希望通過 9 個 demo 和 18 張圖,和大家一起學習或溫故 JavaScript 執行機制,本文大綱:

  1. hoisting 是什麼
  2. 一段 JavaScript 程式碼是怎樣被執行的
  3. 呼叫棧是什麼

文末有總結大圖

如果對本文有什麼疑問或發現什麼錯漏的地方,可在評論區留言~

本文是夯實基礎系列的上篇,請關注接下來的中篇~

如果對你有幫助,希望三連~

hoisting 是什麼

先來個總結圖壓壓驚~

image.png

正文開始~

提問環節:下面這段程式碼列印什麼?為什麼?

    showSinger()
    console.log('第1次列印:', singer)
    var singer = 'Jaychou'
    console.log('第2次列印:', singer)
    function showSinger() { 
      console.log('showSinger函式')
    }

答案是:

image.png
showSinger 函式正常執行,第 1 次列印 singer 變數是 undefined,第 2 次列印 singer 變數是Jaychou,看上去 var 變數 singer 和函式宣告 showSinger 被提升了,像是下面的模擬:

    // 函式宣告被提升了
    function showSinger() { 
      console.log('showSinger函式')
    }
    // var 變數被提升了
    var singer = undefined

    showSinger()
    console.log('第1次列印:', singer) // undefined
    singer = 'Jaychou'
    console.log('第2次列印:', singer) // Jaychou

在 JavaScript 裡,這種現象被稱為 hoisting 提升:var 宣告的變數會提升和函式宣告會提升,在執行程式碼之前會先被新增到執行上下文的頂部。

關於提升的細節

  1. let 變數和 const 變數不會被提升,只能在宣告變數之後才能使用,宣告之前被稱為“暫時性死區”,以下程式碼會報錯:
    console.log('列印:', singer)
    let singer = 'Jaychou'

image.png
2. 在全域性執行上下文宣告的 var 變數會成為 window 物件的屬性,let 變數和 const 變數不會

    var singer = 'Jaychou'
    console.log(window.singer) // Jaychou

    let age = 40
    console.log(window.age) // undefined
  1. var 宣告是函式作用域,let 宣告和 const 宣告是塊作用域
    if (true) {
      var singer = 'Jaychou'
      console.log(singer) // Jaychou
    }
    console.log(singer) // Jaychou

    if (true) {
      let age = 40
      console.log(age) // 40
    }
    // 報錯:Uncaught ReferenceError: age is not defined
    console.log(age);
  1. let 不允許同一個塊作用域中出現冗餘宣告,會報錯,var 宣告則允許有重複宣告
    let age;
    // Uncaught SyntaxError: Identifier 'age' has already been declared
    var age;
    
    var age = 10
    var age = 20
    var age = 30
    console.log(age) // 正常列印 30
  1. 函式宣告會被提升,函式表示式不會(除了函式什麼時候真正有定義這個區別之外,這兩種語法是等價的)
    // 沒有問題,因為 sum 函式有被提升
    console.log(sum(10, 10)); 
    // 函式宣告
    function sum(num1, num2) {
      return num1 + num2;
    } 

    // 會報錯: Uncaught TypeError: sum1 is not a function
    // 因為 sum1 函式不會被提升
    console.log(sum1(10, 10));
    // 函式表示式
    var sum1 = function (num1, num2) {
      return num1 + num2;
    };

提升發生在什麼時候

都在說提升,那這個步驟是發生在什麼時候?執行程式碼之前嗎?

這就引出了下面這個問題:一段 JavaScript 程式碼是怎樣被執行的?

一段 JavaScript 程式碼是怎樣被執行的

提問環節:下面的 html 裡的 JavaScript 程式碼是怎樣被執行的?

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    showSinger()
    var singer = 'Jaychou'
    console.log(singer)

    function showSinger() {
      console.log('showSinger函式')
    }
  </script>
</body>
</html>

簡述:html 和 css 部分會被瀏覽器的渲染引擎拿來渲染,進行計算dom樹、計算style、計算佈局、計算分層、計算繪製等等一系列的渲染操作,而 JavaScript 程式碼的執行由 JavaScript 引擎負責。

市面上的 JavaScript 引擎有很多,例如 SpiderMonkey、V8、JavaScriptCore 等,可以簡單理解成 JavaScript 引擎將人類能夠理解的程式語言 JavaScript,翻譯成機器能夠理解的機器語言,大致流程是:

image.png
是的,在執行之前,會有編譯階段,而不是直接就執行了。

編譯階段

輸入一段程式碼,經過編譯後,會生成兩部分內容:執行上下文和可執行程式碼,執行上下文就是剛才提的那個執行上下文,它是執行一段 JavaScript 程式碼時的執行環境。

image.png
執行上下文具體的分類和對應的建立時機如下:

image.png

而執行上下文具體包括什麼內容,怎樣存放剛才提的 var 宣告變數、函式宣告、以及 let 和 const 宣告變數請看:

image.png

執行上下文案例分析

結合下面的案例來具體分析:

    var a = 1 // var 宣告
    let b = 2 // let 宣告
    { 
      let b = 3 // let 宣告
      var c = 4 // var 宣告
      let d = 5 // let 宣告
      console.log(a)
      console.log(b) 
    } 
    console.log(b) 
    console.log(c)
    // 函式宣告
    function add(num1, num2){
      return num1 + num2
    }

第一步是編譯上面的全域性程式碼,並建立全域性執行上下文:

image.png

  • var 宣告的變數在編譯階段放到了變數環境,例如 a 和 c;
  • 函式宣告在編譯階段放到了變數環境,例如 add 函式;
  • let 宣告的變數在編譯階段放到了詞法環境,例如 b(不包括其內部的塊作用域)
  • 內部的塊作用域的 let 宣告還沒做處理

接下來是執行程式碼,執行程式碼到塊{}裡面時,a 已被設定成 1,b 已被設定成 2,塊作用域裡的 b 和 d 作為新的一層放在詞法環境裡
image.png
詞法環境內部的小型棧結構,棧底是函式最外層的變數,進入一個塊作用域後,就把該塊作用域內部的變數壓到棧頂;當作用域執行完成之後,該作用域的資訊就會從棧頂彈出。

繼續執行,執行到console.log(a);console.log(b);時,進入變數查詢過程:沿著詞法環境的棧頂向下查詢,如果在詞法環境中的某個塊中查詢到了,就直接返回給 JavaScript 引擎,如果沒有查詢到,那麼繼續在變數環境中查詢,所以塊作用域裡面的 b 會找到 3:
image.png
當塊作用域執行結束之後,其內部定義的變數就會從詞法環境的棧頂彈出,最終執行上下文如下:

image.png

這個過程不清楚的同學可以多看幾次案例,有不明白的可以在評論區討論~

呼叫棧是什麼

剛才聊到,函式執行上下文的建立時機在函式被呼叫時,它的過程是取出函式體的程式碼 》對這段程式碼進行編譯 》建立該函式的執行上下文和可執行程式碼 》執行程式碼輸出結果,其中編譯和建立執行上下文的過程和剛才演示的對全域性程式碼的處理類似。

而呼叫棧就是用來管理函式呼叫關係的一種資料結構,在執行上下文建立好後,JavaScript 引擎會將執行上下文壓入棧中。

呼叫棧案例分析

    var a = 2
    function add(b, c) {
      return b + c
    }
    function addAll(b, c) {
      var d = 10
      var result = add(b, c)
      return a + result + d
    }
    addAll(3, 6)

第一步,建立全域性上下文,並將其壓入棧底

image.png
接下來執行程式碼,a = 2 把 a 從 undefined 設為 2

第二步,呼叫 addAll 函式,會編譯該函式,併為其建立一個執行上下文,將該函式的執行上下文壓入棧中

image.png
接下來執行 addAll 函式的程式碼,把 d 置為 10,然後執行 add 函式

第三步,呼叫 add 函式,為其建立執行上下文,並壓入棧中

image.png
當 add 函式返回時,該函式的執行上下文就會從棧頂彈出,並將 result 的值設定為 add 函式的返回值,也就是 9

image.png
addAll 執行最後一個相加操作後並返回,addAll 的執行上下文也會從棧頂部彈出,此時呼叫棧中就只剩下全域性上下文

image.png
這就是呼叫棧經歷的過程~

而平時開發過程中,打斷點除錯就可以看到 Call Stack 呼叫棧了,比如剛才的 add 函式裡打個斷點:

image.png

總結

image.png
本文串聯了宣告提升、JavaScript 編譯和執行、呼叫棧,來講述 JavaScript 執行機制,希望有幫助到大家~

本文是夯實基礎系列的上篇,預告正在碼字的中篇:作用域鏈 + 閉包 + this。

相關文章