天天看點

十年阿裡p8架構師詳解!瞬間了解JavaScript作用域鍊

作者:程式員阿遠

前言

關于作用域和作用域鍊的讨論非常多,但少有人來講清楚JS中相關的機制,這裡我就撿一些大佬們看剩的知識,來講講了解作用域之前的準備。帶着這些問題看文章:

  • ​JavaScript​​ 是如何編譯執行的?
  • 查找作用域時是如何一層層往上查詢的?
  • ​​JavaScript​​作用域鍊的本質是?

​還有速記口訣:​ ​作用域鍊口訣​​​

1. 了解前的普及:編譯原理

十年阿裡p8架構師詳解!瞬間了解JavaScript作用域鍊

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是如何執行的?

十年阿裡p8架構師詳解!瞬間了解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 執行階段

十年阿裡p8架構師詳解!瞬間了解JavaScript作用域鍊

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 執行階段

十年阿裡p8架構師詳解!瞬間了解JavaScript作用域鍊

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 執行階段

十年阿裡p8架構師詳解!瞬間了解JavaScript作用域鍊

到這裡,很多人會犯迷糊:​​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.           

按 寫入/讀取記憶體來了解,是不是比書中的好了解多了?

十年阿裡p8架構師詳解!瞬間了解JavaScript作用域鍊

3.3 關于​​LHS​​和​​RHS​​抛錯

拿兩個最簡單的例子将:

3.3.1 不合理的操作

十年阿裡p8架構師詳解!瞬間了解JavaScript作用域鍊

​​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​​抛錯

十年阿裡p8架構師詳解!瞬間了解JavaScript作用域鍊

4. 作用域鍊口訣

這裡我們拿《你不知道的Javascript(上)》中的一張圖解釋:

十年阿裡p8架構師詳解!瞬間了解JavaScript作用域鍊

我也總結了一個​作用域鍊口訣​,教你快速找到輸出:

  • 分析階段創AO,參數看完找變量,變量不頂函數頂,頂完之後定乾坤。
  • 執行階段看LR,内層不行找外層,翻遍樓層找不到,抛個異常連連看。

繼續閱讀