理解js中的作用域

303 阅读4分钟

这是我参与8月更文挑战的第17天,活动详情查看:8月更文挑战

编译

程序中的一段源代码在执行之前会经历三个步骤:

  • 分词/词法分析
  • 解析/语法分析
  • 代码生成

词法分析

这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。例如,考虑程序 var a = 2;。这段程序通常会被分解成 为下面这些词法单元:vara=2;。空格是否会被当作词法单元,取决于空格在这门语言中是否具有意义。

语法分析

这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract SyntaxTree,AST)。var a = 2;的抽象语法树中可能会有一个叫作VariableDeclaration 的顶级节点,接下 来是一个叫作 Identifier(它的值是 a)的子节点,以及一个叫作 AssignmentExpression 的子节点。AssignmentExpression 节点有一个叫作 NumericLiteral(它的值是 2)的子 节点。

代码生成

AST 转换为可执行代码的过程被称为代码生成。这个过程与语言、目标平台等息息相关。 抛开具体细节,简单来说就是有某种方法可以将 var a = 2;AST 转化为一组机器指 令,用来创建一个叫作a的变量(包括分配内存等),并将一个值储存在 a 中。

以上是代码在编译器中必须要经过的三个流程,但JavaScript 引擎要复杂得多。例如,在 语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化 等。

作用域理解

对于操作变量的代码,编译器会按照专门的规则在作用域链中查询该变量是否已被创建。

  • LHS查询 当变量出现在赋值操作的左侧时进行
  • RHS查询 当变量出现在赋值操作的右时进行

举个栗子

function foo(a) { 
 console.log( a ); // 2 
} 
foo( 2 );
  • 最后一行 foo(..) 函数的调用需要对 foo 进行 RHS 查询
  • foo函数声明中的a = 2是一个隐式LHS查询,在 2 被当作参数传递给foo(..) 函数时,2 会被分配给参数 a
  • console.log( a ),对 console 对象进行 RHS 查询,并且检查得到的值中是否有一个叫作 log 的方法

作用域嵌套

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

遍历嵌套作用域链的规则很简单:引擎从当前的执行作用域开始查找变量,如果找不到, 就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都 会停止。

异常

正确区分 LHSRHS 是一件重要的事情

在变量还没有声明(在任何作用域中都无法找到该变量)的情况下,这两种查询的行为是不一样的。

function foo(a) { 
  //当前作用域中,b未创建,引擎回到上一个作用域中查找
 console.log( a + b ); 
 b = a; 
} 
//这里是顶级作用域
//引擎仍然未找到b,此时全局作用域中就会创建一个b变量个体引擎
//仅限于非严格模式 
foo( 2 );

严格模式中 LHS查询失败时,并不会创建并返回一个全局变量,引擎会抛出同RHS查询失败时类似的ReferenceError 异常。

如果 RHS 查询找到了一个变量,但是你尝试对这个变量值进行不合理的操作,比如试图对一个非函数类型的值进行函数调用,或者引用 nullundefined 类型的值中的 属性,那么引擎会抛出另外一种类型的异常,叫作TypeError

ReferenceError说明在作用域链中找不到相关变量,TypeError说明作用域链中存在相关变量,但是对该变量的操作不合理,即对该变量执行了与它类型不符的操作。

总结

  • 如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询
  • 如果目的是获取变量的值,就会使用 RHS 查询
  • 赋值操作符会导致 LHS 查询
  • =操作符既会导致LHS查询,也会导致RHS查询
  • LHSRHS 查询都会在当前执行作用域中开始
  • 如果在当前作用域找不到相关变量,会往上一级作用域查询,直到顶级作用域为止
  • 到达顶级作用域后,非严格模式下,找不到,则会自动生成相关的全局变量,严格模式下,报ReferenceError
  • 查询到达顶级作用域后,无论查询结果如何都会停止。