作用域(二)——词法作用域

260 阅读4分钟

在上一篇文章中,我们将“作用域”定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。大家还可以关注我的微信公众号,蜗牛全栈。

作用域共有两种主要的工作模型。第一种是最为普通的,被大多数编程语言所采用的词法作用域,我们会对这种作用域进行深入讨论。另外一种叫做动态作用域,仍有一些编程语言在使用(比如Bash脚本、Perl中的一些模式等)。

简单的说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域就是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法处理器处理代码时会保持作用域不变(大部分情况是这样的)。对一部分比较特殊的,会出现一些欺骗词法作用域,主要是JavaScript中的with和eval关键字,非常不建议用在项目中。

考虑以下代码:

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

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

图片

1、包含整个全局作用域,其中之后一个标识符:foo

2、包含着foo所创建的作用域,其中有三个标识符:a、bar和b

3、包含着bar所创建的作用域,其中只有一个标识符:c

作用域气泡由其对应的作用域块代码写在哪决定,他们是逐级包含的。在后续的文章中会讨论不同类型的作用域,但现在主要假设每一个函数都会创建一个新的气泡作用域就好了。

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

注意,这里所说的气泡是严格包含的。我们并不是在讨论文氏图这种可以跨越边界的气泡。换句话说,没有任何函数的气泡可以(部分地)同时出现在两个外部作用域的气泡中,就如同没有任何函数可以部分地同时出现在两个父级函数中一样。(简单理解就是内部作用域一定是上层作用域的子集)

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

在上一个代码片段中,引擎执行console.log声明,并查找a、b、c三个变量的引用。它首先从最内部的作用域,也就是bar函数的作用域气泡开始查找。引擎无法在这里找到a,因此会去上一级到所嵌套的foo的作用域中继续查找。在这里找到了a,因此引擎使用这个引用。对b来讲也是一样的。而对c来说,引擎在bar中就找到了它。

如果a、c都存在bar和foo的内部,congsole.log就可以直接使用bar中的变量,而无需到外面的foo中去查找。

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

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

window.a

通过这种技术可以访问那些被同名变量所遮蔽的全局变量,但非全局变量如果被遮蔽了,无论如何都无法被访问到。 无论函数在哪里被调用,也无论它如何被调用,他的词法作用域都只由函数被声明时所处的位置决定。

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

参考文献:《你不知道的JavaScript(上)》