本篇内容
  • 浏览器报告的错误
  • 错误处理
  • 调试技术
  • 常见的IE错误

由于JavaScript本身是动态语言,而且多年来一直没有固定的开发工具,因此人们普遍认为它是一种最难于调试的编程语言。脚本出错时,浏览器通常会给出类似于“object expected”(缺少对象)这样的消息。没有上下文信息,让人摸不着头脑。ECMAScript第3版致力于解决这个问题,专门引入了try-catch和throw语句以及一些错误类型,意在让开发人员能够适当地处理错误。几年之后,Web浏览器中也出现了一些JavaScript调试程序和工具。2008年以来,大多数Web浏览器都已经具备了一些调试JavaScript代码的能力。
在有了语言特性和工具支持之后,现在的开发人员已经能够适当地实现错误处理,并且能够找到错误的根源。

1 浏览器报告的错误

IE、Firefox、Safari、Chrome和Opera等主流浏览器,都具有某种向用户报告JavaScript错误的机制。默认情况下,所有浏览器都会隐藏此类信息,毕竟除了开发人员之外,很少有人关心这些内容。 因此,在基于浏览器编写JavaScript脚本时,别忘了启用浏览器的JavaScript报告功能,以便及时收到错误通知。

1.1 IE

IE是唯一一个在浏览器的界面窗体中显示JavaScript错误信息的浏览器。在发生JavaScript错误时浏览器左下角会出现一个黄色的图标,圈标旁边则显示着”Error on page“(页面中有错误)。
假如不是存心去看的话,你很可能不会注意这个图标。双击这个图标,就会看到一个包含错误消息的对话框,其中还包含诸如行号、字符数、错误代码及文件名(其实就是你在查看的页面的URL)等相关信息。

如果启用了脚本测试功能的话(默认是禁用的),那么在发生错误时,你不仅会显示错误通知,而且还会看到另一个对话框,询问是否想要调试错误。
要启用脚本调试功能,必须要在IE中安装某种脚本调试器。(IE8和IE9也自带调试器。)

在IE7及更早版本中,如果错误发生在位于外部文件的脚本中,行号通常会与错误所在的行号差1。如果是嵌入在页面中的脚本发生错误,则行号就是错误所在的行号。

1.2 Firefox

默认情况下,Firefox在JavaScript发生错误时不会通过浏览器界面给出提示。但它会在后台将错误记录到错误控制台中。单击“Tools”(工具)菜单中的“ErrorConsole”(错误控制台)可以显示错误控制台。你会发现,错误控制台中实际上还包含与JavaScript、css和IHTML相关的警告和信息,可以通过筛选找到错误。

在发生JavaScript错误时,Firefox会将其记录为一个错误,包括错误消息、引发错误的URL和错误所在的行号等信息。单击文件名即可以只读方式打开发生错误的脚本,发生错误的代码行会突出显示。
目前,最流行(感觉过时了已经)的Firefox插件Firebug,已经成为开发人员必备的JavaScript纠错工具。这个插件,会在Firefox状态栏的右下角区域添加一个图标。默认情况下,右下角区域显示的是一个绿色对勾图标。在有JavaScript错误发生时,图标会变成红叉,同时旁边显示错误的数量。单击这个红叉会打开Firebug控制台,其中显示有错误消息、错误所在的代码行(不包含上下文)、错误所在的URL以及行号。

在Firebug中单击导致错误的代码行,将在一个新Firebug视图中打开整个脚本,该代码行在其中突出显示。

除了显示错误之外,Firebug还有更多的用处。实际上,它还是针对Firefox的成熟的调试环境,为调试JavaScript、css、DOM和网络连接错误提供了诸多功能。

1.3 Safari

Windows和MacOS平台的Safari在默认情况下都会隐藏全部JavaScript错误。为了访问到这些信息,必须启用”Develop“(开发)菜单。为此,需要单击”Edit“(编辑)菜单中的”Preferences“(偏好设置)。然后再”Advanced“(高级)选项卡中,选中”Show develop menu in menubar“(在菜单栏中显示”开发“菜单)。启用此项设置之后,就好在Safari的菜单栏中看到一个”Develop“菜单。

”Develop“菜单中提供了一些与调试有关的选项,还有一些选项可以影响当前加载的页面。单击“Show Error Console”(显示错误控制台)选项,将会看到一组JavaScript及其他错误。控制台中显示着错误消息、错误的URL及错误的行号。
单击控制台中的错误消息,就可以打开导致错误的源代码。除了被输出到控制台之外,JavaScript错误不会影响Safari窗口的外观。

1.4 Opera

Opera在默认情况下也会隐藏JavaScript错误,所有错误都会被记录到错误控制台中。要打开错误控制台,需要单击“Tools”(工具)菜单,在“Advanced”(高级)子菜单项下面再单击“ErrorConsole”(错误控制台)。与Firefox一样,Opera的错误控制台中也包含了除JavaScript错误之外的很多来源(如HTML、css、XML,XSLT等)的错误和警告信息。要分类查看不同来源的消息,可以使用左下角的下拉选择框。
错误消息中显示着导致错误的URL和错误所在的线程。有时候,还会有横跟踪信息。除了错误控制台中显示的信息之外,没有其他途径可以获得更多信息。
也可以让Opera一发生错误就弹出错误控制台。为此,要在“Tools”(工具)菜单中单击“Preferences”(首选项),再单击“Advanced”(高级)选项卡,然后从左侧菜单中选择“Content”(内容)。单击“JavaScrip Options”(JavaScript选项)按钮,显示选项对话框。在这个选项对话框中,选中“Open console on error”(出错时打开控制台),单击“OK”(确定)按钮。这样,每当发生JavaScript错误时,就会弹出错误控制台。另外,还可以针对特定的站点来作此设置,方法是单击“Tools”(工具)、“Quick Preferences”(快速参数)、“Edit Site Preferences”(编辑站点首选项),选择“Scripting”(脚本)选项卡,最后选中“Open console on error”(出错时打开控制台)。

