寫了那么久的 JavaScript满力,似乎真的沒有很認(rèn)真地去了解 try...catch...finally
的各種用法,真是慚愧了轻纪!Anyway油额,不懂就學(xué)...
一、錯(cuò)誤與異常
錯(cuò)誤刻帚,在程序中是很常見的潦嘶。它可以是 JS 引擎在執(zhí)行代碼時(shí)內(nèi)部拋出的,也可以是代碼開發(fā)人員針對一些不合法的輸入而主動(dòng)拋出的崇众,或者是網(wǎng)絡(luò)斷開連接導(dǎo)致的錯(cuò)誤等等...
可能很多人會(huì)認(rèn)為掂僵,「錯(cuò)誤」和「異常」是同一回事顷歌,其實(shí)不然蚜退,一個(gè)錯(cuò)誤對象只有在被拋出時(shí)才成為異常覆致。
1.1 錯(cuò)誤
在 JavaScript 中,錯(cuò)誤通常是指 Error 實(shí)例對象或 Error 的派生類實(shí)例對象(比如 TypeError
芥备、ReferenceError
溪窒、SyntaxError
等等)坤塞。創(chuàng)建 Error
實(shí)例對象很簡單冯勉,如下:
const error = new Error('oops') // 等價(jià)于 Error('oops')
const typeError = new TypeError('oops')
// ...
雖然 Error
及其派生類是構(gòu)造函數(shù),但是當(dāng)作函數(shù)調(diào)用也是允許的(即省略 new
關(guān)鍵字)摹芙,同樣會(huì)返回一個(gè)錯(cuò)誤實(shí)例對象灼狰。
一個(gè)錯(cuò)誤實(shí)例對象,包含以下屬性和方法:
const errorInstance = {
name: String, // 標(biāo)準(zhǔn)屬性浮禾,所有瀏覽器均支持(默認(rèn)值為構(gòu)造方法名稱)
message: String, // 標(biāo)準(zhǔn)屬性交胚,所有瀏覽器均支持(默認(rèn)值為空字符串,實(shí)例化時(shí)傳入的第一個(gè)參數(shù)可修改其屬性值)
stack: String, // 非標(biāo)準(zhǔn)屬性盈电,但所有瀏覽器均支持(棧屬性蝴簇,可以追蹤發(fā)生錯(cuò)誤的具體信息)
columnNumber: Number, // 非標(biāo)準(zhǔn)屬性,僅 Firefox 瀏覽器支持(列號)
lineNumber: Number, // 非標(biāo)準(zhǔn)屬性匆帚,僅 Firefox 瀏覽器支持(行號)
fileName: String, // 非標(biāo)準(zhǔn)屬性熬词,僅 Firefox 瀏覽器支持(文件路徑)
column: Number, // 非標(biāo)準(zhǔn)屬性,僅 Safari 瀏覽器支持(同上述三個(gè)屬性)
line: Number, // 非標(biāo)準(zhǔn)屬性吸重,僅 Safari 瀏覽器支持
sourceURL: String, // 非標(biāo)準(zhǔn)屬性互拾,僅 Safari 瀏覽器支持
toString: Function, // 標(biāo)準(zhǔn)方法(其返回值是 name 和 message 屬性的字符串表示)
}
我們寫個(gè)最簡單的示例,打印看下各大瀏覽器的情況:
try {
throw new TypeError('oops')
} catch (e) {
console.log(e.toString())
console.dir(e)
}
插個(gè)題外話:
不知道有人沒有對此有疑惑的嚎幸,為什么 console.log()
一個(gè) Error
對象颜矿,打印出來的是字符串,而不是一個(gè)對象呢嫉晶?
const err = new Error('wrong')
console.log(err) // "Error: wrong"
console.log(typeof err) // "object"
那么骑疆,如果想打印出 Error
對象,使用 console.dir()
即可车遂。
前面 console.log()
打印結(jié)果為字符串的原因其實(shí)很簡單封断,那就是 console.log()
內(nèi)部「偷偷地」做了一件事,當(dāng)傳入的實(shí)參為 Error
對象(或其派生類錯(cuò)誤對象)舶担,它會(huì)先調(diào)用 Error
對象的 Error.prototype.toString()
方法坡疼,然后將其結(jié)果輸出到控制臺(tái),所以我們看到的打印結(jié)果為字符串衣陶。
其實(shí)現(xiàn)如下:
// 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
}
細(xì)心的同學(xué)會(huì)發(fā)現(xiàn)柄瑰,在不同瀏覽器下,其打印結(jié)果可能會(huì)不相同(但不重要)剪况。原因也非常簡單教沾,console
并不是 ECMAScript 標(biāo)準(zhǔn),而是瀏覽器 BOM 對象提供的一個(gè)接口译断,其標(biāo)準(zhǔn)由 WHATWG 機(jī)構(gòu)制定授翻,雖然標(biāo)準(zhǔn)是統(tǒng)一的,但實(shí)現(xiàn)的是各瀏覽器廠商大爺們,它們有可能不會(huì)嚴(yán)格遵守規(guī)范去實(shí)現(xiàn)堪唐,因而產(chǎn)生差異化巡语。比如,此前寫過一篇文章是關(guān)于不同宿主環(huán)境下 async/await 和 promise 執(zhí)行順序的差異淮菠,就因?yàn)?JS 引擎實(shí)現(xiàn)差異導(dǎo)致的男公。
1.2 異常
前面提到,當(dāng)錯(cuò)誤被拋出時(shí)就會(huì)成為異常合陵。
假設(shè)我們編寫的代碼存在語法錯(cuò)誤枢赔,那么在編譯階段的語法分析過程就會(huì)被聰明的 JS 引擎發(fā)現(xiàn),因而在編譯階段便會(huì)拋出 SyntaxError拥知。
假設(shè)我們代碼沒有語法錯(cuò)誤踏拜,但錯(cuò)誤地引用了一個(gè)不存在的變量,那么在執(zhí)行階段的執(zhí)行上下文過程(代碼執(zhí)行之前的一個(gè)過程)举庶,聰明的 JS 引擎發(fā)現(xiàn)在其作用域鏈上找不到該變量执隧,那么就會(huì)拋出 ReferenceError。
假設(shè)即不存在語法錯(cuò)誤户侥,也沒有引用錯(cuò)誤镀琉,但我們對一個(gè)變量做了“不合法”的操作,比如 null.name
蕊唐、'str'.push('ing')
屋摔,那么 JS 引擎就會(huì)拋出 TypeError。
還有很多很多替梨,就不舉例了钓试。
前面都是 JS 引擎主動(dòng)拋出的錯(cuò)誤,那么副瀑,我們開發(fā)者則可通過 throw
關(guān)鍵字來拋出錯(cuò)誤弓熏,語法很簡單:
// throw expression
throw 123
throw 'abc'
throw { name: 'Frankie' }
// ...
請注意,在 JavaScript 中 throw
關(guān)鍵字和 return
糠睡、break
挽鞠、continue
等關(guān)鍵字一樣,會(huì)受到 ASI(Automatic Semicolon Insertion)規(guī)則的影響狈孔,它不能在 throw
與 expression
之間插入任意換行符信认,否則可能得不到預(yù)期結(jié)果。
語法很簡單均抽,但通常項(xiàng)目中「不建議」直接拋出一個(gè)字面量嫁赏,而是拋出 Error
對象或其派生類對象,應(yīng)該這樣:
throw new Error('oops')
throw new TypeError('arguments must be a number.')
// ...
原因是 Error
對象會(huì)記錄引發(fā)此錯(cuò)誤的文件的路徑油挥、行號潦蝇、列號等信息款熬,這應(yīng)該是排除錯(cuò)誤最有效的信息。在 ESLint 中的 no-throw-literal 規(guī)則护蝶,正是用來約束上述直接拋出字面量的寫法的华烟。
除了 throw
關(guān)鍵字之外,ES6 中強(qiáng)大的 Generator 函數(shù)也提供了一個(gè)可拋出異常的方法:Generator.prototype.throw()
持灰。它可以在函數(shù)體外拋出異常,然后在函數(shù)體內(nèi)捕獲異常负饲。
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)
}
打印結(jié)果是 inner --> Error: oops
堤魁。如果生成器函數(shù)體內(nèi)沒有 try...catch
去捕獲異常,那么它所拋出的異撤凳可以被外部的 try...catch
語句捕獲到妥泉。
當(dāng)生成器「未開始執(zhí)行之前」或者「執(zhí)行結(jié)束之后」,調(diào)用生成器的 throw()
方法洞坑。它的異常只會(huì)被生成器函數(shù)外部的 try...catch
捕獲到盲链。若外部沒有 try...catch
語句,則會(huì)報(bào)錯(cuò)且代碼就會(huì)停止執(zhí)行迟杂。詳看
需要注意的是刽沾,生成器函數(shù)雖然是一個(gè)很強(qiáng)大的異步編程的解決方案,但它本身是同步的排拷,而且執(zhí)行生成器函數(shù)并不會(huì)立刻執(zhí)行函數(shù)體的邏輯侧漓,它需要主動(dòng)調(diào)用生成器實(shí)例對象的
next()
、return()
监氢、throw()
方法去執(zhí)行函數(shù)體內(nèi)的代碼布蔗。當(dāng)然,你也可以通過for...of
浪腐、解構(gòu)等語法去遍歷它纵揍,因?yàn)樯善鞅旧砭褪且粋€(gè)可迭代對象。
二议街、try...catch
對于可能存在異常的代碼泽谨,我們通常會(huì)使用 try...catch...finally
去處理一些可預(yù)見或不可預(yù)見的錯(cuò)誤。語法有以下三種形式:
try...catch
try...finally
try...catch...finally
且必須至少存在一個(gè) catch
塊或 finally
塊傍睹。
try {
throw new Error('oops')
} catch (e) {
// some statements...
}
以上這些語法隔盛,寫過 JavaScript 相信都懂。
曾經(jīng) Firefox 59 及以下版本的瀏覽器拾稳,有一種 Conditional catch-blocks 的「條件 catch
子句」的語法(請注意吮炕,其他瀏覽器并不支持該語法,即便是遠(yuǎn)古神器 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 標(biāo)準(zhǔn)的「條件 catch
子句」應(yīng)該這樣寫:
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 等異步處理是無法捕獲其錯(cuò)誤的,究其原因就是 Event Loop 嘛拂盯。當(dāng)然實(shí)際中可能結(jié)合 async/await
來控制會(huì)更多一些佑女。
2.1 catch子句
我們知道,若 try
塊中拋出異常時(shí)谈竿,會(huì)立即轉(zhuǎn)至 catch
子句執(zhí)行团驱。若 try
塊中沒有異常拋出,會(huì)跳過 catch
子句空凸。
try {
// try statements
} catch (exception_var) {
// catch statements
}
其中 exception_var
表示異常標(biāo)識符(如 catch(e)
中的 e
)嚎花,它是「可選」的,因此可以這樣編寫 try { ... } catch { ... }
呀洲。通過該標(biāo)識符我們可以獲取關(guān)于被拋出異常的信息紊选。
請注意,該標(biāo)識符的「作用域」僅在
catch
塊中有效道逗。當(dāng)進(jìn)入catch
子句時(shí)兵罢,它被創(chuàng)建,當(dāng)catch
子句執(zhí)行完畢憔辫,此標(biāo)識符將不可再用趣些。也可以理解為(在 ES6 以前)異常標(biāo)識符是 JavaScript 中含有“塊級作用域”的變量。
2.2 finally 子句
而 finally
子句在 try
塊和 catch
塊之后執(zhí)行贰您,但在下一個(gè) try
聲明之前執(zhí)行坏平。無論是否異常拋出,finally
子句總是會(huì)執(zhí)行锦亦。
如果從
finally
塊中返回一個(gè)值舶替,那么這個(gè)值將成為整個(gè)try...catch...finally
的返回值,無論是否有return
語句在try
和catch
塊中(即使catch
塊中拋出了異常)杠园。
對于這個(gè)我表示很無語顾瞪,可能整個(gè)前端圈子就我還不知道吧,原來 finally
還能 return
一個(gè)值抛蚁,在做項(xiàng)目的過程中陈醒,確實(shí)沒寫過和見過在 finally
中 return
某個(gè)值的,讓您見笑了瞧甩,實(shí)在慚愧钉跷。
但請注意,若要在 try...catch...finally
中使用 return
肚逸,它只能在函數(shù)中運(yùn)行爷辙,否則是不允許的彬坏,會(huì)拋出語法錯(cuò)誤。
try {
doSomething()
} catch (e) {
console.warn(e)
throw e
} finally {
return 'completed' // SyntaxError: Illegal return statement
}
2.3 執(zhí)行順序
在平常的項(xiàng)目中膝晾,一般的 try...catch
寫法是在 try
塊中 return
栓始,catch
塊則作相應(yīng)的異常處理,少數(shù)情況也會(huì)在 catch
塊中 return
血当。因此幻赚,大家對這種常規(guī)寫法的執(zhí)行順序應(yīng)該沒什么問題。
先來個(gè)誰都會(huì)的示例:
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 函數(shù)返回一個(gè) "fail" 值
接著再看巍扛,它打印什么,函數(shù)又返回什么呢乏德?
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 函數(shù)返回值是 "complete"
前面提到胧瓜,如果 finally
塊中含有 return
語句,那么它的 return
值將作為當(dāng)前函數(shù)的返回值郑什,因此 foo()
結(jié)果為 "complete"
府喳。
然后我們再稍微改動(dòng)一下,在 try
塊中 return
一個(gè)值蘑拯,看下結(jié)果又有什么不同钝满?
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 函數(shù)返回值是 "complete"
由于 try
塊中沒有拋出異常弯蚜,因此 catch
塊會(huì)被跳過,不執(zhí)行剃法,但是 finally
塊還是會(huì)執(zhí)行的碎捺,而且它里面返回了 "complete"
,因此這個(gè)值也就作為 foo
函數(shù)的返回值了贷洲。
因此收厨,我們大致可以得出一個(gè)結(jié)論,
finally
塊的代碼總會(huì)在return
之前執(zhí)行优构,不管return
是存在于try
诵叁、catch
還是finally
塊中。
但是俩块,這就完了嗎黎休?
還沒有浓领,我們再看一個(gè)示例,看看里面這個(gè) bar()
函數(shù)是惰性求值势腮?還是怎樣联贩?
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()
以上示例,打印順序和結(jié)果是什么呢捎拯?
// 打印順序泪幌,依次是:
"try statement"
"catch statement"
"bar statement"
"finally statement"
// 結(jié)果是 "complete"
假設(shè) catch
塊中的 return bar()
換成 throw bar()
呢,結(jié)果又有什么變化呢署照?如果換成這個(gè)你就猶豫了祸泪,說明你理解得不夠深刻,因此這里我不給出答案建芙,你自己去試試没隘,效果更佳!
綜上所述禁荸,finally
塊的執(zhí)行時(shí)機(jī)如下:
在所有
try
塊和catch
塊(如果有右蒲,且觸發(fā)進(jìn)入的話)執(zhí)行完之后,即便此時(shí)try
塊或catch
塊中存在return
或throw
語句赶熟,它們將會(huì)被 Hold 住先不返回或拋出異常瑰妄,繼續(xù)執(zhí)行finally
塊中的代碼:
- 如果
finally
中存在return
語句,其返回值將作為整個(gè)函數(shù)的返回值(前面try
塊或catch
中的return
或throw
都會(huì)被忽略映砖,可以理解為沒有了return
或throw
關(guān)鍵字一樣)间坐。- 如果
finally
中存在throw
語句,前面try
塊或catch
中的return
或throw
同樣會(huì)被忽略邑退,最后整個(gè)函數(shù)將會(huì)拋出finally
塊中的異常竹宋。
2.4 嵌套使用
它是可以嵌套使用的,當(dāng)內(nèi)部的 try...catch...finally
中拋出異常瓜饥,它會(huì)被離它最近的 catch
塊捕獲到逝撬。
function foo() {
try {
try {
return 'success'
} finally {
throw new Error('inner oops') // 它將會(huì)被外層的 catch 塊所捕獲到
}
} catch (e) {
console.log(e) // Error: inner oops
}
}
foo()
注意,本節(jié)內(nèi)容所述都是同步代碼乓土,而不存在任何異步代碼宪潮。
到此,已徹底弄懂 try...catch...finally
語句了趣苏,再也不慌了狡相!
三、異常有哪些食磕?
在 Web 中尽棕,主要有以下幾種異常類型:
- JavaScript 異常
- DOM 和 BOM 異常
- 網(wǎng)絡(luò)資源加載異常
- Script Error
- 網(wǎng)頁異常
3.1 JavaScript 異常
try...catch
可以捕獲同步任務(wù)導(dǎo)致的異常,也可以捕獲 async/await
中的異常彬伦。
Promise
中拋出的異常滔悉,則可通過 Promise.prototype.catch()
或 Promise.prototype.then(onResolved, onRejected)
捕獲伊诵。
3.2 DOM Exception
在調(diào)用 DOM API 時(shí)發(fā)生的,都屬于 DOM Exception回官。比如:
<!DOCTYPE html>
<html>
<body>
<video id="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>
未完待續(xù)...