JavaScript 究竟是怎樣執行的?

来源:https://www.cnblogs.com/fundebug/archive/2019/06/24/how-does-javascript-execute.html
-Advertisement-
Play Games

摘要: 理解 JS 引擎運行原理。 作者:前端小智 原文: "搞懂 JavaScript 引擎運行原理" "Fundebug" 經授權轉載,版權歸原作者所有。 一些名詞 JS 引擎 — 一個讀取代碼並運行的引擎,沒有單一的“JS 引擎”;每個瀏覽器都有自己的引擎,如谷歌有 V。 作用域 — 可以從中 ...


摘要: 理解 JS 引擎運行原理。

Fundebug經授權轉載,版權歸原作者所有。

一些名詞

JS 引擎 — 一個讀取代碼並運行的引擎,沒有單一的“JS 引擎”;每個瀏覽器都有自己的引擎,如谷歌有 V。

作用域 — 可以從中訪問變數的“區域”。

詞法作用域— 在詞法階段的作用域,換句話說,詞法作用域是由你在寫代碼時將變數和塊作用域寫在哪裡來決定的,因此當詞法分析器處理代碼時會保持作用域不變。

塊作用域 — 由花括弧{}創建的範圍

作用域鏈 — 函數可以上升到它的外部環境(詞法上)來搜索一個變數,它可以一直向上查找,直到它到達全局作用域。

同步 — 一次執行一件事, “同步”引擎一次只執行一行,JavaScript 是同步的。

非同步 — 同時做多個事,JS 通過瀏覽器 API模擬非同步行為

事件迴圈(Event Loop) - 瀏覽器 API 完成函數調用的過程,將回調函數推送到回調隊列(callback queue),然後當堆棧為空時,它將回調函數推送到調用堆棧。

堆棧 —一種數據結構,只能將元素推入並彈出頂部元素。 想想堆疊一個字形的塔樓; 你不能刪除中間塊,後進先出。

— 變數存儲在記憶體中。

調用堆棧 — 函數調用的隊列,它實現了堆棧數據類型,這意味著一次可以運行一個函數。 調用函數將其推入堆棧並從函數返回將其彈出堆棧。

執行上下文 — 當函數放入到調用堆棧時由 JS 創建的環境。

閉包 — 當在另一個函數內創建一個函數時,它“記住”它在以後調用時創建的環境。

垃圾收集 — 當記憶體中的變數被自動刪除時,因為它不再使用,引擎要處理掉它。

變數的提升— 當變數記憶體沒有賦值時會被提升到全局的頂部並設置為undefined

this —由 JavaScript 為每個新的執行上下文自動創建的變數/關鍵字。

調用堆棧(Call Stack)

看看下麵的代碼:

var myOtherVar = 10;

function a() {
    console.log("myVar", myVar);
    b();
}

function b() {
    console.log("myOtherVar", myOtherVar);
    c();
}

function c() {
    console.log("Hello world!");
}

a();

var myVar = 5;

有幾個點需要註意:

  • 變數聲明的位置(一個在上,一個在下)
  • 函數a調用下麵定義的函數b, 函數 b 調用函數c

當它被執行時你期望發生什麼? 是否發生錯誤,因為ba之後聲明或者一切正常? console.log 列印的變數又是怎麼樣?

以下是列印結果:

"myVar" undefined
"myOtherVar" 10
"Hello world!"

來分解一下上述的執行步驟。

1. 變數和函數聲明(創建階段)

第一步是在記憶體中為所有變數和函數分配空間。 但請註意,除了undefined之外,尚未為變數分配值。 因此,myVar在被列印時的值是undefined,因為 JS 引擎從頂部開始逐行執行代碼。

函數與變數不一樣,函數可以一次聲明和初始化,這意味著它們可以在任何地方被調用。

所以以上代碼看起來像這樣子:

var myOtherVar = undefined
var myVar = undefined

function a() {...}
function b() {...}
function c() {...}

這些都存在於 JS 創建的全局上下文中,因為它位於全局空間中。

在全局上下文中,JS 還添加了:

  1. 全局對象(瀏覽器中是 window 對象,NodeJs 中是 global 對象)
  2. this 指向全局對象

2. 執行

接下來,JS 引擎會逐行執行代碼。

myOtherVar = 10`在全局上下文中,`myOtherVar`被賦值為`10