1.5 Chrome

与Safari和Opera一样,Chrome在默认情况下也会隐藏JavaScript错误。所有错误都将被记录到Web Inspector控制台中。要查看错误消息,必须要打开Web Inspector控制台。

打开的Web Inspector中包含着有关页面的信息和JavaScript控制台。控制台中显示着错误消息、错误的URL和错误的行号。

2 错误处理

错误处理在程序设计中的重要性是勿庸置疑的。任何有影响力的Web应用程序都需要一套完善的错误处理机制,当然,大多数佼佼者确实做到了这一点,但通常只有服务器端应用程序才能做到如此。 实际上,服务器端团队往往会在错误处理机制上投入较大的精力,通常要考虑按照类型、频率,或者其他重要的标准对错误进行分类。这样一来,开发人员就能够理解用户在使用简单数据库查询或者报告生成脚本时,应用程序可能会出现的问题。
虽然客户端应用程序的错误处理也同样重要,但真正受到重视,还是最近几年的事。实际上,我们要面对这样一个不争的事实:使用Web的绝大多数人都不是技术高手,其中甚至有很多人根本就不明白浏览器到底是什么,更不用说让他们说喜欢哪一个了。前面讨论过,每个浏览器在发生JavaScript错误时的行为都或多或少有一些差异。有的会显示小图标,有的则什么动静也没有,浏览器对JavaScript错误的这些默认行为对最终用户而言,毫无规律可循。最理想的情况下,用户遇到错误搞不清为什么,他们会再试着重做一次;最糟糕的情况下,用户会恼羞成怒,一去不复返了。良好的错误处理机制可以让用户及时得到提醒,知道到底发生了什么事,因而不会惊惶失措。为此,作为开发人员,我们必须理解在处理JavaScript错误的时候,都有哪些手段和工具可以利用。

2.1 try-catch语旬

ECMA-262第3版引入了try-catch语句,作为JavaScript中处理异常的一种标准方式。基本的语法如下所示,显而易见,这与Java中的try-catch语句是完全相同的。

1
2
3
4
5
try {
// 可能会导致错误的代码
} catch (error) {
// 在错误发生时怎么处理
}

也就是说,我们应该把所有可能会抛出错误的代码都放在try语句块中,而把那些用于错误处理的代码放在catch块中。例如:

1
2
3
4
5
try {
window.showMichealWayne();
} catch (e) {
alert('Error!');
}

如果try块中的任何代码发生了错误,就会立即退出代码执行过程,然后接着执行catch块。此时,catch块会接收到一个包含错误信息的对象。与在其他语言中不同的是,即使你不想使用这个错误对象,也要给它起个名字。这个对象中包含的实际信息会因浏览器而异,但共同的是有一个保存着错误消息的message属性。ECMA-262还规定了一个保存错误类型的name属性;当前所有浏览器都支持这个属性(Opera9之前的版本不支持这个属性)。因此,在发生错误时,就可以像下面这样实事求是地显示浏览器给出的消息。

1
2
3
4
5
try {
window.showMichealWayne();
} catch (e) {
alert(e.message);
}

这个例子在向用户显示错误消息时,使用了错误对象的message属性。这个message属性是唯一一个能够保证所有浏览器都支持的属性,除此之外,IE、Firefox、Safari、Chrome以及Opera都为事件对象添加了其他相关信息。IE添加了与message属性完全相同的description属性,还添加了保存着内部错误数量的number属性。Firefox添加了fileName、lineNumber和stack(包含栈跟踪信息)属性。Safari添加了line(表示行号)、sourceId(表示内部错误代码)和sourceURL属性。当然,在跨浏览器编程时,最好还是只使用message属性。

2.1.1 finally子句

虽然在try-catch语句中是可选的,但finally子句一经使用,其代码无论如何都会执行。换句话说,try语句块中的代码全部正常执行,finally子句会执行;如果因为出错而执行了catch语句块,finally子句照样还会执行。只要代码中包含finally子句,则无论try或catch语句块中包含什么代码——甚至return语句,都不会阻止finally子句的执行。来看下面这个函数。

1
2
3
4
5
6
7
8
9
function testFinally () {
try {
return 2;
} catch (error) {
return 1;
} finally {
return 0;
}
}

这个函数在try-catch语句的每一部分都放了一条return语句。表面上看,调用这个函数会返回2,因为返回2的return语句位于try语句块中,而执行该语句又不会出错。可是,由于最后还有一个finally子句,结果就会导致该return语句被忽略;也就是说,调用这个函数只能返回0。如果把finally子句拿掉,这个函数将返回2。如果提供finally子句,则catch子句就成了可选的(catch或finally有一个即可)。IE7及更早版本中有一个bug:除非有catch子句,否则finally中的代码永远不会执行。如果你仍然要考虑IE的早期版本,那就只好提供一个catch子句,哪怕里面什么都不写。IE8修复了这个bug。

