作用域是个语言无关的概念,你要接触过Lisp或者Scheme等语言,应该对这个概念会非常熟悉。我在这篇文章会介绍词法作用域和动态作用域的基本知识,让你刚好对这个概念了解,此外,我们还会讨论下JavaScript的词法作用域。
首先你得明白程序设计中作用域这个概念:通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
词法作用域,也叫静态作用域,它的作用域是指在词法分析阶段就确定了,不会改变。动态作用域是在运行时根据程序的流程信息来动态确定的,而不是在写代码时进行静态确定的。
我们以下面这段Javascript代码来说明词法作用域和动态作用域,但是你要明白,阐述的概念是与Javascript无关的。
var a = 2;
function foo() {
console.log(a); // 会输出2还是3?
}
function bar() {
var a = 3;
foo();
}
bar();
如果是词法作用域,它会让 foo()
函数引用到全局作用域中的 a
,因此会输出 2
。我们说过,词法作用域是写代码的时候就静态确定下来的。Javascript中的作用域就是词法作用域(事实上大部分语言都是基于词法作用域的),所以这段代码在浏览器中运行的结果是输出 2
。
而动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。因此,如果Javascript具有动态作用域,理论上输出结果是 3
。
为什么会这样?因为当 foo()
无法找到 a
的变量引用时,会顺着调用栈在调用 foo()
的地方查找 a
,而不是在嵌套的词法作用域链中向上查找。由于 foo()
是在 bar()
中调用的,引擎会检查 bar()
的作用域,并在其中找到值为 3
的变量 a
。
你可能会觉得很奇怪,但这其实是因为你可能只写过基于词法作用域的代码(或者至少以词法作用域为基础进行了深入的思考),因此对动态作用域感到陌生。如果你只用基于动态作用域的语言写过代码,就会觉得这是很自然的,而词法作用域看上去才怪怪的。
需要明确的是,Javascript并不具有动态作用域,它只有词法作用域,简单明了。但是,它的 eval()
、with
、this
机制某种程度上很像动态作用域,使用上要特别注意。
主要区别:词法作用域是在写代码或者定义时确定的,而动态作用域是在运行时确定的(this也是!)。词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。
参考资料
- 书籍:《你不知道的JavaScript:上卷》
- 百度百科词条:作用域
- cnblogs:《浅谈静态作用域和动态作用域》