已經創建了所有函數,下一步是執行函數 a()

每次調用函數時,都會為該函數創建一個新的上下文(重覆步驟 1),並將其放入調用堆棧。

function a() {
    console.log("myVar", myVar);
    b();
}

如下步驟:

  1. 創建新的函數上下文
  2. a 函數裡面沒有聲明變數和函數
  3. 函數內部創建了 this 並指向全局對象(window)
  4. 接著引用了外部變數 myVarmyVar 屬於全局作用域的。
  5. 接著調用函數 b ,函數b的過程跟 a一樣,這裡不做分析。

下麵調用堆棧的執行示意圖:

  1. 創建全局上下文,全局變數和函數。
  2. 每個函數的調用,會創建一個上下文,外部環境的引用及 this
  3. 函數執行結束後會從堆棧中彈出,並且它的執行上下文被垃圾收集回收(閉包除外)。
  4. 當調用堆棧為空時,它將從事件隊列中獲取事件。

作用域及作用域鏈

在前面的示例中,所有內容都是全局作用域的,這意味著我們可以從代碼中的任何位置訪問它。 現在,介紹下私有作用域以及如何定義作用域。

函數/詞法作用域

考慮如下代碼:

function a() {
    var myOtherVar = "inside A";

    b();
}

function b() {
    var myVar = "inside B";

    console.log("myOtherVar:", myOtherVar);

    function c() {
        console.log("myVar:", myVar);
    }

    c();
}

var myOtherVar = "global otherVar";
var myVar = "global myVar";
a();

需要註意以下幾點:

  1. 全局作用域和函數內部都聲明瞭變數
  2. 函數c現在在函數b中聲明

列印結果如下:

myOtherVar: "global otherVar";
myVar: "inside B";

執行步驟:

  1. 全局創建和聲明 - 創建記憶體中的所有函數和變數以及全局對象和 this
  2. 執行 - 它逐行讀取代碼,給變數賦值,並執行函數 a
  3. 函數 a創建一個新的上下文並被放入堆棧,在上下文中創建變數myOtherVar,然後調用函數 b
  4. 函數 b 也會創建一個新的上下文,同樣也被放入堆棧中

5,函數b 的上下文中創建了 myVar 變數,並聲明函數 c

上面提到每個新上下文會創建的外部引用,外部引用取決於函數在代碼中聲明的位置。

  1. 函數 b試圖列印myOtherVar,但這個變數並不存在於函數 b中,函數 b 就會使用它的外部引用上作用域鏈向上找。由於函數 b是全局聲明的,而不是在函數 a內部聲明的,所以它使用全局變數 myOtherVar
  2. 函數 c執行步驟一樣。由於函數 c本身沒有變數myVar,所以它它通過作用域鏈向上找,也就是函數 b,因為myVar函數 b內部聲明過。

下麵是執行示意圖:

請記住,外部引用是單向的,它不是雙向關係。例如,函數 b不能直接跳到函數 c的上下文中並從那裡獲取變數。

最好將它看作一個只能在一個方向上運行的鏈(範圍鏈)。

  • a -> global
  • c -> b -> global

在上面的圖中,你可能註意到,函數是創建新作用域的一種方式。(除了全局作用域)然而,還有另一種方法可以創建新的作用域,就是塊作用域

塊作用域

下麵代碼中,我們有兩個變數和兩個迴圈,在迴圈重新聲明相同的變數,會列印什麼(反正我是做錯了)?

function loopScope() {
    var i = 50;
    var j = 99;

    for (var i = 0; i < 10; i++) {}

    console.log("i =", i);

    for (let j = 0; j < 10; j++) {}

    console.log("j =", j);
}

loopScope();

列印結果:

i = 10;
j = 99;

第一個迴圈覆蓋了var i,對於不知情的開發人員來說,這可能會導致 bug。

第二個迴圈,每次迭代創建了自己作用域和變數。 這是因為它使用let關鍵字,它與var相同,只是let有自己的塊作用域。 另一個關鍵字是const,它與let相同,但const常量且無法更改(指記憶體地址)。

塊作用域由大括弧 {} 創建的作用域

再看一個例子:

function blockScope() {
    let a = 5;
    {
        const blockedVar = "blocked";
        var b = 11;

        a = 9000;
    }

    console.log("a =", a);
    console.log("b =", b);
    console.log("blockedVar =", blockedVar);
}