请读者务必要记住,只要代码中包含finally子句,那么无论try还是catch语句块中的return语句都将被忽略。因此,在使用finally子句之前,一定要非常清楚你想让代码怎么样。

2.1.2 错误类型

执行代码期间可能会发生的错误有多种类型。每种错误都有对应的错误类型,而当错误发生时,就会抛出相应类型的错误对象。ECMA-262定义了下列7种错误类型:

  • Error
  • EvalError
  • RangeError
  • ReferenceError
  • SyntaxError
  • TypeError
  • URIError

其中,Error是基类型,其他错误类型都继承自该类型。因此,所有错误类型共享了一组相同的属性(错误对象中的方法全是默认的对象方法)。Error类型的错误很少见,如果有也是浏览器抛出的;这个基类型的主要目的是供开发人员抛出自定义错误。
EvalError类型的错误会在使用eval()函数而发生异常时被抛出。ECMA-262中 对这个错误有如下描述:“如果以非直接调用的方式使用eval属性的值(换句话说,没有明确地将其名称为作一个Identifier,即用作Call Expression中的MemberExpression),或者为eval属性赋值。”简单地说,如果没有把eval()当成函数调用,就会抛出错误,例如:

1
2
new eval();		// EvalError
eval = foo; // EvalError

在实践中,浏览器不一定会在应该抛出错误时就抛出EvalError。例如,Firefox4+和IE8对第一种情况会抛出TypeError,而第二种情况会成功执行,不发生错误。有鉴于此,加上在实际开发中极少会这样使用eval(),所以遇到这种错误类型的可能性极小。
RangeError类型的错误会在数值超出相应范围时触发。例如,在定义数组时,如果指定了数组不支持的项数(如-20或Number.MAX_VALUE),就会触发这种错误。下面是具体的例子。

1
2
var itemsl = new Array(-20); 	// RangeError
var items2 = new Array(Number.MAX__VALUE); // RangeError

JavaScript中经常会出现这种范围错误。
在找不到对象的情况下,会发生ReferenceError(这种情况下,会直接导致人所共知的”object expected”浏览器错误)。通常,在访问不存在的变量时,就会发生这种错误,例如:

1
var obj = x;	// 在x未定义时抛出ReferenceError

至于SyntaxError,当我们把语法错误的JavaScript字符串传入eval()函数时,就会导致此类错误。例如:

1
eval('a ++ b');		// ReferenceError

如果语法错误的代码出现在eval()函数之外,则不太可能使用SyntaxError,因为此时的语法错误会导致JavaScript代码立即停止执行。
TypeError类型在JavaScript中会经常用到,在变握中保存着意外的类型时,或者在访问不存在的方法时,都会导致这种错误。错误的原因虽然多种多样,但归根结底还是由于在执行特定于类型的操作时,变量的类型并不符合要求所致。下面来看几个例子。

1
2
3
var obj = new 10;	// TypeError
alert('name' in true); // TypeError
Function.prototype.toString.call('name'); // TypeError

最常发生类型错误的情况,就是传递给函数的参数事先未经检查,结果传入类型与预期类型不相符。在使用encodeURI()或decodeURI(),而URI格式不正确时,就会导致URIError错误。这种错误也很少见,因为前面说的这两个函数的容错性非常高。
利用不同的错误类型,可以获悉更多有关异常的信息,从而有助于对错误作出恰当的处理。要想知道错误的类型,可以像下面这样在try-catch语句的catch语句中使用instanceof操作符。

1
2
3
4
5
6
7
8
9
try {
someFunction();
} catch (error) {
if (error instanceof TypeError) {
// ...
} else if (error instanceof RangeError) {
// ...
} // ...
}

在跨浏览器编程中,检查错误类型是确定处理方式的最简便途径;包含在message属性中的错误消息会因浏览器而异。

2.1.3 合理使用try-catch

当try-catch语句中发生错误时,浏览器会认为错误已经被处理了,因而不会通过本章前面讨论的机制记录或报告错误。对于那些不要求用户懂技术,也不需要用户理解错误的Web应用程序,这应该说是个理想的结果。不过,try-catch能够让我们实现自己的错误处理机制。
使用try-catch最适合处理那些我们无法控制的错误。假设你在使用一个大型JavaScript库中的函数,该函数可能会有意无意地抛出一些错误。由于我们不能修改这个库的源代码,所以大可将对该函数的调用放在try-catch语句当中,万一有什么错误发生,也好恰当地处理它们。
在明明白白地知道自己的代码会发生错误时,再使用try-catch语句就不太合适了。例如,如果传递给函数的参数是字符串而非数值,就会造成函数出错,那么就应该先检查参数的类型,然后再决定如何去做。这种情况就不应该使用try-catch。

2.2 抛出错误

与try-catch语句相配的还有一个throw操作符,用于随时抛出自定义错误。抛出错误时.必须要给throw操作符指定一个值,这个值是什么类型,没有要求。下列代码都是有效的。

1
2
3
throw 12345;
throw "Hello";
throw { a: '1' }

在遇到throw操作符时,代码会立即停止执行。仅当有try-catch语句捕获到被抛出的值时,代码才会继续执行。
通过使用某种内置错误类型,可以更真实地模拟浏览器错误。每种错误类型的构造函数接收一个参数,即实际的错误消息。下面是一个例子。

1
throw new Error('throw a new error')

