【共讀】你不知道的js 上 (1)作用域是什麼?

諍陽發表於2019-04-05

前言

本文會用思維導圖的形式列出本書該部分的知識點(剔除案例),構建知識脈絡。由於是導讀,正文部分只會列舉部分的內容。本文適合未讀過此書的同學參考,另外讀過此書的同學,如果能純熟得答出文初的問題,那麼相信您對於這部分的內容可以說是記憶深刻了。

建議在閱讀前瞭解作者的生平,背景,核心貢,獻思想,相信會對理解本書以及後續的選書讀書會有所幫助。

豆瓣讀書

問題

  1. 談談你對作用域的理解。
  2. 引擎,編譯器,作用域分別是什麼?它們如何共同協作?
  3. 介紹一下 ReferenceError 異常型別 和 TypeError 異常型別。
  4. 談談你對作用域鏈的理解。

作用域

談談你對作用域的理解。(個人理解,求拍磚)

作用域收集並且維護由所有宣告的識別符號組成的查詢,有自己非常嚴格的規則確定當前執行程式碼對識別符號的訪問許可權。

JavaScript 是一門編譯語言,在執行程式碼前的編譯中,編譯器需要和作用域溝通是否存在某個變數來決定建立還是忽略。

接著引擎需要為變數賦值,它會通過 LHS查詢 或者 RHS查詢 查詢變數,在當前作用域找不到時還要沿著作用域鏈一直往上往上找,如果在最外層的全域性作用域也找不到,那麼丟擲叫做 ReferenceError 的異常

這裡如果是使用 LHS查詢 當全域性作用域也不存在查詢的變數時會自動建立並返還給引擎。

一、簡單介紹編譯原理

JavaScript 是一門編譯語言,但是它不像傳統語言那樣僅僅只經歷編譯的三個步驟,分詞/詞法分析,解析/語法分析,程式碼生成。

我們的 JavaScript 引擎要複雜的多,JavaScript 會用盡各辦法(比如用JIT)來保證效能最佳。並且我們要記住的是任何 JavaScript 程式碼片段在執行前都要進行編譯,大部分情況下編譯發生在程式碼執行前的幾微秒(甚至更短)。

  • 分詞/詞法分析:這個過程會將字串分解成對程式語言來說有意義的詞法單元(程式碼塊)

    var a = zhengyang;
    複製程式碼

    以上程式碼會被分解成 var、a、=、; 空格是否會被當成語法單元取決於它是否在此處具有意義。

    分詞和詞法分析其實是一件事,詞在這裡指的是帶有某種歸類的字串,詞通過詞法來劃分,分詞是目的,詞法分析是手段。

  • 解析/語法分析:這個過程會將詞法單元流轉化成一個由元素逐級巢狀所組成的代表程式語法結構的樹,這個樹叫做抽象語法樹(Abstract Syntax Tree,AST)。

    var a = zhengyang;
    複製程式碼

    經過分詞/詞法分析,我們把劃分好的程式碼塊組成抽象語法樹,它有一個 VariableDeclaration(變數宣告) 的頂級節點,下面是一個 Identitier (值為a)的子節點和一個叫做 AssignmentExpression(賦值表示式)的子節點,AssignmentExpression 有一個叫做 NumericLiteral(數值文字)的值為 2 的子節點。

  • 程式碼生成:將 AST 轉化為課執行程式碼的過程被稱為程式碼生成。

    簡單來說就是將 var a = 2AST 轉化為一組機器指令來創造一個叫做 a 的變數,並將一個值儲存在 a 中。

二、引擎,編譯器,作用域分別是什麼?它們如何共同協作?

  1. 引擎:從頭到尾負責整個 JavaScript 的編譯及執行過程。

  2. 編譯器:引擎的同事負責語法分析及程式碼生成等髒活累活。

  3. 作用域:引擎的另一位同事,負責收集並且維護所有宣告的識別符號組成的一些列諮詢。它由一套非常嚴格個規則,確定當前執行的程式碼對這些識別符號的訪問許可權。

  4. 三位一體工作流:

var a = zhengyang
複製程式碼
  • 遇到 var a,編譯器會諮詢作用域是否已經存在該名稱的變數存在於同一個作用域集合中。是,就忽略 var a 繼續編譯;否則就會要求在當前作用域集合中生命一個新的變數,命名為 a
  • 編譯器會為引擎生成執行時所需的程式碼,用來處理 a = 2 這個賦值操作。引擎在執行時會先諮詢作用域,當前的作用域集合中是否存在一個叫做a的變數。是,就會使用這個變數;否,引擎就會繼續查詢該變數。
  • 如果最後引擎找到了 a 變數會將 2 賦值給它;否,引擎會丟擲一個異常。
  1. 編譯器在第二步中生成了程式碼,引擎執行它的過程中會查詢 a 判斷是否宣告過,這個查詢方式會影響最終的查詢結果
  2. LHS查詢RHS查詢:簡單來說 LHS 查詢就是當變數出現在賦值操作的左側時進行的查詢, RHS查詢 就是變數出現在賦值操作的右側時進行的查詢。要注意,查詢只會在當前作用域進行。
