探究JS中的词法作用域

这两天在对原生 JavaScript 的一些概念做一个回顾。在看一篇名为 JavaScript 语言精粹 的文章时,看到作者认为 JavaScript 的优秀思想中有一条是“函数:基于词法来划分作用域,而不是动态划分作用域”。无独有偶,在二刷廖雪峰的 JavaScript 教程时,在 箭头函数 一节中有提到“箭头函数内部的 this 是词法作用域,由上下文确定。”

这里的“词法作用域”这个概念我之前没有留意过,在《JavaScript 高级程序设计》中好像也没有看到过(也有可能是我压根没注意到)。作为一只刚入前端坑的菜鸡,我在查阅各种资料后总结了自己对词法作用域的一些理解。

词法作用域

《JavaScript 权威指南》第5章“8.8.1 词法作用域”中对“词法作用域”的解释如下:

“JavaScript 中的函数是通过词法来划分作用域的,而不是动态地划分作用域的。这意味着它们在定义它们的作用域里运行,而不是在执行它们的作用域里运行。当定义了一个函数,当前的作用域链就保存起来,并且成为函数的内部状态的一部分。”

当然,这几句话还是玄之又玄,摸不着头脑。它还是没有具体解释“词法作用域”中的“词法”二字代表什么。不过我这学期的《编译原理》课程上经常见到这个词:编译过程被划分为词法分析、语法分析、语义分析、中间代码生成、代码优化和目标代码生成 6 个阶段(清华大学出版社《编译原理》)。在查阅资料后,我确信这个概念确实与编译有关。

JavaScript 引擎在代码执行前会对其进行编译,而所谓的词法作用域指作用域是由书写代码时函数声明的位置决定,在词法解析阶段就已经确定,之后不会改变。也就是说,JS 中的“词法作用域”等同于静态作用域,即与动态作用域(运行时确定)相对。

词法作用域关注函数在何处声明;而动态作用域关注函数从何处调用,其作用域链是基于运行时的调用栈的。换言之,在遇到既不是形参也不是函数内部定义的局部变量的变量时,词法作用域的函数会去函数定义时的环境中查询;而动态作用域的函数会到函数调用时的环境中查询。

一个例子:

1
2
3
4
5
6
7
8
9
10
11
function foo(){
print a;
}
function bar(){
var a = 1;
foo();
}
var a = 2;
bar();

采用词法作用域的语言会从函数定义位置开始向上层查找,最后输出 2。

而采用动态作用域的语言会输出 1。

顺便一提,在 JS 中evalwith可以产生动态作用域的效果。但《JavaScript 高级程序设计》不推荐使用此二者。

强调与补充

函数的作用域基于函数定义的位置。

为了去实现这种词法作用域,JavaScript 函数对象的内部状态不仅包含函数逻辑的代码,除此之外还包含当前作用域链的引用。函数对象可以通过这个作用域链相互关联起来。

如此,函数体内部的变量都可以保存在函数的作用域内,在程序语言范畴内这被称为闭包。而我们常说的闭包是指让外部函数访问到内部的变量,也就是说,按照一般的做法,是使内部函数返回一个函数,然后操作其中的变量。这样做的话一是可以读取函数内部的变量,二是可以让这些变量的值始终保存在内存中。

这时,调用函数的时候闭包所指向的作用域链和定义函数时的作用域链不是同一个作用域链。

参考资料:

动态作用域和词法域的区别是什么?- 知乎

JavaScript深入之词法作用域和动态作用域 · Issue #3 · mqyqingfeng/Blog