0 前言
目前读到了《高程3》的错误检测部分,现在先挖一个坑,关于错误检测应该写三篇总结:firebug检测错误和输出信息;try-catch错误捕获;常见错误种类。
本篇逻辑思路如下:首先介绍进行错误捕获的try-catch语句;然后介绍常见的错误类型,这点会结合firebug来说明;其次介绍try-catch语句和错误类型结合使用以捕获错误;但是如果捕获的错误以浏览器的语言表示,还是很难找到错误,因此推荐使用throw抛出开发者自定义的错误,这样,错误的位置和原因就会更易发现;最后就错误抛出和错误捕获进行了总结讨论。
1 try-catch简介
良好的错误处理机制可以让用户和开发者及时得到提醒,知道到底发生了什么事,因而不会惊惶失措。ECMA-262 第 3 版引入了 try-catch 语句,作为 JavaScript 中处理异常的一种标准方式。基本的语法如下所示,
try{
// 可能会导致错误的代码
} catch(error){
// 在错误发生时怎么处理
}
我们应该把所有可能会抛出错误的代码都放在 try 语句块中,而把那些用于错误处理的代码放在 catch 块中。如果 try 块中的任何代码发生了错误,就会立即退出代码执行过程,然后接着执行 catch 块。此时, catch 块会接收到一个包含错误信息的对象。即使你不想使用这个错误对象,也要指定一个参数名。这个对象中包含的实际信息会因浏览器而异,但共同的是有一个保存着错误消息的 message 属性。这个 message 属性是唯一一个能够保证所有浏览器都支持的属性,除此之外, IE、 Firefox、 Safari、 Chrome 以及 Opera 都为错误对象添加了其他相关信息。在跨浏览器编程时,最好还是只使用 message 属性。
一个测试例子:
只要代码中包含 finally 子句,则无论 try 或 catch 语句块中包含什么代码——甚至 return 语句,都不会阻止 finally 子句的执行。如下例所示:
function testFinally() {
try {
return 2;
} catch (error) {
return 1;
} finally {
return 0;
}
}
function testWithoutFinally() {
try {
return 2;
} catch (error) {
return 1;
}
}
console.log(testFinally());//输出0
console.log(testWithoutFinally());//输出2
如果提供 finally 子句,则 catch 子句就成了可选的(catch 或 finally 有一个即可)。 IE7 及更早版本中有一个 bug:除非有 catch 子句,否则 finally 中的代码永远不会执行。IE8 修复了这个 bug。如果你仍然要考虑 IE 的早期版本,那就只好提供一个 catch 子句,哪怕里面什么都不写。
另一个测试实例:
var example = function() {
try {
window.someNonexistentFunction();
} catch (error) {
console.log(error.name);
console.log(error.message);
return 1;
} finally {
console.log(‘everything is over‘);
}
};
example();
测试结果:先后执行了try/catch/finally中的语句,问题是为什么会在最后执行一次catch中的return语句?留给自己一个问题吧。
2 错误类型
每种错误都有对应的错误类型,而当错误发生时,就会抛出相应类型的错误对象(error object)。ECMA-262 定义了下列 7 种错误类型:
1) Error 是基类型,其他错误类型都继承自该类型。因此,所有错误类型共享了一组相同的属性(错误对象中的方法全是默认的对象方法)。
2)EvalError 类型的错误会在使用 eval()函数而发生异常时被抛出,但是抛出的错误不一定是EvalError 类型。如果没有把 eval()当成函数调用,Firefox 4+和 IE8 对此抛出 TypeError。
3)RangeError 类型的错误会在数值超出相应范围时触发。例如,在定义数组时,如果指定了数组不支持的项数(如-20 或 Number.MAX_VALUE),就会触发这种错误。
4)在找不到对象的情况下,会发生 ReferenceError(这种情况下,会直接导致人所共知的"object expected"浏览器错误)。通常,在访问不存在的变量时,就会发生这种错误。
5) 至于 SyntaxError,当我们把语法错误的 JavaScript 字符串传入 eval()函数时,就会导致此类错误。如果语法错误的代码出现在 eval()函数之外,则不太可能使用 SyntaxError,因为此时的语法错误会导致 JavaScript 代码立即停止执行。
6)TypeError 类型在 JavaScript 中会经常用到,在变量中保存着意外的类型时,或者在访问不存在的方法时,都会导致这种错误。错误的原因虽然多种多样,但归根结底还是由于在执行特定于类型的操作时,变量的类型并不符合要求所致。最常发生类型错误的情况,就是传递给函数的参数事先未经检查,结果传入类型与预期类型不相符。
7) 在使用 encodeURI()或 decodeURI(),而 URI 格式不正确时,就会导致 URIError 错误。这种错误也很少见,因为前面说的这两个函数的容错性非常高。
上述测试过程在FF如下:
3 使用try-catch语句捕获错误
要想知道错误的类型,可以像下面这样在 try-catch 语句的 catch 语句中使用 instanceof 操作符。
try {
someFunction();
} catch (error){
if (error instanceof TypeError){
//处理类型错误
} else if (error instanceof ReferenceError){
//处理引用错误
} else {
//处理其他类型的错误
}
}
在跨浏览器编程中,检查错误类型是确定处理方式的最简便途径;而包含在 message 属性中的错误消息会因浏览器而异。
使用 try-catch 最适合处理那些我们无法控制的错误。假设你在使用一个大型 JavaScript 库中的函数,该函数可能会有意无意地抛出一些错误。由于我们不能修改这个库的源代码,所以大可将对该函数的调用放在 try-catch 语句当中,万一有什么错误发生,也好恰当地处理它们。
4 throw抛出自定义错误
与 try-catch 语句相配的还有一个 throw 操作符,用于随时抛出自定义错误。抛出错误时,必须要给 throw 操作符指定一个值,这个值是什么类型,没有要求。在遇到 throw 操作符时,代码会立即停止执行。仅当有 try-catch 语句捕获到被抛出的值时,代码才会继续执行。
通过使用自定义的内置错误类型,可以更真实地模拟浏览器错误。每种错误类型的构造函数接收一个参数,即实际的错误消息。下面是例子。
throw new Error("Something bad happened.");
throw new SyntaxError("I don’t like your syntax.");
throw new TypeError("What type of variable do you take me for?");
throw new RangeError("Sorry, you just don’t have the range.");
throw new EvalError("That doesn’t evaluate.");
throw new URIError("Uri, is that you?");
throw new ReferenceError("You didn’t cite your references properly.");
浏览器会像处理自己生成的错误一样,来处理这些代码抛出的错误。换句话说,浏览器会以常规方式报告这一错误,并且会显示这里的自定义错误消息。
抛出自定义错误的意义:方便快速定位错误并纠错。虽然浏览器会自己报错,但是这些报错信息在浏览器中达不到统一,而且如果出现同种类型错误,查找来源是复杂的,尤其是在数千行代码中。但是如果你知道可能的代码错误,可以直接在代码中添加这些自定义的错误,一旦发生这些错误,浏览器就报出自定义错误,关键是这个错误的位置和类型显而易见。
实例:一个必须接收数组作为参数的函数如果接收字符串作为参数就会报错。
function process(values){
values.sort();
for (var i=0, len=values.length; i < len; i++){
if (values[i] > 100){
return values[i];
}
}
return -1;
}
var a = process("string");
console.log(a);
firebug中结果:
添加自定义错误后的代码:
function process(values){
if (!(values instanceof Array)){
throw new Error("process(): Argument must be an array.");
}
values.sort();
for (var i=0, len=values.length; i < len; i++){
if (values[i] > 100){
return values[i];
}
}
return -1;
}
var a = process("string");
console.log(a);
firebug中结果:
如果 values 参数不是数组,就会抛出一个错误。错误消息中包含了函数的名称,以及为什么会发生错误的明确描述。如果一个复杂的 Web 应用程序发生了这个错误,那么查找问题的根源也就容易多了。
利用原型链还可以通过继承 Error 来创建自定义错误类型。此时,需要为新创建的错误类型指定 name 和 message 属性。
一个实例:
//通过继承Error实现自定义错误类型,要定义name和message属性
function CustomError(message){
this.name = "CustomError";
this.message = message;
}
CustomError.prototype = new Error();
function process(values){
//如果出现错误,就抛出自定义错误类型的对象实例
if (!(values instanceof Array)){
throw new CustomError("process(): Argument must be an array.");
}
values.sort();
for (var i=0, len=values.length; i < len; i++){
if (values[i] > 100){
return values[i];
}
}
return -1;
}
process("string");
firebug显示自定义类型的错误:
5 抛出错误与使用 try-catch捕获错误
关于何时该抛出错误,而何时该使用 try-catch 来捕获它们,是一个老生常谈的问题。一般来说,应用程序架构的较低层次中经常会抛出错误,但这个层次并不会影响当前执行的代码,因而错误通常得不到真正的处理。如果你打算编写一个要在很多应用程序中使用的 JavaScript 库,甚至只编写一个可能会在应用程序内部多个地方使用的辅助函数,我都强烈建议你在抛出错误时提供详尽的信息。然后,即可在应用程序中捕获并适当地处理这些错误。
说到抛出错误与捕获错误,我们认为只应该捕获那些你确切地知道该如何处理的错误。捕获错误的目的在于避免浏览器以默认方式处理它们;而抛出错误的目的在于提供错误发生具体原因的消息。