动态作用域
相比于静态作用域,动态作用域会假设变量与值(环境)的约束关系不能在解析阶段确定。一般来说,这里变量与值并不是在词法环境中被绑定在一起的,而是通过一个全局的动态变量栈来管理的。每当遇到一个变量声明,就会把这个变量名放入这个栈中,当这个变量的生命周期结束时,又会把这个变量从栈中移除。
这意味着,在一个简单的函数中就可以存在无限多的关于同一个变量的解释,这都取决于函数的调用上下文。和词法变量类似,采用动态作用域的变量叫做的动态变量。
我们用类Pascal伪代码语法改写上面的例子,这次采用动态作用域:
// *pseudo* code – 使用动态作用域
y = 20;
procedure foo()
print(y)
end
// 在当前的全局变量栈上,变名为y的变量
// 当前只拥有一个值20
// {y: [20]}
foo() // 20, OK
procedure bar()
//在这里变量栈上拥有两个y值{y: [20, 30]};
//首先使用的是栈顶的
y = 30
// 因此:
foo() // 30!, not 20
end
bar()
我们看到,调用者的环境影响了变量的解析。我们知道,一个函数(一个 callee)可能会在程序中的多个不同位置被调用,调用时拥有的状态也都不一样,因此很难在解析阶段确定函数被调用的准确环境,所以这是为什么这种作用域形式被称作动态的。(译注:动态作用域并不关心函数是如何声明或者在何处声明的,它只关心函数是在哪里调用的,它的作用域链是基于调用栈,不像静态作用域那样基于作用域嵌套。)
由此,动态作用域中变量与值的约束关系是在执行环境下确定的,而静态作用域下变量与值的约束是在定义环境下确定的。
动态作用域的一个优点是允许同样的代码应用于具有不同状态(随着时间变化)的系统。然而,这种优点需要你对在函数运行过程中所有可能出现的情况都了然于心。
很明显,使用动态作用域无法为一个变量创建闭包。(因为这时候自由变量相对于内部函数已经失去了意义,在外部调用的时候就失去了对自由变量的访问)。
今天,大部分流行编程语言不使用动态作用域。但在一些语言,尤其是perl中(或在Lisp的一些方言中),允许程序员决定一个变量是采用静态作用域或者动态作用域。
看一眼下面这个Perl例子。通过关键字my声明了一个词法变量,而local声明了一个使用动态作用域的动态变量。
$a = 0;
sub foo {
return $a;
}
sub staticScope {
my $a = 1; # lexical (static)
return foo();
}
print staticScope(); # 0 (from the saved global frame)
$b = 0;
sub bar {
return $b;
}
sub dynamicScope {
local $b = 1; # dynamic
return bar();
}
print dynamicScope(); # 1 (from the caller's frame)
“With” 和 “eval” — ECMAScript中的动态作用域
在最开始提到过,ECMAScript 中不适用动态作用域。但是,在ES中有两个方法有时候被认为是可以给静态作用域添加动态性。通过这两种方法修改后的静态作用域可以认为是类似动态作用域。但注意,这种模拟的动态作用域不存在标准动态作用域中的全局变量堆栈,只是在解析阶段无法确定变量与之绑定的环境。这两种方法就是with和eval,通过它们修改ECMAScript静态作用域的行为叫做”运行时作用域增强“。
看下面一段代码:
var x = 10;
var o = {x: 30};
var storage = {};
(function foo(flag) {
if (flag == 2) {
eval("var x = 20;");
}
if (flag == 3) {
storage = o;
}
with (storage) {
// x可能被解析到全局作用域中,值为10
// 也可能被解析的函数的局部作用域中,值为20
// 来源于eval方法执行结果的,或者从storage对象中获取
alert(x); // ? – 变量x的作用域在编译阶段无法确定
}
// 递归调用3次
if (flag < 3) {
foo(++flag);
}
})(1);
我们看到,使用with和eval虽然加强了静态作用域,但相比之下,with和eval在实现上也可能会削弱变量查找和词法环境储存的性能(译注:因为这相当于扩充了当前作用域,增加了存储消耗和查找范围)。因此在ES5中,with在严格模式下被移除了,此外严格模式下的eval方法不再会在当前上下文创建变量。所以,ES5的严格模式下是完全遵守词法环境的实现。
在本文的其余部分将主要介绍词法(静态)作用域,包括它的内部细节和实现。但在这之前,我们需要接单介绍下一些关于变量绑定(name binding)的概念,因为它经常会与环境概念出现在一起。
引用
英文原版链接 Lexical environments: Common Theory.