本篇文章总结自《你不知道的 JavaScript (上卷)》的第一章,是基于书籍内容和我个人的了解总结的,所以或许会有一些纰漏,请酌情阅读(尽管或许只有我自己才会读)。

编译原理

JS 代码编译有三个步骤:

  • 分词/词法剖析(Tokenizing/Lexing) 这个进程会将代码分解为多个词法单元(token)。比方 var a = 1; 就会被分解为:vara=1;

  • 解析/词法剖析(Parsing) 这个进程会将词法单元流(数组)转换为笼统语法树(Abstract Syntax Tree, AST)。比方 var a = 1; 会变成下面这样子:

    {
      "type": "Program",
      "start": 0,
      "end": 11,
      "body": [
        {
          "type": "VariableDeclaration",
          "start": 0,
          "end": 10,
          "declarations": [
            {
              "type": "VariableDeclarator",
              "start": 4,
              "end": 9,
              "id": {
                "type": "Identifier",
                "start": 4,
                "end": 5,
                "name": "a"
              },
              "init": {
                "type": "Literal",
                "start": 8,
                "end": 9,
                "value": 1,
                "raw": "1"
              }
            }
          ],
          "kind": "var"
        }
      ],
      "sourceType": "module"
    }
    
  • 代码生成 这个进程会将 AST 转换为可履行代码。抛开具体细节,其实便是将 var a = 1; 的 AST 转化为一组机器指令(字节码 => 机器码),用来创立一个叫作 a变量(包含分配内容等),并将一个值存储a 中。

JS 代码的编译大部分时候都是发生在代码履行前。

效果域

首先介绍三个概念:

  • 引擎 负责整个 JavaScript 程序的编译及履行进程
  • 编译器 负责词法剖析、语法剖析及代码生成等作业
  • 效果域 负责收集并维护由所有声明的标识符(变量)组成的一系列查询,而且办理当前履行代码对这些标识符的访问权限

var a = 1 为例,编译器与效果域的交互如下:

  1. 遇到 var a 时,编译器会询问效果域是否已经有一个该称号的变量存在于同一个效果域的调集中。假如是,那么编译器会忽略该变量的声明,然后持续编译;否则它会要求效果域在当前效果域的调集中声明一个新的变量,并命名为 a
  2. 接下来编译器会为引擎生成运转时所需的代码,这些代码用来处理 a = 2 的赋值操作。引擎运转时会先询问效果域,当前的效果域调集中是否存在一个名为 a 的变量。假如存在,引擎会使用这个变量。假如不存在,引擎会持续查找变量
  3. 假如引擎最终找到了 a 变量,则会将 2 赋值给它;假如没找到,则会抛出一个反常

引擎的两种查询方式

引擎在寻觅变量时有两种查询方式:LHS(Left Hand Side) 和 RHS(Right Hand Side)

能够笼统地以为当变量出现在赋值操作的左侧时会进行 LHS 查询,出现在右侧时进行 RHS 查询。

例如 console.log(a)a 的引证便是一个 RHS 引证,由于 a 并没有被赋予任何值。相应的,需求去查找并取得 a 的值,这样才干将值传递给 console.log(...)

例如 a = 2a 的引证是一个 LHS 引证,由于我们并不关怀当前 a 的值是什么,只想为 = 2 这个赋值操作找到一个方针

综上所述,能够将 LHS 了解为“赋值操作的方针是谁”,将 RHS 了解为“谁是操作赋值的源头”。也便是说 LHS赋值操作RHS寻值操作

下面的代码既包含 LHS 也包含 RHS

function foo(a) {
  console.log(a)
}
foo(2)

首先 foo(2)函数调用需求对 foo 进行 RHS 引证,也便是去寻觅 foo 的值。找 foo 的值后函数开端履行,当 2 被当作参数传递给 foo(...) 时,2 被赋值给了参数 a,因而需求进行 LHS 查询。这里还有对 a 进行的 RHS 引证,并将得到值传给了 console.log(...)。而 console.log(...) 自身也需求一个引证才干履行,因而会对 console 目标进行 RHS 查询,并检查得到的值是否有一个叫 log 的办法。最后,假设 log(...) 函数能够接收参数,则在将 2 赋值给其第一个参数前,这个参数需求进行一次 LHS 引证查询。

《你不知道的 JavaScript》读书笔记(1)

效果域嵌套

function foo(a) {
  console.log(a + b)
}
var b = 2
foo(2) // 4

上述代码中,对 b 进行的 RHS 引证无法在函数 foo 内部完成,但能够在上一级效果域中完成。遍历嵌套效果域链的规则很简单:引擎会从当前的履行效果域开端查找变量,假如找不到,就去上一级持续查找。当抵达最外层的大局效果域时,无论找到还是没找到,查找进程都会中止。

反常

function foo(a) {
  console.log(a + b)
  b = a
}
foo(2) // Uncaught ReferenceError: b is not defined

console.log(a + b) 时,对 b 进行 RHS 查询是无法找到该变量的,由于它未声明,引擎会抛出 ReferenceError 反常。

但假如履行的是 LHS 查询,且程序运转在“非严格形式”下,假如在大局效果域中也无法找到方针变量,则会在大局效果域下隐式地创立一个具有该称号的变量,并将其返回给引擎,比方下面的代码是能够正常运转的:

function foo(a) {
  b = a
  console.log(a + b)
}
foo(2) // 4

但假如是“严格形式”下运转程序,则也会抛出 ReferenceError 反常:

'use strict'
function foo(a) {
  b = a
  console.log(a + b)
}
foo(2) // Uncaught ReferenceError: b is not defined

假如 RHS 查询找到了一个变量,但你尝试对这个变量的值进行不合理的操作,比方试图对一个字符串的值进行函数调用,那么引擎会抛出 TypeError 反常。