E 调试
当浏览器不理解某个JavaScript命令时,它就会产生一条JavaScript错误消息。你需要用这些消息来调试,但不幸的是——有时很难读懂它们。
有时候,错误消息不能帮助你,或是根本就没有用,即使是你的脚本很明显出了什么错误。这种情况下,你就必须转向其他的猎杀bug(bug-hunting)技术。
错误消息
首先,你必须确保当错误发生时,你可以看到错误消息。对绝大多数浏览器来说,这意味着打开JavaScript错误控制台。
l Mozilla: 工具(Tools)> [Web开发(Web Development)>]JavaScript控制台(JavaScript Console)(“Web开发”子菜单在Mozilla中存在,但Firefox没有)。
l Explorer:双击状态栏左下角的黄色惊叹号,接着选中错误消息弹出对话框中的复选框。
l Safari:调试(Debug)>显示JavaScript控制台(Show JavaScript Console)。
l
Opera:首选项(Preferences)>高级(Advanced) > 内容(Content) > JavaScript选项(JavaScript Options)>遇到错误时打开JavaScript控制台(Open JavaScript console on
error)(对8.54版本有效。不幸的是,在我的9.0 beta 2版中内容菜单已消失。一般来说,Opera总是喜欢把这些选项在标签或菜单中移来移去)。
你将发现错误消息的质量会大大改变。以下面这段错误脚本为例:
var x =
document.getElementById('tes');
alert(x.nodeName);
<div id="test"></div>
我不小心把test最后的一个t忘记了,所以浏览器不知道我所指的东西。我们来看看它们是怎么反应的(如图3-2)。

图3-2 Mozilla的错误消息。清晰并切中要害
Mozilla的消息是最清楚的:“x has no properties”。点击该链接就能立即带你到相应的那行代码处(如图3-3)。

图3-3 Opera也给出了有用的信息
尽管Opera的无法转换(could not convert)的消息有一点含义模糊,但它不只是显示错误在哪里发生,还指明包含错误的函数是在哪里被调用。本质上它是跟踪这个轨迹直到它发现被调用的函数并反馈一个事件,这被称为栈跟踪(stack trace)(如图3-4)。
不幸的是,Explorer的消息很模糊。它虽然告诉你缺少对象(object required),但大概60%的问题中,你看到的都将是这条消息,而且Explorer永远不会指明到底缺少哪个对象或者为什么。

图3-4 Explorer的行数不能处理被引入的脚本
此外,Explorer还不能处理被引入的脚本:
<script src="included.js"></script>
如果你添加单独的脚本文件到页面中——在2C中我们已经看到这是当今的最佳实践,但Explorer依然认为错误出现在主HTML页面中。所以,它报告的错误行数通常毫无意义(如图3-5)。

