# JavaScript的『預解釋』與『變量提升』
點擊關注本[公眾號](http://www.hmoore.net/book/dsh225/javascript_vue_css/edit#_118)獲取文檔最新更新,并可以領取配套于本指南的《**前端面試手冊**》以及**最標準的簡歷模板**.
[TOC]
## 前言
JavaScript的作用域一直是JavaScript比較讓人頭痛的一部分,也是面試中幾乎必考的內容,因此,我們將從更深層次來講述js作用域。
## 從一個實例開始
仔細閱讀以下JavaScript代碼,你覺得運行結果會是什么呢?是`1`還是`2`?
~~~
var a= 1;
function f() {
console.log(a);
var a = 2;
}
f();
~~~
答案是undefined.
那么到底是什么原因導致了這個讓人意外的結果呢?這就要從JavaScript解釋階段說起。
## JavaScript預解釋
我們可以大致把JavaScript在瀏覽器中運行的過程分為兩個階段`預解釋階段`(有人說準確的說法是應該是Parser,我們以預解釋方便理解)`執行階段`,在JavaScript引擎對JavaScript代碼進行執行之前,需要進行預先處理,然后再對處理后的代碼進行執行。
> 我們平時書寫的JavaScript代碼并不是JavaScript執行的代碼(V8引擎讀取一行執行一行這種理解是錯誤的),它需要預解釋后,再由引擎進行執行.
具體的解釋過程涉及到瀏覽器內核的技術不屬于前端領域,不過我們可以淺顯的理解一下V8在處理JavaScript的一般過程:
以上例中的`var a = 2;`為例,我們一般人的理解為**聲明了一個值為2的變量a**,但是在JavaScript引擎處理時卻分為了兩個步驟:
> 1. 讀取`var a`后,在當前作用域中查找是否有相同聲明,如果沒有就在當前作用域集合中創建一個名為`a`的變量,否則忽略此聲明繼續進行解析.
> 2. 接下來,V8引擎會處理`a = 2`的賦值操作,首先會詢問當前作用域中是否有名為`a`的變量,如果有進行賦值,否則繼續向上級作用域詢問.
## JavaScript執行環境
我們上面提到的所謂javascript預解釋正是創建函數的**執行環境**(又稱“執行上下文”),只有搞定了javascript的執行環境我們才能搞清楚一段代碼在執行過后為什么產生這樣的結果。
我們用一段偽代碼表示創立的**執行環境**
~~~
executionContextObj = {
'scopeChain': { /* 變量對象 + 所有父級執行上下文中的變量對象 */ },
'variableObject': { /* 函數參數 / 參數, 內部變量以及函數聲明 */ },
'this': {}
}
~~~
作用域鏈(scopeChain)包括下面提到的變量對象(variableObject)和所有父級執行上下文中的變量對象.
變量對象(variableObject)是與執行上下文相關的數據作用域,一個與上下文相關的特殊對象,其中存儲了在上下文中定義的變量和函數聲明:
* 變量
* 函數聲明
* 函數的形參
在有了這些基板概念之后我們可以梳理一下js引擎創建執行的過程:
* 創建階段
* 創建Scope chain
* 創建variableObject
* 設置this
* 執行階段
* 變量的值、函數的引用
* 執行代碼
而變量對象的創建細節如下:
* 根據函數的參數,創建并初始化arguments object
* 掃描函數內部代碼,查找函數聲明(Function declaration)
* 對于所有找到的函數聲明,將函數名和函數引用存入變量對象中
* 如果變量對象中已經有同名的函數,那么就進行覆蓋
* 掃描函數內部代碼,查找變量聲明(Variable declaration)
* 對于所有找到的變量聲明,將變量名存入變量對象中,并初始化為"undefined"
* 如果變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性
## 變量提升
正是由于以上的處理,產生了大家熟知的JavaScript中的**變量提升**,具體以上代碼的執行過程如以下偽代碼所示:
~~~
// global context
executionContextObj = {
'scopeChain': { ... },
'variableObject': { a: undefined, f: pointer to function f() },
'this': {...}
}
...
}//首先在全局執行環境中聲明了變量a以及函數f,此時a雖然被聲明,但是尚未賦值
x = 1;
function f() {
executionContextObj {
'scopeChain': { ... },
'variableObject': {
arguments: {},
a: undefined
},
'this': {...}
}
//內部詞法環境中聲明了變量a,此時a雖然被聲明,但是尚未賦值
console.log(a);//此時a需要被被打印出來,在作用域內尋找a變量賦值,于是被賦值undefined
a = 2;
}
~~~
我們可以明顯看到,`a`變量在預解釋階段已經被賦值`undefined`,在執行階段js是自上而下單線執行,當`console.log(a)`執行之時,`a=2`還沒有被執行,`a`變量的值便是預處理階段被賦予的`undefined`,
## 函數聲明與函數表達式
我們看到,在編譯器處理階段,除了被`var`聲明的變量會有變量提升這一特性之外,函數也會產生這一特性,但是函數聲明與函數表達式兩種范式創建的函數卻表現出不同的結果.
我們先看一個實例,運行以下代碼
~~~
f();
g();
//函數聲明
function f() {
console.log('f');
}
//函數表達式
var g = function() {
console.log('g');
};
~~~
`f`成功被打印出來,而`g函數`出現了類型錯誤,這是什么原因呢?
~~~
executionContextObj = {
'scopeChain': { ... },
'variableObject': { f: pointer to function f(), g: undefined},
'this': {...}
}
f();
g();
//函數聲明
function f() {
console.log('f');
}
//函數表達式
var g = function() {
console.log('g');
};
~~~
我們看到,在預解釋階段函數聲明的`f`是被指向了正確的函數得以執行,而函數表達式`g`被賦予`undefined`,`undefined`無法被當作函數執行因此報錯`g is not a function`.
## 沖突處理
通常情況下我們不會將同一變量變量重復聲明,但是出現了類似情況后,編譯器會如何處理這些沖突呢?
1. 變量之間沖突
執行以下函數:
~~~
var a = 3;
var a = 4;
console.log(a);
~~~
結果顯而易見,后聲明變量值覆蓋前者的值 2. 函數之間沖突
~~~
f();
function f() {
console.log('f');
}
function f () {
console.log('g');
};
~~~
結果同變量沖突,后者覆蓋前者.
3.函數與變量之間沖突
~~~
console.log(f);
function f() {
console.log('f');
}
var f ='g';
~~~
結果如下,函數聲明將覆蓋變量聲明
`[Function: f]`
## ES6中的let
在ES6中出現了兩個最新的聲明語法`let`與`const`,我們以`let`為例,進行測試看看與`var`的區別.
~~~
function f() {
console.log(a);
let a = 2;
}
f(); // ReferenceError: a is not defined
~~~
這段代碼直接報錯顯示未定義,`let`與`const`擁有類似的特性,阻止了變量提升,當代碼執行到`console.log(a)`時,執行換將中`a`還從未被定義,因此產生了錯誤.
* * *
## 公眾號
想要實時關注筆者最新的文章和最新的文檔更新請關注公眾號**程序員面試官**,后續的文章會優先在公眾號更新.
**簡歷模板**:關注公眾號回復「模板」獲取
《**前端面試手冊**》:配套于本指南的突擊手冊,關注公眾號回復「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變化