这行代码抛出了一个通用错误,带有-条自定义错误消息。浏览器会像处理自己生成的错误一样,来处理这行代码抛出的错误。换句话说,浏览器会以常规方式报告这一错误,并且会显示这里的自定义错误消息。像下面使用其他错误类型,也可以模拟出类似的浏览器错误。

1
2
throw new SyntaxError('this is a SyntaxError');
throw new TypeError('this is a TypeError');

在创建自定义错误消息时最常用的错误类型是Error、RangeError、ReferenceError和TypeError。

另外,利用原型链还可以通过继承Error来创建自定义错误类型(原型链在第6章中介绍)。此时,需要为新创建的错误类型指定name和message属性。来看一个例子。

1
2
3
4
5
6
7
8
9
function CustomError(message) {
this.name = 'CustomError';
this.message = message;
}

CustomError.prototype = new Error();

// use
throw new CustomError('this is a CustomError');

浏览器对待继承自Error的自定义错误类型,就像对待其他错误类型一样。如果要捕获自己抛出的错误并且把它与浏览器错误区别对待的话,创建自定义错误是很有用的。

IE只有在抛出Error对象的时候才会显示自以错误消息。对于其他类型,它都无一例外地显示”exception thrown and not caught”(抛出了井常,且未被捕获)。

2.2.1 抛出错误的时机

要针对函数为什么会执行失败给出更多信息,抛出自定义错误是一种很方便的方式。应该在出现某种特定的已知错误条件,导致函数无法正常执行时抛出错误。换句话说,浏览器会在某种特定的条件下执行函数时抛出错误。例如,下面的函数会在参数不是数组的情况下失败。

1
2
3
4
5
6
7
8
function process (values) {
values.sort();
for (var i = 0, len = values.length; i < len; i++) {
if (values[i] > 100) return values[i];
}

return -1
}

如果执行这个函数时传给它一个字符审参数,那么对sort()的调用就会失败。对此,不同浏览器会给出不同的错误消息,但都不是特别明确,如下所示。

  • IE:属性或方法不存在。
  • Firefox: values.sort()不是函数。
  • Safari:值undefined(表达式values.sort的结果)不是对象。
  • Chrome:对象名没有方法’sort’。
  • Opera:类型不匹配(通常是在需要对象的地方使用了非对象值)。

尽管Firefox、Chrome和Safari都明确指出了代码中导致错误的部分,但错误消息并没有清楚地告诉我们到底出了什么问题,该怎么修复问题。在处理类似前面例子中的那个函数时,通过调试处理这些错误消息没有什么困难。但是在面对包含数千行JavaScript代码的复杂的Web应用程序时,要想查找错误来源就没那么容易了,这种情况下,带有适当信息的自定义错误能够显著提升代码的可维护性。来看下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
function process (values) {
if (!values instanceof Array) {
throw new Error('Argument must be Array.');
}

values.sort();
for (var i = 0, len = values.length; i < len; i++) {
if (values[i] > 100) return values[i];
}

return -1
}

在重写后的这个函数中,如果values参数不是数组,就会抛出一个错误。错误消息中包含了函数的名称,以及为什么会发生错误的明确描述。如果一个复杂的Web应用程序发生了这个错误,那么查找问题的根源也就容易多了。
建议读者在开发JavaScript代码的过程中,重点关注函数和可能导致函数执行失败的因素。良好的错误处理机制应该可以确保代码中只发生你自己抛出的错误。

2.2.2 抛出错误与使用 try-catch

关于何时该抛出错误,而何时该使用try-catch来捕获它们,是一个老生常谈的问题。一般来说,应用程序架构的较低层次中经常会抛出错误,但这个层次并不会影响当前执行的代码,因而错误通常得不到真正的处理。如果你打算编写一个要在很多应用程序中使用的JavaScript库,甚至只编写一个可能会在应用程序内部多个地方使用的辅助函数,我都强烈建议你在抛出错误时提供详尽的信息。然后,即可在应用程序中捕获并适当地处理这些错段。
说到抛出错误与捕获错误,我们认为只应该捕在那些你确切地知道该如何处理的错误。捕获错误的目的在于避免浏览器以默认方式处理它们;而抛出错误的目的在于提供错误发生具体原因的消息。

2.3 错误(error)事件

任何没有通过try-catch处理的错误都会触发window对象的error事件。这个事件是Web浏览器最早支持的事件之一,IE、Firefox和Chrome为保持向后兼容,并没有对这个事件作任何修改(Opera和Safari不支持error事件)。在任何Web浏览器中,onerror事件处理程序都不会创建event对象,但它可以接收三个参数:错误消息、错误所在的URL和行号。多数情况下,只有错误消息有用,因为URL只是给出了文档的位置,而行号所指的代码行既可能出自嵌入的JavaScript代码,也可能出自外部的文件。要指定onerror事件处理程序,必须使用如下所示的DOM0级技术,它没有遵循“DOM2级事件”的标准格式。

1
2
3
window.onerror = function (message, url, line) {
alert(message);
};

只要发生错误,无论是不是浏览器生成的,都会触发error事件,并执行这个事件处理程序。然后,浏览器默认的机制发挥作用,像往常一样显示出错误消息。 像下面这样在事件处理程序中返回false,可以阻止浏览器报告错误的默认行为。

1
2
3
4
window.onerror = function (message, url, line) {
alert(message);
return false;
};

