这次讲词法作用域。这个概念其实跟JS没太大关系,它一定程度上属于更广泛的层次:程序设计语言。
词法作用域又被成为静态作用域,它的关键特性就是“作用域由定义语句决定”。而和它相对的“作用域由调用语句决定”的,被成为动态作用域。这几个概念,实际上就是在描述构建作用域的规则。
《程序设计语言-实践之路》中说,“在采用静态(词法)作用域的语言里,名字与对象的约束可以在编译时通过检查程序正文确定,完全不需要考虑运行时的控制流。”这句话看起来揭露了为什么“词法作用域”被称为静态作用域。
但是仔细回忆“作用域链 词法作用域 与 闭包(一)”中所讲,JS的一个函数的作用域链事实上是在它被调用时才建立的。这里就有困惑了。既然JS的作用域其实是在调用时才建立,为啥又说它是遵循静态作用域规则呢?
更让人困惑的是,之前不是说JS的作用域链是“由定义它的语句决定”的么?这似乎和“在它被调用时才建立”有矛盾。这个问题让我困惑了很久。事实上呢,两句话都没错。我们还是把上次那个例子拿出来:
function f(x) {
var g = function () { return x; }
return g;
}
var g1 = f(1);
var g2 = f(2);
alert(g1()); //输出 1
alert(g2()); //输出 2
g1的作用域链,确实是在调用它的时候才建立起来的。这个作用域链的内容是:g1的调用对象 -> f(1)的调用对象 -> 全局对象。注意哦,你会发觉,其实这个作用域链中,只有第一个是g1()调用时新建的,剩下的部分其实是f(1)的作用域链。
事实上呢,虽然g1()的作用域链“在它被调用时才建立”,但是,由于除了这个作用域链头部以外,其他部分都定死了,所以这个作用域链的内容也就被“决定”了:无论它在何处被调用,调用几次,即使每次调用时都新建一个调用对象,它的作用域也肯定是“g1的调用对象 -> f(1)的调用对象 -> 全局对象”这个形式。
OK,《程序设计语言-实践之路》的作者Michael L.Scott会说,“可以在编译时通过检查程序正文确定,完全不需要考虑运行时的控制流”呢?按理说,是在调用f(1)时,定义了g1。而f(1)何时何处如何被调用,仍然可能是在编译时无法预知的。
如果要在编译时,就能确定作用域,只有一种可能性,在函数定义时就为函数开辟一个唯一的执行环境。每次调用此函数时,把变量值放到这个执行环境中运行即可。
我试着以这样的假设来重新解释上面那段JS运行时的步骤:
1. 解析到function f(x)时,为其创建一个调用对象F,在该调用对象中定义x变量。
2. 解析到function () { return x; },再创建一个调用对象G,将其加在F的头部,形成作用域链。
3. 执行f(1)的时候,将F的x变量赋值为1,并返回调用对象G的指针。
4. G的指针被赋给g1。
5. 执行g1()的时候,会通过指针找到G,再通过作用域链找到F中的x,返回x。
这样看来,调用对象在解析函数的function定义语句时就生成,好像也是合理的。但问题是,例子中,f(1)执行后紧接着执行f(2)。如果它们俩只用一个调用对象的话,在接下来执行g1()的时候,x就应该是2,而不是1。由此可看出,JS确实是“动态的创建作用域”的。
换句话说,将JS称为是“静态作用域”语言,确实有点问题!
而将JS称为“词法作用域”语言,却不无道理。虽然说,JS是动态的创建作用域链。但由于g1的定义是写在f的执行语句中的,因此,从代码上看,g1只有在f被调用执行的时候,才会被定义。换句话说,g1的调用对象建立的时候,相应的f的调用对象肯定已经被建立了。因此它的作用域结构一开始就由代码结构定死了。(当然,我个人觉得,或许叫“语法作用域”更恰当。词法(Lexical)和语法(Syntax)的区别,请见编译原理教材)。
对此我确实提出几个质疑:
1. 或许Scott对“静态作用域”的解释有误,静态作用域的语言,并非都可以在编译时确定作用域。
2. 或许,他文中说能够确定的是“名字与对象的约束”,这句话并非是指具体的作用域,而是作用域的结构。
3. 如果上面两个质疑都不对,那么“词法作用域”和“静态作用域”不应该是同一概念。至少不应该叫“静态作用域”。
最后,值得一提的是,“词法作用域”和“作用域链”,彼此并不相干。很简单,采用“词法作用域”,也就是“静态作用域”的语言,不一定有“作用域链”。比如C语言,采用的就是“静态作用域”。何以证明?C语言中存在函数指针的概念,因此,一个函数g可能会在另一个函数f中被调用。而无论g在哪里被调用,它的作用域都不会包括调用它的f的作用域,因此,C语言函数的作用域并不是“由调用环境决定的”,它是“静态作用域”。但是C语言不存在作用域链的概念,原因很简单:C语言的作用域被严格限定在函数体内,未在函数体内被声明的变量是不允许使用的。所以它不需要作用域链的概念。
此外,有作用域链(或类似概念)的语言,未必是“静态作用域”的。Perl就是一个例子。
由此看来,是否有作用域链,取决于是否允许使用本代码块中未定义的变量。这种变量被称为自由变量。而自由变量,则是闭包概念中的关键要素。这个下次再讲。


