3.调试技术:在不那么容易找到JavaScript 调试程序的年代,开发人员不得不发挥自己的创造力,通过各种方法来调试自己的代码。结果,就出现了以这样或那样的方式置入代码,从而输出调试信息的做法。其中,最常见的做法就是在要调试的代码中随处插入alert()函数。但这种做法一方面比较麻烦(调试之后还需要清理),另一方面还可能引入新问题(想象一下把某个alert()函数遗留在产品代码中的结果)。如今,已经有了很多更好的调试工具,因此我们也不再建议在调试中使用alert()了。
- 将消息记录到控制台:IE8、Firefox、Opera、Chrome 和Safari 都有JavaScript 控制台,可以用来查看JavaScript 错误。而且,在这些浏览器中,都可以通过代码向控制台输出消息。对Firefox 而言,需要安装Firebug(www.getfirebug.com),因为Firefox 要使用Firebug 的控制台。对IE8、Firefox、Chrome 和Safari 来说,则可以通过console 对象向JavaScript 控制台中写入消息,这个对象具有下列方法。
- error(message):将错误消息记录到控制台
- info(message):将信息性消息记录到控制台
- log(message):将一般消息记录到控制台
- warn(message):将警告消息记录到控制台
在IE8、Firebug、Chrome 和Safari 中,用来记录消息的方法不同,控制台中显示的错误消息也不一样。错误消息带有红色图标,而警告消息带有黄色图标。以下函数展示了使用控制台输出消息的一个示例。
function sum(num1, num2){
console.log("Entering sum(), arguments are " + num1 + "," + num2);
console.log("Before calculation");
var result = num1 + num2;
console.log("After calculation");
console.log("Exiting sum()");
return result;
}
在调用这个sum()函数时,控制台中会出现一些消息,可以用来辅助调试。在Safari 中,通过“Develop”(开发)菜单可以打开其JavaScript 控制台(前面讨论过);在Chrome 中,单击“Control thispage”(控制当前页)按钮并选择“Developer”(开发人员)和“JavaScript console”(JavaScript 控制台)即可;而在Firefox 中,要打开控制台需要单击Firefox 状态栏右下角的图标。IE8 的控制台是其DeveloperTools(开发人员工具)扩展的一部分,通过“Tools”(工具)菜单可以找到,其控制台在“Script”(脚本)选项卡中。
Opera 10.5 之前的版本中,JavaScript 控制台可以通过opera.postError()方法来访问。这个方法接受一个参数,即要写入到控制台中的参数,其用法如下。
function sum(num1, num2){
opera.postError("Entering sum(), arguments are " + num1 + "," + num2);
opera.postError("Before calculation");
var result = num1 + num2;
opera.postError("After calculation");
opera.postError("Exiting sum()");
return result;
}
别看opera.postError()方法的名字好像是只能输出错误,但实际上能通过它向JavaScript 控制台中写入任何信息。
还有一种方案是使用LiveConnect,也就是在JavaScript 中运行Java 代码。Firefox、Safari 和Opera都支持LiveConnect,因此可以操作Java 控制台。例如,通过下列代码就可以在JavaScript 中把消息写入到Java 控制台。
java.lang.System.out.println("Your message");
可以用这行代码替代console.log()或opera.postError(),如下所示。
function sum(num1, num2){
java.lang.System.out.println("Entering sum(), arguments are " + num1 + "," + num2);
java.lang.System.out.println("Before calculation");
var result = num1 + num2;
java.lang.System.out.println("After calculation");
java.lang.System.out.println("Exiting sum()");
return result;
}
如果系统设置恰当,可以在调用LiveConnect 时就立即显示Java 控制台。在Firefox 中,通过“Tools”(工具)菜单可以打开Java 控制台;在Opera 中,要打开Java 控制台,可以选择菜单“Tools”(工具)及“Advanced”(高级)。Safari 没有内置对Java 控制台的支持,必须单独运行。
不存在一种跨浏览器向JavaScript 控制台写入消息的机制,但下面的函数倒可以作为统一的接口。
function log(message){
if (typeof console == "object"){
console.log(message);
} else if (typeof opera == "object"){
opera.postError(message);
} else if (typeof java == "object" && typeof java.lang == "object"){
java.lang.System.out.println(message);
}
}
这个log()函数检测了哪个JavaScript 控制台接口可用,然后使用相应的接口。可以在任何浏览器中安全地使用这个函数,不会导致任何错误,例如:
function sum(num1, num2){
log("Entering sum(), arguments are " + num1 + "," + num2);
log("Before calculation");
var result = num1 + num2;
log("After calculation");
log("Exiting sum()");
return result;
}
向JavaScript 控制台中写入消息可以辅助调试代码,但在发布应用程序时,还必须要移除所有消息。在部署应用程序时,可以通过手工或通过特定的代码处理步骤来自动完成清理工作。
记录消息要比使用alert()函数更可取,因为警告框会阻断程序的执行,而在测定异步处理对时间的影响时,使用警告框会影响结果。
- 将消息记录到当前页面:另一种输出调试消息的方式,就是在页面中开辟一小块区域,用以显示消息。这个区域通常是一个元素,而该元素可以总是出现在页面中,但仅用于调试目的;也可以是一个根据需要动态创建的元素。例如,可以将log()函数修改为如下所示:
function log(message){
var console = document.getElementById("debuginfo");
if (console === null){
console = document.createElement("div");
console.id = "debuginfo";
console.style.background = "#dedede";
console.style.border = "1px solid silver";
console.style.padding = "5px";
console.style.width = "400px";
console.style.position = "absolute";
console.style.right = "0px";
console.style.top = "0px";
document.body.appendChild(console);
}
console.innerHTML += "<p>" + message + "</p>";
}
这个修改后的log()函数首先检测是否已经存在调试元素,如果没有则会新创建一个<div>元素,并为该元素应用一些样式,以便与页面中的其他元素区别开。然后,又使用innerHTML 将消息写入到这个<div>元素中。结果就是页面中会有一小块区域显示错误消息。这种技术在不支持JavaScript 控制台的IE7 及更早版本或其他浏览器中十分有用。
与把错误消息记录到控制台相似,把错误消息输出到页面的代码也要在发布前删除。
- 抛出错误:如前所述,抛出错误也是一种调试代码的好办法。如果错误消息很具体,基本上就可以把它当作确定错误来源的依据。但这种错误消息必须能够明确给出导致错误的原因,才能省去其他调试操作。来看下面的函数:
function divide(num1, num2){
return num1 / num2;
}
这个简单的函数计算两个数的除法,但如果有一个参数不是数值,它会返回NaN。类似这样简单的计算如果返回NaN,就会在Web 应用程序中导致问题。对此,可以在计算之前,先检测每个参数是否都是数值。例如:
function divide(num1, num2){
if (typeof num1 != "number" || typeof num2 != "number"){
throw new Error("divide(): Both arguments must be numbers.");
}
return num1 / num2;
}
在此,如果有一个参数不是数值,就会抛出错误。错误消息中包含了函数的名字,以及导致错误的真正原因。浏览器只要报告了这个错误消息,我们就可以立即知道错误来源及问题的性质。相对来说,这种具体的错误消息要比那些泛泛的浏览器错误消息更有用。
对于大型应用程序来说,自定义的错误通常都使用assert()函数抛出。这个函数接受两个参数,一个是求值结果应该为true 的条件,另一个是条件为false 时要抛出的错误。以下就是一个非常基本的assert()函数。
function assert(condition, message){
if (!condition){
throw new Error(message);
}
}
可以用这个assert()函数代替某些函数中需要调试的if 语句,以便输出错误消息。下面是使用这个函数的例子。
function divide(num1, num2){
assert(typeof num1 == "number" && typeof num2 == "number",
"divide(): Both arguments must be numbers.");
return num1 / num2;
}
可见,使用assert()函数可以减少抛出错误所需的代码量,而且也比前面的代码更容易看懂。
4.常见的IE 错误:多年以来,IE 一直都是最难于调试JavaScript 错误的浏览器。IE 给出的错误消息一般很短又语焉不详,而且上下文信息也很少,有时甚至一点都没有。但作为用户最多的浏览器,如何看懂IE 给出的错误也是最受关注的。下面几小节将分别探讨一些在IE 中难于调试的JavaScript 错误。
- 操作终止:在IE8 之前的版本中,存在一个相对于其他浏览器而言,最令人迷惑、讨厌,也最难于调试的错误:操作终止(operation aborted)。在修改尚未加载完成的页面时,就会发生操作终止错误。发生错误时,会出现一个模态对话框,告诉你“操作终止。”单击确定(OK)按钮,则卸载整个页面,继而显示一张空白屏幕;此时要进行调试非常困难。下面的示例将会导致操作终止错误。
<!DOCTYPE html>
<html>
<head>
<title>Operation Aborted Example</title>
</head>
<body>
<p>The following code should cause an Operation Aborted error in IE versions prior to 8.</p>
<div>
<script type="text/javascript">
document.body.appendChild(document.createElement("div"));
</script>
</div>
</body>
</html>
这个例子中存在的问题是:JavaScript 代码在页面尚未加载完毕时就要修改document.body,而且<script>元素还不是<body>元素的直接子元素。准确一点说,当<script>节点被包含在某个元素中,而且JavaScript 代码又要使用appendChild()、innerHTML 或其他DOM 方法修改该元素的父元素或祖先元素时,将会发生操作终止错误(因为只能修改已经加载完毕的元素)。
要避免这个问题,可以等到目标元素加载完毕后再对它进行操作,或者使用其他操作方法。例如,为document.body 添加一个绝对定位在页面上的覆盖层,就是一种非常常见的操作。通常,开发人员都是使用appendChild()方法来添加这个元素的,但换成使用insertBefore()方法也很容易。因此,只要修改前面例子中的一行代码,就可以避免操作终止错误。
<!DOCTYPE html>
<html>
<head>
<title>Operation Aborted Example</title>
</head>
<body>
<p>The following code should not cause an Operation Aborted error in IE versions
prior to 8.</p>
<div>
<script type="text/javascript">
document.body.insertBefore(document.createElement("div"),
document.body.firstChild);
</script>
</div>
</body>
</html>
在这个例子中,新的<div>元素被添加到document.body 的开头部分而不是末尾。因为完成这一操作所需的所有信息在脚本运行时都是已知的,所以这不会引发错误。
除了改变方法之外,还可以把<script>元素从包含元素中移出来,直接作为<body>的子元素。例如:
<!DOCTYPE html>
<html>
<head>
<title>Operation Aborted Example</title>
</head>
<body>
<p>The following code should not cause an Operation Aborted error in IE versions
prior to 8.</p>
<div>
</div>
<script type="text/javascript">
document.body.appendChild(document.createElement("div"));
</script>
</body>
</html>
这一次也不会发生错误,因为脚本修改的是它的直接父元素,而不再是间接的祖先元素。
在同样的情况下,IE8 不再抛出操作终止错误,而是抛出常规的JavaScript 错误,带有如下错误消息:HTML Parsing Error: Unable to modify the parent container element before the childelement is closed (KB927917).
不过,虽然浏览器抛出的错误不同,但解决方案仍然是一样的。
- 无效字符:根据语法,JavaScript 文件必须只包含特定的字符。在JavaScript 文件中存在无效字符时,IE 会抛出无效字符(invalid character)错误。所谓无效字符,就是JavaScript 语法中未定义的字符。例如,有一个很像减号但却由Unicode 值8211 表示的字符(\u2013),就不能用作常规的减号(ASCII 编码为45),因为JavaScript 语法中没有定义该字符。这个字符通常是在Word 文档中自动插入的。如果你的代码是从Word 文档中复制到文本编辑器中,然后又在IE 中运行的,那么就可能会遇到无效字符错误。其他浏览器对无效字符做出的反应与IE 类似,Firefox 会抛出非法字符(illegal character)错误,Safari 会报告发生了语法错误,而Opera 则会报告发生了ReferenceError(引用错误),因为它会将无效字符解释为未定义的标识符。
- 未找到成员:如前所述,IE 中的所有DOM 对象都是以COM 对象,而非原生JavaScript 对象的形式实现的。这会导致一些与垃圾收集相关的非常奇怪的行为。IE 中的未找到成员(Member not found)错误,就是由于垃圾收集例程配合错误所直接导致的。
具体来说,如果在对象被销毁之后,又给该对象赋值,就会导致未找到成员错误。而导致这个错误的,一定是COM 对象。发生这个错误的最常见情形是使用event 对象的时候。IE 中的event 对象是window 的属性,该对象在事件发生时创建,在最后一个事件处理程序执行完毕后销毁。假设你在一个闭包中使用了event 对象,而该闭包不会立即执行,那么在将来调用它并给event 的属性赋值时,就会导致未找到成员错误,如下面的例子所示。
document.onclick = function(){
var event = window.event;
setTimeout(function(){
event.returnValue = false; //未找到成员错误
}, 1000);
};
在这段代码中,我们将一个单击事件处理程序指定给了文档。在事件处理程序中,window.event被保存在event 变量中。然后,传入setTimeout()中的闭包里又包含了event 变量。当单击事件处理程序执行完毕后,event 对象就会被销毁,因而闭包中引用对象的成员就成了不存在的了。换句话说,由于不能在COM 对象被销毁之后再给其成员赋值,在闭包中给returnValue 赋值就会导致未找到成员错误。
- 未知运行时错误:当使用innerHTML 或outerHTML 以下列方式指定HTML 时,就会发生未知运行时错误(Unknown runtime error):一是把块元素插入到行内元素时,二是访问表格任意部分(<table>、<tbody>等)的任意属性时。例如,从技术角度说,<span>标签不能包含<div>之类的块级元素,因此下面的代码就会导致未知运行时错误:
span.innerHTML = "<div>Hi</div>"; //这里,span 包含了<div>元素
在遇到把块级元素插入到不恰当位置的情况时,其他浏览器会尝试纠正并隐藏错误,而IE 在这一点上反倒很较真儿。
- 语法错误:通常,只要IE 一报告发生了语法错误(syntax error),都可以很快找到错误的原因。这时候,原因可能是代码中少了一个分号,或者花括号前后不对应。然而,还有一种原因不十分明显的情况需要格外注意。
如果你引用了外部的JavaScript 文件,而该文件最终并没有返回JavaScript 代码,IE 也会抛出语法错误。例如,<script>元素的src 特性指向了一个HTML 文件,就会导致语法错误。报告语法错误的位置时,通常都会说该错误位于脚本第一行的第一个字符处。Opera 和Safari 也会报告语法错误,但它们会给出导致问题的外部文件的信息;IE 就不会给出这个信息,因此就需要我们自己重复检查一遍引用的外部JavaScript 文件。但Firefox 会忽略那些被当作JavaScript 内容嵌入到文档中的非JavaScript 文件中的解析错误。
在服务器端组件动态生成JavaScript 的情况下,比较容易出现这种错误。很多服务器端语言都会在发生运行时错误时,向输出中插入HTML 代码,而这种包含HTML 的输出很容易就会违反JavaScript语法。如果在追查语法错误时遇到了麻烦,我们建议你再仔细检查一遍引用的外部文件,确保这些文件中没有包含服务器因错误而插入到其中的HTML。
- 系统无法找到指定资源:系统无法找到指定资源(The system cannot locate the resource specified)这种说法,恐怕要算是IE给出的最有价值的错误消息了。在使用JavaScript 请求某个资源URL,而该URL 的长度超过了IE 对URL最长不能超过2083 个字符的限制时,就会发生这个错误。IE 不仅限制JavaScript 中使用的URL 的长度,而且也限制用户在浏览器自身中使用的URL 长度(其他浏览器对URL 的限制没有这么严格)。IE 对URL路径还有一个不能超过2048 个字符的限制。下面的代码将会导致错误。
function createLongUrl(url){
var s = "?";
for (var i=0, len=2500; i < len; i++){
s += "a";
}
return url + s;
}
var x = new XMLHttpRequest();
x.open("get", createLongUrl("http://www.somedomain.com/"), true);
x.send(null);
在这个例子中,XMLHttpRequest 对象试图向一个超出最大长度限制的URL 发送请求。在调用open()方法时,就会发生错误。避免这个问题的办法,无非就是通过给查询字符串参数起更短的名字,或者减少不必要的数据,来缩短查询字符串的长度。另外,还可以把请求方法改为POST,通过请求体而不是查询字符串来发送数据。