重讀你不知道的JS (上) 第一節一章

言月發表於2019-02-16

你不知道的JS(上卷)筆記

你不知道的 JavaScript

JavaScript 既是一門充滿吸引力、簡單易用的語言,又是一門具有許多複雜微妙技術的語言,即使是經驗豐富的 JavaScript 開發者,如果沒有認真學習的話也無法真正理解它們.

上捲包括倆節:

  • 作用域和閉包
  • this 和物件原型

作用域和閉包

希望 Kyle 對 JavaScript 工作原理每一個細節的批判性思 考會滲透到你的思考過程和日常工作中。知其然,也要知其所以然。

作用域是什麼?

倆個事實

  1. 能夠儲存變數值並能在之後對這個值進行訪問和修改(幾乎所有程式語言最基本的功能之一)
  2. 若沒有了狀態這個概念,程式雖然也能夠執行一些簡單的任務,但它會受到高度限制,做 不到非常有趣。(正是這種儲存和訪問變數的值的能力將狀態帶給了程式)

提出問題

  • 變數存在哪?
  • 程式需要時如何找到他們?

需要一套規則來處理變數的問題,解決上述問題

需要一套設計良好的規則來儲存變數,並且之後可以方便的找到變數,這套規則被稱為 作用域。

新的問題

  • 作用域規則在哪裡?
  • 怎麼樣設定這些規則?

編譯原理

JavaScript是一門編譯語言。

  • 不是提前編譯
  • 編譯結果也不能在分散式系統中移植

傳統語言的編譯流程:

  1. 分詞/詞法分析(Tokenizing/Lexing)

    • 這個過程會將由字元組成的字串分解成(對程式語言來說)有意義的程式碼塊,這些代 碼塊被稱為詞法單元(token)。例如,考慮程式var a = 2;。這段程式通常會被分解成 為下面這些詞法單元:var、a、=、2 、;。空格是否會被當作詞法單元,取決於空格在 這門語言中是否具有意義。。
    • 分詞(tokenizing)和詞法分析(Lexing)之間的區別主要差異在於詞法單元的識別是通過有狀態還是無狀態的方式進行的。簡 單來說,如果詞法單元生成器在判斷 a 是一個獨立的詞法單元還是其他詞法單元的一部分時,呼叫的是有狀態的解析規則,那麼這個過程就被稱為詞法分析。
  2. 解析/語法分析(Parsing)

    • 這個過程是將詞法單元流(陣列)轉換成一個由元素逐級巢狀所組成的代表了程式語法 結構的樹。這個樹被稱為“抽象語法樹”(Abstract Syntax Tree,AST)。var a = 2; 的抽象語法樹中可能會有一個叫作 VariableDeclaration 的頂級節點,接下 來是一個叫作 Identifier(它的值是 a)的子節點,以及一個叫作 AssignmentExpression 的子節點。AssignmentExpression 節點有一個叫作 NumericLiteral(它的值是 2)的子節點。
  3. 程式碼生成

    • 將 AST 轉換為可執行程式碼的過程稱被稱為程式碼生成。這個過程與語言、目標平臺等息 息相關。拋開具體細節,簡單來說就是有某種方法可以將 var a = 2; 的 AST 轉化為一組機器指 令,用來建立一個叫作 a 的變數(包括分配記憶體等),並將一個值儲存在 a 中。

JavaScript 引擎不會有大量的(像其他語言編譯器那麼多的)時間用來進行優化,因 為與其他語言不同,JavaScript 的編譯過程不是發生在構建之前的。

任何 JavaScript 程式碼片段在執行前都要進行編譯(通常就在執行前)。因此, JavaScript 編譯器首先會對 var a = 2; 這段程式進行編譯,然後做好執行它的準備,並且 通常馬上就會執行它。

理解作用域

對話形式模擬作用域的工作方式

演員表

參與到對程式 var a = 2; 進行處理的過程中的演員們

  • 引擎
    從頭到尾負責整個 JavaScript 程式的編譯及執行過程。
  • 編譯器
    負責語法分析及程式碼生成等。
  • 作用域
    負責收集並維護由所有宣告的識別符號(變數)組成的一系列查 詢,並實施一套非常嚴格的規則,確定當前執行的程式碼對這些識別符號的訪問許可權。
對話

編譯器首先會將這段程式分解成詞法單元,然後將詞法單元解析成一個樹結構。但是當編 譯器開始進行程式碼生成時,它對這段程式的處理方式會和預期的有所不同。
可以合理地假設編譯器所產生的程式碼能夠用下面的虛擬碼進行概括:“為一個變數分配內 存,將其命名為 a,然後將值 2 儲存進這個變數。”然而,這並不完全正確。
事實上編譯器會進行如下處理。

  1. 遇到 var a,編譯器會詢問作用域是否已經有一個該名稱的變數存在於同一個作用域的 集合中。如果是,編譯器會忽略該宣告,繼續進行編譯;否則它會要求作用域在當前作 用域的集合中宣告一個新的變數,並命名為 a。
  2. 接下來編譯器會為引擎生成執行時所需的程式碼,這些程式碼被用來處理 a = 2 這個賦值 操作。引擎執行時會首先詢問作用域,在當前的作用域集合中是否存在一個叫作 a 的 變數。如果是,引擎就會使用這個變數;如果否,引擎會繼續查詢該變數(檢視 1.3 節)。

總結:變數的賦值操作會執行兩個動作,首先編譯器會在當前作用域中宣告一個變數(如 果之前沒有宣告過),然後在執行時引擎會在作用域中查詢該變數,如果能夠找到就會對 它賦值。

編譯器中的操作
  • LHS
  • RHS

異常

為什麼區分 LHS 和 RHS 是一件重要的事情?

因為在變數還沒有宣告(在任何作用域中都無法找到該變數)的情況下,這兩種查詢的行 為是不一樣的。

如果 RHS 查詢在所有巢狀的作用域中遍尋不到所需的變數,引擎就會丟擲 ReferenceError 異常。值得注意的是,ReferenceError 是非常重要的異常型別。

相較之下,當引擎執行 LHS 查詢時,如果在頂層(全域性作用域)中也無法找到目標變數,
全域性作用域中就會建立一個具有該名稱的變數,並將其返還給引擎,前提是程式執行在非 “嚴格模式”下。
嚴格模式下,未宣告的RHS和LHS倆者行為相同,都會是 ReferenceError。

ReferenceError 同作用域判別失敗相關,而 TypeError 則代表作用域判別成功了,但是對 結果的操作是非法或不合理的。

相關文章