【譯】JavaScript 是如何工做的(上)

原文地址:How JavaScript Works. Why understanding the fundamentals is… | by Ionel Hindorean | Better Programmingjavascript

爲何咱們要理解基本原理java

你可能想知道爲何有人會在 2019 年費心寫一篇關於 JavaScript 核心的長文。git

這是由於我相信,若是沒有對基礎知識的紮實瞭解,很容易在JS生態系統中迷失方向,並且幾乎不可能探索更高級的內容。github

瞭解 JavaScript 的工做原理可使閱讀和編寫代碼變得更容易,減小挫折感,讓你專一於你的應用程序的邏輯,而不是與語言的語法做鬥爭。瀏覽器

它是如何工做的?

計算機不懂JavaScript,瀏覽器才懂。服務器

除了處理網絡請求、監聽鼠標點擊、解釋 HTML 和 CSS 以在屏幕上繪製像素外,瀏覽器還內置有一個 JavaScript 引擎。markdown

JavaScript 引擎是一個用 C++ 編寫的程序,它逐字逐句地查看全部的 JavaScript 代碼,並將其 "轉化 "爲計算機 CPU 可以理解和執行的東西--機器代碼網絡

這個過程是同步進行的,也就是說,他們在一條時間線上,並且是按順序進行。多線程

他們這樣作是由於機器代碼很難,並且不一樣的 CPU 製造商的機器代碼指令是不一樣的。因此,他們把全部這些麻煩從JavaScript 開發者那裏抽象出來,不然,網絡開發會更難,更不受歡迎,咱們也不會有像 Medium 這樣的東西,讓咱們能夠寫像這樣的文章(而我如今就在睡覺)。app

JavaScript 引擎能夠機器的地瀏覽每一行 JavaScript,一遍又一遍(解釋器),或者它能夠變得更聰明,檢測出一些東西,好比常常被調用而且老是產生相同結果的函數。

而後,它能夠把這些東西編譯成機器代碼,只需一次,這樣下次遇到它時,它就會運行已經編譯好的代碼,這就快多了(及時編譯)。

或者,它能夠提早將整個東西編譯成機器代碼,而後執行(見編譯器)。

V8 就是這樣一個 JavaScript 引擎,谷歌在2008年將其開源。2009年,一個叫 Ryan Dahl 的人想到用 V8 來建立 Node.js,這是一個在瀏覽器以外的 JavaScript 運行環境,這意味着該語言也能夠用於服務器端應用。

函數執行上下文

像其餘語言同樣,JavaScript 對於函數、變量、數據類型,以及這些數據類型能夠存儲的確切數值,在代碼中哪些地方能夠訪問,哪些地方不能夠,等等都有本身的規則。

這些規則由一個名爲 Ecma International 的組織定義標準,它們共同構成了語言規範文件(你能夠在這裏找到最新版本)。

所以,當引擎將 JavaScript 代碼轉換爲機器代碼時,它須要考慮這些規範。

若是代碼中包含一個非法的賦值,或者它試圖訪問一個變量,而根據語言的規範,這個變量不該該從代碼的特定部分被訪問,怎麼辦?

每次函數被調用時,它都須要弄清全部這些事情。它經過建立一個被稱爲 "執行上下文 "的包裝來實現這一目的。

爲了更具體一些,避免未來出現混淆,我將把這個稱爲函數執行上下文,由於每次調用函數都會建立一個。不要被這個術語所嚇倒,暫時不要想太多,後面會詳細說明。

只要記住,它決定了一些事情,好比。"在那個特定的函數中,哪些變量是能夠訪問的,在它裏面這個值是什麼,哪些變量和函數在它裏面被聲明?"

全局執行上下文

可是,並非全部的 JavaScript 代碼都在一個函數裏面(儘管大部分代碼都在裏面)。

在任何函數以外,在全局層面上也可能有代碼,所以,JavaScript 引擎首先要作的一件事就是建立一個全局執行上下文。

這就像一個函數執行上下文,在全局層面上起到一樣的做用,但它有一些特殊性。

好比,有且只有一個全局執行上下文,在執行開始時建立,全部的 JavaScript 代碼都在其中運行。

