作用域

作用域就是变量(标识符)适用范围,控制着变量的可见性。

《You don‘t know js》对作用域的定义:

使用一套严格的规则来分辨哪些标识符对哪些语法有访问权限。

《JavaScript 权威指南》中对变量作用域的描述:

一个变量的作用域(Scope)是程序源代码中定义这个变量的区域。全局变量拥有全局作用域,在 JavaScript 代码中的任何地方都是有定义的。然而在函数内声明的变量只在函数体内有定义。它们是局部变量,作用域是局部性的。函数参数也是局部变量,它们只是在函数体内有定义。

作用域共有两种主要的工作模式:

  • 词法作用域/静态作用域
  • 动态作用域

JavaScript 采用 词法作用域(Lexical Scope),也称为 静态作用域

因为 JavaScript 采用的是词法作用域,因此函数的作用域在函数定义的时候就决定了。

而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。

词法作用域/静态作用域

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

简单来说,词法作用域就是定义在词法阶段的作用域。换句话来说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下时这样的)。

🌰 代码示例

function foo(a) {
var b = a * 2;
function brc(c) {
console.log(a, b, c);
}
bar(b * 3);
}
foo(2); // 2, 4, 12

在这个例子中有三个逐级嵌套的作用域。为了帮助理解,可以将它们想象成几个逐级包含的气泡。

Webpack执行流程

  • 包含着整个全局作用域,其中只有一个标识符:foo
  • 包含着 foo 所创建的作用域,其中有三个标识符:abarb
  • 包含着 bar 所创建的作用域,其中只有一个标识符:c

作用域气泡由其对应的作用域代码写在哪里决定,它们是 逐级包含 的。现在只需要假设每一个函数都会创建一个新的作用域气泡就好了。

bar 的气泡被完全包含在 foo 所创建的气泡中,唯一的原因是那里就是我们希望定义函数 bar 的位置。

查找

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

在上个代码片段中,引擎执行 console.log 声明,并依次查找 abc 三个变量的引用。

  • 它首先从最内部的作用域,也就是 bar 函数的作用域气泡开始查找
  • 引擎无法在这里找到 a,因此会去上一级到所嵌套的 foo 的作用域中继续查找。在这里找到了 a,因此引擎使用了这个引用
  • b 来讲也是一样的
  • 而对 c 来说,引擎在 bar 中就找到了它

如果 ac 都存在于 barfoo 的内部,console.log 就可以直接使用 bar 中的变量,而无需到外面的 foo 中查找。

遮蔽

作用域查找会在找到第一个匹配的标识符时停止

在多层嵌套作用域中允许定义同名标识符,称为 遮蔽效应(内部的标识符遮蔽了外部的标识符)。

抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上层作用域进行查询,直到遇见第一个匹配的标识符为止。

全局变量会自动成为全局对象的属性(比如浏览器中的 Window 对象),因此可以不直接使用全局对象的词法名称,而是间接地通过对全局对象属性的引用来对其进行访问。

🌰 代码示例

window.a;

通过这种技术可以访问那些被同名变量所遮蔽的全局变量。但非全局的变量如果被遮蔽了,无论如何都无法被访问到。

无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。

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

动态作用域

词法作用域最重要的特征是它的定义过程发生在代码的书写阶段。

那为什么要介绍动态作用域呢?

实际上动态作用域是 JavaScript 另一个重要机制 this 的表亲。作用域混乱多数是因为词法作用域和 this 机制相混淆。

动态作用域 并不关心函数和作用域是如何声明以及在何处声明,它只关心它们从何处调用。

换句话说,作用域链 是基于 调用栈 的,而不是代码中的作用域嵌套。

const a = 2;
function foo() {
console.log(a);
}
function bar() {
const a = 3;
foo();
}
bar();
  • 如果处于词法作用域,变量 a 首先在 foo 函数中查找,没有找到。于是 顺着作用域链到全局作用域 中查找,找到并赋值为 2。所以控制台输出 2
  • 如果处于动态作用域,同样地,变量 a 首先在 foo 中查找,没有找到。这里会 顺着调用栈 在调用 foo 函数的地方,也就是 bar 函数中查找,找到并赋值为 3。所以控制台输出 3

对于两种作用域的区别,简而言之,词法作用域是在 定义 时确定的,而动态作用域是在 运行 时确定的。