深入分析javascript中的错误处理机制

深入分析 javascript 中的错误处理机制

前面的话

错误处理对于 web 应用程序开发至关重要,不能提前预测到可能发生的错误,不能提前采取恢复策略,可能导致较差的用户体验。由于任何 javascript 错误都可能导致网页无法使用,因此作为开发人员,必须要知道何时可能出错,为什么会出错,以及会出什么错。本文将详细介绍 javascript 中的错误处理机制

error 对象

error 对象是包含错误信息的对象,是 javascript 的原生对象。当代码解析或运行时发生错误,javascript 引擎就会自动产生并抛出一个 error 对象的实例,然后整个程序就中断在发生错误的地方

console.log(t); //Uncaught ReferenceError: t is not defined

ECMA-262 规定了 error 对象包括两个属性:message 和 name。message 属性保存着错误信息,而 name 属性保存错误类型

1
2
3
4
5
6
7
//一般地,使用try-catch语句来捕获错误
try {
t;
} catch(ex) {
console.log(ex.message); //t is not defined
console.log(ex.name); //ReferenceError
}

浏览器还对 error 对象的属性做了扩展,添加了其他相关信息。其中各浏览器厂商实现最多的是 stack 属性,它表示栈跟踪信息(safari 不支持)

1
2
3
4
5
try {
t;
} catch(ex) {
console.log(ex.stack); //@file:///D:/wamp/www/form.html:12:2
}

当然,可以使用 error()构造函数来创建错误对象。如果指定 message 参数,则该 error 对象将把它用做它的 message 属性;若不指定,它将使用一个预定义的默认字符串作为该属性的值

1
2
3
4
5
6
new Error();
new Error(message);

//一般地,使用throw语句来抛出错误
throw new Error('test'); //Uncaught Error: test
throw new Error(); //Uncaught Error
1
2
3
4
5
6
7
function UserError(message) {
this.message = message;
this.name = "UserError";
}
UserError.prototype = new Error();
UserError.prototype.constructor = UserError;
throw new UserError("errorMessage"); //Uncaught UserError: errorMessage

当不使用 new 操作符,直接将 Error()构造函数像一个函数一样调用时,它的行为和带 new 操作符调用时一样

1
2
3
4
Error();
Error(message);
throw Error('test'); //Uncaught Error: test
throw Error(); //Uncaught Error

error 对象有一个 toString()方法,返回’Error:’+ error 对象的 message 属性

1
2
var test = new Error('testError');
console.log(test.toString()); //'Error: testError'

error 类型

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

1
2
3
4
5
6
7
Error
EvalError(eval错误)
RangeError(范围错误)
ReferenceError(引用错误)
SyntaxError(语法错误)
TypeError(类型错误)
URIError(URI错误)

其中,Error 是基类型,其他错误类型都继承自该类型。因此,所有错误类型共享了一组相同的属性。Error 类型的错误很少见,如果有也是浏览器抛出的;这个基类型的主要目的是供开发人员抛出自定义错误

【EvalError(eval 错误)】

eval 函数没有被正确执行时,会抛出 EvalError 错误。该错误类型已经不再在 ES5 中出现了,只是为了保证与以前代码兼容,才继续保留

【RangeError(范围错误)】

RangeError 类型的错误会在一个值超出相应范围时触发,主要包括超出数组长度范围以及超出数字取值范围等

1
2
3
4
new Array(-1);  //Uncaught RangeError: Invalid array length
new Array(Number.MAX_VALUE); //Uncaught RangeError: Invalid array length
(1234).toExponential(21); //Uncaught RangeError: toExponential() argument must be between 0 and 20
(1234).toExponential(-1); //Uncaught RangeError: toExponential() argument must be between 0 and 20
【ReferenceError(引用错误)】

引用一个不存在的变量或左值(lvalue)类型错误时,会触发 ReferenceError(引用错误)

a; //Uncaught ReferenceError: a is not defined

1++; //Uncaught ReferenceError: Invalid left-hand side expression in postfix operation

【SyntaxError(语法错误)】

当不符合语法规则时,会抛出 SyntaxError(语法错误)