通过返回false,这个函数实际上就充当了整个文档中的try-catch语句,可以捕获所有无代码处理的运行时错误。这个事件处理程序是避免浏览器报告错误的最后一道防线,理想情况下,只要可能就不应该使用它。 只要能够适当地使用try-catch语句,就不会有错误交给浏览器,也就不会触发error事件。

浏览器在使用这个事件处理错误时的方式有明显不同。在IE中,即使发生error事件,代码仍然会正常执行;所有交量和数据都将得到保留,因此,能在onerror事件处理程序中访问它们。但在Firefox中,常规代码会停止执行,事件发生之前的所有变量和数据都将被销毁,因此几乎就无法判断错误了。

图像也支持error事件。只要图像的src特性中的URL不能返回可以被识别的图像格式,就会触发error事件。此时的error事件遵循DOM格式,会返回一个以图像为目标的event对象。下面是一个例子。

1
2
3
4
5
6
7
8
9
10
var image = new Image();
EventUtil.addHandler(image, 'load', function () {
alert('image loaded');
});

EventUtil.addHandler(image, 'error', function () {
alert('image load failed');
});

image.src = 'eeeeeee.gif';

在这个例子中,当加载图像失败时就会显示一个警告框。需要注意的是,发生error事件时,图像下载过程已经结束,也就是说不能再重新下载了。

2.4 处理错误的策略

过去,所谓Web应用程序的错误处理策略仅限于服务器端。在谈到错误与错误处理时,通常要考虑很多方面,涉及一些工具,例如记录和监控系统。这些工具的用途在于分析错误模式,追查错误原因,同时帮助确定错误会影响到多少用户。

在Web应用程序的JavaScript这一端,错误处理策略也同样重要。由于任何JavaScript错误都可能导致网页无法使用,因此搞清楚何时以及为什么发生错误至关重要。绝大多数Web应用程序的用户都不懂技术,遇到错误时容易心烦意乱。有时候,他们可能会刷新页面以期解决问题,而有时候则会放弃努力。作为开发人员,必须要知道代码何时可能出错,会出什么错,同时还要有一个跟踪此类问题系统。

2.5 常见的错误类型

错误处理的核心,是首先要知道代码里会发生什么错误。由于JavaScript是松散类型的,而且也不会验证函数的参数,因此错误只会在代码运行期间出现。一般来说,需要关注三种错误:

  • 类型转换错误
  • 数据类型错误
  • 通信错误

以上错误分别会在特定的模式下或者没有对值进行足够的检查的情况下发生。

2.5.1 类型转换错误

类型转换错误发生在使用某个操作符,或者使用其他可能会自动转换值的数据类型的语言结构时。在使用相等(=)和不相等(!=)操作符,或者在if、for及while等流控制语句中使用非布尔值时,最常发生类型转换错误。
由于在非动态语言中,开发人员都使用相同的符号执行直观的比较,因此在JavaScript中往往也会以相同方式错误地使用它们。多数情况下,我们建议使用全等(===)和不全等(!==)操作符,以避免类型转换。来看-个例子。

容易发生类型转换错误的另一个地方,就是流控制语句。像if之类的语句在确定下一步操作之前,会自动把任何值转换成布尔值。尤其是if语句,如果使用不当 ,最容易出错。来看下面的例子。

1
2
3
4
5
function concat(str1, str2, str3) {
var result = str1 + str2;
if (str3) result += str3;
return result;
}

这个函数的用意是拼接两或三个字符串,然后返回结果。其中,第三个字符串是可选的,因此必须要检查。曾经介绍过,未使用过的命名变量会自动被赋予undefined值。而undefined值可以被转换成布尔值false,因此这个函数中的if语句实际上只适用于提供了第三个参数的情况。问题在与,并不是只有undefined才会被转换成false,也不是只有字符串值才可以转换为true。例如,假设第三个参数是数值0,那么if语句的测试就会失败,而对数值1的测试则会通过。
在流控制语句中使用非布尔值,是极为常见的一个错误来源。为避免此类错误,就要做到在条件比较时切实传入布尔值。实际上,执行某种形式的比较就可以达到这个目的。例如,我们可以将前面的面数重写如下。

1
2
3
4
5
function concat(str1, str2, str3) {
var result = str1 + str2;
if (typeof str3 === 'string') result += str3;
return result;
}

2.5.2 数据类型错误

JavaScript是松散类型的,也就是说,在使用变量和函数参数之前,不会对它们进行比较以确保它们的数据类型正确。为了保证不会发生数据类型错误,只能依靠开发人员编写适当的数据类型检测代码。在将预料之外的值传递给函数的情况下,最容易发生数据类型错误。
在前面的例子中,通过检测第三个参数可以确保它是一个字符串,但是并没有检测另外两个参数。 如果该函数必须要返回一个字符串,那么只要给它传入两个数值,忽略第三个参数,就可以轻易地导致它的执行结果错误。

与null进行比较只能确保相应的值不是null和undefined(这就相当于使用相等和不相等操作)。要确保传入的值有效,仅检测null值是不够的;因此,不应该使用这种技术。同样,我们也不推荐将某个值与undefined作比较。

另一种错误的做法,就是只针对要使用的某一个特性执行特性检测。来看下面的例子。

1
2
3
4
5
6
7
8
9
// 不推荐
if (typeof values.sort === 'function') {
values.sort();
}

// 推荐
if (values instanceof Array) {
values.sort();
}

在确切知道应该传入什么类型的情况下,最好是使用instanceof来检测其数据类型。