全局執行上下文建立了兩個東西,這兩個東西對它來講是特定的,即便沒有代碼要執行。

  • 一個全局對象。當 JavaScript 在瀏覽器內運行時,這個對象是窗口對象。當它在瀏覽器外運行時,就像在 Node.js 中那樣,它將是相似 global 的對象。不過爲了簡單起見,我將在本文中使用 window

  • 一個特殊的變量 this

在全局執行上下文中,也只有 this,這實際上等於全局對象 window。它基本上是一個對 window 的引用。

this === window // logs true
複製代碼

全局執行上下文和函數執行上下文之間的另外一個微妙區別是,任何在全局層面上聲明的變量或函數(在任何函數以外),都會自動做爲屬性附加到窗口對象上,並隱含在特殊變量 this 上。

儘管函數也有特殊變量 this,但在函數執行環境中不會發生這種狀況。

foo; // 'bar'
window.foo; // 'bar'
this.foo; // 'bar'
(window.foo === foo && this.foo === foo && window.foo === this.foo) // true
複製代碼

全部的 JavaScript 內置變量和函數都附着在全局窗口對象上: setTimeout(), localStorage, scrollTo(), Math, fetch(),等等。這就是爲何它們能夠在代碼的任何地方被訪問。

執行棧

咱們知道,每次函數被調用時都會建立一個函數執行上下文。

因爲即便是最簡單的 JavaScript 程序也有至關多的函數調用,全部這些函數執行上下文都須要以某種方式進行管理。

請看下面的例子:

function a() {
  // some code
}

function b() {
  // some code
}

a();
b();
複製代碼

當遇到函數 a() 的調用時,如上所述建立一個函數執行上下文,並執行該函數內的代碼。

當代碼的執行完成後返回語句或到達函數的包圍},函數 a() 的函數執行上下文被銷燬。

而後,會遇到 b() 的調用,對函數 b() 重複一樣的過程。

但這種狀況不多發生,即便在很是簡單的 JavaScript 程序中。大多數狀況下,會有一些函數在其餘函數中被調用:

function a() {
  // some code
  b();
  // some more code
}

function b() {
  // some code
}

a();
複製代碼

在這種狀況下,a() 的函數執行上下文被建立,但就在 a() 的執行過程當中,遇到了 b() 的調用。

b() 建立了一個全新的函數執行上下文,可是沒有破壞 a() 的執行上下文,由於它的代碼尚未徹底執行。

這意味着在同一時間有許多函數執行上下文。然而,在任什麼時候候,它們中只有一個在實際運行。

爲了跟蹤當前正在運行的函數,咱們使用了一個堆棧,其中當前正在運行的函數執行上下文位於棧的頂部。

一旦它執行完畢,它將被從堆棧中彈出,下一個執行上下文的執行將繼續,以此類推,直到執行堆棧爲空。

這個棧被稱爲執行棧,以下圖所示:

image.png

當執行堆棧爲空時,咱們以前討論過的、從未被銷燬的全局執行上下文就成爲當前運行的執行上下文。

事件隊列

還記得我說過,JavaScript 引擎只是瀏覽器的一個組件,與渲染引擎或網絡層並列。

這些組件都有內置的 Hooks,引擎用這些 Hooks 來通訊,以啓動網絡請求,在屏幕上繪製像素,或者監聽鼠標點擊。

當你在 JavaScript 中使用相似 fetch 的東西來作一個 HTTP 請求時,引擎實際上會將其傳達給網絡層。每當請求的響應到來時,網絡層將把它傳回給 JavaScript 引擎。

但這可能須要幾秒鐘的時間,當請求正在進行時,JavaScript 引擎會作什麼?

簡單地中止執行任何代碼,直到響應到來?繼續執行剩下的代碼,每當響應到來時,就中止一切並執行其回調?當回調完成後,繼續執行它離開的地方?

以上都不是,儘管第一個能夠經過使用 await 來實現。

在多線程語言中,這能夠經過一個線程在當前運行的執行環境中執行代碼,另外一個線程執行事件的回調來處理。但這在 JavaScript 中是不可能的,由於它是單線程的。

