个人学习笔记,对基础知识的整理和巩固。
JavaScript提供了捕获和处理异常的功能。
类似以下操作均会触发错误:
// ①
console.log(不存在的变量);
// ②
console.log(window.不存在的属性.不存在的属性);
当发生错误时,代码将停止运行,同时抛出一个错误对象,它是Error类的一个实例。
我们会在控制台看到它:
![e44e02e8067dfdae333875cdf27850f4.png](https://img-blog.csdnimg.cn/img_convert/e44e02e8067dfdae333875cdf27850f4.png)
但更多情况下,我们会看到更具体的错误,它们总共有六类,包括EvalError、InternalError等等(更多:Error类型),它们都是Error的子类,分别表示不同的错误原因。
除此之外,我们自己也可以主动触发错误:
▉ throw[1]
首先,我们需要创建一个错误对象,只需要把错误信息传递给Error(或者具体的EvalError等)就可以了:
let error = new Error("发生了一个错误");
现在我们得到了一个错误对象,接下来只要使用throw语句把它抛出:
throw error;
控制台立刻输出了这个错误:
![3b3cc9e609cfb3a0fea4b1c9b2a4948c.png](https://img-blog.csdnimg.cn/img_convert/3b3cc9e609cfb3a0fea4b1c9b2a4948c.png)
这样,我们就主动触发了一个自己创建的错误,执行到这里的时候,代码的运行将中断。
事实上,throw可以抛出的内容不仅仅是Error对象,数字、字符串甚至是对象、函数等等都可以。
throw true?21:20; // 表达式 抛出数字21
throw "出错了"; // 字符串
throw function(){console.log("出错了")}; // 函数
throw {status:"error",msg:"出错了"}; // 对象
控制台都会如实输出:
![7bdd8c2a86a844f0416ece7d79480453.png](https://img-blog.csdnimg.cn/img_convert/7bdd8c2a86a844f0416ece7d79480453.png)
不过大多数时候,只是中断执行并且抛出和输出错误并不是我们的目的,或者甚至说是我们想要避免的。
我们希望代码即使出现错误的时候也能继续运行而不至于影响大局,我们希望能够对可预见的错误做出相应的处理。
所以我们需要异常(错误)处理:
▉ try、catch[2]
try和catch就是用来处理错误的工具,在“try块”里我们运行可能会出错的代码,然后当出错的时候用catch来捕获并作出相应处理。
try{
// 一段可能会出错的代码 出错时抛出一个错误
}catch(e){
// 捕获
console.log(e);
}
// 后面的代码将接着执行
这里的错误可能是执行出错(被动抛出),也可能是我们用throw主动抛出错误,前者会抛出一个Error(也可能是子类如EvalError)的实例对象,后者可能是throw出的任何值。
任何错误的抛出,try块里的代码都会立刻中断,同时将它传递到catch块,并接着运行catch块代码,之后,如果catch块内没有发生错误的话,将继续往下执行。
catch后面的括号用来接收捕获到的错误,可用作判断处理。
被捕获(catch)的错误不会输出到控制台(我们很容易可以注意到前面控制台输出的错误都以“Uncaught”开头)。
如果try块内没有抛出错误,catch块将不会执行。
▉ 异常处理的嵌套
既然try块内可以像其他地方一样执行代码,那么当然也可以在try块(或者catch块)里再写一个try和catch的组合:
try {
try {
throw 1;
} catch (e) {
throw e; // 捕获了错误 但又重新抛出
}
} catch (e) {
console.log(e); // 输出1
}
在这个示例里,次级try块内抛出1,被其相应的catch捕获,然而这个catch块并未对这个错误做任何处理,而是把它又一次抛了出来,因为它运行在上级的try块里,所以这个错误又再一次被上级try对应的catch捕获,从而输出1。
所以我们知道:
- try或catch里可以嵌套try...catch
- catch块内仍然可能发生错误
- 错误会逐级向上抛出,直到被捕捉,到最顶层而未被捕捉的错误将被输出到控制台
- 错误总是被包裹它的最内层的try所对应的catch捕获
▉ finally
除了catch之外,还可以加上finally语句块。
try{
// 一段可能会出错的代码 出错时抛出一个错误
}catch(e){
// 捕获
console.log(e);
}finally{
// 一些善后代码
}
不管try块内有没有抛出错误,finally语句块总会被执行。
如果try块内发生错误,finally块将在catch块之后被执行;如果没有发生错误,将跳过catch块,直接运行finally块。
我们其实还可以不写catch块(try块后必须至少跟一个catch或finally,不能只写try):
try {
try {
throw 1;
} finally {
// 一些善后代码
console.log("finally"); // 输出:finally
}
// 不会向下执行
console.log("不会输出");
} catch (e) {
console.log(e); // 输出1
}
假如不写catch块,try块内抛出错误之后,会先执行finally块,之后,因为错误并未被捕捉,所以将继续向外层抛出(抛出错误即意味着原位置运行中断)。
如果finally块内发生错误会怎样呢?
■ 错误的覆盖
不管try块和catch块内有没有抛出错误,如果finally块内抛出错误,外层代码只能接收到finally块内抛出的错误。
try {
try {
throw 1; // 位置①
} catch (err) {
throw 2; // 位置②
} finally {
throw 3; // 位置③
}
} catch (err) {
console.log(err); // 输出3
}
在上面这个例子里,内层try块内(位置①)抛出的错误1被捕获,接着其catch块内(位置②)又抛出了一个错误2,之后将立刻运行finally块,finally块内(位置③)竟然又抛出了一个错误3,这个错误替代了catch块内抛出的错误,所以这一组try{}catch{}finally{}最终抛出错误3。它们处在外层try块内,所以这个错误被外层try对应的catch捕捉,并被最终输出:3。
另外一个特殊情形是当运行在函数里的时候,如果finally返回了一个值,try或catch块内抛出的错误将同样被忽略:
function f() {
try {
throw 1; // 抛出错误
} catch (e) {
throw 2; // 捕获了错误 抛出了一个新错误
} finally {
return 3; // 返回了一个值
}
}
console.log(f()); // 输出3
上面这个例子里,catch块内抛出了一个错误,在一般情况下,如果finally块内没有返回值,错误将在执行完finally块后被层层抛出,直到被输出到控制台(没有被捕捉),而函数的运行将中断,所以也不会输出任何值。但是finally块内返回了一个值,所以catch块内的错误被忽略了,返回值3被正常输出,控制台看不到任何错误信息。
▉ 总结
- 任何错误的抛出都会导致程序在原位置的运行立刻停止。
- try、catch、finally块内都可以再嵌套try、catch、finally的组合。
- 当错误被抛出时,程序会在该位置立刻停止运行,然后层层上溯,直到发现自己被包裹在一个try块里,而把该错误传递到其相应的catch块,然后在此接着运行,如果其没有对应的catch块,将在运行完finally块后继续上溯,直到被捕捉或到达代码顶端而被输出到控制台。
- finally块内的返回值或抛出的错误将覆盖任何原有的错误。
▉ 应用
我们已经有了在JavaScript中处理异常的办法,那么,应该怎样使用呢?
■ 首先想到的用法:流程控制(应当禁止)
说实话,当我看到throw与catch默契的关系的时候,第一时间不是想到用来处理错误,而是用它来进行流程控制,“多么完美啊!”,我只需要这样写:
try {
// 一些代码
if (情况1) throw 1;
// 一些代码
if (情况2) throw 2;
// 一些代码
throw 3;
} catch (code) {
if (code instanceof Error) {
throw (code);
}
switch (code) {
case 1:
case 2:
case 3:
}
}
好像还是很美的,我甚至还考虑到假如try块真的出现错误:只要把它抛出。
可事实是,即使有这种需求,就算使用匿名函数也并不比它麻烦多少,甚至或许还更简单:
switch ((function() {
// 一些代码
if (情况1) return 1;
// 一些代码
if (情况2) return 2;
// 一些代码
throw 3;
}
)()) {
case 1:
case 2:
case 3:
}
尤其它不会打乱原有的错误处理逻辑。
■ 最暴力直接的用法(不推荐)
看来try...catch最好还是用来处理错误,但是显然,它不是用来在开发阶段处理bug的工具,因为我们有更好的debugger和开发者工具。
事实上,回想我不长的代码生涯,我都是这样用的:
有些时候,我们想运行一段代码,但是我们知道这段代码很可能会出错,而我又不希望它影响接下来的步骤,也就是说,我想运行一段代码,即使失败了也没事儿。如此,这样写显然是最简单的:
try{
// 一段无关紧要的代码
}catch(e){}
也许你会发现这样写也能运行:
try{
// 一段无关紧要的代码
}catch{}
能省则省嘛,但是它在IE上会报错,可能因为它并不符合标准,所以尽量还是写上吧。另外,参数是不能省略的,即使是在chrome上。
这样写也不行:
try{
// 一段无关紧要的代码
}finally{}
因为没有catch,错误没有捕捉,仍旧会往上抛。
用这种方法来运行“一段无关紧要的代码”时,应该确定该内容真的无关紧要,以至于它发生任何错误都可以忽视它。
所以,也许我们应该再保险一点,虽然它无关紧要,但还是把这个错误记录下来:
try{
// 一段无关紧要的代码
}catch(e){console.warn("也许无关紧要的",e)}
也可以用console.error或者console.log,更多可参阅 Console - MDN 。
即便如此,这样做似乎仍然显得不那么好,我们也许应该对错误做出更精确的判断和处理。
■ 基本用法的思考
我已经知道“错误处理”就应当用来处理错误,但是,错误不应该是在开发阶段就处理掉的吗?我们应该考虑到可能遇到的各种情形,尽量不出错误。
可现实是,各种环境等因素总是在不停地发生变化,而我们的代码不是总能预知,我们确实有暂时容忍错误存在的需求,或者有时候,容忍错误存在在一定程度上是简化我们代码的一种方法(应该适度),再或者说,即使部分功能出现错误,但是其他功能还是得继续运行。
总而言之,错误处理是我们把错误考虑进程序的一种方法。
错误具体应该怎样处理呢?
我们可以首先把错误分为两种:
- 已预知的错误(已知错误)
- 未预料的错误(未知错误)
然后可以按照意义分类:
- 程序可以处理的
- 程序不能处理的
这两种分类不完全重合,但存在相当的对应关系。
而针对错误,抛出与不抛出的区别在于:
- 抛出错误表示程序必须中断
- 不抛出错误表示程序还可以运行
具体应该采取什么样的做法要视情况而定,比如程序的重要程序、精密程度,程序之间的相关性,当然,还有具体的逻辑关系。
大多数时候,我觉得应该秉承这样的原则:
- 抛出不能处理的错误。
- 通知未知的错误(包括抛出、输出、记录等)。
- 程序应当中断时就中断。
基于这样的原则,我们可以根据实际情况选择抛出部分或者不抛出。
下面是一些示例(非范例或实例):
【例①】
try{
// 一段代码
}catch(error){
if(error instanceof ReferenceError){
// 预料到的错误 做出一些处理
// 尽管这种错误类型是我们预料到的 但是仍不排除有其他可能同样触发了这个错误
// 或者尽管这种错误预料到了 但仍需要告知
console.warn("Expected",error);
}else{
// 未预料的 抛出
throw error;
}
}
// 如果try块内没有发生错误,或者发生的错误是预料到的,将继续往下运行
// 否则,如果catch块抛出错误,将不会往下运行
// 一些代码;
【例②】
// 此例仅提供意会的示例,现实不一定应该这样写
// 似乎前端的错误处理必要程度还是比较低的
function weakButDispensable(){} // 容易发生错误但可有可无的
function essential(){} // 必要的
function important(){} // 重要的
try{
weakButDispensable();
}catch(e){
console.warn("**功能一如既往的发生了错误",e);
}
essential(); // 未在try块内 所以如果发生错误 程序将直接从此中断
try{
important();
}catch(e){
if(e instanceof somekindofError){
// 如果发生这种错误 表示问题可以处理 不耽误后续执行
console.error("**功能出错!",e); // 控制台输出
ui.notice("**功能出了一些问题,请稍后尝试。"); // 告知用户
}else{
// 否则 不能处理 将抛出错误
// 执行将中断 不会执行[后续代码]
throw e;
}
}
// 一些[后续代码]
console.log("end");
■ 创建自己的错误
一般情况下,前端代码复杂度应该不会很高,我觉得没有必要抛出自定义的错误或其他值,而用流程控制和函数返回值等等代替,因为感觉会在某种程度上增大错误处理的复杂度。但是现在JS也能写后端了,而且即使是前端,可能工程性较强或者对错误处理有特殊需求的时候也需要用到。
这个时候我们可以通过继承Error来创建自己的错误:
class MyError extends Error {
constructor(...args) {
super(...args);
// name属性对Error对象比较重要,默认为“Error”,此处会自动修改为相应的类名
this.name = this.constructor.name;
}
}
throw new MyError("some message");
Error对象有三个比较重要的属性:
- name 表示错误的类型,如内置错误类型"TypeError",或者我们自定义的"MyError"。
- message 表示错误信息,如“Cannot read property '***' of undefined”。
- stack 是错误的追踪堆栈信息,创建Error对象的时候会自动创建。
可以多重继承,对自定义的错误进行分类:
// 以下 我尝试写了一个自定义错误的继承机制
// 注释没有写的很详细
// 想写一个规范的注释,看注释规范啥的也没看太明白,随便写了一下
/* 我自己的错误类
* 继承自Error 此后所有自定义错误都继承MyError
* @constructor
* @param {string|object} 详细信息 可以只写字符串 表示Error的message,可以是一个对象 用来包含各种相关信息(如message)
**/
class MyError extends Error {
constructor(arg) {
super();
// 自动设置name属性
this.name = this.constructor.name;
// 写入详细信息 this.details
this.details = {
...{
message: ""
},
...((arg instanceof Object) ? arg : ({
message: (arg!==undefined)?String(arg):""
}))
}
// 设置 message 属性 (包含入关于相应错误类别的特定信息 errorTypeMessage)
let _t = [];
if (this.errorTypeMessage)
_t.push(this.errorTypeMessage);
if (this.details.message)
_t.push(this.details.message);
this.message = _t.join("n");
}
}
// 继承MyError
class ValidationError extends MyError {
}
// 继承ValidationError
class PropertyRequiredError extends ValidationError {
get errorTypeMessage() {
return `property required: ${this.details.requiredPropertyName || ""}`;
}
}
throw new PropertyRequiredError({
message: "需要用户名啊啊啊啊",
requiredPropertyName: "username"
});
throw new ValidataionError("验证错误");
以上按照自己的理解和思考,看了一些别人写的,大概写了这么个东西出来,以后学到更多东西肯定还会对很多东西再改动,可能会再回来改。
■ 异步编程下的错误处理
try{}catch{}只能捕获同步编程下的错误,对于异步编程时可能比较麻烦。
倒是看了一些NodeJS错误处理的文章,但是这一块等以后补吧,等再巩固了Promise异步编程啥的知识,了解了一些后端JS的编程之后。
■ 更多
可能忘了还有一些其他东西没写,但是好累啊。。。
还是得换种写法,感觉自己像唠叨的老太太,同一件事情反复说,还是得精简一点,写成这样没有意义,浪费时间,给自己看就够了,不能老觉得是给别人看的。
下次的宗旨:
- 都覆盖到
- 道理清晰,看得懂
- 文字精炼,一目了然
- 写得快,学习占大头,整理占小头
另外感觉就算写这个也还是会忘,可能记性不太好。。。弄完之后还是得尽快进入到实战,再回头翻,再熟练。
参考
- ^throw - MDN https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/throw
- ^try...catch - MDN https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/try...catch