大体上来说,基本类型的值应该使用typeof来检测,而对象的值则应该使用instanceof来检测。 根据使用函数的方式,有时候并不稀要逐个检测所有参数的敏据类型。但是,面向公众的API则必须无条件地执行类型检查,以确保函数始终能够正常地执行。

2.5.3 通信错误

随着Ajax编程的兴起,Web应用程序在其生命周期内动态加载信息或功能,已经成为一件司空见惯的事。不过,JavaScript与服务器之间的任何一次通信,都有可能会产生错误。
第一种通信错误与格式不正确的URL,或发送的数据有关。 最常见的问题是在将数据发送给服务器之前,没有使用encodeURIComponent()对数据进行编码。例如,下面这个URL的格式就是不正确的:
http://www.yourdomain.com/?redir=http://www.someotherdomain.com?a=b&c=d
针对”redir=“后面的所有字符串调用encodeURIComponent()就可以解决这个问题,结果将产生如下字符串:
http://www.yourdorrmain.com/?redir=http%3A%2F%2Fwww.eotherdomain.com%3Fa%3Db%26c%3Dd
对于查询字符串,应该记住必须要使用encodeURIComponent()方法。 为了确保这一点,有时候可以定义一个处理查询字符串的函数,例如:

1
2
3
4
5
6
7
function addQueryStringArg(url, name, value) {
if (url.indexOf('?') === -1) {
url += '?';
} else url += '&';

url += name + '=' + encodeURIComponent(value)
}

另外,在服务器响应的数据不正确时,也会发生通信错误。此前曾经讨论过动态加载脚本和动态加载样式,运用这两种技术都有可能遇到资源不可用的情况。在没有返回相应资源的情况下,Firefox、Chrome和Safari会默默地失败,IE和Opera则都会报错。然而,对于使用这两种技术产生的错误,很难判断和处理。在某些情况下,使用Ajax通信可以提供有关错误状态的更多信息。

2.6 区分致命错误和非致命错误

任何错误处理策略中的最重要的一个部分,就是确定错误是否致命。对于非致命错误,可以根据下列一或多个条件来确定:

  • 不影响用户的主要任务;
  • 只影响页面的一部分;
  • 可以恢复;
  • 重复相同操作可以消除错误;

致命错误,可以通过以下一或多个条件来确定:

  • 应用程序根本无法继续运行;
  • 错误明显影响到了用户的主要操作;
  • 会导致其他连带错误。

要想采取适当的措施,必须要知道JavaScript在什么情况下会发生致命错误。在发生致命错误时,应该立即给用户发送一条消息,告诉他们无法再继续手头上的事情了。假如必须刷新页面才能让应用程序正常运行,就必须通知用户,同时给用户提供一个点击即可刷新页面的按钮。

区分致命错误和非致命错误的主要依据,就是要看它们对用户的影响。设计良好的代码,可以做到应用程序某一部分发生错误不会不必要地影响另一个实际上毫不相干的部分。如果每个模块都需要通过JavaScript调用来初始化,那么你可能会看到类似下面这样的代码:

1
2
3
for (var i = 0, len = mods.length; i < len; i++) {
mods[i].init();
}

表面上来看,这些代码没什么问题:依次对每个模块调用init()方法。问题在于,任何模块的init()方法如果出错,都会导致数组中后续的所有模块无法再进行初始化。从逻辑上说,这样编写代码没有什么意义。毕竟,每个模块相互之间没有依赖关系,各自实现不同功能。可能会导致致命错误的原因是代码的结构。不过,经过下面这样修改,就可以把所有模块的错误变成非致命的:

1
2
3
4
5
for (var i = 0, len = mods.length; i < len; i++) {
try {
mods[i].init();
} catch (e) {}
}

通过在for循环中添加try-catch语句,任何模块初始化时出错,都不会影响其他模块的初始化。在以上重写的代码中,如果有错误发生,相应的错误将会得到独立的处理,并不会影响到用户的体验。

2.7 把错误记录到服务器

开发web应用程序过程中的一种常见的做法,就是集中保存错误日志,以便查找重要错误的原因。例如数据库和服务器错误都会定期写入日志,而且会按照常用API进行分类。在复杂的Web应用程序中,我们同样推荐你把JavaScript错误也回写到服务器。换句话说,也要将这些错误写入到保存服务器端错误的地方,只不过要标明它们来自前端。把前后端的错误集中起来,能够极大地方便对数据的分析。
要建立这样一种JavaScript错误记录系统,首先需要在服务器上创建一个页面(或者一个服务器入口点),用于处理错误数据。这个页面的作用无非就是从查询字符串中取得数据,然后再将数据写入错误日志中。这个页面可能会使用如下所示的函数:

1
2
3
4
function logError (sev, msg) {
var img = new Image();
img.src = 'log.php?sev=' + encodeURIComponent(sev) + '&msg=' + encodeURIComponent(msg);
}

这个logError()函数接收两个参数:表示严重程度的数值或字符串(视所用系统而异)及错误消息。其中,使用了Image对象来发送请求,这样做非常灵活,主要表现如下几个方面:

  • 所用浏览器都支持Image对象,包括那些不支持XMLHttpRequest对象的浏览器。
  • 可以避免跨域限制。通常都是一台服务器要负责处理多台服务器的错误,而这种情况下使用XMLHttpRequest是不行的。
  • 在记录错误的过程中出问题的概率比较低。大多数Ajax通信都是由JavaScript库提供的包装函数来处理的,如果库代码本身有问题,而你还在依赖库记录错误,可想而知,错误消息是不可能得到记录的。

