爲什麼你始終理解不了JavaScript做用域鏈?

前言

掘金上關於做用域和做用域鏈的討論很是多,但少有人來說清楚JS中相關的機制,這裏我就撿一些大佬們看剩的知識,來說講理解做用域以前的準備。 帶着這些問題看文章:javascript

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

想直接看解析的請跳到:2. JavaScript是如何執行的?前端

還有速記口訣:做用域鏈口訣vue

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

1.1 分詞/詞法解析

這些代碼塊被稱爲詞法單元(token) ,這些詞法單元組成了詞法單元流數組java

var sum = 30;
// 詞法分析後的結果
[
  "var" : "keyword",
  "sum" : "identifier",
  "="   : "assignment",
  "30"  : "integer",
  ";"   : "eos" (end of statement)
]
複製代碼

1.2 語法分析

把詞法單元流數組轉換成一個由元素逐級嵌套所組成的表明程序語法結構的樹,這個樹被稱爲「抽象語法樹」 (Abstract Syntax Tree, 簡稱AST)。chrome

1.3 代碼生成

將抽象語法樹(AST)轉換爲一組機器指令,也就是可執行代碼,簡單說,就是用來建立一個變量a,並將3這個值儲存在a中。vue-cli

1.4 JavaScript 編譯過程的不一樣處

  • JavaScript 大部分狀況下編譯發生在代碼執行前的幾微秒(甚至更短!)的時間內
  • JavaScript 引擎用盡了各類辦法(好比 JIT,能夠延 遲編譯甚至實施重編譯)來保證性能最佳

2. JavaScript是如何執行的?

  • 核心重點:變量和函數在內的全部聲明都會在任何代碼被執行前首先 被處理。數組

  • 函數運行的瞬間,建立一個AO (Active Object 活動對象)運行載體。bash

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 活動對象) 至關於載體ide

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.LHSRHS查詢:做用域鏈的兩大利器

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 關於LHSRHS拋錯

拿兩個最簡單的例子將:

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
相關文章
相關標籤/搜索