# JavaScript的運行機制
點擊關注本[公眾號](http://www.hmoore.net/book/dsh225/javascript_vue_css/edit#_118)獲取文檔最新更新,并可以領取配套于本指南的《**前端面試手冊**》以及**最標準的簡歷模板**.
了解JavaScript運行機制有助于我們避免bug,并寫出高性能的代碼,當然還有一大用處就是有助于我們通過造火箭環節的面試。
具體而言你會搞清楚以下問題:
* 作用域鏈本質上是如何產生的
* this是如何被綁定的
* JavaScript代碼到底運行原理是什么
* 閉包產生的根本原因
而產生的『后果』是,你可以應對幾乎所有的JavaScript作用域、閉包、執行等層面的面試題,還有一個可能的后果,就是面對復雜度不是那么高的代碼時,你的腦子中會自己把執行過程像放動畫一樣過一遍(雖然這個動畫也不非常準確)。
[TOC]
## **JavaScript的執行環境**
在了解JavaScript運行機制之前,我們需要搞清楚幾個主要概念,這有助于我們接下來的理解。
### JavaScript引擎(JavaScript Engine)
賦予一段代碼意義的正是JavaScript引擎,目前JavaScript引擎有許多種:
* V8?—?開源,由 Google 開發,用 C ++ 編寫
* Rhino?—?由 Mozilla 基金會管理,開源,完全用 Java 開發
* SpiderMonkey?—?是第一個支持 Netscape Navigator 的 JavaScript 引擎,目前正供 Firefox 使用
* JavaScriptCore —?開源,以Nitro形式銷售,由蘋果為Safari開發
* KJS?—?KDE 的引擎,最初由 Harri Porten 為 KDE 項目中的 Konqueror 網頁瀏覽器開發
* Chakra (JScript9)?—?Internet Explorer
* Chakra (JavaScript)?—?Microsoft Edge
* Nashorn, 作為 OpenJDK 的一部分,由 Oracle Java 語言和工具組編寫
* JerryScript?—? 物聯網的輕量級引擎
而最為大家熟知的無疑是V8引擎,他用于Chrome瀏覽器和Node中。

V8引擎由兩個主要部件組成:
* emory Heap(內存堆)?—?內存分配地址的地方
* Call Stack(調用堆棧) — 代碼執行的地方
### JavaScript運行時(JavaScript Runtime)
想讓JavaScript真正運作起來,單單靠JavaScript Engine是不夠的,JavaScript Engine的工作是**編譯并執行 JavaScript 代碼,完成內存分配、垃圾回收等**,但是缺乏與外部交互的能力。
比如單靠一個V8引擎是無法進行ajax請求、設置定時器、響應事件等操作的,這就需要JavaScript運行時(JavaScript Runtime)的幫助,它為 JavaScript 提供一些對象或機制,使它能夠與外界交互。
比如,雖然Chrome和node都是用了V8引擎,但是他們的運行時卻不同,比如process、fs瀏覽器都無法提供。
### 可執行代碼
一段JavaScript代碼的運行我們可以分為兩個階段:
* 編譯階段:
* 分詞/詞法分析(Tokenizing/Lexing)
* 解析/語法分析(Parsing)
* 預編譯(解釋)
* 執行階段
本文的重點在于執行階段。
JavaScript并非簡單的一行行解釋執行,而是將JavaScript代碼分為一塊塊的可執行代碼塊進行執行,那么如何劃分代碼塊?
目前有三類代碼塊:
* 函數代碼塊(Function code)
* 全局代碼塊(Global code)
* eval代碼塊(Eval code)
## JavaScript執行
我們先看一個簡單的例子:

看到這個例子思考一下JavaScript應該是如何執行它的?
如果你頭腦里沒有任何細節的概念,那么接下來的內容就很適用于你了。
### 堆
我們之前提到過JavaScript引擎兩個重要部分:
* emory Heap(內存堆)?—?內存分配地址的地方
* Call Stack(調用棧) — 代碼執行的地方
而上面的代碼聲明正是被存放在『堆』中。

此時雖然變量和函數都被聲明了,但是函數還沒有執行,我們現在執行`say`函數。

那么接下來又會發生什么呢?
### 調用棧
調用棧(Call Stack)這個概念對于經常調試JavaScript代碼的同學應該不陌生。

我們聲明的函數與變量被儲存在『內存堆』中,而當我們要執行的時候,就必須借助于『調用棧』來解決問題。
如果熟悉數據結構的同學應該知道,棧是一個基礎的數據結構,它的特點就是先進后出。
我們仍然看這個例子,當`say`函數被調用的時候,他會被壓入棧底。

那么是不是將函數壓入棧內就結束了?肯定沒有這么簡單,這里需 要在引入一個概念,執行上下文(execution context)。
### 執行上下文(execution context)
執行上下文在代碼塊執行前創建,作為代碼塊運行的基本執行環境,那么執行上下文分為幾種?
前面我們提到過,JavaScript中有三種可執行代碼塊,當然也對應著三種執行上下文。
* 全局執行上下文 — 這是基礎上下文,任何不在函數內部的代碼都在全局上下文中。它會執行兩件事:創建一個全局的 window 對象(瀏覽器的情況下),并且設置 this 的值等于這個全局對象。一個程序中只會有一個全局執行上下文。
* 函數執行上下文 — 每當一個函數被調用時, 都會為該函數創建一個新的上下文。每個函數都有它自己的執行上下文,不過是在函數被調用時創建的。函數上下文可以有任意多個。每當一個新的執行上下文被創建。
* Eval 執行上下文 — 執行在 eval 內部的代碼也會有它屬于自己的執行上下文,除非你想搞黑魔法,不然不要輕易使用它。
肯定會有人好奇,這個執行上下文到底包含哪些東西呢,他是如何運行的呢?
執行上下文分為兩個階段:
* 創建階段
* 執行階段
我們主要討論創建階段,執行階段的主要工作就是分配變量
#### 執行上下文的創建階段
執行上下文的創建階段主要解決以下三點:
* 決定 this 的指向
* 創建詞法環境(LexicalEnvironment)
* 創建變量環境(VariableEnvironment)
> 你可能在一些過時的教材或者文章中見過變量對象(VO)這種說法,它的意思與詞法環境類似,但是那是ES3的標準,現在早已經改了,改變的原因討論如下[Why variable object was changed to lexical environment in ES5?](https://stackoverflow.com/questions/40544709/why-variable-object-was-changed-to-lexical-environment-in-es5)
偽代碼如下:

##### this指向
我們應該知道this的指向是在代碼執行階段確定的,所謂的『代碼執行階段』正是『執行上下文的創建階段』。
默認情況下this指向全局對象,比如瀏覽器中的window.
此外可能存在隱式綁定的情況,比如通過對象調用函數:

這個時候this指向對象。
然后就是顯示綁定對象(call apply bind)等,最后優先級最高的就是new調用構造函數生成一個對象。
##### 詞法環境(LexicalEnvironment)
詞法環境分為三大類:
* 全局環境:全局環境的外部環境引用是 null,它擁有內建的 Object/Array/等、在環境記錄器內的原型函數(關聯全局對象,比如 window 對象)還有任何用戶定義的全局變量,并且 this的值指向全局對象。
* 模塊環境:包含模塊頂級聲明的綁定以及模塊顯式導入的綁定。 模塊環境的外部環境是全局環境。
* 函數環境:函數內部用戶定義的變量存儲在環境記錄器中,外部引用既可以是其它函數的內部詞法環境,也可以是全局詞法環境
詞法環境本身包括兩個部分:
* 『環境記錄器(Environment Record)』是存儲變量和函數聲明的實際位置
* 『外部環境的引用(outer Lexical Environment)』指它可以訪問其父級詞法環境(即作用域)
對于『環境記錄器』而言,它又分為兩個主要的環境記錄器類型:
* 聲明式環境記錄器(DecarativeEnvironmentRecord):范圍包含函數定義,變量聲明,try...catch等,此類型對應其范圍內包含的聲明定義的標識符集
* 對象式環境記錄器(ObjectEnvironmentRecord):由程序級別的(Program)對象、聲明、with語句等創建,與稱為其綁定對象的對象相關聯,此類型對應于其綁定對象的屬性名稱的字符串標識符名稱集
比如我們在全局聲明一個函數:

那么他的詞法環境可以這樣表示(下圖我們省略了this綁定、變量環境等信息,便于理解):

##### 變量環境(VariableEnvironment)
變量環境的定義在es5標準和es6標準是略有不同的,我們采用[es6的標準](http://www.ecma-international.org/ecma-262/6.0/#sec-for-statement-runtime-semantics-labelledevaluation)
變量環境也是一個詞法環境,但不同的是詞法環境被用來存儲函數聲明和變量(let 和 const)綁定,而變量環境只用來存儲 var 變量綁定。
### 執行過程
在了解了這么多概念之后,我們就可以把本節開頭的例子再拓展一下:

我們就一步步復盤一下上述代碼是如何執行的(不考慮解析、預解釋等操作,只考慮執行):
1. 變量`name`和函數聲明`say`被白存在堆中。

2. 創建全局可執行上下文:
全局上下文的偽代碼如下:

示意圖:

3. 創建函數執行上下文
say函數的執行上下文偽代碼如下:

4. 創建創建say函數體內的函數執行上下文
play函數的執行上下文偽代碼如下

示意圖:

5. 開始執行
將上下文中的變量賦值,然后執行代碼,執行完畢棧頂的play函數后彈出,接著執行say函數,完畢后彈出。
## 小結
我們通過本文了解了相關的JavaScript執行機制,現在可以回答這幾個問題了。
### this是怎么被綁定的?
在創建可執行上下文的時候,根據代碼的執行條件,來判斷分別進行默認綁定、隱式綁定、顯示綁定等。
### 作用域鏈是怎么形成的?
可執行上下文中的詞法環境中含有外部詞法環境的引用,我們可以通過這個引用獲取外部詞法環境的變量、聲明等,這些引用串聯起來一直指向全局的詞法環境,因此形成了作用域鏈。
### 閉包是怎么形成的?
可執行上下文中的詞法環境中含有外部詞法環境的引用,我們可以通過這個引用獲取外部詞法環境的變量、聲明等,因此形成了閉包。
* * *
參考
1. [ecma標準](http://www.ecma-international.org/ecma-262/6.0/#sec-for-statement-runtime-semantics-labelledevaluation)
2. [JavaScript調用棧到異步](https://www.valentinog.com/blog/engines/)
* * *
## 公眾號
想要實時關注筆者最新的文章和最新的文檔更新請關注公眾號**程序員面試官**,后續的文章會優先在公眾號更新.
**簡歷模板**:關注公眾號回復「模板」獲取
**《前端面試手冊》**:配套于本指南的突擊手冊,關注公眾號回復「fed」獲取

- 前言
- 指南使用手冊
- 為什么會有這個項目
- 面試技巧
- 面試官到底想看什么樣的簡歷?
- 面試回答問題的技巧
- 如何通過HR面
- 推薦
- 書籍/課程推薦
- 前端基礎
- HTML基礎
- CSS基礎
- JavaScript基礎
- 瀏覽器與新技術
- DOM
- 前端基礎筆試
- HTTP筆試部分
- JavaScript筆試部分
- 前端原理詳解
- JavaScript的『預解釋』與『變量提升』
- Event Loop詳解
- 實現不可變數據
- JavaScript內存管理
- 實現深克隆
- 如何實現一個Event
- JavaScript的運行機制
- 計算機基礎
- HTTP協議
- TCP面試題
- 進程與線程
- 數據結構與算法
- 算法面試題
- 字符串類面試題
- 前端框架
- 關于前端框架的面試須知
- Vue面試題
- React面試題
- 框架原理詳解
- 虛擬DOM原理
- Proxy比defineproperty優劣對比?
- setState到底是異步的還是同步的?
- 前端路由的實現
- redux原理全解
- React Fiber 架構解析
- React組件復用指南
- React-hooks 抽象組件
- 框架實戰技巧
- 如何搭建一個組件庫的開發環境
- 組件設計原則
- 實現輪播圖組件
- 性能優化
- 前端性能優化-加載篇
- 前端性能優化-執行篇
- 工程化
- webpack面試題
- 前端工程化
- Vite
- 安全
- 前端安全面試題
- npm
- 工程化原理
- 如何寫一個babel
- Webpack HMR 原理解析
- webpack插件編寫
- webpack 插件化設計
- Webpack 模塊機制
- webpack loader實現
- 如何開發Babel插件
- git
- 比較
- 查看遠程倉庫地址
- git flow
- 比較分支的不同并保存壓縮文件
- Tag
- 回退
- 前端項目經驗
- 確定用戶是否在當前頁面
- 前端下載文件
- 只能在微信中訪問
- 打開新頁面-被瀏覽器攔截
- textarea高度隨內容變化 vue版
- 去掉ios原始播放大按鈕
- nginx在MAC上的安裝、啟動、重啟和關閉
- 解析latex格式的數學公式
- 正則-格式化a鏈接
- 封裝的JQ插件庫
- 打包問題總結
- NPM UI插件
- 帶你入門前端工程
- webWorker+indexedDB性能優化
- 多個相鄰元素切換效果出現邊框重疊問題的解決方法
- 監聽前端storage變化