blockScope();

列印結果:

a = 9000
b = 11
ReferenceError: blockedVar is not defined
  1. a是塊作用域,但它在函數中,而不是嵌套的,本例中使用var是一樣的。
  2. 對於塊作用域的變數,它的行為類似於函數,註意var b可以在外部訪問,但是const blockedVar不能。
  3. 在塊內部,從作用域鏈向上找到 a 並將let a更改為9000

使用塊作用域可以使代碼更清晰,更安全,應該儘可能地使用它。

事件迴圈(Event Loop)

接下來看看事件迴圈。 這是回調,事件和瀏覽器 API 工作的地方

我們沒有過多討論的事情是,也叫全局記憶體。它是變數存儲的地方。由於瞭解 JS 引擎是如何實現其數據存儲的實際用途並不多,所以我們不在這裡討論它。

來個非同步代碼:

function logMessage2() {
    console.log("Message 2");
}

console.log("Message 1");

setTimeout(logMessage2, 1000);

console.log("Message 3");

上述代碼主要是將一些 message 列印到控制台。 利用setTimeout函數來延遲一條消息。 我們知道 js 是同步,來看看輸出結果

Message 1
Message 3
Message 2
  1. 列印 Message 1
  2. 調用 setTimeout
  3. 列印 Message 3
  4. 列印 Message 2

它記錄消息 3

稍後,它會記錄消息 2

setTimeout是一個 API,和大多數瀏覽器 API 一樣,當它被調用時,它會向瀏覽器發送一些數據和回調。我們這邊是延遲一秒列印 Message 2

調用完setTimeout 後,我們的代碼繼續運行,沒有暫停,列印 Message 3 並執行一些必須先執行的操作。

瀏覽器等待一秒鐘,它就會將數據傳遞給我們的回調函數並將其添加到事件/回調隊列中( event/callback queue)。 然後停留在隊列中,只有當調用堆棧(call stack)為空時才會被壓入堆棧。

代碼示例

要熟悉 JS 引擎,最好的方法就是使用它,再來些有意義的例子。

簡單的閉包

這個例子中 有一個返回函數的函數,併在返回的函數中使用外部的變數, 這稱為閉包

function exponent(x) {
    return function(y) {
        //和math.pow() 或者x的y次方是一樣的
        return y ** x;
    };
}

const square = exponent(2);

console.log(square(2), square(3)); // 4, 9

console.log(exponent(3)(2)); // 8

塊代碼

我們使用無限迴圈將將調用堆棧塞滿,會發生什麼,回調隊列被會阻塞,因為只能在調用堆棧為空時添加回調隊列。

function blockingCode() {
    const startTime = new Date().getSeconds();

    // 延遲函數250毫秒
    setTimeout(function() {
        const calledAt = new Date().getSeconds();
        const diff = calledAt - startTime;

        // 列印調用此函數所需的時間
        console.log(`Callback called after: ${diff} seconds`);
    }, 250);

    // 用迴圈阻塞堆棧2秒鐘
    while (true) {
        const currentTime = new Date().getSeconds();

        // 2 秒後退出
        if (currentTime - startTime >= 2) break;
    }
}

blockingCode(); // 'Callback called after: 2 seconds'

我們試圖在250毫秒之後調用一個函數,但因為我們的迴圈阻塞了堆棧所花了兩秒鐘,所以回調函數實際是兩秒後才會執行,這是 JavaScript 應用程式中的常見錯誤

setTimeout不能保證在設置的時間之後調用函數。相反,更好的描述是,在至少經過這段時間之後調用這個函數。

延遲函數

setTimeout 的設置為 0,情況是怎麼樣?

function defer() {
    setTimeout(() => console.log("timeout with 0 delay!"), 0);
    console.log("after timeout");
    console.log("last log");
}

defer();

你可能期望它被立即調用,但是,事實並非如此。

執行結果:

after timeout
last log
timeout with 0 delay!

它會立即被推到回調隊列,但它仍然會等待調用堆棧為空才會執行。

用閉包來緩存

Memoization是緩存函數調用結果的過程。

例如,有一個添加兩個數字的函數add。調用add(1,2)返回3,當再次使用相同的參數add(1,2)調用它,這次不是重新計算,而是記住 1 + 2是3的結果並直接返回對應的結果。 Memoization可以提高代碼運行速度,是一個很好的工具。