爲了理解這其實是如何工做的,讓咱們考慮一下咱們以前看過的 a()b() 函數,可是增長一個點擊處理程序和一個 HTTP 請求處理程序。

function a() {
  // some code
  b();
  // some more code
}

function b() {
  // some code
}

function httpHandler() {
  // some code here
}

function clickHandler() {
  // some more code here
}

a();
複製代碼

JavaScript 引擎從瀏覽器的其餘組件收到的任何事件,如鼠標點擊或網絡響應,都不會被當即處理。

在這一點上,JavaScript 引擎可能正忙於執行代碼,因此它將把事件放在一個隊列中,稱爲事件隊列。

image.png

咱們已經談過了執行棧,以及一旦相應函數中的代碼執行完畢,當前運行的函數執行上下文是如何從堆棧中彈出的。

而後,下一個執行上下文恢復執行,直到它完成,以此類推,直到堆棧爲空,全局執行上下文成爲當前運行的執行上下文。

當執行棧中有代碼要執行時,事件隊列中的事件被忽略,由於引擎正忙於執行棧中的代碼。

只有當它完成了,而且執行棧是空的,JavaScript 引擎纔會處理事件隊列中的下一個事件(固然,若是有的話),而且會調用它的處理程序。

因爲這個處理程序是一個 JavaScrip t函數,它的處理就像 a()b() 的處理同樣,也就是說,一個函數的執行上下文被建立並推到執行棧中。

若是該處理程序反過來調用另外一個函數,那麼另外一個函數的執行上下文就會被建立並推到堆棧的頂部,以此類推。 只有當執行棧再次爲空時,JavaScript 引擎纔會再次檢查事件隊列中的新事件。

這一樣適用於鍵盤和鼠標事件。當鼠標被點擊時,JavaScript 引擎會獲得一個點擊事件,把它放在事件隊列中,只有當執行棧爲空時纔會執行它的處理程序。

你能夠經過把下面的代碼複製到你的瀏覽器控制檯,輕鬆地看到這個過程:

function documentClickHandler() {
  console.log('CLICK!!!');
}

document.addEventListener('click', documentClickHandler);

function a() {
  const fiveSecondsLater = new Date().getTime() + 5000;
  while (new Date().getTime() < fiveSecondsLater) {}
}

a();
複製代碼

while 循環只是讓引擎忙碌五秒鐘,不用太擔憂。在這五秒鐘內開始點擊文檔上的任何地方,你會看到沒有任何東西被記錄到控制檯。

當五秒鐘過去,執行棧爲空時,第一次點擊的處理程序被調用。

因爲這是一個函數,一個函數執行上下文被建立,推送到堆棧,執行,並從堆棧中彈出。而後,第二次點擊的處理程序被調用,以此類推。

實際上,setTimeout()(和 setInterval() )的狀況也是如此。你提供給 setTimeout() 的處理程序實際上被放在事件隊列中。

這意味着,若是你將超時設置爲 0,但執行堆棧上還有代碼要執行,那麼 setTimeout() 的處理程序只有在堆棧爲空時纔會被調用,這多是許多毫秒以後。

setTimeout(() => {
  console.log('TIMEOUT HANDLER!!!');
}, 0);

const fiveSecondsLater = new Date().getTime() + 5000;
while (new Date().getTime() < fiveSecondsLater) {}
複製代碼

注意:被放入事件隊列的代碼被稱爲異步的。這是不是一個好的術語是另外一個話題,但人們就是這樣稱呼它的,因此我想你必須習慣於它。

函數執行上下文步驟

如今咱們已經熟悉了JavaScript程序的執行週期,讓咱們再深刻了解一下函數執行上下文究竟是如何建立的。

它發生在兩個步驟中:建立步驟和執行步驟。

建立步驟 "設置了一些東西",以便代碼能夠被執行,而執行步驟其實是執行它。

在建立步驟中發生的兩件事很是重要:

  • 肯定 scope.
  • 肯定值。(我將假設你已經熟悉 JavaScript 中的 this 關鍵字)。

在接下來的兩個相應章節中,將分別詳細介紹這些內容。

相關文章
相關標籤/搜索