The World of TomasRan

词法作用域和动态作用域的点点剖析

作用域和作用域链

不同的国家制定不同的法律来依法量刑。但是法律有界限,它们只对自己的国民有着束缚之力。各国的法律只有在自己国土上才有效,才被认可,这个有效范围的限制,就是法律的作用域。

作用域即是对权限的界定。作用域的主体是不可忽视的,它明确了是对什么事物的权限界定。上面所讲的,法律就是作用域的主体。

那么什么是作用域链呢?假设现在从地方乡镇到市政府到省政府指定了不完全相同的法律。某人做了一件有违公共道德的事情,地方乡镇发现没有对该罪行制定相应的处罚措施,但是我们可是依法治国、以德治国的国家啊,不能就这样罢休!于是乡镇法院将人犯转移到市级法院,想让市法院依据指定的法律法规对其进行审判,市法院扫了一眼法律法规,也无可奈何,那就交给省法院吧(人犯走了不少路),省法院发现,哎,根据我们的法律法规,他这么做是要处刑的,哈哈,终于可以给这家伙定刑了吧,于是给他定了个xx犯的头衔。

在这里,各级法院的法律法规就构成了一条法律的作用域链。我们从最底层的法院(靠人犯犯案地点最近的法院)进行向上查找,直到找到明确可以给他量刑的法律,然后名正言顺地把他打入大牢。

我们可以理解为作用域就是法律制定的时候规定的有效范围,而作用域链是法律执行时候的一系列依据。

在计算机中也存在着相同的概念,现在我们所要谈论的作用域,是编程语言中标识符的作用域;谈论的作用域链,是程序执行过程中标识符的取值的查找依据。

因为编程语言设计的差异,目前,针对标识符,存在着两种不同的作用域规则,也就是两种不同的作用域链生成机制,一是词法作用域(Lexical Scoping),二是动态作用域(Dynamic Scoping)。它们规定了标识符(以下都称为变量)在程序中的可访问性。

词法作用域

词法作用域又称静态作用域(Static Scoping),词法作用域是根据编译的阶段进行命名(编译阶段依次经历词法分析、语法分析、语义分析等阶段),意味着变量的作用域在编译的词法分析阶段就已经确定了。

词法作用域规定变量的作用域链在声明的时候就已经确定,或者说在编译的时候已经确定,在声明它的代码块内部才可以被访问。以Javascript语言为例:

var a = 'a';

function f() {
var b = 'b';
console.log(a);
}

f();
console.log(b);

上面的例子执行的最终结果是输出了字符 “a”、“undefined”,为什么?变量a的声明和函数f的声明都在同一个代码块中,所以在函数f的内部也可以访问到变量a,也就是外部的变量a在函数f内部访问的作用域链上;但是变量b的声明是在函数f的内部,因此不在外部代码块的作用域链上。

这就是词法作用域指定的规则,需要牢记变量的作用域链是在声明时就已经生成确定。

动态作用域

说完了静态作用域,现在可以谈谈它的老对手动态作用域,动态作用域的规则是:变量的作用域链的形成是在代码执行阶段。我们从一个例子说起:

var a = 'a';

function f1() {
console.log(a);
}

function f2() {
var a = 'b';
f1();
}

f2();

上面的代码采用的Javascript的语法,执行结果是什么? 答案是 “a”。再次搬出词法作用域的规则:变量的作用域链是在声明的时候就已经确定。上段代码在声明的时候,f1中的变量a已指向最外层的变量a。因此,当执行f2的时候,f2内部重新声明的变量a,只是开辟了新的内存地址,不会覆盖最外层的变量a。在f2中调用f1,由于f1中的变量a在声明时已经确定指向了最外层的变量a,所以只要最外层的变量a的值不变,它就会输出 “a”。

这是以上这段代码在静态作用域下的表现。那么现在,我们假设Javascript是采用动态作用域,上述函数的执行结果又会是什么呢?

鉴于大家恐是无法通过编码去验证这一段代码,我也就只能公布一下正确答案,传递传递思想了(如果想要实践的话可以去接触接触Lisp的早期版本),这一次的输出将会是 “b”。

动态作用域规则指出变量作用域的确定是在执行过程中。当语句执行到f1中的输出语句时,需要查询变量a的值,此时,动态作用域规则会根据函数的调用栈,从语句所在的函数开始,依次向外直到最外层全局环境,去搜索变量的定义直到找到为止。因此,在不知道具体执行之前,我们是无法直到f1中的变量a到底指向了哪里,这就是动态作用域设定的游戏规则。

青梅煮酒论英雄

总结来看,个人还是认为静态作用域的优势明显要强于动态,对于静态作用域,我们可以只通过分析代码文件就能够确定变量的作用域和程序的输出,无论从bug调试、编码逻辑上来看都会轻松不少,增强了程序代码的可维护性。其次,动态作用域在调用的时候动态生成作用域链肯定会影响执行的速度,从性能上来说静态占优。看看Lisp已经成功完成了向静态作用域的华丽丽转型,再看看静态作用域的语言类型压倒性的优势,似乎都足以说明问题。