摘要: 理解 JS 引擎運行原理。javascript
Fundebug經受權轉載,版權歸原做者全部。前端
JS 引擎 — 一個讀取代碼並運行的引擎,沒有單一的「JS 引擎」;每一個瀏覽器都有本身的引擎,如谷歌有 V。java
做用域 — 能夠從中訪問變量的「區域」。編程
詞法做用域— 在詞法階段的做用域,換句話說,詞法做用域是由你在寫代碼時將變量和塊做用域寫在哪裏來決定的,所以當詞法分析器處理代碼時會保持做用域不變。小程序
塊做用域 — 由花括號{}建立的範圍segmentfault
做用域鏈 — 函數能夠上升到它的外部環境(詞法上)來搜索一個變量,它能夠一直向上查找,直到它到達全局做用域。微信小程序
同步 — 一次執行一件事, 「同步」引擎一次只執行一行,JavaScript 是同步的。瀏覽器
異步 — 同時作多個事,JS 經過瀏覽器 API模擬異步行爲緩存
事件循環(Event Loop) - 瀏覽器 API 完成函數調用的過程,將回調函數推送到回調隊列(callback queue),而後當堆棧爲空時,它將回調函數推送到調用堆棧。安全
堆棧 —一種數據結構,只能將元素推入並彈出頂部元素。 想一想堆疊一個字形的塔樓; 你不能刪除中間塊,後進先出。
堆 — 變量存儲在內存中。
調用堆棧 — 函數調用的隊列,它實現了堆棧數據類型,這意味着一次能夠運行一個函數。 調用函數將其推入堆棧並從函數返回將其彈出堆棧。
執行上下文 — 當函數放入到調用堆棧時由 JS 建立的環境。
閉包 — 當在另外一個函數內建立一個函數時,它「記住」它在之後調用時建立的環境。
垃圾收集 — 當內存中的變量被自動刪除時,由於它再也不使用,引擎要處理掉它。
變量的提高— 當變量內存沒有賦值時會被提高到全局的頂部並設置爲undefined
。
this —由 JavaScript 爲每一個新的執行上下文自動建立的變量/關鍵字。
看看下面的代碼:
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
當它被執行時你指望發生什麼? 是否發生錯誤,由於b
在a
以後聲明或者一切正常? console.log
打印的變量又是怎麼樣?
如下是打印結果:
"myVar" undefined "myOtherVar" 10 "Hello world!"
來分解一下上述的執行步驟。
第一步是在內存中爲全部變量和函數分配空間。 但請注意,除了undefined
以外,還沒有爲變量分配值。 所以,myVar
在被打印時的值是undefined
,由於 JS 引擎從頂部開始逐行執行代碼。
函數與變量不同,函數能夠一次聲明和初始化,這意味着它們能夠在任何地方被調用。
因此以上代碼看起來像這樣子:
var myOtherVar = undefined var myVar = undefined function a() {...} function b() {...} function c() {...}
這些都存在於 JS 建立的全局上下文中,由於它位於全局空間中。
在全局上下文中,JS 還添加了:
window
對象,NodeJs 中是 global
對象)接下來,JS 引擎會逐行執行代碼。
myOtherVar = 10`在全局上下文中,`myOtherVar`被賦值爲`10
已經建立了全部函數,下一步是執行函數 a()
每次調用函數時,都會爲該函數建立一個新的上下文(重複步驟 1),並將其放入調用堆棧。
function a() { console.log("myVar", myVar); b(); }
以下步驟:
a
函數裏面沒有聲明變量和函數this
並指向全局對象(window)myVar
,myVar
屬於全局做用域的。b
,函數b
的過程跟 a
同樣,這裏不作分析。下面調用堆棧的執行示意圖:
this
。在前面的示例中,全部內容都是全局做用域的,這意味着咱們能夠從代碼中的任何位置訪問它。 如今,介紹下私有做用域以及如何定義做用域。
考慮以下代碼:
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();
須要注意如下幾點:
c
如今在函數b
中聲明打印結果以下:
myOtherVar: "global otherVar"; myVar: "inside B";
執行步驟:
this
myOtherVar
,而後調用函數 b5,函數b 的上下文中建立了 myVar
變量,並聲明函數 c
上面提到每一個新上下文會建立的外部引用,外部引用取決於函數在代碼中聲明的位置。
myOtherVar
,但這個變量並不存在於函數 b中,函數 b 就會使用它的外部引用上做用域鏈向上找。因爲函數 b是全局聲明的,而不是在函數 a內部聲明的,因此它使用全局變量 myOtherVar。myVar
,因此它它經過做用域鏈向上找,也就是函數 b,由於myVar
是函數 b內部聲明過。下面是執行示意圖:
請記住,外部引用是單向的,它不是雙向關係。例如,函數 b不能直接跳到函數 c的上下文中並從那裏獲取變量。
最好將它看做一個只能在一個方向上運行的鏈(範圍鏈)。
在上面的圖中,你可能注意到,函數是建立新做用域的一種方式。(除了全局做用域)然而,還有另外一種方法能夠建立新的做用域,就是塊做用域。
下面代碼中,咱們有兩個變量和兩個循環,在循環從新聲明相同的變量,會打印什麼(反正我是作錯了)?
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
a
是塊做用域,但它在函數中,而不是嵌套的,本例中使用var
是同樣的。var b
能夠在外部訪問,可是const blockedVar
不能。a
並將let a
更改成9000
。使用塊做用域可使代碼更清晰,更安全,應該儘量地使用它。
接下來看看事件循環。 這是回調,事件和瀏覽器 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
它記錄消息 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專一於JavaScript、微信小程序、微信小遊戲、支付寶小程序、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有陽光保險、核桃編程、荔枝FM、掌門1對一、微脈、青團社等衆多品牌企業。歡迎你們免費試用!
轉載時請註明做者 Fundebug以及本文地址:https://blog.fundebug.com/2019/06/24/how-does-javascript-execute/