不知道也可以好好写程序。

定义

写代码的时候,经常使用变量名来代替字面量和输入的内容。

1
2
3
String prefix = "Your name is ";
String name = Console.readLine();
Console.WriteLine(prefix + name);

定义变量是将值(字面量或者输入)绑定到变量名字的一个过程。

那使用变量的时候,就要有一个相反的过程。

解析变量是将变量名字按照绑定解析为值的一个过程。

在上述例子中 String prefix = "Your name is " 定义了一个绑定 prefix -> "Your name is "

Console.WriteLine(prefix + name) 解析 prefix 时,

按照绑定 prefix -> "Your name is " 替换 prefix"Your name is "

变量的有效作用区域称为作用域。

按变量解析的规则,作用域分为词法作用域和动态作用域。

词法作用域是解析变量时从词法环境查找变量值的规则。在实际应用中,词法环境通常是块级作用域,函数作用域等。也就是会跟源码有关,先从定义时的作用域查找,找不到再查找外层作用域。因为查找的路径跟源码有关,可以在编译时确定,所以又称为静态作用域。现代语言大多使用静态作用域。

动态作用域是解析变量时从执行环境查找变量值的规则。在实际应用中,执行环境通常是调用栈。也就是会跟运行状态有关。先从运行的函数中查找,找不到再顺着调用栈中的其他函数查找。因为查找的路径与调用顺序有关,只能在运行时确定,所以称为动态作用域。只有少数语言使用动态作用域。

例子

定义不是那么直观。看一个例子

1
2
3
4
5
6
7
8
9
var x = 0;
function f (){
return x;
}
function g (){
var x = 1;
return f();
}
g(); // 返回 0 还是返回 1 ?

按照直觉,也就是现代语言的方式(静态作用域),执行 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
2
3
4
5
6
7
#include <stdio.h>
#define a (x + 1)

int x = 1;
void b () { int x = 0; printf("%d\n", a); }
void c () { printf("%d\n", a); }
void main () { b(); c(); }

按照词法作用域的原则,宏 a 中的 x 应该为 2 。与被谁调用无关。

但实际是 b() 打印出 1 。c() 打印出 2 。

所以宏展开是动态作用域。

因此 《C ++ Primer 》推荐少用宏。当然除了这个原因还有一个宏卫生的原因。

JavaScript 也用静态作用域,但可以使用 eval() 模拟动态作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var print = x => console.log(x);

var x = 1;

function g() {
print(x);
x = 2;
}

function f() {
var g_ = eval("(" + String(g) + ")");
/* g_ = function g() {
print(x);
x = 2;
} */
var x = 3;
g_();
}

f(); // prints 3

print(x); // prints 1

参考