这次讲词法作用域。这个概念其实跟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就是一个例子。
由此看来,是否有作用域链,取决于是否允许使用本代码块中未定义的变量。这种变量被称为自由变量。而自由变量,则是闭包概念中的关键要素。这个下次再讲。
大仙: 2008-11-23 11:51 am
哎,看不懂。希望博主能够更新点CSS标准的东东。
starflash: 2008-11-26 3:28 pm
js 在每个函数被调用时都会创建相应的执行环境,每个函数都是在不同的执行环境中运行的。在该执行环境中会存在一个作用域链,这个作用域链是由——在被调用函数中[[scope]]属性所保存的作用域链的头部添加当前执行环境的调用对象所组成的。
所以回到你举的这个例子来说,f(1)和f(2)运行时创建了2个不同的执行环境,也就产生了2个不同的作用域链,而返回的函数g1和g2内部也就拥有了不同的作用域链,在g1的作用域链中保存x=1,而在g2的作用域链中保存x=2。
hax: 2008-11-29 2:36 am
【将JS称为是“静态作用域”语言,确实有点问题!】
哎,你不要把 定义 和 实现 混淆起来嘛。
可以通过静态分析确定作用域,不等于在实践中一定要通过一次性的静态编译过程来确定作用域。否则解释语言岂不是都只能用动态作用域?
再者说,我已经强调过了,JS并不是完全的lexical scope,所以无法仅仅通过静态编译过程来确定作用域。
由上述两点,可以知道认为JS不是“静态作用域”而是“lexical scope”,进而认为static scope不等于lexical scope,纯粹是认识上的误区。Scott的解释完全没有问题。纯粹的静态作用域语言,肯定可以在编译时确定作用域。但是当然也可以留待到运行时确定,这并不矛盾。现有的JS为什么采用在运行时才确定作用域的scope chain机制,是因为JS不是纯粹的静态作用域语言,无法在编译时确定所有的作用域。
再,关于lexical scope和scope chain的关系。
之前已经说过,scope chain只是一种实现手段,可以用来实现静态作用域(当然也可以实现非静态作用域,例如with的构造)。唯一独特的是,ES3规范把scope chain机制写到了规范之内,这可能使人误以为所有JS引擎都必须采用scope chain的方式。从某种程度上说,scope chain不应出现在ES规范中,现在ES3规范中对于scope chain的描述,应该被理解为,通过对此种机制的描述来更准确的定义JS语言的行为,而不是说,JS引擎就必须采用scope chain。
实际上未必如此,只要效果等价,你完全可以用其他方式。许多JS引擎都有优化,即在某些条件下并不使用scope chain机制。
反过来说,之所以现在JS要使用scope chain,也是因为JS不是完全的lexical scope。
未来JS可能加入一个pragma:“use lexical scope”,在开启此pragma之后,不允许使用with结构,不允许在eval中修改当前scope,不允许scope chain上的任何一个属性被delete,总是使用初始的global绑定(即动态的增加一个global变量是不能被访问到的,除非显式的使用window.xxx来访问)……总而言之一句话,允许引擎在这种情况下按照完全的静态作用域方式来确定变量,而不必缓慢的在scope chain上逐级检索。
此外,scope chain机制可以实现closure,但是也并不表示closure一定要用scope chain实现。Python 2.1开始支持static nested scope,它是采用flat closures方式来实现的。Lua也采用flat closures来实现,参见:http://blog.csdn.net/hunhun1981/archive/2008/04/10/2276334.aspx 。如果JS没有with和eval捣蛋,也可以抛开scope chain转用其他方式实现closure。
小麦: 2008-12-1 9:34 pm
呵呵,好长。容许我过段时间再来看。