作用域是什么

7/11/2020 js

# 设计一套良好的规则存储变量,并且之后可以很方便的访问(引擎去查询)到这些变量,这就是作用域

# 编译原理

通常JavaScript被归类为“动态”或者是“解释执行”,但是它确实是一门编程语言。在传统的语言中,一段源代码在执行之前都需要经历三个步骤,统称“编译”

  1. 分词/词法分析(Tokkenizing/Lexing) 这个过程会将有字符组成的字符串分解成(对编程与语言来说)有意义的代码块,这些代码被称为词法单元(token).例如 var a = 2;会被分解为这些词法单元,var、a、=、2、;、。空格是否会被当做词法单元,取决自身是否有意义。

分词(tokkenizing)和词法分析(Lexing)之间的区别是非常微妙的。只要的差异在于词法单元的识别是通过有状态还是无状态的方式进行的。简单来说,就是在判断a是一个独立的词法单元还是其他词法单元的一部分时,调用的是有状态的解析规则,纳闷这个过程就被称为词法分析。

  1. 解析/语法分析(Parsing) 这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。例如:var a = 2
{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "value": 2,
            "raw": "2"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "script"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  1. 代码生成 将AST转换化为可执行代码的过程被称为代码生成。这个过程和语言、目标平台等有关。大概就是有种方法将var a = 2;的AST转化为一组机器指令,用来创建一个变量为a(包括给a分配内存),并将一个值存储在a中。

但是JavaScript的引擎要复杂很多,在语法分析和代码生成等阶段还会有语法检查,运行性能优化等等。但是JavaScript引擎不会有大量的优化,大部分情况下编辑只发生在代码执行的几微妙,JavaScript引擎有许多的策略来优化编译,可以理解为编辑即执行。

# 理解作用域

在了解作用域时有几个朋友需要介绍一下

  • 引擎 从头到尾负责整个JavaScript程序的编译及执行过程

  • 编译器 引擎的一个好朋友,负责词法分析及代码生成等脏活累活

  • 作用域 引擎的一另个好朋友,负责收集和维护由所有声明的标识符(变量)组成的一系列查询,实施一套严厉规则,确定当前执行的代码对这些标识符的访问权限。

遇到var a = 2;是编译器会如下处理

  1. 遇到var a,编译器会向作用域询问是否存在变量a的声明,如果有,编译器会忽略,继续编译;否则的话,它会要求作用域在当前的作用域声明一个新的变量,命名为a

  2. 引擎运行时会首先向作用域询问是否有这个变量,如果有直接赋值;如果没有的话,就继续寻找。

总结:变量的赋值操作会这行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有),然后运行时引擎会在作用域中寻找,如果找到就对它赋值

引擎对变量的查询有两种方式,一种是LHS,另一种是RHS,当变量出现在赋值操作的左侧时进行LHS查询,其他就会进行RHS(可以理解为retrieve his source value)查询。

# 作用域嵌套

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量是,引擎就会在外层的作用域中继续查找,直到全局作用域为止。

示例:1

function foo (a) {
    return a + b;
}
var b = 2;
foo(2) // 4
1
2
3
4
5

示例:2

function foo (a) {
    return a + b;
}
foo(2) // Uncaught ReferenceError: Cannot access 'b' before initialization
const b = 2; // const 执行时才会声明
1
2
3
4
5

示例:3

function foo (a) {
    return a + b;
}
foo(2) // NaN
var b = 2; // 2 + undefined
1
2
3
4
5

# 总结

作用域是套规则。如果查找的目的是对变量赋值,那么就会使用LHS查询;如果是为了获取变量的值,就会使用RHS查询。不成功的RHS引用会导致ReferenceError异常。不成功的LHS引用会导致自动隐式创建一个全局变量(非严格模式),或者抛出ReferenceError异常(严格模式)。但是上面的示例2是一个赋值操作,但是不会隐式创建一个全局变量,因为const和let都是执行时声明。

上次更新: 2/11/2025, 1:00:27 PM