前言
掘金上關於作用域和作用域鏈的討論非常多,但少有人來講清楚JS中相關的機制,這裡我就撿一些大佬們看剩的知識,來講講理解作用域之前的準備。 帶著這些問題看文章:
JavaScript
是如何編譯執行的?- 查詢作用域時是如何一層層往上查詢的?
JavaScript
作用域鏈的本質是?
想直接看解析的請跳到:2. JavaScript是如何執行的?
還有速記口訣:作用域鏈口訣
1. 理解前的普及:編譯原理
1.1 分詞/詞法解析
這些程式碼塊被稱為詞法單元(token) ,這些詞法單元組成了詞法單元流陣列
var sum = 30;
// 詞法分析後的結果
[
"var" : "keyword",
"sum" : "identifier",
"=" : "assignment",
"30" : "integer",
";" : "eos" (end of statement)
]
複製程式碼
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);
複製程式碼
2.1.1 分析階段
函式執行的瞬間,建立一個AO
(Active Object 活動物件
)
AO (Active Object 活動物件) 相當於載體
AO = {}
複製程式碼
第一步,分析函式引數:
形式引數:AO.age = undefined
實參:AO.age = 18
複製程式碼
第二步,分析變數宣告:
// 第3行程式碼有var age
// 但此前第一步中已有AO.age = 18, 有同名屬性,不做任何事
即AO.age = 18
複製程式碼
第三步,分析函式宣告:
// 第5行程式碼有函式age
// 則將function age(){}付給AO.age
AO.age = function age() {}
複製程式碼
函式宣告特點:AO上如果有與函式名同名的屬性,則會被此函式覆蓋。
因為函式在JS領域,也是變數的一種型別
分析階段最終結果是:
AO.age = function age() {}
複製程式碼
2.1.2 執行階段
2.2 例子二
function a(age) {
console.log(age);
var age = function () {
console.log('25');
}
}
a(18);
複製程式碼
2.2.1 分析階段
第一步,分析函式引數:
形式引數:AO.age = undefined
實參:AO.age = 18
複製程式碼
第二步,分析變數宣告:
// 第3行程式碼有函式表示式 var age = function () { console.log('25');}
// 但此前第一步中已有AO.age = 18, 有同名屬性,不做任何事
即AO.age = 18
複製程式碼
第三步,分析函式宣告(無)
分析階段最終結果是:
AO.age = 18
複製程式碼
2.2.2 執行階段
2.3 例子三
function a(age) {
console.log(age);
var age = function () {
console.log(age);
}
age();
}
a(18);
複製程式碼
2.3.1 分析階段
第一步,分析函式引數:AO.age = 18
第二步,分析變數宣告:有同名屬性,不做任何事 AO.age = 18
第三步,分析函式宣告(無)
分析階段最終結果是:
AO.age = 18
複製程式碼
2.3.2 執行階段
到這裡,很多人會犯迷糊:age();
不是應該輸出18
嗎?
程式碼執行到age();
時,其實又會再分析 & 執行。
2.3.3 age()
的分析&執行
// 分析階段
建立AO物件,AO = {}
第一步,分析函式引數(無)
第二步,分析變數宣告(無)
第三步,分析函式宣告(無)
分析階段最終結果是:AO = {}
複製程式碼
- 當
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 );
複製程式碼
直接看執行查詢:
LHS(寫入記憶體):
c=, a=2(隱式變數分配), b=
複製程式碼
RHS(讀取記憶體):
讀foo(2), = a, a ,b
(return a + b 時需要查詢a和b)
複製程式碼
按 寫入/讀取記憶體來理解,是不是比書中的好理解多了?
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);
複製程式碼
3.3.3 RHS
拋錯
4. 作用域鏈口訣
這裡我們拿《你不知道的Javascript(上)》中的一張圖解釋:
我也總結了一個作用域鏈口訣,教你快速找到輸出:
-
分析階段創AO,引數看完找變數,變數不頂函式頂,頂完之後定乾坤。
-
執行階段看LR,內層不行找外層,翻遍樓層找不到,拋個異常連連看。
感悟:
這幾天摸爬滾打的找了很多資料,發現很多都講得語焉不詳。要麼非常複雜,講得賊深奧。要麼就是粗略概括,沒有系統介紹。這也是為啥這麼多將作用域與作用域鏈,卻沒一個徹底看明白的原因(大概率也是因為菜)
作者文章總集
求一份深圳的內推
目前本人在準備跳槽,希望各位大佬和HR小姐姐可以內推一份靠譜的深圳前端崗位!
- 微信:
huab119
- 郵箱:
454274033@qq.com