似乎没有真正理解 try...catch...finally
写了那么久的 JavaScript,似乎真的没有很认真地去了解 try...catch...finally
的各种用法,真是惭愧了!Anyway,不懂就学。
一、错误与异常
错误,在程序中是很常见的。它可以是 JS 引擎在执行代码时内部抛出的,也可以是代码开发人员针对一些不合法的输入而主动抛出的,或者是网络断开连接导致的错误等等...
可能很多人会认为,「错误」和「异常」是同一回事,其实不然,一个错误对象只有在被抛出时才成为异常。
1.1 错误
在 JavaScript 中,错误通常是指 Error 实例对象或 Error 的派生类实例对象(比如 TypeError
、ReferenceError
、SyntaxError
等等)。创建 Error
实例对象很简单,如下:
const error = new Error('oops') // 等价于 Error('oops') const typeError = new TypeError('oops') // ...
虽然 Error
及其派生类是构造函数,但是当作函数调用也是允许的(即省略 new
关键字),同样会返回一个错误实例对象。
一个错误实例对象,包含以下属性和方法:
const errorInstance = { name: String, // 标准属性,所有浏览器均支持(默认值为构造方法名称) message: String, // 标准属性,所有浏览器均支持(默认值为空字符串,实例化时传入的第一个参数可修改其属性值) stack: String, // 非标准属性,但所有浏览器均支持(栈属性,可以追踪发生错误的具体信息) columnNumber: Number, // 非标准属性,仅 Firefox 浏览器支持(列号) lineNumber: Number, // 非标准属性,仅 Firefox 浏览器支持(行号) fileName: String, // 非标准属性,仅 Firefox 浏览器支持(文件路径) column: Number, // 非标准属性,仅 Safari 浏览器支持(同上述三个属性) line: Number, // 非标准属性,仅 Safari 浏览器支持 sourceURL: String, // 非标准属性,仅 Safari 浏览器支持 toString: Function, // 标准方法(其返回值是 name 和 message 属性的字符串表示) }
我们写个最简单的示例,打印看下各大浏览器的情况:
try { throw new TypeError('oops') } catch (e) { console.log(e.toString()) console.dir(e) }
插个题外话:
不知道有人没有对此有疑惑的,为什么 console.log()
一个 Error
对象,打印出来的是字符串,而不是一个对象呢?
const err = new Error('wrong') console.log(err) // "Error: wrong" console.log(typeof err) // "object"
那么,如果想打印出 Error
对象,使用 console.dir()
即可。
前面 console.log()
打印结果为字符串的原因其实很简单,那就是 console.log()
内部「偷偷地」做了一件事,当传入的实参为 Error
对象(或其派生类错误对象),它会先调用 Error
对象的 Error.prototype.toString()
方法,然后将其结果输出到控制台,所以我们看到的打印结果为字符串。
其实现如下:
// polyfill Error.prototype.toString = function () { 'use strict' var obj = Object(this) if (obj !== this) throw new TypeError() var name = this.name name = name === undefined ? 'Error' : String(name) var msg = this.message msg = msg === undefined ? '' : String(msg) if (name === '') return msg if (msg === '') return name return name + ': ' + msg }
细心的同学会发现,在不同浏览器下,其打印结果可能会不相同(但不重要)。原因也非常简单,console
并不是 ECMAScript 标准,而是浏览器 BOM 对象提供的一个接口,其标准由 WHATWG 机构制定,虽然标准是统一的,但实现的是各浏览器厂商大爷们,它们有可能不会严格遵守规范去实现,因而产生差异化。比如,此前写过一篇文章是关于不同宿主环境下 async/await 和 promise 执行顺序的差异,就因为 JS 引擎实现差异导致的。
1.2 异常
前面提到,当错误被抛出时就会成为异常。
假设我们编写的代码存在语法错误,那么在编译阶段的语法分析过程就会被聪明的 JS 引擎发现,因而在编译阶段便会抛出 SyntaxError。
假设我们代码没有语法错误,但错误地引用了一个不存在的变量,那么在执行阶段的执行上下文过程(代码执行之前的一个过程),聪明的 JS 引擎发现在其作用域链上找不到该变量,那么就会抛出 ReferenceError。
假设即不存在语法错误,也没有引用错误,但我们对一个变量做了“不合法”的操作,比如 null.name
、'str'.push('ing')
,那么 JS 引擎就会抛出 TypeError。
还有很多很多,就不举例了。
前面都是 JS 引擎主动抛出的错误,那么,我们开发者则可通过 throw
关键字来抛出错误,语法很简单:
// throw expression throw 123 throw 'abc' throw { name: 'Frankie' } // ...
请注意,在 JavaScript 中 throw
关键字和 return
、break
、continue
等关键字一样,会受到 ASI(Automatic Semicolon Insertion)规则的影响,它不能在 throw
与 expression
之间插入任意换行符,否则可能得不到预期结果。
语法很简单,但通常项目中「不建议」直接抛出一个字面量,而是抛出 Error
对象或其派生类对象,应该这样:
throw new Error('oops') throw new TypeError('arguments must be a number.') // ...
原因是 Error
对象会记录引发此错误的文件的路径、行号、列号等信息,这应该是排除错误最有效的信息。在 ESLint 中的 no-throw-literal 规则,正是用来约束上述直接抛出字面量的写法的。
除了 throw
关键字之外,ES6 中强大的 Generator 函数也提供了一个可抛出异常的方法:Generator.prototype.throw()
。它可以在函数体外抛出异常,然后在函数体内捕获异常。
function* genFn() { try { yield 1 } catch (e) { console.log('inner -->', e) } } try { const gen = genFn() gen.next() gen.throw(new Error('oops')) } catch (e) { console.log('outer -->', e) }
打印结果是 inner --> Error: oops
。如果生成器函数体内没有 try...catch
去捕获异常,那么它所抛出的异常可以被外部的 try...catch
语句捕获到。
当生成器「未开始执行之前」或者「执行结束之后」,调用生成器的 throw()
方法。它的异常只会被生成器函数外部的 try...catch
捕获到。若外部没有 try...catch
语句,则会报错且代码就会停止执行。详看
需要注意的是,生成器函数虽然是一个很强大的异步编程的解决方案,但它本身是同步的,而且执行生成器函数并不会立刻执行函数体的逻辑,它需要主动调用生成器实例对象的
next()
、return()
、throw()
方法去执行函数体内的代码。当然,你也可以通过for...of
、解构等语法去遍历它,因为生成器本身就是一个可迭代对象。
二、try...catch
对于可能存在异常的代码,我们通常会使用 try...catch...finally
去处理一些可预见或不可预见的错误。语法有以下三种形式:
try...catch
try...finally
try...catch...finally
且必须至少存在一个 catch
块或 finally
块。
try { throw new Error('oops') } catch (e) { // some statements... }
以上这些语法,写过 JavaScript 相信都懂。
曾经 Firefox 59 及以下版本的浏览器,有一种 Conditional catch-blocks 的「条件 catch
子句」的语法(请注意,其他浏览器并不支持该语法,即便是远古神器 IE5,因此知道有这回事就行了)。它的语法如下:
try { // may throw three types of exceptions willThrowError() } catch (e if e instanceof TypeError) { // statements to handle TypeError exceptions } catch (e if e instanceof RangeError) { // statements to handle RangeError exceptions } catch (e if e instanceof EvalError) { // statements to handle EvalError exceptions } catch (e) { // statements to handle any unspecified exceptions }
那么符合 ECMAScript 标准的「条件 catch
子句」应该这样写:
try { // may throw three types of exceptions willThrowError() } catch (e) { if (e instanceof TypeError) { // statements to handle TypeError exceptions } else if (e instanceof RangeError) { // statements to handle RangeError exceptions } else if (e instanceof EvalError) { // statements to handle EvalError exceptions } else { // statements to handle any unspecified exceptions } }
请注意,try...catch
只能以「同步」的形式处理异常,因此对于 XHR、Fetch API、Promise 等异步处理是无法捕获其错误的,究其原因就是 Event Loop 嘛。当然实际中可能结合 async/await
来控制会更多一些。
2.1 catch 子句
我们知道,若 try
块中抛出异常时,会立即转至 catch
子句执行。若 try
块中没有异常抛出,会跳过 catch
子句。
try { // try statements } catch (exception_var) { // catch statements }
其中 exception_var
表示异常标识符(如 catch(e)
中的 e
),它是「可选」的,因此可以这样编写 try { ... } catch { ... }
。通过该标识符我们可以获取关于被抛出异常的信息。
**请注意,该标识符的「作用域」仅在
catch
块中有效。**当进入catch
子句时,它被创建,当catch
子句执行完毕,此标识符将不可再用。也可以理解为(在 ES6 以前)异常标识符是 JavaScript 中含有“块级作用域”的变量。
2.2 finally 子句
而 finally
子句在 try
块和 catch
块之后执行,但在下一个 try
声明之前执行。无论是否异常抛出,finally
子句总是会执行。
如果从
finally
块中返回一个值,那么这个值将成为整个try...catch...finally
的返回值,无论是否有return
语句在try
和catch
块中(即使catch
块中抛出了异常)。
对于这个我表示很无语,可能整个前端圈子就我还不知道吧,原来 finally
还能 return
一个值,在做项目的过程中,确实没写过和见过在 finally
中 return
某个值的,让您见笑了,实在惭愧。
但请注意,若要在 try...catch...finally
中使用 return
,它只能在函数中运行,否则是不允许的,会抛出语法错误。
try { doSomething() } catch (e) { console.warn(e) throw e } finally { return 'completed' // SyntaxError: Illegal return statement }
2.3 执行顺序
在平常的项目中,一般的 try...catch
写法是在 try
块中 return
,catch
块则作相应的异常处理,少数情况也会在 catch
块中 return
。因此,大家对这种常规写法的执行顺序应该没什么问题。
先来个谁都会的示例:
function foo() { try { console.log('try statement') throw new Error('oops') } catch (e) { console.log('catch statement') return 'fail' } } foo() // 以上,先后打印 "try statement"、"catch statement",foo 函数返回一个 "fail" 值
接着再看,它打印什么,函数又返回什么呢?
function foo() { try { console.log('try statement') throw new Error('oops') } catch (e) { console.log('catch statement') return 'fail' } finally { console.log('finally statement') return 'complete' } } foo() // 先后打印:"try statement"、"catch statement"、"finally statement" // foo 函数返回值是 "complete"
前面提到,如果 finally
块中含有 return
语句,那么它的 return
值将作为当前函数的返回值,因此 foo()
结果为 "complete"
。
然后我们再稍微改动一下,在 try
块中 return
一个值,看下结果又有什么不同?
function foo() { try { console.log('try statement') return 'success' } catch (e) { console.log('catch statement') return 'fail' } finally { console.log('finally statement') return 'complete' } } foo() // 先后打印:"try statement"、"finally statement" // foo 函数返回值是 "complete"
由于 try
块中没有抛出异常,因此 catch
块会被跳过,不执行,但是 finally
块还是会执行的,而且它里面返回了 "complete"
,因此这个值也就作为 foo
函数的返回值了。
因此,我们大致可以得出一个结论,
finally
块的代码总会在return
之前执行,不管return
是存在于try
、catch
还是finally
块中。
但是,这就完了吗?
还没有,我们再看一个示例,看看里面这个 bar()
函数是惰性求值?还是怎样?
function foo() { try { console.log('try statement') throw new Error('oops') } catch (e) { console.log('catch statement') return bar() } finally { console.log('finally statement') return 'complete' } } function bar() { console.log('bar statement') return 'something' } foo()
以上示例,打印顺序和结果是什么呢?
// 打印顺序,依次是: "try statement" "catch statement" "bar statement" "finally statement" // 结果是 "complete"
假设 catch
块中的 return bar()
换成 throw bar()
呢,结果又有什么变化呢?如果换成这个你就犹豫了,说明你理解得不够深刻,因此这里我不给出答案,你自己去试试,效果更佳!
综上所述,finally
块的执行时机如下:
在所有
try
块和catch
块(如果有,且触发进入的话)执行完之后,即便此时try
块或catch
块中存在return
或throw
语句,它们将会被 Hold 住先不返回或抛出异常,继续执行finally
块中的代码:
- 如果
finally
中存在return
语句,其返回值将作为整个函数的返回值(前面try
块或catch
中的return
或throw
都会被忽略,可以理解为没有了return
或throw
关键字一样)。- 如果
finally
中存在throw
语句,前面try
块或catch
中的return
或throw
同样会被忽略,最后整个函数将会抛出finally
块中的异常。
2.4 嵌套使用
它是可以嵌套使用的,当内部的 try...catch...finally
中抛出异常,它会被离它最近的 catch
块捕获到。
function foo() { try { try { return 'success' } finally { throw new Error('inner oops') // 它将会被外层的 catch 块所捕获到 } } catch (e) { console.log(e) // Error: inner oops } } foo()
注意,本节内容所述都是同步代码,而不存在任何异步代码。到此,已彻底弄懂 try...catch...finally
语句了,再也不慌了!
三、异常有哪些?
在 Web 中,主要有以下几种异常类型:
- JavaScript 异常
- DOM 和 BOM 异常
- 网络资源加载异常
- Script Error
- 网页异常
3.1 JavaScript 异常
try...catch
可以捕获同步任务导致的异常,也可以捕获 async/await
中的异常。
Promise
中抛出的异常,则可通过 Promise.prototype.catch()
或 Promise.prototype.then(onResolved, onRejected)
捕获。
3.2 DOM Exception
在调用 DOM API 时发生的,都属于 DOM Exception。比如:
<!DOCTYPE html> <html> <body> <video controls src="https://dl.ifanr.cn/hydrogen/landing-page/ifanr-products-introduce-v1.1.mp4"></video> <script> window.onload = function () { const video = document.querySelector('#video') video.play() // Uncaught (in promise) DOMException: play() failed because the user didn't interact with the document first. } </script> </body> </html>
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论