1
2
3
4
//变量名错误
var 1a; //Uncaught SyntaxError: Unexpected number
// 缺少括号
console.log 'hello'); //Uncaught SyntaxError: Unexpected string
【TypeError(类型错误)】

在变量中保存着意外的类型时,或者在访问不存在的方法时,都会导致 TypeError 类型错误。错误的原因虽然多种多样,但归根结底还是由于在执行特定类型的操作时,变量的类型并不符合要求所致

1
2
3
var o = new 10;  //Uncaught TypeError: 10 is not a constructor
alert('name' in true); //Uncaught TypeError: Cannot use 'in' operator to search for 'name' in true
Function.prototype.toString.call('name'); //Uncaught TypeError: Function.prototype.toString is not generic
【URIError(URI 错误)】

URIError 是 URI 相关函数的参数不正确时抛出的错误,主要涉及 encodeURI()、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape()和 unescape()这六个函数

decodeURI('%2'); // URIError: URI malformed

error 事件

任何没有通过 try-catch 处理的错误都会触发 window 对象的 error 事件

error 事件可以接收三个参数:错误消息、错误所在的 URL 和行号。多数情况下,只有错误消息有用,因为 URL 只是给出了文档的位置,而行号所指的代码行既可能出自嵌入的 JavaScript 代码,也可能出自外部的文件

要指定 onerror 事件处理程序,可以使用 DOM0 级技术,也可以使用 DOM2 级事件的标准格式

1
2
3
4
5
6
7
8
//DOM0级
window.onerror = function(message,url,line) {
alert(message);
}
//DOM2级
window.addEventListener("error",function(message,url,line) {
alert(message);
});

浏览器是否显示标准的错误消息,取决于 onerror 的返回值。如果返回值为 false,则在控制台中显示错误消息;如果返回值为 true,则不显示

1
2
3
4
5
6
7
8
9
10
11
12
//控制台显示错误消息
window.onerror = function(message,url,line) {
alert(message);
return false;
}
a;
//控制台不显示错误消息
window.onerror = function(message,url,line) {
alert(message);
return true;
}
a;

这个事件处理程序是避免浏览器报告错误的最后一道防线。理想情况下,只要可能就不应该使用它。只要能够适当地使用 try-catch 语句,就不会有错误交给浏览器,也就不会触发 error 事件

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

加载图像失败时会显示一个警告框。发生 error 事件时,图像下载过程已经结束,也就是不能再重新下载了

1
2
3
4
5
6

var image = new Image();
image.src = 'smilex.gif';
image.onerror = function(e) {
console.log(e);
}

throw 语句与抛出错误

throw 语句用于抛出错误。抛出错误时,必须要给 throw 语句指定一个值,这个值是什么类型,没有要求

[注意]抛出错误的过程是阻塞的,后续代码将不会执行

1
2
3
4
throw 12345;
throw 'hello world';
throw true;
throw {name: 'javascript'};

可以使用 throw 语句手动抛出一个 Error 对象

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

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

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

1
2
3
4
5
6
function CustomError(message){
this.name = 'CustomError';
this.message = message;
}
CustomError.prototype = new Error();
throw new CustomError('my message');

在遇到 throw 语句时,代码会立即停止执行。仅当有 try-catch 语句捕获到被抛出的值时,代码才会继续执行

更详细的解释为:当抛出异常时,javascript 解释器会立即停止当前正在执行的逻辑,并跳转到就近的异常处理程序。异常处理程序是用 try-catch 语句的 catch 从句编写的。如果抛出异常的代码块没有一条相关联的 catch 从句,解释器会检查更高层的闭合代码块,看它是否有相关联的异常处理程序。以此类推,直到找到一个异常处理程序为止。如果抛出异常的函数没有处理它的 try-catch 语句,异常将向上传播到调用该函数的代码。这样的话,异常就会沿着 javascript 方法的词法结构和调用栈向上传播。如果没有找到任何异常处理程序,javascript 将把异常当成程序错误来处理,并报告给用户

try catch 语句与捕获错误