图3-5 Safari的消息不是很有用
最后是Safari,它给出的是所有浏览器中最含糊的错误消息:“Null value”。没错,但没有什么指导作用。
因为 Mozilla 有最好的错误消息,所以只要我遇到问题,我就立刻切换到Mozilla去获取优质可靠的错误消息来帮助我确切地知道错误是什么。我建议你也这样做。
当然,如果错误只在Explorer或Safari中才发生,你就有麻烦了,现在得更深入地猎杀bug了。
调 试 工 具
Mozilla和微软都提供了据说很强大的脚本调试工具。我没怎么用过它们,所以我只是提供它们的链接。
针对Mozilla的“Venkman”JavaScript调试器可以从这个地址中找到:http://www.mozilla.org/ projects/ venkman/。
这篇Explorer的博客文章讨论了Microsoft Script Debugger:http://blogs.msdn.com/ie/archive/2004/ 10/26/247912.aspx。
处理浏览器的bug
有时错误消息可以立即揭示出错误的对象和原因。而有时候,即使是Mozilla的错误消息也不能提供更多的帮助,你根本不知道正在发生什么。甚至有时明明知道有什么东西出错了,但却没有错误消息出现。
在所有这些情况下,你只能人工分离出bug。通常来说这涉及如下3个步骤:
(1) 如果不能确认是哪个函数引起了bug,通过在函数的第一行加入return语句,我会一个接一个地关闭所有的函数。如果某个函数不再运行,而bug也消失了,就能确认bug是由它造成的。注意,如果已经找到一个引起bug的函数,应该继续检查其他所有的函数;bug可能是由两个函数结合才引起的。
(2) 找到这个讨厌的函数后,就会添加alert语句。我把alert放在第一段代码块之后,然后再测试一次。如果bug出现了,就归咎于alert之上的语句,如果bug没有出现,我就把return或alert移到下一个逻辑块之后。一旦找到了引起错误的块,会使用alert作更进一步的搜索微调,直到分离出那一行。
(3) 当成功地识别出浏览器bug后,我会创建一个bug报告,以及一个测试页面。
使用alert
alert语句可以使它所在的函数停止执行,这就给了你检查bug是否已经发生的机会。以我们之前看到的test拼写错误的脚本为例。假设我还没有发现少了一个t,并且还在寻找bug产生的原因。
var x =
document.getElementById('tes');
alert(x.nodeName);
<div id="test"></div>
alert语句给出一条明确的线索,因为正是它自己引起了bug。那将迅速地指引你去怀疑x的值是不正确的。在实际情形中,我会更改这个alert语句:
alert(x);
现在,alert弹出对话框会显示undefined。这样我就能肯定bug是由于x没有定义引起的;我只要去细看对x的赋值过程即可,然后我将很可能发现那个缺失的t。
alert与comfirm
有时候alert会很烦人。我们假设错误发生在一个遍历某HTML页面上所有的<tr>的for循环中。我怀疑其中某一行的单元格数量有些问题,所以就想查看每一行的单元格数量。alert允许这么做:
var rows = document.getElementsByTagName('tr');
for (var i=0;i<rows.length;i++) {
var cells = rows[i].getElementsByTagName('td');
alert(i + ': ' +cells.length);
var name = cells[1].firstChild.nodeValue;
}
从一定程度上说,这样是可行的。现在我可以看到表格每一行的索引编号和它的单元格数量。不幸的是,每次脚本找到一个新的<tr>,就会弹出一个alert窗口并一直继续下去。如果你的HTML含有几百个<tr>,那你就会见到几百个alert弹出对话框。那可够你手忙脚乱好一阵了。
你可以用confirm来代替:
var rows = document.getElementsByTagName('tr');
for (var i=0;i<rows.length;i++) {
var cells = rows[i].getElementsByTagName('td');
if (!confirm(i + ': ' +cells.length + '. Continue?'))
break;
var name = cells[1].firstChild.nodeValue;
}
alert和confirm
我们将在6E正式讨论alert和confirm。
confirm包含了与alert完全相同的消息,但它增加了使你可以在找到出错那一行后终止函数继续执行的选择。如果confirm返回false(即点击“取消”按钮),就会for循环,而你也不会被迫在更多的alert中费力前进。
错误控制台
在复杂的调试活动中,有时我会创建一个错误控制台。
function initConsole() {
var console = document.createElement('div');
console.id = 'errorConsole';
document.body.appendChild(console);
}
function writeToConsole(message) {
var newMessage = document.createElement('p');
newMessage.innerHTML = message;
var console = document.getElementById('errorConsole');
console.appendChild(newMessage);
}
现在我就可以在需要一段调试文本的时候随时调用writeToConsole(),如下:
function complicated() {
var x = [an object that isn't what I expect it to be];
writeToConsole('x is now ' + x.nodeName);
}
在过去,类似的功能是由弹出窗口来实现的;6F有一个代码实例。
范例
为了说明该如何对付浏览器bug带给你的每日苦差,我准备给你一个真实的例子。为本书准备“易用的表单”的时候,我注意到当软重载(soft-reload)页面时,Mozilla的行为古怪。在软重载(即用户没有按住Shift键时重载页面)之后,Mozilla和Explorer保留了所有表单域的值。
不管怎样,Mozilla总是会选中在重载之前被选中的单选框的上面那个单选框。它没有给出错误消息,所有看起来都一切正常。很明显这是一个bug,要么是我的脚本的,要么就是Mozilla的,我需要更深入地查找(如图3-6)。

图3-6 Mozilla的bug,进入“易用的表单”页面,选中离婚,
然后软重载页面。现在已婚将会被选中
从一开始我就怀疑是Mozilla的bug,因为Explorer使用了完全一样的代码,而没有发生任何问题。所以Mozilla误解了一些代码。但是是哪一些呢?我需要分离出这个bug。
我开始了步骤(1),给一个函数加入了return语。比如,我关闭了setDefaults(),如下:
[易用的表单,第56~67行,变动]
function setDefaults() {
return;
var y = document.getElementsByTagName('input');
for (var i=0;i<y.length;i++) {
if (y[i].checked && y[i].getAttribute('show'))
intoMainForm(y[i].getAttribute('show'))
}
// 等等
}
如果bug是由这个函数造成的,bug就会消失不见。不幸的是它还在,所以还要继续检查错误的函数。我把return移到了后一个函数,并重载页面。
bug还是顽固地存在,直到我关闭了初始化函数prepareForm(),这下bug消失了。所以,错误似乎是在prepareForm()中的某处。
为了找出具体是哪一行代码引起了bug,我转而使用alert如下:
[易用的表单,第16~19行,变动]
function prepareForm() {
if (!compatible) return;
var marker = document.createElement(relatedTag);
marker.style.display = 'none';
alert('Made it to here');
我在prepareForm()的第一小段代码之后放入了alert语句,并重载页面。alert对话框弹了出来,而我那令人惊异的错误单选框已经被选上了。似乎是到目前为止已经执行的代码引起了这个bug。这很奇怪,因为这几行代码没有对表单做任何事情。
我把alert移到了函数的第一行:
[易用的表单,第16~19行,变动]
function prepareForm() {
alert('Made it to here');
if (!compatible) return;
var marker = document.createElement(relatedTag);
marker.style.display = 'none';
错误的单选框还是被选中的。很明显这个bug完全不是由我的脚本引起的。在脚本获得运行的机会之前,它已经出现了。
现在怎么办?这是Mozilla的原生bug吗?Mozilla是否总是错误地选中单选框?我禁用了脚本,并且软重载页面。当我第二次这么做的时候,正确的单选框被选中。接着我再次启用了脚本,问题行为又复发了,但只在我第二次软重载页面的时候,而不在第一次。
这值得更细致地调查。我多次禁用和启用脚本,最后发现这个bug总是在上一个页面执行了我的脚本后发生。似乎和脚本是否在当前页面运行无关。
发现浏览器bug需要99%的汗水加上1%的灵感。我已经完成了最艰苦的工作,现在我将得到一瞬间灵感的奖励。如果我的脚本被启用,那么上一个页面会隐藏一些表单域!
我在单选框之前创建了第二个表单域,并确保它是隐藏的,而且注意到:被选中的单选框不再上移一个,而是两个。因此,这个bug是由上一个页面的隐藏表单域造成的。很明显,在有隐藏表单域的情况下,Mozilla在编制索引(index)上出了些纰漏。
现在,对这个bug我有了肯定的把握后,创建了一个测试页面。如我在3B说到的,这一步是非常非常关键的。因为它迫使你创建那些引起bug的环境,并且去除那些不相关的代码。作为回报,你会加深对这个bug的理解。
所以我创建了一个简单的测试页面,它有1个文本输入框和4个单选框。我选择了最后一个单选框并重载页面,什么也没有发生。接着我加入了一点脚本来移除唯一的输入框,那个bug很光彩地又出现了。
bug已经被成功地分离和描述。在Mozilla的Bugzilla数据库(见下一页)中作了一番搜索后,找到一个在2002年就已知的类似bug。
bug报告
可以在这里找到我在这些测试后撰写的报告:http://www.quirksmode.org/bugreports/archives/ 2005/11/radio_check_mov.html。
那么,我是不是就不管这个bug了?或者我应该试着去解决它?比如设置一个cookie,记住所有单选框的状态?最后我决定不这么麻烦了,因为要解决这个bug会使我的脚本变得很复杂,也因为这个bug只在特定环境下影响一种浏览器(人们在Mozilla中重载一个表单页会有多频繁?)。
你完全可以不同意我在这种情况下作出的决定。但是,与带来的麻烦相比,有时确实不值得去解决一个浏览器的bug。
报告浏览器bug
一旦你成功地分离出一个bug,你应该把它报告给相关的浏览器开发商。4家主要的开发商都提供了bug报告工具。
Safari
Safari提供了最简便的反馈机制。只要使用它的“向Apple报告Bug(Report Bugs to Apple)”功能(如图3-7),并且回答几个问题即可。

图3-7 Safari的“bug报告”特性会把bug报告直接发送到Apple的数据库
Opera
Opera也比较方便。访问http://www.opera.com/support/bugs/ 并且仔细阅读该页面,因为它包含了正确报告bug的过程的实用概要。
Opera要求提供可以尽可能分离出bug的有效测试用例(test case)。一旦你创建了这个页面,就可以顺着链接到达bug报告表单。
Mozilla
报告Mozilla的bug要困难一些。整个Mozilla的bug数据库是在线的:http://bugzilla.mozilla. org。但是你得有一些Bugzilla的经验才能使用它。
首先你得创建一个账号。然后Mozilla会要求你先检查一下bug是否已经被报告了。这是个合理的要求,但如果你不了解Mozilla和Bugzilla的内容结构,绝大多数的bug报告都很难阅读。我仅有那么几次去报告bug,它就告诉我这个bug是已知的,但我却找不到它,因为我不理解这些结构。
微软
微软的bug报告工具在这里:https://connect.microsoft.com/feedback/default.aspx?SiteID=136。他们只接受Explorer 7和更新的版本,请不要报告老版本Explorer的bug。注意你需要一个微软Connect账号来获得访问权限。
QuirksMode
最后,我有自己的bug报告工具:http://www.quirksmode.org/bugreports/。这里的报告主要面向Web开发者,尽管浏览器厂商会偶尔也会来看看这些列表。(报告时)需要合适的测试页面。
如果你在这里报告bug,其他Web开发者就能找到它们,而且他们会感谢你挽救了他们宝贵的部分时间(和头发)。
保持耐心
你不用指望提交给浏览器开发商的bug会在几周内被解决。通常浏览器开发商们同时忙于一大堆bug,而他们的时间是有限的。另外,你报告的bug有可能依赖于其他bug,或者它没有重要到需要立即采取行动补救。耐心是处理浏览器bug时的优良品德。