只要是使用try-catch语句,就应该吧相应错误记录到日志中。来看下面的例子:

1
2
3
4
5
6
7
for (var i = 0, len = mods.length; i < len; i++) {
try {
mods[i].init();
} catch (e) {
logError('nonfatal', 'Module init failed:' + ex.message);
}
}

在这里,一旦模块初始化失败,就会调用logError()。第一个参数是”nonfatal”(非致命),表示错误的严重程度。第二个参数是上下文信息加上真正的JavaScript错误消息。记录到服务器中的错误消息应该尽可能多地带有上下文消息,以鉴别导致错误的真正原因。

3 调试技术

在不那么容易找到JavaScript调试程序的年代,开发人员不得不发挥自己的创造力,通过各种方法来调试自己的代码。结果,就出现了以这样或那样的方式置入代码,从而输出信息的做法。其中,最常见的做法就是在要调试的代码中随处插入alert()函数。但这种做法一方面比较麻烦(调试之后还需要清理),另一方面还可能引入新问题。如今,以及有了很多更好的调试工具,因此我们也不再建议在调试中使用alert()了。

3.1 将消息记录到控制台

IE8、Firefox、Opera、Chrome和Safari都有JavaScript控制台,可以用来查看JavaScript错误。而且,在这些浏览器中,都可以通过代码项控制台输出消息。对Firefox而言,需要安装Firebug。对IE8、Firefox、Chrome和Safari来说,则可以通过console对象向JavaScript控制台写入消息,这个对象具有以下方法:

  • error(message): 将错误消息记录到控制台;
  • info(message): 将消息性消息记录到控制台;
  • log(message): 将一般消息记录到控制台;
  • warn(message): 将警告消息记录到控制台;

在IE8、Firebug、Chrome和Safari中,用来记录消息的方法不同,控制台中显示的错误消息也不一样。错误消息带有红色图标,而警告消息带有黄色图标。以下函数展示了使用控制台输出消息的一个实例。

1
2
3
4
5
6
7
8
9
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;
}

(Opera的”opera.postError()”方法及Firefox、Safari和Opera的LiveConnect方法在此不作介绍,见下面方法)。

1
2
3
4
5
6
7
8
9
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);
}
}

向JavaScript控制台写入消息可以辅助调试代码,但在发布应用程序时,还必须要移除所有消息。在部署应用程序时,可以通过手工或通过特定的代码处理步骤来自动完成清理工作。

消息记录要比使用alert()函数更可取,因为警告框会阻断程序的执行,而在测定异步处理对时间的影响时,使用警告框会影响结果。

3.2 将消息记录到当前页面

另一种输出调试信息的方式,就是在页面中开辟一小块区域,用以显示消息。这个区域通常是一个元素,而该元素可以总是出现在页面中,例如:

1
2
3
4
5
6
7
8
9
10
function log (message) {
var console = document.getElementById('debuginfo');
if (console === null) {
console = document.createElement('div');
console.id = 'debuginfo';
console.style.background = '#dedede';
console.innerText = message;
document.body.appendChild(console);
} else console.innerText = message;
}

这种技术在不支持JavaScript控制台的IE7以及更早版本或其他浏览器中十分有用。

3.3 抛出错误

如前所述,抛出错误也是一种调试代码的好方法。如果错误消息很具体,基本上就可以把它们当作确定错误来源的依据。但这种错误消息必须能够明确给出导致错误的原因,才能省去其他调试操作。来看下面的函数:

1
2
3
function divide(num1, num2) {
return num1 / num2;
}

这个简单的函数计算两个数的除法,但如果有一个参数不是数值,它会返回NaN。类似这样简单的计算如果返回NaN,就会在Web应用程序中导致问题。对此,可以在计算之前,先检测每个参数是否都是数值。例如:

1
2
3
4
5
6
7
8
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时要抛出的错误。如

1
2
3
4
5
6
7
8
9
10
11
12
function assert(condition, message) {
if (condition) {
throw new Error(message);
}
}

// use
function divide(num1, num2) {
assert(typeof num1 === 'number' && typeof num2 === 'number', 'divide():Both arguments must be numbers.');

return num1 / num2;
}

4 常见的IE错误

4.1 操作终止

在IE8之前的版本中,存在一个相对于其他浏览器而言,最令人迷惑、讨厌,也最难于调试的错误:操作终止(operation aborted)。在修改尚未加载完成的页面时,就会发生操作终止错误。发生错误时,会出现一个模态对话框,告诉你“操作终止”。单击确定按钮,则卸载整个页面,继而显示一张空白屏幕;此时要进行调试非常困难。下面实例会导致操作终止错误。

1
2
3
4
5
6
7
8
9
...
<body>
<div>
<script>
document.body.appendChild(document.createElement('div'));
</script>
</div>
</body>
...

准确一点说,当<script>节点被包含在某个元素中,而且JavaScript代码又要使用appendChild()、innerHTML或其他DOM方法修改该元素的父元素或祖先元素时,将会发生操作终止错误(因为只能修改已经加载完毕的元素)。

避免:

1
2
3
4
5
6
7
8
9
...
<body>
<div>
<script>
document.body.insertBefore(document.createElement('div'), document.body.firstChild);
</script>
</div>
</body>
...

1
2
3
4
5
6
7
8
9
10
...
<body>
<div>

