这次我们要一起重温JS有关异常和错误处理的内容。“人们都希望生活在一个不会出错的世界里,但这只是一种奢望。即便是最小的应用,都难免因为一些无法事先预料的情况而产生错误。所以,编写能够稳定运行的高质量软件的第一步,就是要承认软件会有错误。第二步就是提前识别出那些错误,并以恰当的方式处理它们”—《JavaScript学习指南》。
注意:《JavaScript学习指南》是一本学习JavaScript的好书,简洁而又全面,本文的示例代码参考了这本书。
异常处理是一种以可控的方式处理错误的机制。之所以叫做异常处理,是因为其本意是处理异常情况。
当代码中有错误发生时,一个好的处理机制可以帮助我们理解错误发生的原因,并且使我们能以一种较为优雅的方式来纠正错误。—《JavaScript面向对象编程指南(第二版)》
1.Error对象的介绍
在JavaScript中,将会使用try、catch及finally语句组合来处理错误。当程序中出现错误时,就会抛出一个Error对象。
Error对象是JS的内建对象,可以用它来处理任意类型的错误。Error对象可能由以下几个内建的构造器中的一个产生而成:
EvalError、RangeError、ReferenceError、SyntaxError、TypeError和URIError,所有的这些对象都继承自Error对象。
如果在浏览器控制台中调用一个不存在函数,那么就会报错,这个错误表示是引用错误ReferenceError。
错误的显示方式根据浏览器和宿主环境的不同会有所差异。
下面我们来看一下Error对象的定义和使用:
function validateEmail(email) {
return email.match(/@/) ? email : new Error('此邮箱非法')
}
const testEmail = 'newname@js.com'
const res = validateEmail(testEmail)
if(res instanceof Error) {
console.error(`Error: ${res.message}`)
} else {
console.log(res)
}
const testEmail2 = 'newName.js.study'
const res2 = validateEmail(testEmail2)
if(res2 instanceof Error) {
console.error(`Error: ${res2.message}`)
} else {
console.log(res2)
}
代码运行结果如下图所示:
说明一下:我们定义了一个邮箱验证函数对邮箱验证,如果邮箱含有@符号则认为是一个合法的邮箱,并返回合法邮箱;否则认为是一个不合法的邮箱,并返回一个错误。使用了两个邮箱对函数进行了测试,分别返回邮箱本身和一个错误对象。
虽然这样使用Error实例是合理合法的,但在实际应用中都是在异常处理中,我们来详细看一下:
2.使用try catch 处理异常
try…catch的思想是:首先尝试做一些事情,如果出错了则捕获这些异常。
例如,如果给validateEmail传一个非字符串的值,那么在调用match方法的时候就会报错,此时就需要捕获异常的逻辑。看一下如下代码:
function validateEmail(email) {
return email.match(/@/) ? email : new Error('此邮箱非法')
}
const testEmail = null
try{
const res = validateEmail(testEmail)
if(res instanceof Error) {
console.error(`Error: ${res.message}`)
} else {
console.log(res)
}
} catch(err) {
console.error(`Error: ${err.message}`)
}
运行结果:
在这段代码中传给validateEmail的值testEmail是一个空值,调用match方法的时候就会报错,但是我们在catch语句中对错误进行了捕获,所以程序就不会崩溃,而是将错误日志打印出来。
注意:如果try语句块中没有错误,那么catch语句块中的语句不会执行。
在上面的例子中,使用了try…catch捕获了JS产生的异常,如果不想捕获,也可以向上一层‘抛出异常’,我们来看一下:
3.使用throw抛出异常
如下代码所示:
const error = new Error('非法的邮件')
function validateEmail(email) {
return email.match(/@/) ? email : new Error('此邮箱非法')
}
const testEmail = null
function testThrow(){
const res = validateEmail(testEmail)
if(res instanceof Error) {
throw Error('我是被抛出来的错误信息')
} else {
console.log(res)
}
}
try{
testThrow()
} catch(err) {
console.error(`Error: ${err.message}`)
}
我们期望的是 输出 ‘Error:我是被抛出来的错误信息’,但是为什么不是呢?这是因为在调用validateEmail的时候已经报错了,所以程序不会执行后面的if判断逻辑了。我们可以改一下代码:
const error = new Error('非法的邮件')
function validateEmail(email) {
return email.match(/@/) ? email : new Error('此邮箱非法')
}
const testEmail = '@'
function testThrow(){
const res = validateEmail(testEmail)
if(res.length === 1) {
throw Error('我是被抛出来的错误信息')
} else {
console.log(res)
}
}
try{
testThrow()
} catch(err) {
console.error(`Error: ${err.message}`)
}
程序运行结果如下图所示:
在上面的代码中,我们把测试邮箱改为只含有一个‘@’的字符串,在if判断逻辑中增加判断,如果邮箱的长度是1,则向外层抛出一个错误。
注意:调用throw的时候,代码会立即停止执行。
很多的时候,try块中含有对一些资源的引用,比如http链接或者文件之类的资源。不管有没有发生错误,总是要释放这些资源,防止应用程序永远占用这些资源。
由于try语句块中处处都可能发生异常,所以在try块中释放资源并不安全。此外,由于catch块中的代码只有在发生错误的时候才会执行,所以在catch块中释放资源也不合适;这时,finally就派上用场了。我们来通过示例代码理解一下
4.try…catch…finally
try{
// 假设这块有引用资源
console.log('try ...')
throw Error('bug')
} catch(err) {
console.log('bug')
} finally {
console.log('always run')
}
运行结果如下:
下面我们把throw Error的代码去掉,再看一下:
try{
// 假设这块有引用资源
console.log('try ...')
} catch(err) {
console.log('bug')
} finally {
console.log('always run')
}
运行结果:
通过以上两段代码的运行结果我们看到了,不管有没有异常finally语句中的代码都会被执行。
在实际的应用程序中会调用很多函数,而这些被调用的函数又会调用其他的函数。例如函数 a 调用了函数b,函数b又调用了函数c。当函数c正在执行的时候,函数b没有完成,函数a也没有完成。我们把这种没有完成的嵌套函数调用称之为调用栈。下面我们看一下调用栈中的异常处理:
5.异常处理和调用栈
如果在函数c中出错,那么函数b也会出错,函数a也会出错。换言之,错误会沿着调用栈传递,直到错误被捕获。
错误可以在调用栈的任意级别被捕获,如果他们没有被捕获,JavaScript解释器就会强制终止程序。这种没有被捕获的异常会导致程序崩溃。当异常被捕获后,调用栈会提供一些用于诊断错误的有用信息,可以帮助我们诊断错误的原因,定位错误的出处。我们来看一个例子:
function a() {
console.log('a调用了b');
b();
console.log('a结束');
}
function b() {
console.log('b调用了c');
c();
console.log('b结束');
}
function c() {
console.log('c抛出错误');
throw new Error('C 出错了');
console.log('c结束') //VSCode 提示:Unreachable code detected
}
function d(){
console.log('d调用了c');
c();
console.log('d结束');
}
try {
a();
} catch(err) {
console.log(err.stack)
}
try {
d();
} catch(err) {
console.log(err.stack)
}
程序运行的效果如图所示:
出现at的地方就是调用栈的轨迹,会从最深层的调用开始,直到没有函数调用为止。这里一共有两个调用栈轨迹:一个表明在b中调用了c,在a中又调用了b;另一个显示在d中调用了c。
以上就是我们今天重温的主要内容,要知道一旦出现异常,就一定要捕获它,除非你想让程序崩溃或者对之视而不见。来一张图总结我们今天学习的内容: