前言

上一篇作用域是什么中,我们将作用域定义为一套规则,这套规则用来定义如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。

作用域有两种主要的工作模型,一种是词法作用域,一种是动态作用域。

1.1 词法阶段

👉🏻👉🏻👉🏻 先读这里
前面介绍过,大部分标准语言编译器的第一个工作阶段就是叫做词法化(也叫单词化),词法化的过程会对源代码中的字符进行检查,如果是有状态的解析过程,还会赋予单词语义。

1.1.1 词法作用域的概念

  1. 词法作用域就是定义在词法阶段的作用域,它是由你在写代码时将变量和作用域写在哪里决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况)。

1.1.2 举例

  1. function foo(a) {
  2. var b = a * 2;
  3. function bar(c) {
  4. console.log(a, b, c)
  5. }
  6. }
  7. foo(2);

例中有三个逐级嵌套的作用域,我们可以想象成几个逐级包含的气泡。

  1. 包含整个全局作用域,其中只有一个标识符: foo。
  2. 包含foo所创建的作用域,其中有三个标识符: a、bar和b。
  3. 包含着bar所创建的作用域,其中只有一个标识符:c。

这里假设每一个函数都会创建一个新的作用域气泡(下一篇会介绍不同类型的作用域)。

没有任何函数的气泡可以同时出现在两个外部作用域的气泡中,就如同没有任何函数可以部分的同时出现在两个父级函数中一样。

查找

作用域气泡的结构和互相之间的位置关系给引擎提供了足够的位置信息,引擎用这些信息来查找标识符的位置。

作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名标识符,这叫做“遮蔽效应”(内部的标识符遮蔽了外部的标识符)。抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者向上进行,直到遇见第一个匹配的标识符为止。

词法作用域只会查找一级标识符,比如a、b和c。如果代码中引用了foo.bar.baz,词法作用域查找只会试图查找foo标识符,找到这个变量后,对象属性访问规则会分别接管对bar和baz属性的访问。

1.2 欺骗词法

前面讲过词法作用域完全由写代码期间函数所声明的位置来定义,怎样才能在运行时来修改词法作用域呢?

eval&with

会导致性能问题,不建议使用,这里不进行赘述。词法分析器处理过后,依然可以修改作用域。引擎无法在编译时对作用域查找进行优化,引擎只能谨慎地认为这样的优化是无效的。