</div>
<script>
document.body.appendChild(document.createElement('div'));
</script>
</body>
...

4.2 无效字符

根据语法,JavaScript文件必须只包含特定的字符。在JavaScript文件中存在无效字符时,IE会抛出无效字符(invalid character)错误。所谓无效字符,就是JavaScript语法中未定义的字符。例如,有一个很像减号但却由Unicode值821l表示的字符(\u2013),就不能用作常规的减号(ASCII编码为45),因为JavaScript语法中没有定义该字符。这个字符通常是在Word文档中自动插入的。如果你的代码是从Word文档中复制到文本编辑器中然后又在IE中运行的,那么就可能会遇到无效字符错误。其他浏览器对无效字符做出的反应与IE类似,Firefox会抛出非法字符(illegal character)错误,Safari会报告发生了语法错误, 而Opera则会报告发生了ReferenceError(引用错误),因为它会将无效字符解释为未定义的标识符。

4.3 未找到成员

4.3 未找到成员
如前所述,IE中的所有DOM对象都是以COM对象,而非原生JavaScript对象的形式实现的。这会导致一些与垃圾收集相关的非常奇怪的行为。IE中的来找到成员( Member not found )错误,就是由于垃圾收集例程配合错误所直接导致的。
具体来说,如果在对象被销毁之后,又给该对象赋值.就会导致未找到成员错误。而导致这个错误的,一定是COM对象。发生这个错误的最常见情形是使用event对象的时候。IE中的event对象是window的属性,该对象在事件发生时创建,在最后一个事件处理程序执行完毕后销毁。假设你在一个闭包中使用了event对象,而该闭包不会立即执行,那么在将来调用它并给event的属性赋值时,就会导致未找到成员错误。如

1
2
3
4
5
6
docuemnt.onclick = function () {
var event = window.event;
setTimeout(function () {
event.returnValue = false; // error
}, 1000);
}

在这段代码中,我们将一个单击事件处理程序指定给了文档。在事件处理程序中,window.event被保存在event变量中。然后,传入setTimeout()中的闭包里又包含了event变量。当单击事件处理程序执行完毕后,event对象就会被销毁,因而闭包中引用对象的成员就成了不存在的了。换句话说,由于不能在COM对象被销毁之后再给其成员赋值,在闭包中给returnValue赋值就会导致未找到成员错误。

4.4 未知运行时错误

当使用innerHTML或outerHTML以下列方式指定HTML时,就会发生未知运行时错误(Unknown runtime error):一是把块元素插入到行内元素时,二是访问表格任意部分(<table><tbody>等)的任意属性时。例如,从技术角度说,<span>标签不能包含<div>之类的块级元素,因此下面的代码就会导致未知运行时错误:

1
span.innerHTML = '<div>hi</div>';	//error

在遇到把块级元素插入到不恰当位置的情况时,其他浏览器会尝试纠正并隐藏错误.而IE在这一点上反倒很较真儿。

4.5 语法错误

通常,只要IE一报告发生了语法错误(syntax error),都可以很快找到错误的原因。这时候,原因可能是代码中少了一个分号,或者花括号前后不对应。然而,还有一种原因不十分明显的情况需要格外注意。
如果你引用了外部的JavaScript文件,而该文件最终并没有返回JavaScript代码,IE也会抛出语法错误。例如,<script>元素的src特性指向了一个HTML文件,就会导致语法错误。报告语法错误的位置时,通常都会说该错误位于脚本第一行的第一个字符处。Opera和Safari也会报告语法错误.但它们会给出导致问题的外部文件的信息:IE就不会给出这个信息.因此就需要我们自己重复检查一遍引用 的外部JavaScript文件。但Firefox会忽略那些被当作JavaScript内容嵌入到文档中的非JavaScript文件中的解析错误。
在服务器端组件动态生成JavaScript的情况下,比较容易出现这种错误。很多服务器端语言都会在发生运行时错误时,向输出中插入HTML代码,而这种包含HTML的输出很容易就会违反JavaScript语法。如果在追查语法错误时遇到了麻烦,我们建议你再仔细检查一遍引用的外部文件,确保这些文件中没有包含服务器因错误而插入到其中的HTML。

4.6 系统无法找到指定资源

系统无法找到指定资源(The system cannot locate the resource specified)这种说法,恐怕要算是IE给出的最有价值的错误消息了。在使用JavaScript请求某个资源URL,而该URL的长度超过了IE对URL最长不能超过2083个字符的限制时,就会发生这个错误。IE不仅限制JavaScript中使用的URL的长度,而且也限制用户在浏览器自身中使用的URL长度(其他浏览器对URL的限制没有这么严格)。IE对URL路径还有一个不能超过2048个字符的限制。下面的代码将会导致错误。

1
2
3
4
5
6
7
8
9
10
11
12
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.michealwayne.cn'), true);
x.send(null);

在这个例子中,XMLHttpRequest对象试图向一个超出最大长度限制的URL发送请求。在调用open()方法时,就会发生错误。避免这个问题的办法,无非就是通过给查询字符串参数起更短的名字,或者减少不必要的数据,来缩短查询字符串的长度。另外,还可以把请求方法改为POST,通过请求体而不是查询字符串来发送数据。

温习:

  • 各主要浏览器报告的错误及展现形式
  • try-catch-finally语句、throw操作符
  • 7种错误类型
  • error错误事件及错误日志
  • 致命错误和非致命错误
  • 常见的IE错误

(完)