一周一章前端書·第1周:《你不知道的JavaScript(上)》S01E01

第1章:作用域是什么

  • 我們通過var聲明變量時,是否考慮過這些問題:
    • 這些變量都存儲在哪里?
    • 程序用到它們時,又是怎么找到它們的?
  • 而答案就是:不僅僅是JavaScript,任何編程語言都會設計一套良好的規則來存取變量,而這套規則就叫做 作用域

1.1 編譯原理

  • 雖然和靜態語言(比如Java)不同,JavaScript是“解釋性”的動態語言。
  • 但實際上,JavaScript代碼在運行之前也是需要編譯的,并且JavaScript引擎編譯的步驟,和傳統的編譯語言非常相似,大致有以下三大步驟:
第1步:分詞/詞法分析(Tokenizing/Lexing)
  • 任何.js文件在解析前,對于JS引擎而言都是一大段文本,不能直接運行。所以當務之急,就是將文本字符串“大卸八塊”般的進行分解。
  • 詞法分析就是 將文本內容分解成有意義的詞法字符串(token) 。
  • 比如var a = 2;最終會分解成詞法字符串數組,得到 [var、a=2、;],而多余的空格則是無意義的。
第2步:解析/語法分析(Parsing)
  • 語法分析則是 將詞法字符串數組轉換成 “抽象語法樹”(Abstract Syntax Tree,AST
  • 比如代碼var a = 2;會生成以下具有層次結構的對象
    /*變量聲明的對象*/
    VariableDeclaration : {
      /*變量名為 a*/
      Identifier : a,
      /*變量賦值表達式*/
      AssignmentExpression : {
          /*數值類型為 2*/
          NumericLiteral : 2 
      }
    }
    
第3步:代碼生成
  • 最后一步就是生成代碼, 將AST轉換為可執行的機器指令
  • 比如代碼var a = 2;會創建一個變量a,并為其分配內存,然后將值2存進這個變量。

1.2 理解作用域

原書將引擎、編譯器以及作用域模擬成三個演員,用來說明在執行一段代碼時,三者分別負責的工作。但我稍微做一些改動,將作用域比喻成一個記錄清單。

  • 執行JS代碼依賴三個東西:
    • 引擎:負責JS代碼的編譯和執行
    • 編譯器:在引擎工作前,負責語法分析和代碼生成
    • 作用域:一個具有嚴格的規則,專門負責收集并維護所有變量的清單列表,通過它來存取變量
  • 閱讀代碼 var a = 2; 其實訪問了兩次作用域,一個是 在編譯器編譯時檢查變量聲明,一個是 引擎運行時檢查使用
    • 如上面所說的,第1步編譯器會進行詞法分析,第2步將詞法單元解析成一個樹結構的對象;
    • 在第3步生成代碼時,編譯器會去查找作用域,檢查 是否存在同名的變量,如果沒有則聲明一個新的變量并賦值 ;
    • 最后引擎運行代碼時,會再次通過作用域 檢查 是否存在同名的變量,如果有則直接 使用,沒有則繼續向上查找
  • 引擎執行代碼到作用域查找變量,分為兩種類型:RHS查詢LHS查詢
    • “L(left)”和“R(right)”分別代表變量處于表達式的左邊還是右邊;
    • RHS查詢就是查找變量,可理解成retrieve his source value(找到它源值)。比如console.log(a)就是RHS查詢,找到變量a的值傳遞給console.log();
    • LHS查詢則是查找變量的容器對其進行賦值。比如var a = 2;就是LHS查詢,找到變量a并為它賦值= 2;
  • 我們嘗試用RHS查詢和LHS查詢的思維來閱讀JS代碼:
    function foo(a){
        console.log(a);
    }
    foo(2);
    
    我們都知道function聲明函數的方式等同于,聲明一個變量并為其賦值一個執行方法體:
    var foo = function(a){
        console.log(a);
    }
    foo(2);
    
    • var foo = function()這是一個LHS查詢:聲明foo變量并為其賦值一個方法;
    • foo(2)屬于RHS查詢:找到foo變量的值并執行它
    • 進到foo方法體中,實際上這里隱藏了一句代碼a = 2;將傳遞的值賦值給形參
    • console.log(a)是RHS查詢:找到a的值,傳遞給console.log(...)
    • 值得一提的是,console.log()本身也屬于RHS查詢,會去找尋log()方法的引用并執行它

1.3 作用域嵌套

  • 不管是RHS查詢還是LHS查詢都從當前作用域開始,如果當前作用域無法找到變量時,引擎會轉移到外層作用域中繼續查找,直至轉移到最頂層的作用域,也就是全局作用域。
  • 舉例:
    function foo(a){
        console.log(a + b);
    }
    var b = 2;
    foo(2);
    
    foo方法體中,變量bfoo的作用域中找不到,將會到外層的全局作用域查找,最后輸出4

1.4 異常

  • 之所以 區分RHS和LHS,是因為當查找到未聲明的變量時,這兩種查詢的行為是不一樣的:
    • 如前文提到的,LHS查詢失敗時會在全局作用域創建一個同名的變量;
    • 而RHS查詢失敗時,則會拋出 ReferenceError異常;另一種情況是,查找到了變量,但是嘗試對這個變量的值做不合理的操作(比如對一個非函數的變量進行調用),則拋出TypeError異常
    • 總而言之,RererenceError異常是作用域判別失敗相關的, TypeError異常 則代表作用域判別成功了,但對結果的操作是非法或不合理的

1.5 小結

  • 作用域是一套存取變量的規則;
  • 在代碼執行前,會先由編譯器進行編譯,JavaScript引擎在執行代碼時會進行LHS查詢和RHS查詢:
    • LHS查詢是對變量進行賦值,其中=操作符或者調用函數時傳參的操作,都會導致相關作用域的賦值操作;
    • RHS查詢是對變量的值進行查找;
  • LHS和RHS查詢都會從當前執行作用域開始,如果當前作用域找不到,就會往上級作用域繼續查找,每次上升一級作用域,直至到頂級的全局作用域
  • 不成功的RHS查詢會拋出Reference異常,而不成功的LHS查詢會自動式地創建一個全局變量
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容