词法作用域和动态作用域
不知道也可以好好写程序。
定义
写代码的时候,经常使用变量名来代替字面量和输入的内容。
1 | String prefix = "Your name is "; |
定义变量是将值(字面量或者输入)绑定到变量名字的一个过程。
那使用变量的时候,就要有一个相反的过程。
解析变量是将变量名字按照绑定解析为值的一个过程。
在上述例子中 String prefix = "Your name is "
定义了一个绑定 prefix -> "Your name is "
在 Console.WriteLine(prefix + name)
解析 prefix
时,
按照绑定 prefix -> "Your name is "
替换 prefix
为 "Your name is "
。
变量的有效作用区域称为作用域。
按变量解析的规则,作用域分为词法作用域和动态作用域。
词法作用域是解析变量时从词法环境查找变量值的规则。在实际应用中,词法环境通常是块级作用域,函数作用域等。也就是会跟源码有关,先从定义时的作用域查找,找不到再查找外层作用域。因为查找的路径跟源码有关,可以在编译时确定,所以又称为静态作用域。现代语言大多使用静态作用域。
动态作用域是解析变量时从执行环境查找变量值的规则。在实际应用中,执行环境通常是调用栈。也就是会跟运行状态有关。先从运行的函数中查找,找不到再顺着调用栈中的其他函数查找。因为查找的路径与调用顺序有关,只能在运行时确定,所以称为动态作用域。只有少数语言使用动态作用域。
例子
定义不是那么直观。看一个例子
1 | var x = 0; |
按照直觉,也就是现代语言的方式(静态作用域),执行 f()
,f
中返回 x
。
x
没有在 f
中定义,所以就去外层(全局)中查找,找到定义为 var x = 0
。
所以返回 0 。
按照动态作用域的定义,执行 f()
,f
中返回 x
。
x
没有在 f
中定义,所以顺着调用栈查找,f
是被 g
调用的。
g
中定义 var x = 1
。
所以返回 1 。
因此词法作用域可以看源代码就能确定 f()
的返回值,而动态作用域得在运行时才能确定,取决于谁调用了 f()
。
历史
静态作用域因为很符合直觉。所以一出现就纷纷被采用。所以现在大部分语言都是静态作用域。
那为什么一开始语言没有采用静态作用域而是动态作用域呢?
因为动态作用域太容易实现了。
定义变量就把变量名和值的绑定推到栈上。
解析变量就是顺着栈查找。
相当于只有一个词法环境,也就是全局词法环境。
而要实现词法作用域就不是那么简单了。首先要规定词法环境,最小的词法环境是块级作用域还是函数作用域。
然后还要在词法分析的时候做相应的工作。
词法作用域首先出现于 ALGOL ,后被 Scheme 和 Common Lisp 采用。
现在大部分语言都使用词法作用域。
现存
即使是采用词法作用域的语言,也有使用动态作用域的地方。
C 语言中,宏展开使用的是动态作用域。
1 |
|
按照词法作用域的原则,宏 a
中的 x
应该为 2 。与被谁调用无关。
但实际是 b()
打印出 1 。c()
打印出 2 。
所以宏展开是动态作用域。
因此 《C ++ Primer 》推荐少用宏。当然除了这个原因还有一个宏卫生的原因。
JavaScript 也用静态作用域,但可以使用 eval()
模拟动态作用域
1 | var print = x => console.log(x); |