console.log(a) 
複製程式碼

以上程式碼就是 RHS查詢 ,我們可以看到變數 a 出現在左側。

a = 2
複製程式碼

以上程式碼就是 LHS查詢, 變數 a 出現在左側。 6. 看一個具體例子

function foo(a) {
    console.log(a);//2
}
foo (2)
複製程式碼

以上程式碼首先進行的宣告 foo 函式,變數在右所以使 RHS查詢

然後是隱式的 a = 2 這裡採用 RHS查詢 ,變數在左所以使用 LHS查詢

console.log console是內建物件,找 log 變數在右使用 RHS查詢

console.log(a) 同上變數在右使用 RHS查詢

三、作用域鏈是什麼?

作用域是根據名稱查詢變臉的一套規則,當一個塊或函式巢狀在另一個塊或函式中時就發生了作用域的巢狀。在當前作用域無法找到某個變數時,引擎就會在外層巢狀的作用域中繼續查詢,直到找到該變數為止。

function foo(a){
    console.log(a + b)
}
var b = 2
foo(2); //4
複製程式碼

上面的程式碼中 console.log(a + b) 我們在函式作用域中找不到 b 只能在上層的全域性作用域中找

遍歷巢狀作用域作用域鏈的規則:引擎從當前的執行作用於開始查詢變數,如果找不到就去上級繼續查詢。當抵達最外層的全域性作用域時,如果還沒有找到,那麼查詢就會停止。

【共讀】你不知道的js 上 (1)作用域是什麼?
如上圖的一條作用域鏈,我們在當前作用域要找到 a 、b、c 當前作用域沒有就去外層作用域找在,找到了 b ,c沒找到繼續往外找,然後在全域性作用域找到了 c 如果到此時還沒有找到,那麼查詢就會停止。

四、ReferenceError 異常型別 和 TypeError 異常型別

  1. ReferenceError 異常型別
function foo(a) {
    console.log( a + b);
    b = a;
}
複製程式碼

以上程式碼會報 ReferenceError的異常,因為我們通過 RHS 查詢 在所有的巢狀作用域中都找不到 b。

相比之下如果是用 LHS查詢 非嚴格模式下,如果在全域性作用於中也找不到就會幫你建立一個具有該名稱的變數,並且返還給引擎。

  1. 嚴格模式

嚴格模式禁止自動或隱式地建立全域性變數。因此在嚴格模式中 LHS查詢 失敗時並不會建立並返回一個全域性變數,而是會丟擲 ReferenceError 異常。

  1. TypeError

如果通過 RHS查詢 找到了一個變數,但是你嘗試對這個變數的值進行不合理的操作,比如對一個非函式型別進行函式呼叫那麼就會丟擲 TypeError 異常。

五、小結

  1. JavaScript 是一門編譯語言,它的編譯過程不僅僅是傳統的三步,分詞/詞法分析,解析/語法分析,程式碼生成。還需要經過大量JIT這樣的優化過程來保證效能。
  2. JavaScipt 引擎沒有大量時間用來優化,他的編譯過程不是發生在構建之前的而是在程式碼執行前的幾微妙。
  3. 作用域是根據名稱查詢變數的一套規則,在 JavaScript 中如果在當前作用域找不到某個變數時,就會到外層巢狀的作用域中繼續查詢,如果在最外層的全域性作用域當中也找不到那me查詢就會停止。作用域鏈就是這一層層往外找的一條路徑。
  4. 引擎從頭到尾負責整個 JavaScrip t的編譯及執行過程。編譯器負責語法分析及程式碼生成。作用域負責收集並且維護由所有生命的識別符號(變數)組成的一系列查詢,並實施一套非常嚴格的規則確定當前執行的程式碼對這些識別符號的訪問許可權。
  5. 引擎,編譯器,作用域的合作過程。
var a = 'zhengyang'
複製程式碼
  • 執行前的編譯中,第一步,編譯器會看變數是否在作用域中已經存在。是,忽略;不是,建立並命名為a。
  • 第二步,為引擎生成執行時所需的程式碼來處理 a = 'zhengyang' 這個賦值操作。
  • 銜接第二步,看變數在賦值操作的左邊還是右邊引擎會使用 LHS查詢 或者 RHS查詢 在當前作用域查詢變數。如果找到了引擎就會使用這個變數將 'zhengyang' 這個值賦給它。
  • 如果在當前作用域找不到就會通過作用域鏈向外找。如果最外層的全域性作用域也找不到就會報 ReferenceError 異常。
  • 注意非嚴格模式下如果使用 LHS查詢 ,當最外層的全域性作用域也不存在要查詢的變數時會自動建立並且返回該變數給引擎,嚴格模式下則不可,因為嚴格模式禁止自動或隱式建立全域性變數。

相關文章