你不知道的 JS-上(一)

33 阅读4分钟

你不知道的 JS-上

作用域和闭包

作用域是什么

编译原理

JavaScript 是一门编译语言,但与传统编译语言不同,它不是提起编译的,编译结果也不能在分布式系统中进行移植。

传统编译语言,程序中的代码在执行前一般都要经历三个步骤,统称“编译”。

  • 分词/语法分析 这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元。如 var a = 2;。这段程序通常会被分解成 var、a、=、2、;。空格是否被当作词法单元,取决于空格在这么语言中是非有意义。

  • 解析/语法分析 这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套组成的代表了程序语法结构的树,被称为“抽象语法树(AST)”。var a = 2;的抽象语法树为:

{
  "type": "Program",
  "start": 0,
  "end": 10,
  "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": 2,
            "raw": "2"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "module"
}

astexplorer.net/ 生成

  • 代码生成 将 AST 转换为可执行的代码的过程被称为代码生成。简单来说就是有某种方法将 var a = 2;的 AST 转化为一组机器指令,用来创建一个叫 a 的变量(包括分配内存),并将一个值储存在 a 中。

比起编译过程只有三个步骤的语言的编译器来说,JS 引擎要复杂得多。JS 的编译不是发生在构建之前,通常发生在代码执行前,因此 JS 引擎用尽各种办法来保证代码的性能最佳,所有相比其他语言的编译器做了更多的操作。

理解作用域

实际的编译主要由以下三个不分协作完成

  • 引擎: 从头到尾负责整个 JS 程序的编译与执行过程

  • 编译器: 负责语法分析及代码生成。

  • 作用域: 负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

对于 var a = 2;我们认为:“为一个变量分配内存,将其命名为 a,然后将值 2 保存进这个变量。”然而这并不完全正确。

事实上编译器会进行如下处理。

  1. 遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为 a。

  2. 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作。引擎运行时会首先询问作用域,在当前集合中是否存在一个叫 a 的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。

如果最终找到 a 变量,就会将 2 赋值给它。否则就会抛出一个错误。

LHS和RHS

在上述查找的过程中由作用域进行协助,但是引擎执行怎样的查找,会最终影响查找的结果。分别有LHS和RHS

  • LHS查询则是试图找到变量的容器本身,可以理解为赋值操作的目标是谁。
  • RHS查询与简单地查找某个变量的值别无二致,可以理解为谁是赋值操作的源头。

如下例子:

function (a){
  var b = a;
  return a + b;
}
var c = foo(2)

其中LHS有:c = ... 、 a = 2(调用foo(2)时的隐式声明) 、 b = ... 其中RHS有:foo(2) 、= a; 、a 、 b

为什么区分LHS和RHS是一件重要的事情?

因为在变量还没声明的情况下,这两种查询的行为是不一样的。 对于未声明的变量进行RHS查询,引擎就会抛出一个referenceError异常,相比之下如果对未声明的变量进行LHS查询,引擎就会在全局作用域下创建这一个变量。严格模式下LHS查询失败时,不会创建并返回一个全局变量,引擎会抛出同RHS查询失败时类似的referenceError异常

作用域嵌套

作用域时是根据名称查找变量的一套规则。当一块或函数嵌套在另一块或函数中时,就发生了作用域嵌套。因此,当当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。