如果你爱上了JavaScript这门诡异的语言,那我相信你一定在与其恋爱期间饱受了其变量作用域所引发的一系列问题的不少摧残。对于任何一门编程语言,变量作用域都是一个关切的话题。正如David Herman在《Effective JavaScript》中的形象比喻,“Scope is like oxygen to a programmer”。当你“呼吸顺畅”的时候,你并不会意识到变量作用域的重要性;然而当你“呼吸受阻”的时候,你便会体会到它的轻重高低。
绝大多数编程语言都有全局作用域的概念。全局作用域是指常量、变量、函数等对象的作用范围在整个应用程序中都是可见的。对于不同的编程语言,全局作用域承担着不同的角色,也因此遭受了不少的骂名。但对于JavaScript,我并不认为它一无是处。我们要做的便是理解它并正确地使用它。
考虑下这样一个场景。Bill和Peter在同一家公司工作,他们的薪水由两部分组成:a和b。以下是表示他们薪水组成的数据结构。
输出的结果并不是你口算的4750,而是2500。这是因为变量i、n和sum都是全局变量,在执行salary(emps0)之后i的值变为了2,再回到averageSalary函数的循环体中时emps数组已然越界,最终sum的值只计算了emps数组中的第一个元素。
如果这样的全局作用域问题并不会困扰你,那下面的问题似乎应当引起你的一些警觉。因为与此相比,它有点意想不到。
问题并不是出在交换数组元素上,而是我们无意间创建了一个全局的变量temp。这要完全归功于JavaScript的语言规范——JavaScript会将未使用var声明的变量视为全局变量。庆幸的是,我们可以借助于类似Lint这样的代码检测工具帮我们尽早地发现这类问题。
虽然全局变量有很多问题,然而它在支撑JavaScript模块之间数据共享、协同合作方面确实承担了重要的角色。此外,程序员在某些不支持ECMAScript 5的环境中利用其特性检查的功能来填补一些ES5特有的特性确实受益良多。
在程序设计语言中,变量可分为自由变量与约束变量两种。简单来说,局部变量和参数都被认为是约束变量;而不是约束变量的则是自由变量。 在冯·诺依曼计算机体系结构的内存中,变量的属性可以视为一个六元组:(名字,地址,值,类型,生命期,作用域)。地址属性具有明显的冯·诺依曼体系结构的色彩,代表变量所关联的存储器地址。类型规定了变量的取值范围和可能的操作。生命期表示变量与某个存储区地址绑定的过程。根据生命期的不同,变量可以被分为四类:静态、栈动态、显式堆动态和隐式堆动态。作用域表征变量在语句中的可见范围,分为词法作用域和动态作用域两种。
在词法作用域的环境中,变量的作用域与其在代码中所处的位置有关。由于代码可以静态决定(运行前就可以决定),所以变量的作用域也可以被静态决定,因此也将该作用域称为静态作用域。在动态作用域的环境中,变量的作用域与代码的执行顺序有关。下面这段代码的输出会是什么?
如果你的回答是1, 2或3, 1都没有错,因为这取决于该段代码所处的环境。如果处于词法作用域中,答案便是1, 2;如果处于动态作用域中,答案便是3, 1。
词法作用域允许程序员根据简单的名称替换就能推导出对象引用,例如常量、参数、函数等。这使得程序员在编写模块化的代码是多么的得心应手。同时,这可能也是动态作用域令人感觉到晦涩的原因之一。词法作用域最早可以追溯到ALGOL语言。尽管最早的Lisp解释器和早期的Lisp变种都采用动态作用域,但随后的动态作用域语言都支持了词法作用域。Common Lisp和Perl的语言演化就是最好的证明。JavaScript和C都是词法作用域语言。不过值得一提的是,不像JavaScript,深受ALGOL语言影响的C语言并不支持嵌套函数。这对后来的C族语言影响深远。除了晦涩难懂之外,现代程序设计语言很少支持动态作用域的原因是动态作用域使得引用透明的所有好处荡然无存。
如果你还在使用类似下面的代码为with语句找借口,那这正好是放弃它的真正原因。
JavaScript会将with语句中的对象插入到词法作用域的链表头。这将使得status函数非常脆弱。例如,
第二次status函数调用并不会得到预期的结果“Status:connected”而是“Status:widget info”。这是因为在第二次status函数调用之前,我们修改了widget的原型对象(增加了一个info属性)。这将导致status函数的参数info会被处于词法作用域链表头的widget对象的原型对象中的info属性所屏蔽。除此之外,with语句还会导致性能问题。这与在采用链地址法解决散列冲突的散列表中查找关键字是异曲同工的。下面是修正的代码。