ECMA-262 第 3 版引入了 try-catch 语句,作为 JavaScript 中处理异常的一种标准方式,用于捕获和处理错误

其中,try 从句定义了需要处理的异常所在的代码块。catch 从句跟随在 try 从句之后,当 try 块内某处发生了异常时,调用 catch 内的代码逻辑。catch 从句后跟随 finally 块,后者中放置清理代码,不管 try 块中是否产生异常,finally 块内的逻辑总是会执行。尽管 catch 和 finally 都是可选的,但 try 从句需要至少二者之一与之组成完整的语句

try/catch/finally 语句块都需要使用花括号括起来,这里的花括号是必需的,即使从句中只有一条语句也不能省略花括号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
//通常来讲,这里的代码会从头到尾而不会产生任何问题
//但有时会抛出一个异常,要么是由throw语句直接抛出,要么通过调用一个方法间接抛出
} catch(e) {
//当且仅当try语句块抛出了异常,才会执行这里的代码
//这里可以通过局部变量e来获得对Error对象或者抛出的其他值的引用
//这里的代码块可以基于某种原因处理这个异常,也可以忽略这个异常,还可以通过throw语句重新抛出异常
} finally {
//不管try语句是否抛出了异常,finally里的逻辑总是会执行,终止try语句块的方式有:
//1、正常终止,执行完语句块的最后一条语句
//2、通过break、continue或return语句终止
//3、抛出一个异常,异常被catch从句捕获
//4、抛出一个异常,异常未被捕获,继续向上传播
}

一般地,把所有可能会抛出错误的代码都放在 try 语句块中,而把那些用于错误处理的代码放在 catch 块中

如果 try 块中的任何代码发生了错误,就会立即退出代码执行过程,然后接着执行 catch 块。此时,catch 块会接收到一个错误信息的对象,这个对象中包含的实际信息会因浏览器而异,但共同的是有一个保存着错误消息的 message 属性

[注意]一定要给 error 对象起个名字,置空会报语法错误

1
2
3
4
5
6
7
8
9
10
11
try {
q;
} catch(error) {
alert(error.message); //q is not defined
}
//Uncaught SyntaxError: Unexpected token
try {
q;
} catch() {
alert(error.message);
}

catch 接受一个参数,表示 try 代码块抛出的值

1
2
3
4
5
6
7
8
9
10
function throwIt(exception) {
try {
throw exception;
} catch (e) {
console.log('Caught: '+ e);
}
}
throwIt(3); // Caught: 3
throwIt('hello'); // Caught: hello
throwIt(new Error('An error happened')); // Caught: Error: An error happened

catch 代码块捕获错误之后,程序不会中断,会按照正常流程继续执行下去

1
2
3
4
5
6
7
8
try {
throw "出错了";
} catch (e) {
console.log(111);
}
console.log(222);
// 111
// 222

为了捕捉不同类型的错误,catch 代码块之中可以加入判断语句

1
2
3
4
5
6
7
8
9
10
try {
foo.bar();
} catch (e) {
if (e instanceof EvalError) {
console.log(e.name + ": " + e.message);
} else if (e instanceof RangeError) {
console.log(e.name + ": " + e.message);
}
// ...
}

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

1
2
3
4
5
6
7
8
9
10
11
12
//由于没有catch语句块,所以错误没有捕获。执行finally代码块以后,程序就中断在错误抛出的地方
function cleansUp() {
try {
throw new Error('出错了……');
console.log('此行不会执行');
} finally {
console.log('完成清理工作');
}
}
cleansUp();
// 完成清理工作
// Error: 出错了……
1
2
3
4
5
6
7
8
9
10
function testFinnally() {
try {
return 2;
} catch(error) {
return 1;
} finally {
return 0;
}
}
testFinnally(); //0

[注意]return 语句的 count 的值,是在 finally 代码块运行之前,就获取完成了

