本章内容:处理错误与调试JavaScript代码
一、错误处理
错误处理在程序设计中的重要性是毋庸置疑的,良好的错误处理机制可以让用户及时得到提醒。
1.1、try-catch 语句
ECMA-263第3版引入了 try-catch 语句,作为 JavaScript 中 处理异常的一种标准方式。基本的语法如下:
try {
// 可能会导致错误的代码
} catch(err) {
// 在错误发生时怎么处理
}
如果 try 块中的任何代码发生了错误,就会立即退出代码执行过程,然后接着执行 catch 块。此时,catch 快会接受到一个包含错误信息的对象。
catch 中 的error(错误信息的对象)中,包含的实际信息回因浏览器的不同而异,但共同的是有一个保存着错误消息的 message 属性。
1.1.1、finally 子句
虽然在 tyr-catch 语句中是可选的,但 finally 子句一经使用,其代码无论如何都会执行。只要代码中包含 finally 子句,则无论 try 或 catch 语句块中包含什么代码——甚至 return 语句,都不会阻止 finally 子句的执行。
function testFinally() {
try {
return 1
} catch(err) {
return 2
} finally {
return 3
}
}
console.log(testFinally()) // 3
在以上代码中,最后又一个 finally 子句,结构就会导致该 try 中 return的值被忽略掉;如果把 finally 子句拿掉,那么这个函数应该返回 1
如果提供了 finally 子句,则 catch 子句就成了可选的(catch 或 finally 有一个即可)。IE7及更早版本中有一个 bug;除非 catch 子句,否则finally 中的代码永远不会执行。如果你仍然要考虑 IE 的早起版本,那就只好提供一个 catch 子句,哪怕里面什么都不写。IE8修复了这个 bug。
1.1.2、错误类型
每种错误都有对应的错误类型,而当错误发生时,就会抛出相应类型的错误对象。
ECMA-262定义了下列7种错误类型:
- Error
- EvalError
- RangeError
- ReferenceError
- SyntaxError
- TypeError
- URIError
Error 是基类型,其他错误类型都继承自该类型。Error 类型的错误很少见,如果有也是浏览器抛出的;这个基类型的主要目的是供开发人员抛出自定义错误。
EvalError 类型的错误会在使用 eval() 函数而发生异常时抛出,简单地说,如果没有把 eval() 当成函数调用,就会抛出错误。
例如:
new eval()
在实践中,浏览器不一定会在抛出错误时就抛出 EvalError,不同浏览器中存在差异。有鉴如此,加上在实际开发中极少会这样使用 eval(),所以遇到这种错误类型的可能性极小。
RangeError 类型的错误会在数组超出相应 范围时触发。
例如:在定义数组时,如果指定了数组不支持的项数(如 -20 或 Number.MAX_VALUE),就会触发这种错误。
var item1 = new Array(-20) // 抛出 RangeError
var item2 = new Array(Number.MAX_VALUE) // 抛出 RangeError
JavaScript中经常会出现这种范围错误
ReferenceError 会发生在找不到对象的情况下,通常在访问不存在的变量是,就会发生这种错误。
例如:
var obj = x // 在 x 未声明的情况下会抛出 ReferenceError
SyntaxError,当我们把语法错误的JavaScript字符串传入 eval() 函数时,就会导致此类错误。
例如:
eval('a ++ b') // 抛出 SyntaxError
TypeError类型在 JavaScript 中经常遇到,在变量中保存着以外的类型时,或者访问不存在方法是,都会导致这种错误。
如下示例:
var o = new 10 // TypeError
alert('name' in true) // TypeError
Function.prototype.toString.call('name') // TypeError
最常发生错误类型的情况,就是传递给函数的参数事先未检查,结果传入类型与预期类型不相符
URIError 类型错误,在使用 encodeURI() 或 decodeURI()时,传递的 URI 格式不正确时。这种错误也很少见,因为前面说的这两个函数的容错性非常高。
利用不同的错误类型,可以获悉更多有关异常的信息,从而有助于对错误作出恰当的处理。
try {
// todo
} catch(error) {
if (error instanceof TypeError) {
} else if(error instanceof ReferenceError) {
} else {
}
}
1.1.3、合理使用 try-catch
try-catch 能够让我们实现自己的错误处理机制。使用 try-catch 最适合处理那些我们无法控制的错误。在明明白白地知道自己的代码会发生错误时,再使用 try-catch 语句就不太合适了。
1.2、抛出错误
与 try-catch 语句相配的还有一个 throw 操作符,用于随时抛出自定义错误。抛出错误时,必须要给 throw 操作符指定一个值,这个值是什么类型,没有要求。
throw 123
throw 'Hello World!'
throw true
在遇到 throw 操作符是,代码会立即停止执行。当仅有 try-catch 语句捕获到被抛出的值时,代码才会继续执行。
通过使用某种内置消息错误类型,可以更真实地模拟浏览器错误,每种错误类型的构找函数接受一个参数,即实际的错误消息。
throw new Error('Something bad happened')
像下面使用其他错误类型,也可以模拟出类似的浏览器错误。
throw new SyntaxError('xxxx')
throw new TypeError('xxxx')
throw new RangeError('xxxx')
throw new EvalError('xxxx')
throw new URIError('xxxx')
throw new ReferenceError('xxxx')
另外,利用原型链还可以通过继承 Error 来创建自定义错误类型。此时,需要为新创建的错误类型指定 name 和 message 属性。
function CustomError(message) {
this.name = 'CustomError'
this.message = message
}
CustomError.prototype = new Error()
throw new CustomError('my message')
浏览器对待继承自 Error 的自定义错误类型,就像对待其他类型错误一样。如果要捕获自己抛出的错误并且把它与浏览器错误区别对待的话,创建自定义错误时很有用的。
1.2.1、抛出错误的时机
要针对函数为什么会执行失败给出更多信息,抛出自定义错误是一种很方便的方式。应该在出现某种特定的已知错误条件,导致函数无法正常执行时抛出错误。
下面的函数会在参数不是数组的情况下失败:
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:对象不支持“sort”属性或方法
- Firefox:values.sort() 不是函数
- Safari:值 undefined (表达式 values.sort 的结果)不是对象。
- Chrome:values.sort() 不是函数
- Opera:类型不匹配(通常是在需要对象的地方使用了非对象值)
这种情况下,带有适当信息的自定义错误能够显著提升代码的可维护性。
function process(values) {
if (!(values instanceof Array)) {
throw new Error('process(): Arguments must be an array.')
}
// ...
}
建议在开发 JavaScript 代码的过程中,重点关注函数和可能导致函数执行失败的因素。良好的错误处理机制应该确保代码中只发生你自己抛出的错误。
1.2.2、抛出错误与使用 try-catch
关于何时抛出错误,而何时该使用 try-catch 来捕获他们,是一个老生常谈的问题。我们认为只应该捕获那些确切知道该如何处理的错误。捕获错误的目的在于米面浏览器以默认的方式处理它们;而抛出错误的目的在于提供错误发生具体原因的消息。
1.3、错误(error)事件
任何没有通过 try-catch 处理的错误都会触发 window 对象的 error 事件。
这个事件时 Web 浏览器最早支持的事件之一,IE、Firefox、Chrome 为保存向后兼容,并没有对这个事件做任何修改(Opera、Safari 不支持 error 事件)。onerror 事件不会创建 event 对象,但它可以接收三个参数:错误消息、错误所在的URL、行号。要指定 onerror 事件处理程序,必须使用如下所示的 DOM0级技术,它没有遵循 “DOM2 级 事件”的标准格式。
window.onerror = function(message, url, line) {
alert(message + '----' + url + '-----' + line)
return false
}
只要发生错误,无论是不是浏览器生成的,都会触发 error 事件。而在 事件处理函数 中返回 false,可以组织浏览器报告错误的默认行为。
通过放回 false,这个函数实际上就充当了整个文档的 try-catch 语句。只要能够适当的使用 try-catch 语句,就不会有错误消息交给浏览器,也就不会触发 error 事件。
图像也支持 error 事件,只要图像的 src 特性中的 URL 不能反悔可以被识别的图形格式,就会触发 error 事件。此时 error 事件遵循 DOM 格式,会返回一个 以图像为目标的 event 对象。
var image = new Image()
image.onload = function(event) {
alert('image loaded!')
}
image.onerror = function(event) {
alert('image not loaded!')
}
image.src = 'smile.gif' //指定不存在的文件
1.4、处理错误的策略
由于 JavaScript 错误都可能导致网页无法使用,因此搞清楚何时以及为什么发生错误至关重要。作为开发人员,必须要知道代码何时可能出错,会出什么错,同时还要有一个跟踪此类问题的系统。
1.5、常见的错误类型
错误处理的核心,是首先要知道代码里会发生什么错误。由于 JavaScript 是松散类型的,而且也不会验证函数的参数,因此错误只会在代码运行期间出现。一般来说,需要关注三种错误:
- 类型转换错误
- 数据类型错误
- 通信错误
1.5.1、类型转换错误
类型转换错误发生在使用某个操作符,或者使用其他可能会自动转换值的数据类型的语言结构时。在使用相等(==)和全等(===)操作符,或者在 if、for 及 while 等流程控制语句中使用 非布尔值是,最常发生类型转换错误。
由于在非动态语言中,开发人员都使用相同的符号执行直观的比较,因此在 JavaScript 中往往也会以相同方式错误地使用他们。
alert(5 == '5') // true
alert(5 === '5') // false
alert(1 == true) // true
alert(1 === true) // false
容易发生类型转成错误的另一个地方,就是流程控制语句。像 if 之类的语句在确定下一步操作之前,会自动把任何值换行成布尔值。尤其是 if 语句,如果使用不当,最容易出错。
function concat(str1, str2, str3) {
var result = str1 + str2
if (str3) { // 不要这样做
result += str3
}
return result
}
假设第三个参数是数值0,那么if鱼护的测试就会失败,而对数值1的测试则会通过
在流程控制语句中使用非布尔值,是极为常见的一个错误来源。为避免此类错误,就要做到在条件比较时传入布尔值。
function concat(str1, str2, str3) {
var result = str1 + str2
if (typeof str3 == 'string') { // 恰当的比较
result += str3
}
return result
}
1.5.2、数据类型错误
JavaScript 是松散型的,在使用变量和函数参数之前,不会对它们进行比较以确保它们的数据类型正确。为了保证不会发生数据类型错误,只能依靠开发人员编写适当的数据类型检测代码。
function getQueryString(url) {
if (typeof url == 'string') {
var pos = url.indexof('?')
if (pos > -1) {
return url.substring(pos + 1)
}
}
return ''
}
前面提到,在流程控制语句中使用 非布尔值作为条件很容易导致类型转换错误。同样,这样做也经常会导致数据类型错误。
function reverseSort(values) {
// if (values) { // 不要这样做!
// if (values != null) { // 不要这样做!
// if (typeof values.sort == 'function') { //不要这样做!
if (values instanceof Array) { // 正确做法
values.sort()
values.reverse()
}
}
大体上来说,基本类型的值应该使用 typeof 来检测,而对象的值则应该使用 instanceof 来检测。
1.5.3、通信错误
JavaScript 与 服务器之间的任何一次通信,都有可能会产生错误。
第一种通信错误与格式不正确的 URL 或发送的数据有光。常见的是没有使用 encodeURIComponent() 对数据进行编码。
例如,下面这个URL的格式就是不正确的:
http://www.yourdomain.com/?redir=http://www.comeotherdomain.com?a=b&c=d
需要针对‘ redir=’ 后面的所有字符串调用 encodeURIComponent() 就可以解决这个问题。
对于查询字符串,应该记住必须要使用 encodeURIComponent() 方法。为了确保这一点,有时候可以定义一个处理查询字符串的函数,例如:
function addQueryStringArg(url, name, value) {
if (url.indexOf('?') == -1) {
url += '?'
} else {
url += '&'
}
url += encodeURIComponent(name) + '=' + encodeURIComponent(value)
return url
}
另外,在服务器相应的数据不正确时,就会发生通信错误。在没有返回相应资源的情况下,Firefox、Chrome、Safari 会默默地失败,IE 和 Opera 则都会报错,
1.6、区分致命错误和非致命错误
任何错误处理策略中最重要的一个部分,就是确定错误是否致命。
非致命错误,可以根据下列一或多个条件来确定:
- 不影响用户的主要任务
- 只影响页面的一部分
- 可以恢复
- 重复相同操作可以取消错误
致命错误,可以通过以下一或多个条件来确定:
- 应用程序根本无法继续运行
- 错误明显影响到了用户的主要操作
- 会导致其他连带错误
区分非致命错误和致命错误的主要依据,就是看他们对用户的影响。设计良好的代码,可以做到应用程序某一部分发生错误不会不必要地影响另一个实际上毫不相干的部分。
for (var i = 0; len = mods.length; i < len; i++) {
mods[1].init() // 可能会导致致命错误
}
表明上看,这些代码没什么问题:依次对每个模块调用 init() 方法。问题在于,任何模块的 init() 方法如果出错,都会导致数组中后续的所有模块无法再进行初始化。
修改如下:
for (var i = 0; len = mods.length; i < len; i++) {
try {
mods[i].init()
} catch (ex) {
// 这里处理错误
}
}
1.7、把错误记录到服务器
开发 Web 引用程序过程中的一种常见的做法,就是集中保存错误日志,以便查找重要错误的原因。例如数据库和服务器错误都会定期写入日志,而且会按照常用 API 进行分类。在复杂的Web 应用程序中,也推荐你把 JavaScript 错误也回写到服务器。
要建立这样一种 JavaScript 错误记录系统,首先需要在服务器上创建一个页面(或者一个服务器入口点),用于处理数据错误。从查询字符串中取得数控,然后再将数据写入错误日志中:
function logError(sev, msg) {
var img = new Image()
img.src = 'log.php?sev=' + encodeURIComponent(sev) + '&msg=' + encodeURIComponent(msg)
}
这个 logError() 函数接收两个参数:表示严重程序的数组或字符串(视所用系统而异)及错误消息。其中,使用了 Image 对象来发生请求,这样做非常灵活,主要表现如下几方面。
- 所有浏览器都支持 Image 对象,包括那些不支持 XMLHttpRequest 对象的浏览器
- 可以避免跨域限制。
- 在记录错误的过程中出问题的概率比较低。大多数 Ajax 通信都是由 JavaScript 库提供的包装函数来处理的,如果库代码本身有问题,而你还在依赖该库记录错误,可想而知,错误消息是不可能得到记录的。
只要使用 try-catch 语句,就应该把相应错误记录到日志中。
for (var i = 0, len = mods.length; i < len; i++) {
try {
mods[i].init()
} catch(ex) {
logError('nonfatal', 'Module init failed: ' + ex.message)
}
}
二、调试技术
起初,最常见的错发就是在要调试的代码中随处 插入 alert() 函数。但这种做法一方面比较麻烦(调试之后还需要清理),另一方面还可能引入新问题。如今,已经有了很多更好的调试工具,因此不建议在调试中使用 alert() 了。
2.1、将消息记录到控制台
通过 console 对象向 JavaScript 控制台中写入消息,这个对象具有下列方法。
- error(message):将错误消息记录到控制台(一般为红色)
- info(message):将信息性消息记录到控制台
- log(message):将一般消息记录到控制台
- warn(message):将警告消息记录到控制台(一般为黄色)
在 Opera 10.5 之前的版本中,JavaScript 控制台可以通过 opera.postError() 方法来访问。这个方法接受一个参数,即要写入到控制台中的参数。它能向 JavaScript 控制台中写入任何信息
还有一种方案是 使用 LiveConnect,也就是在 JavaScript中 运行 java 代码。Firefox、Safari、Opera都支持 LiveConnect,因此可以操作 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)
}
}
2.2、将消息记录到当前页面
另一种输出调试消息的方式,就是在页面中开辟一块小区域,用以显示消息。这个区域通常是一个元素,该元素可以总是出现在页面中,但仅用于调试目的;也可以是一个根据需要动态创建的元素,
例如:将 log() 函数修改如下所示
window.onload = function(event) {
function log(message) {
var log = document.getElementById('debuginfo')
if (log === null) {
log = document.createElement('div')
log.id = 'debuginfo'
log.style.backgroundColor = 'rgba(0, 0, 0, .5)'
log.style.width = document.body.clientWidth + 'px'
log.style.height = document.body.clientHeight + 'px'
log.style.position = 'absolute'
log.style.left = 0
log.style.top = 0
document.body.appendChild(log)
}
log.innerHTML += '<p style="font-size: 16px; text-align: center;color: white; line-height:' + document.body.clientHeight + 'px">'+ message +'</p>'
}
log('Something bad happended')
}
2.3、抛出错误
抛出错误也是一种调试代码的好办法,如果错误消息很具体,基本上就可以把它当做确定错误来源的依据。但这种错误消息必须能够明确给出导致错误的原因,才能省去其他调试操作。
function divide(num1, num2) {
if (typeof num1 != 'number' || typeof num2 != 'number') {
throw new Error('divide(): Both argyments must be numvers.')
}
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 bumbers.')
return num1 / num2
}
三、常见的IE错误
多年以来,IE一直都是最难与调试 JavaScript 错误的浏览器。IE给出的错误消息一般很短有语焉不详,而且上下文信息也很少。但作为用户最多的浏览器,如何看懂IE给出的错误也是很受关注的。
3.1、操作终止
在 IE8 之前的版本只,存在一个相对其他浏览器而言,最令人迷惑、讨厌,也最难与调试的错误:w(operation aborted)。在修改尚未加载完成页面时,就会发生这个错误。发生错误时,会出现一个模态对话框,告诉你“操作终止”。单机 确定(ok)按钮,就会卸载整个页面,继而显示一张空白屏幕;
3.2、无效字符
JavaScript 文件必须包含特定的字符。在 JavaScript 文件中存在无效字符时,IE会抛出 无效字符(invalid character)错误。所谓无效字符,就是 JavaScript 语法中未定义的字符。其它浏览器对无效字符做出的反应与 IE 类似,Firefox 会抛出非法字符(illegal character)错误,Safari会报告发生了语法错误,而Opera会报告发生了 ReferenceError(引用错误),因为它会将无效字符串解释为未定义的标识符。
3.3、未找到成员
IE中的未找到成员(Member not found)错误,是由于垃圾收集例程配合错误所直接导致的。如果在对象被销毁之后,又给对象赋值,就会导致未找到成员错误。而导致这个错误的,一定是 COM 对象。发生这个错误的最常见情形是使用 event 对象的时候。假设你在一个闭包中使用了 event 对象,而该闭包不会立即执行,那么在将来调用它并给 event 的属性赋值时,就会导致未找到成员错误。
document.onclick = function() {
var event = window.event
setTimeout(function() {
event.returnfalse = false // 未找到成员错误
}, 1000)
}
3.4、未知运行时错误
当使用 innerHTML 或 outHTML 以下列方式指定 HTML 时,就会发生未知运行错误(Unknown runtime erro):
- 把块元素插入到行内元素时
- 访问表格任意部分(<table>、<tbody> 等)的任意属性时
3.5、语法错误
通常,只要IE 一报告发生了语法错误(syntax error),都可以很快找到错误的原因。这时候,原因可能是代码少了一个分号,或者花括号前后不对应。然而,还有一种原因不十分明显的情况需要格外注意。
如果你引用了外部的 JavaScript 文件,而该文件最终并没有返回 JavaScript 代码,IE 也会跑出语法错误。通常都会说该错误位于脚本第一行的第一个字符处。
3.6、系统无法找到指定资源
系统无法找到指定资源(The system cannot locate the resource specified)这种说法,恐怕算是 IE 给出的最有价值的错误消息了。在使用 JavaScript 请求某个 URL,而该URL的长度超过了 IE 对 URL最长不能超过 2083 个字符的限制时,就会触发这个错误。IE不仅限制了 JavaScript 中使用 的URL的长度,而且也限制了用户在浏览器自身中使用的URL长度(其它浏览器对URL的限制没有这么严格)。IE对URL路径还有一个不能超过 2048 个字符的限制。
四、小结
错误处理对于今天复杂的 Web 应用程序开发而言至关重要。不能提前预测到可能发生的错误,不能提前采取恢复策略,可能导致较差的用户体验,最终引发用户不满。多数浏览器在默认情况下都不会想用户报告错误,因此在开发和调试期间需要启用浏览器的错误报告功能。然而,在投入运行的产品代码中,则不应该再有诸如此类的错误报告出现。
下面是几种避免浏览器响应 JavaScript 错误的方法:
- 在可能发生错误的地方使用 try-catch 语句,这样你还有机会以适当的方式对 错误给出响应,而不必沿用浏览器处理错误的机制。
- 使用 window.onerror 事件处理程序,这种方式可以接收 try-catch 不能处理的所有错误(仅限于 IE、Firefox、Chrome)
另外,对任何 Web 应用程序都应该分析可能的错误来源,并制定处理错误的方案
- 首先,必须要明确是什么致命错误,什么是非致命错误。
- 其次,再分析代码,以判断最可能发生的错误。JavaScript 中发生错误的主要原因如下。
- 类型转换
- 未充分检测数据类型
- 发送给服务器或从服务器接收到的数据有错误