再说本文的内容之前,我们先回溯到1995年,当Brendan Eich在设计第一版JavaScript时,他搞错了许多东西,当然这也包括曾属于语言本身的一部分,例如Date对象,对象相乘被自动转换为NaN等。然而现在回过头看,语言最重要的部分都是设计合理的:对象、原型、具有词法作用域的一等函数、默认情况下的可变性等。语言的骨架非常优秀,甚至超越了人们对它的初步印象。
话说回来,正是Brendan当初的设计错误才诞生了今天这篇文章。我们这次关注的目标非常小,在你使用这门语言多年后可能根本不会注意到这个问题,但是它又如此重要,因为我们可能会误认为这个错误就是语言设计中的“the good parts”(译者注:请参考《JavaScript语言精粹》一书中附录A:毒瘤中有关作用域的描述)。
今天我们一定要把这些与变量有关的问题拿下。
问题 #1:JS没有块级作用域
请看这样一条规则: 在JS函数中的var声明,其 作用域 是函数体的全部 。乍一听没什么问题,但是如果碰到以下两种情况就不会得到令人满意的结果。
其一,在代码块内声明的变量,其作用域是整个函数作用域而不是块级作用域。
你之前可能没有关注到这一点,但我担心这个问题确实是你不能够轻易忽视的。我们一起重现一下由这个问题引发的bug。
假如你现在的代码使用了一个变量t:
function runTowerExperiment(tower, startTime) { var t = startTime; tower.on("tick", function () { ... 使用了变量t的代码 ... }); ... 更多代码 ... }
到目前为止,一切都很顺利。现在你想添加测量保龄球速度的功能,所以你在回调函数内部添加了一个简单的if语句。
function runTowerExperiment(tower, startTime) { var t = startTime; tower.on("tick", function () { ... 使用了变量t的代码 ... if (bowlingBall.altitude() <= 0) { var t = readTachymeter(); ... } }); ... 更多代码 ... }
哦,亲爱的,之前那段“使用了变量t的代码”运行良好,现在你无意中添加了第二个变量t,这里的t指向的是一个新的内部变量t而不是原来的外部变量。
JavaScript中var声明的作用域像是Photoshop中的油漆桶工具,从声明处开始向前后两个方向扩散,直到触及函数边界才停止扩散。你想啊,这种变量t的作用域甚广,所以一进入函数就要马上将它创建出来。这就是所谓的提升(hoisting)。变量提升就好比是,JS引擎用一个很小的代码起重机将所有var声明和function函数声明都举起到函数内的最高处。
现在看来,提升特性自有它的优点。如果没有提升的动作,许多在全局作用域范围内看似合理的完美技术在立即调用函数表达式( IIFE )中通通失效。但在上面演示的这种情况下,提升会引发令人不愉快的bug:所有使用变量t进行的计算最终的结果都是NaN。这种问题极难定位,尤其是当你的代码量远超上面这个玩具一般的示例,你会发狂到崩溃。
在原有代码块之前添加新的代码块会导致诡异的错误,这时候我就会想,到底是谁的问题,我的还是系统的?我们可不希望自己搞砸了系统。
而这个问题与接下来这个问题相比就相形见绌了。
问题 #2:循环内变量过度共享
你可以猜一下当执行以下这段代码时会发生什么,非常简单:
var messages = ["嗨!", "我是一个web页面!", "alert()方法非常有趣!"]; for (var i = 0; i < messages.length; i++) { alert(messages[i]); }
如果你一直跟随这个专栏的文章,你知道我喜欢在示例代码中使用alert()方法。可能你也知道alert()不是一个好的API,它是一个同步方法,所以当弹出一个警告对话框时,输入事件不会触发,你的JS代码,包括你的整个UI,直到用户点击OK确认之前完全处于暂停状态。
请不要轻易使用alert()来实现Web页面中的功能,我之所以在代码中使用是因为alert()特性使它变成一个非常有教学意义的工具。
而且,如果放弃所有笨重的方法和糟糕的行为就可以做出一只会说话的猫,何乐而不为呢?
var messages = ["喵!", "我是一只会说话的猫!", "回调(callback)非常有趣!"]; for (var i = 0; i < messages.length; i++) { setTimeout(function () { cat.say(messages[i]); }, i * 1500); }
然而一定是哪里不对,这只会说话的猫并没有按照预期连说三条消息,它说了三次“undefined”。
你知道问题出在哪里么?
你能看到树上的毛毛虫(bug)吗?(图片来源: nevil saveri )