我們可以使用閉包實現一個簡單的memoize函數。

// 緩存函數,接收一個函數
const memoize = func => {
    // 緩存對象
    // keys 是 arguments, values are results
    const cache = {};

    // 返回一個新的函數
    // it remembers the cache object & func (closure)
    // ...args is any number of arguments
    return (...args) => {
        // 將參數轉換為字元串,以便我們可以存儲它
        const argStr = JSON.stringify(args);

        // 如果已經存,則列印
        console.log("cache", cache, !!cache[argStr]);

        cache[argStr] = cache[argStr] || func(...args);

        return cache[argStr];
    };
};

const add = memoize((a, b) => a + b);

console.log("first add call: ", add(1, 2));

console.log("second add call", add(1, 2));

執行結果:

cache {} false
first add call:  3
cache { '[1,2]': 3 } true
second add call 3

第一次 add 方法,緩存對象是空的,它調用我們的傳入函數來獲取值3.然後它將args/value鍵值對存儲在緩存對象中。

在第二次調用中,緩存中已經有了,查找到並返回值。

對於add函數來說,有無緩存看起來無關緊要,甚至效率更低,但是對於一些複雜的計算,它可以節省很多時間。這個示例並不是一個完美的緩存示例,而是閉包的實際應用。

關於Fundebug

Fundebug專註於JavaScript、微信小程式、微信小游戲、支付寶小程式、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有陽光保險、核桃編程、荔枝FM、掌門1對1、微脈、青團社等眾多品牌企業。歡迎大家免費試用

img

版權聲明

轉載時請註明作者Fundebug以及本文地址:https://blog.fundebug.com/2019/06/24/how-does-javascript-execute/


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • [toc] d2 admin基本使用 官方演示: "https://d2admin.fairyever.com/ /index" 官方文檔: "https://doc.d2admin.fairyever.com/zh/" 1 安裝 1.1 全局安裝 d2 admin 1.2 創建項目 或者 之後選擇 ...
  • 最近做項目,使用的是echarts顯示圖表數據,但是數據量比較多的時候,有卡頓的情況。後來同事拿echarts和HighCharts做了對比,僅供大家參考。同時感謝同事做的工作。 一、查詢1天的源數據,屬性1、屬性2、屬性3、屬性4 Echarts查詢3.61s,渲染0.786s(約8.6M數據) ...
  • 在jqGrid中設置multiselect: true可以實現全選的操作,但怎麼設置被選中的checkbox裡面的值呢,做法如下:jQuery("#listTable").jqGrid({ url: 'queryList.do', datatype: 'json', colNames: ['','編 ...
  • 我們在學習JavaScript,或其他任何編碼技能的時候,往往是因為這些攔路虎而裹足不前: 有些概念可能會造成混淆,尤其當你是從其他語言轉過來的時候。 找不到時間(有時是動力)學習。 很容易忘記已經理解了的東西。 工具多又在不斷變化,所以不知道從哪裡開始。 幸運的是,這些攔路虎是可以被識別,並消滅的 ...
  • 很多人說js 閉包是必須要學的,類似於c++,java的封裝型。比如為多個元素註冊點擊事件那些。那些代碼是大致意思是能看懂。但是按照自己的想法改動了一些代碼,那些事件就不成功了,可能就是沒有對閉包理解到。那就先學一個focus事件的閉包案例吧,先大致瞭解一些用法,以及對閉包的初認識。以後有其他閉包知... ...
  • Vue – 基礎學習(3):事件修飾符 ...
  • 任何一門語言中,相信this的指向問題都是一個重點,js也不例外。 js中的作用域分為全局作用域和局部作用域,在全局作用域中,this指向的是他的全局對象window,如下 下麵看一下this在局部作用域(函數)中的指向 在函數中,this的指向大致分為四種: 一、在普通函數中this指向windo ...
  • 在2010年上半年的蘋果與Adobe的衝突,使HTML5的存在一夜之間被很多人所知曉。在喬布斯的煽動下,這一已經在科技界潛行數年的下一代Web標準,被迅速拎到了臺面上,蘋果、谷歌、微軟這科技界三巨頭,連同眾多業界明星,似乎突然對HTML5變得情有獨鍾,利益集團的之間的爭奪,成了這個技術最好的催化劑。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...