前言
關于作用域和作用域鍊的讨論非常多,但少有人來講清楚JS中相關的機制,這裡我就撿一些大佬們看剩的知識,來講講了解作用域之前的準備。帶着這些問題看文章:
- JavaScript 是如何編譯執行的?
- 查找作用域時是如何一層層往上查詢的?
- JavaScript作用域鍊的本質是?
還有速記口訣: 作用域鍊口訣
1. 了解前的普及:編譯原理
1.1 分詞/詞法解析
這些代碼塊被稱為詞法單元(token) ,這些詞法單元組成了詞法單元流數組
var sum = 30;
// 詞法分析後的結果
[
"var" : "keyword",
"sum" : "identifier",
"=" : "assignment",
"30" : "integer",
";" : "eos" (end of statement)
]1.2.3.4.5.6.7.8.9.
1.2 文法分析
把詞法單元流數組轉換成一個由元素逐級嵌套所組成的代表程式文法結構的樹,這個樹被稱為“抽象文法樹” (Abstract Syntax Tree, 簡稱AST)。
1.3 代碼生成
将抽象文法樹(AST)轉換為一組機器指令,也就是可執行代碼,簡單說,就是用來建立一個變量a,并将3這個值儲存在a中。
1.4 JavaScript 編譯過程的不同處
- JavaScript 大部分情況下編譯發生在代碼執行前的幾微秒(甚至更短!)的時間内
- JavaScript 引擎用盡了各種辦法(比如JIT,可以延 遲編譯甚至實施重編譯)來保證性能最佳
2. JavaScript是如何執行的?
- 核心重點:變量和函數在内的所有聲明都會在任何代碼被執行前首先 被處理。
- 函數運作的瞬間,建立一個AO (Active Object 活動對象)運作載體。
2.1 例子一
function a(age) {
console.log(age);
var age = 20
console.log(age);
function age() {
}
console.log(age);
}
a(18);1.2.3.4.5.6.7.8.9.
2.1.1 分析階段
函數運作的瞬間,建立一個AO (Active Object 活動對象)
AO (Active Object 活動對象) 相當于載體
AO = {}1.
第一步,分析函數參數:
形式參數:AO.age = undefined
實參:AO.age = 181.2.
第二步,分析變量聲明:
// 第3行代碼有var age
// 但此前第一步中已有AO.age = 18, 有同名屬性,不做任何事
即AO.age = 181.2.3.
第三步,分析函數聲明:
// 第5行代碼有函數age
// 則将function age(){}付給AO.age
AO.age = function age() {}1.2.3.
函數聲明特點:AO上如果有與函數名同名的屬性,則會被此函數覆寫。
因為函數在JS領域,也是變量的一種類型
分析階段最終結果是:
AO.age = function age() {}1.
2.1.2 執行階段
2.2 例子二
function a(age) {
console.log(age);
var age = function () {
console.log('25');
}
}
a(18);1.2.3.4.5.6.7.
2.2.1 分析階段
形式參數:AO.age = undefined
實參:AO.age = 181.2.
// 第3行代碼有函數表達式 var age = function () { console.log('25');}
// 但此前第一步中已有AO.age = 18, 有同名屬性,不做任何事
即AO.age = 181.2.3.
第三步,分析函數聲明(無)
AO.age = 181.
2.2.2 執行階段
2.3 例子三
function a(age) {
console.log(age);
var age = function () {
console.log(age);
}
age();
}
a(18);1.2.3.4.5.6.7.8.
2.3.1 分析階段
第一步,分析函數參數:AO.age = 18
第二步,分析變量聲明:有同名屬性,不做任何事 AO.age = 18
AO.age = 181.
2.3.2 執行階段
到這裡,很多人會犯迷糊:age();不是應該輸出18 嗎?
代碼執行到age();時,其實又會再分析 & 執行。
2.3.3 age()的分析&執行
// 分析階段
建立AO對象,AO = {}
第一步,分析函數參數(無)
第二步,分析變量聲明(無)
第三步,分析函數聲明(無)
分析階段最終結果是:AO = {}1.2.3.4.5.6.
- 當age() 自己的AO對象,即age.AO是個空對象時,它會往上調用。
- 上一級的AO對象是a,即a.AO,a.AO下有個執行完後得到的a.AO.age = function(){console.log(age);}
- 輸出ƒ () { console.log(age); } `
2.4 執行總結:何為作用域鍊
JavaScript上每一個函數執行時,會先在自己建立的AO上找對應屬性值。若找不到則往父函數的AO上找,再找不到則再上一層的AO,直到找到大boss:window(全局作用域)。
而這一條形成的“AO鍊” 就是JavaScript中的作用域鍊。
3.LHS和RHS查詢:作用域鍊的兩大利器
LHS,RHS 這兩個術語就是出現在引擎對變量進行查詢的時候。在《你不知道的Javascript(上)》也有很清楚的描述。在這裡,我想引用freecodecamp 上面的回答來解釋:
LHS = 變量指派或寫入記憶體。想象為将文本檔案儲存到硬碟中。 RHS = 變量查找或從記憶體中讀取。想象為從硬碟打開文本檔案。 Learning Javascript, LHS RHS
3.1 兩者的特性
- 都會在所有作用域中查詢
- 嚴格模式下,找不到所需的變量時,引擎都會抛出ReferenceError異常。
- 非嚴格模式下,LHR稍微比較特殊: 會自動建立一個全局變量
- 查詢成功時,如果對變量的值進行不合理的操作,比如:對一個非函數類型的值進行函數調用,引擎會抛出TypeError異常
3.2 拿書中的例子來講
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );1.2.3.4.5.
直接看執行查找:
LHS(寫入記憶體):
c=, a=2(隐式變量配置設定), b=1.
RHS(讀取記憶體):
讀foo(2), = a, a ,b
(return a + b 時需要查找a和b)1.2.
按 寫入/讀取記憶體來了解,是不是比書中的好了解多了?
3.3 關于LHS和RHS抛錯
拿兩個最簡單的例子将:
3.3.1 不合理的操作
LHS執行查詢階段,原本查詢成功,但将a作用函數調用a();,故引擎會抛出TypeError異常。
3.3.2 LHS抛錯
LHS比較少見的情況是:很多時候我們都沒開啟嚴格模式,即:“use strict”。
你們可以現在打開chrome調試工具,分别試下以下代碼嚴格/非嚴格模式的輸出:
“use strict”
function init(a){
b=a+3;
}
init(2);
console.log(b);1.2.3.4.5.6.
“use strict”function init(a){ b=a+3;}init(2);console.log(b);複制代碼1.
3.3.3 RHS抛錯
4. 作用域鍊口訣
這裡我們拿《你不知道的Javascript(上)》中的一張圖解釋:
我也總結了一個作用域鍊口訣,教你快速找到輸出:
- 分析階段創AO,參數看完找變量,變量不頂函數頂,頂完之後定乾坤。
- 執行階段看LR,内層不行找外層,翻遍樓層找不到,抛個異常連連看。