1
2
3
4
5
6
7
8
9
10
var count = 0;
function countUp() {
try {
return count;
} finally {
count++;
}
}
countUp(); // 0
console.log(count); // 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function f() {
try {
console.log(0);
throw "bug";
} catch(e) {
console.log(1);
return true; // 这句原本会延迟到finally代码块结束再执行
console.log(2); // 不会运行
} finally {
console.log(3);
return false; // 这句会覆盖掉前面那句return
console.log(4); // 不会运行
}
console.log(5); // 不会运行
}
var result = f();
// 0
// 1
// 3
console.log(result); // false
【tips】块级作用域

try-catch 语句的一个常见用途是创建块级作用域,其中声明的变量仅仅在 catch 内部有效

ES6 引入了 let 关键字,为其声明的变量创建块级作用域。但是,在目前 ES3 和 ES5 的情况下,常常使用 try-catch 语句来实现类似的效果

由下面代码可知,e 仅存在于 catch 分句内部,当试图从别处引用它时会抛出错误

1
2
3
4
5
6
try {
throw new Error(); //抛出错误
} catch(e) {
console.log(e); //Error(…)
}
console.log(e); //Uncaught ReferenceError: e is not defined

常见错误

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

【类型转换错误】

类型转换错误发生在使用某个操作符,或者使用其他可能自动转换值的数据类型的语言结构时

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

未使用过的命名变量会自动被赋予 undefined 值。而 undefined 值可以被转换成布尔值 false,因此下面这个函数中的 if 语句实际上只适用于提供了第三个参数的情况。问题在于,并不是只有 undefined 才会被转换成 false,也不是只有字符串值才可以转换为 true。例如,假设第三个参数是数值 0,那么 if 语句的测试就会失败,而对数值 1 的测试则会通过

1
2
3
4
5
6
7
function concat(str1,str2,str3) {
var result = str1 + str2;
if(str3) { //绝对不要这样
result += str3;
}
return result;
}

在流控制语句中使用非布尔值,是极为常见的一个错误来源。为避免此类错误,就要做到在条件比较时切实传入布尔值。实际上,执行某种形式的比较就可以达到这个目的

1
2
3
4
5
6
7
function concat(str1,str2,str3) {
var result = str1 + str2;
if(typeof str3 == 'string') { //更合适
result += str3;
}
return result;
}
【数据类型错误】

javascript 是松散类型的,在使用变量和函数参数之前,不会对它们进行比较以确保它们的数据类型正确。为了保证不会发生数据类型错误,只能编写适当的数据类型检测代码。在将预料之外的值传递绘函数的情况下,最容易发生数据类型错误

1
2
3
4
5
6
7
//不安全的函数,任何非数组值都会导致错误
function reverseSort(values) {
if(values) {
values.sort();
values.reverse();
}
}

另一个常见的错误就是将参数与 null 值进行比较。与 null 进行比较只能确保相应的值不是 null 和 undefined。要确保传入的值有效,仅检测 null 值是不够的

1
2
3
4
5
6
7
//不安全的函数,任何非数组值都会导致错误
function reverseSort(values) {
if(values != null) {
values.sort();
values.reverse();
}
}

如果传入一个包含 sort()方法的对象(而不是数组)会通过检测,但调用 reverse()函数时可能会出错

1
2
3
4
5
6
7
//不安全的函数,任何非数组值都会导致错误
function reverseSort(values) {
if(typeof values.sort == 'function') {
values.sort();
values.reverse();
}
}

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

1
2
3
4
5
6
7
//安全,非数组值被忽略
function reverseSort(values) {
if(values instanceof Array) {
values.sort();
values.reverse();
}
}
【通信错误】

随着 Ajax 编程的兴起,Web 应用程序在其生命周期内动态加载信息或功能,已经成为一件司空见惯的事。不过,javascript 与服务器之间的任何一次通信,都有可能会产生错误

最常见的问题是在将数据发送给服务器之前,没有使用 encodeURIComponent()对数据进行编码

1
2
3
4
//错误
http://www.yourdomain.com/?redir=http://www.sometherdomain.com?a=b&c=d
//针对'redir='后面的所有字符串调用encodeURIComponent()就可以解决这个问题
http://www.yourdomain.com/?redir=http:%3A%2F%2Fwww.sometherdomain.com%3Fa%3Db%26c%3Dd

尊重原创,感谢原创分享

0%