您好,登錄后才能下訂單哦!
這篇文章主要講解了“有哪些前端異常處理”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“有哪些前端異常處理”吧!
什么是異常
用直白的話來解釋異常的話,就是程序發(fā)生了意想不到的情況,這種情況影響到了程序的正確運(yùn)行。
從根本上來說,異常就是一個(gè)數(shù)據(jù)結(jié)構(gòu),其保存了異常發(fā)生的相關(guān)信息,比如錯(cuò)誤碼,錯(cuò)誤信息等。以 JS 中的標(biāo)準(zhǔn)內(nèi)置對(duì)象 Error 為例,其標(biāo)準(zhǔn)屬性有 name 和 message。然而不同的瀏覽器廠商有自己的自定義屬性,這些屬性并不通用。比如 Mozilla 瀏覽器就增加了 filename 和 stack 等屬性。
值得注意的是錯(cuò)誤只有被拋出,才會(huì)產(chǎn)生異常,不被拋出的錯(cuò)誤不會(huì)產(chǎn)生異常。比如:
function t() { console.log("start"); new Error(); console.log("end"); } t();
(動(dòng)畫演示)
這段代碼不會(huì)產(chǎn)生任何的異常,控制臺(tái)也不會(huì)有任何錯(cuò)誤輸出。
異常的分類
按照產(chǎn)生異常時(shí)程序是否正在運(yùn)行,我們可以將錯(cuò)誤分為編譯時(shí)異常和運(yùn)行時(shí)異常。
編譯時(shí)異常指的是源代碼在編譯成可執(zhí)行代碼之前產(chǎn)生的異常。而運(yùn)行時(shí)異常指的是可執(zhí)行代碼被裝載到內(nèi)存中執(zhí)行之后產(chǎn)生的異常。
編譯時(shí)異常
我們知道 TS 最終會(huì)被編譯成 JS,從而在 JS Runtime中執(zhí)行。既然存在編譯,就有可能編譯失敗,就會(huì)有編譯時(shí)異常。
比如我使用 TS 寫出了如下代碼:
const s: string = 123;
這很明顯是錯(cuò)誤的代碼, 我給 s 聲明了 string 類型,但是卻給它賦值 number。
當(dāng)我使用 tsc(typescript 編譯工具,全稱是 typescript compiler)嘗試編譯這個(gè)文件的時(shí)候會(huì)有異常拋出:
tsc a.ts a.ts:1:7 - error TS2322: Type '123' is not assignable to type 'string'. 1 const s: string = 123; ~ Found 1 error.
這個(gè)異常就是編譯時(shí)異常,因?yàn)槲业拇a還沒有執(zhí)行。
然而并不是你用了 TS 才存在編譯時(shí)異常,JS 同樣有編譯時(shí)異常。有的人可能會(huì)問 JS 不是解釋性語言么?是邊解釋邊執(zhí)行,沒有編譯環(huán)節(jié),怎么會(huì)有編譯時(shí)異常?
別急,我舉個(gè)例子你就明白了。如下代碼:
function t() { console.log('start') await sa console.log('end') } t()
上面的代碼由于存在語法錯(cuò)誤,不會(huì)編譯通過,因此并不會(huì)打印start,側(cè)面證明了這是一個(gè)編譯時(shí)異常。盡管 JS 是解釋語言,也依然存在編譯階段,這是必然的,因此自然也會(huì)有編譯異常。
總的來說,編譯異??梢栽诖a被編譯成最終代碼前被發(fā)現(xiàn),因此對(duì)我們的傷害更小。接下來,看一下令人心生畏懼的運(yùn)行時(shí)異常。
運(yùn)行時(shí)異常
相信大家對(duì)運(yùn)行時(shí)異常非常熟悉。這恐怕是廣大前端碰到最多的異常類型了。眾所周知的 NPE(Null Pointer Exception) 就是運(yùn)行時(shí)異常。
將上面的例子稍加改造,得到下面代碼:
function t() { console.log("start"); throw 1; console.log("end"); } t();
(動(dòng)畫演示)
注意 end 沒有打印,并且 t 沒有彈出棧。實(shí)際上 t 最終還是會(huì)被彈出的,只不過和普通的返回不一樣。
如上,則會(huì)打印出start。由于異常是在代碼運(yùn)行過程中拋出的,因此這個(gè)異常屬于運(yùn)行時(shí)異常。相對(duì)于編譯時(shí)異常,這種異常更加難以發(fā)現(xiàn)。上面的例子可能比較簡(jiǎn)單,但是如果我的異常是隱藏在某一個(gè)流程控制語句(比如 if else)里面呢?程序就可能在客戶的電腦走入那個(gè)拋出異常的 if 語句,而在你的電腦走入另一條。這就是著名的 《在我電腦上好好的》 事件。
異常的傳播
異常的傳播和我之前寫的瀏覽器事件模型有很大的相似性。只不過那個(gè)是作用在 DOM 這樣的數(shù)據(jù)結(jié)構(gòu),這個(gè)則是作用在函數(shù)調(diào)用棧這種數(shù)據(jù)結(jié)構(gòu),并且事件傳播存在捕獲階段,異常傳播是沒有的。不同 C 語言,JS 中異常傳播是自動(dòng)的,不需要程序員手動(dòng)地一層層傳遞。如果一個(gè)異常沒有被 catch,它會(huì)沿著函數(shù)調(diào)用棧一層層傳播直到???。
異常處理中有兩個(gè)關(guān)鍵詞,它們是throw(拋出異常) 和 catch(處理異常)。 當(dāng)一個(gè)異常被拋出的時(shí)候,異常的傳播就開始了。異常會(huì)不斷傳播直到遇到第一個(gè) catch。 如果程序員沒有手動(dòng) catch,那么一般而言程序會(huì)拋出類似unCaughtError,表示發(fā)生了一個(gè)異常,并且這個(gè)異常沒有被程序中的任何 catch 語言處理。未被捕獲的異常通常會(huì)被打印在控制臺(tái)上,里面有詳細(xì)的堆棧信息,從而幫助程序員快速排查問題。實(shí)際上我們的程序的目標(biāo)是避免 unCaughtError這種異常,而不是一般性的異常。
一點(diǎn)小前提
由于 JS 的 Error 對(duì)象沒有 code 屬性,只能根據(jù) message 來呈現(xiàn),不是很方便。我這里進(jìn)行了簡(jiǎn)單的擴(kuò)展,后面很多地方我用的都是自己擴(kuò)展的 Error ,而不是原生 JS Error ,不再贅述。
oldError = Error; Error = function ({ code, message, fileName, lineNumber }) { error = new oldError(message, fileName, lineNumber); error.code = code; return error; };
手動(dòng)拋出 or 自動(dòng)拋出
異常既可以由程序員自己手動(dòng)拋出,也可以由程序自動(dòng)拋出。
throw new Error(`I'm Exception`);
(手動(dòng)拋出的例子)
a = null; a.toString(); // Thrown: TypeError: Cannot read property 'toString' of null
(程序自動(dòng)拋出的例子)
自動(dòng)拋出異常很好理解,畢竟我們哪個(gè)程序員沒有看到過程序自動(dòng)拋出的異常呢?
“這個(gè)異常突然就跳出來!嚇我一跳!”,某不知名程序員如是說。
那什么時(shí)候應(yīng)該手動(dòng)拋出異常呢?
一個(gè)指導(dǎo)原則就是你已經(jīng)預(yù)知到程序不能正確進(jìn)行下去了。比如我們要實(shí)現(xiàn)除法,首先我們要考慮的是被除數(shù)為 0 的情況。當(dāng)被除數(shù)為 0 的時(shí)候,我們應(yīng)該怎么辦呢?是拋出異常,還是 return 一個(gè)特殊值?答案是都可以,你自己能區(qū)分就行,這沒有一個(gè)嚴(yán)格的參考標(biāo)準(zhǔn)。 我們先來看下拋出異常,告訴調(diào)用者你的輸入,我處理不了這種情況。
function divide(a, b) { a = +a; b = +b; // 轉(zhuǎn)化成數(shù)字 if (!b) { // 匹配 +0, -0, NaN throw new Error({ code: 1, message: "Invalid dividend " + b, }); } if (Number.isNaN(a)) { // 匹配 NaN throw new Error({ code: 2, message: "Invalid divisor " + a, }); } return a / b; }
上面代碼會(huì)在兩種情況下拋出異常,告訴調(diào)用者你的輸入我處理不了。由于這兩個(gè)異常都是程序員自動(dòng)手動(dòng)拋出的,因此是可預(yù)知的異常。
剛才說了,我們也可以通過返回值來區(qū)分異常輸入。我們來看下返回值輸入是什么,以及和異常有什么關(guān)系。
異常 or 返回
如果是基于異常形式(遇到不能處理的輸入就拋出異常)。當(dāng)別的代碼調(diào)用divide的時(shí)候,需要自己 catch。
function t() { try { divide("foo", "bar"); } catch (err) { if (err.code === 1) { return console.log("被除數(shù)必須是除0之外的數(shù)"); } if (err.code === 2) { return console.log("除數(shù)必須是數(shù)字"); } throw new Error("不可預(yù)知的錯(cuò)誤"); } }
然而就像上面我說的那樣,divide 函數(shù)設(shè)計(jì)的時(shí)候,也完全可以不用異常,而是使用返回值來區(qū)分。
function divide(a, b) { a = +a; b = +b; // 轉(zhuǎn)化成數(shù)字 if (!b) { // 匹配 +0, -0, NaN return new Error({ code: 1, message: "Invalid dividend " + b, }); } if (Number.isNaN(a)) { // 匹配 NaN return new Error({ code: 2, message: "Invalid divisor " + a, }); } return a / b; }
當(dāng)然,我們使用方式也要作出相應(yīng)改變。
function t() { const res = divide("foo", "bar"); if (res.code === 1) { return console.log("被除數(shù)必須是除0之外的數(shù)"); } if (res.code === 2) { return console.log("除數(shù)必須是數(shù)字"); } return new Error("不可預(yù)知的錯(cuò)誤"); }
這種函數(shù)設(shè)計(jì)方式和拋出異常的設(shè)計(jì)方式從功能上說都是一樣的,只是告訴調(diào)用方的方式不同。如果你選擇第二種方式,而不是拋出異常,那么實(shí)際上需要調(diào)用方書寫額外的代碼,用來區(qū)分正常情況和異常情況,這并不是一種良好的編程習(xí)慣。
然而在 Go 等返回值可以為復(fù)數(shù)的語言中,我們無需使用上面蹩腳的方式,而是可以:
res, err := divide("foo", "bar"); if err != nil { log.Fatal(err) }
這是和 Java 和 JS 等語言使用的 try catch 不一樣的的地方,Go 是通過 panic recover defer 機(jī)制來進(jìn)行異常處理的。感興趣的可以去看看 Go 源碼關(guān)于錯(cuò)誤測(cè)試部分
可能大家對(duì) Go 不太熟悉。沒關(guān)系,我們來繼續(xù)看下 shell。實(shí)際上 shell 也是通過返回值來處理異常的,我們可以通過 $? 拿到上一個(gè)命令的返回值,這本質(zhì)上也是一種調(diào)用棧的傳播行為,而且是通過返回值而不是捕獲來處理異常的。
作為函數(shù)返回值處理和 try catch 一樣,這是語言的設(shè)計(jì)者和開發(fā)者共同決定的一件事情。
上面提到了異常傳播是作用在函數(shù)調(diào)用棧上的。當(dāng)一個(gè)異常發(fā)生的時(shí)候,其會(huì)沿著函數(shù)調(diào)用棧逐層返回,直到第一個(gè) catch 語句。當(dāng)然 catch 語句內(nèi)部仍然可以觸發(fā)異常(自動(dòng)或者手動(dòng))。如果 catch 語句內(nèi)部發(fā)生了異常,也一樣會(huì)沿著其函數(shù)調(diào)用棧繼續(xù)執(zhí)行上述邏輯,專業(yè)術(shù)語是 stack unwinding。
實(shí)際上并不是所有的語言都會(huì)進(jìn)行 stack unwinding,這個(gè)我們會(huì)在接下來的《運(yùn)行時(shí)異??梢曰謴?fù)么?》部分講解。
偽代碼來描述一下:
function bubble(error, fn) { if (fn.hasCatchBlock()) { runCatchCode(error); } if (callstack.isNotEmpty()) { bubble(error, callstack.pop()); } }
從我的偽代碼可以看出所謂的 stack unwinding 其實(shí)就是 callstack.pop()
這就是異常傳播的一切!僅此而已。
異常的處理
我們已經(jīng)了解來異常的傳播方式了。那么接下來的問題是,我們應(yīng)該如何在這個(gè)傳播過程中處理異常呢?
我們來看一個(gè)簡(jiǎn)單的例子:
function a() { b(); } function b() { c(); } function c() { throw new Error("an error occured"); } a();
我們將上面的代碼放到 chrome 中執(zhí)行, 會(huì)在控制臺(tái)顯示如下輸出:
我們可以清楚地看出函數(shù)的調(diào)用關(guān)系。即錯(cuò)誤是在 c 中發(fā)生的,而 c 是 b 調(diào)用的,b 是 a 調(diào)用的。這個(gè)函數(shù)調(diào)用棧是為了方便開發(fā)者定位問題而存在的。
上面的代碼,我們并沒有 catch 錯(cuò)誤,因此上面才會(huì)有uncaught Error。
那么如果我們 catch ,會(huì)發(fā)生什么樣的變化呢?catch 的位置會(huì)對(duì)結(jié)果產(chǎn)生什么樣的影響?在 a ,b,c 中 catch 的效果是一樣的么?
我們來分別看下:
function a() { b(); } function b() { c(); } function c() { try { throw new Error("an error occured"); } catch (err) { console.log(err); } } a();
(在 c 中 catch)
我們將上面的代碼放到 chrome 中執(zhí)行, 會(huì)在控制臺(tái)顯示如下輸出:
可以看出,此時(shí)已經(jīng)沒有uncaught Error啦,僅僅在控制臺(tái)顯示了標(biāo)準(zhǔn)輸出,而非錯(cuò)誤輸出(因?yàn)槲矣玫氖?console.log,而不是 console.error)。然而更重要是的是,如果我們沒有 catch,那么后面的同步代碼將不會(huì)執(zhí)行。
比如在 c 的 throw 下面增加一行代碼,這行代碼是無法被執(zhí)行的,無論這個(gè)錯(cuò)誤有沒有被捕獲。
function c() { try { throw new Error("an error occured"); console.log("will never run"); } catch (err) { console.log(err); } }
我們將 catch 移動(dòng)到 b 中試試看。
function a() { b(); } function b() { try { c(); } catch (err) { console.log(err); } } function c() { throw new Error("an error occured"); } a();
(在 b 中 catch)
在這個(gè)例子中,和上面在 c 中捕獲沒有什么本質(zhì)不同。其實(shí)放到 a 中捕獲也是一樣,這里不再貼代碼了,感興趣的自己試下。
既然處于函數(shù)調(diào)用棧頂部的函數(shù)報(bào)錯(cuò), 其函數(shù)調(diào)用棧下方的任意函數(shù)都可以進(jìn)行捕獲,并且效果沒有本質(zhì)不同。那么問題來了,我到底應(yīng)該在哪里進(jìn)行錯(cuò)誤處理呢?
答案是責(zé)任鏈模式。我們先來簡(jiǎn)單介紹一下責(zé)任鏈模式,不過細(xì)節(jié)不會(huì)在這里展開。
假如 lucifer 要請(qǐng)假。
如果請(qǐng)假天數(shù)小于等于 1 天,則主管同意即可
如果請(qǐng)假大于 1 天,但是小于等于三天,則需要 CTO 同意。
如果請(qǐng)假天數(shù)大于三天,則需要老板同意。
這就是一個(gè)典型的責(zé)任鏈模式。誰有責(zé)任干什么事情是確定的,不要做自己能力范圍之外的事情。比如主管不要去同意大于 1 天的審批。
舉個(gè)例子,假設(shè)我們的應(yīng)用有三個(gè)異常處理類,它們分別是:用戶輸入錯(cuò)誤,網(wǎng)絡(luò)錯(cuò)誤 和 類型錯(cuò)誤。如下代碼,當(dāng)代碼執(zhí)行的時(shí)候會(huì)報(bào)錯(cuò)一個(gè)用戶輸入異常。這個(gè)異常沒有被 C 捕獲,會(huì) unwind stack 到 b,而 b 中 catch 到這個(gè)錯(cuò)誤之后,通過查看 code 值判斷其可以被處理,于是打印I can handle this。
function a() { try { b(); } catch (err) { if (err.code === "NETWORK_ERROR") { return console.log("I can handle this"); } // can't handle, pass it down throw err; } } function b() { try { c(); } catch (err) { if (err.code === "INPUT_ERROR") { return console.log("I can handle this"); } // can't handle, pass it down throw err; } } function c() { throw new Error({ code: "INPUT_ERROR", message: "an error occured", }); } a();
而如果 c 中拋出的是別的異常,比如網(wǎng)絡(luò)異常,那么 b 是無法處理的,雖然 b catch 住了,但是由于你無法處理,因此一個(gè)好的做法是繼續(xù)拋出異常,而不是吞沒異常。不要畏懼錯(cuò)誤,拋出它。只有沒有被捕獲的異常才是可怕的,如果一個(gè)錯(cuò)誤可以被捕獲并得到正確處理,它就不可怕。
舉個(gè)例子:
function a() { try { b(); } catch (err) { if (err.code === "NETWORK_ERROR") { return console.log("I can handle this"); } // can't handle, pass it down throw err; } } function b() { try { c(); } catch (err) { if (err.code === "INPUT_ERROR") { return console.log("I can handle this"); } } } function c() { throw new Error({ code: "NETWORK_ERROR", message: "an error occured", }); } a();
如上代碼不會(huì)有任何異常被拋出,它被完全吞沒了,這對(duì)我們調(diào)試問題簡(jiǎn)直是災(zāi)難。因此切記不要吞沒你不能處理的異常。正確的做法應(yīng)該是上面講的那種只 catch 你可以處理的異常,而將你不能處理的異常 throw 出來,這就是責(zé)任鏈模式的典型應(yīng)用。
這只是一個(gè)簡(jiǎn)單的例子,就足以繞半天。實(shí)際業(yè)務(wù)肯定比這個(gè)復(fù)雜多得多。因此異常處理絕對(duì)不是一件容易的事情。
如果說誰來處理是一件困難的事情,那么在異步中決定誰來處理異常就是難上加難,我們來看下。
同步與異步
同步異步一直是前端難以跨越的坎,對(duì)于異常處理也是一樣。以 NodeJS 中用的比較多的讀取文件 API 為例。它有兩個(gè)版本,一個(gè)是異步,一個(gè)是同步。同步讀取僅僅應(yīng)該被用在沒了這個(gè)文件無法進(jìn)行下去的時(shí)候。比如讀取一個(gè)配置文件。而不應(yīng)該在比如瀏覽器中讀取用戶磁盤上的一個(gè)圖片等,這樣會(huì)造成主線程阻塞,導(dǎo)致瀏覽器卡死。
// 異步讀取文件 fs.readFileSync(); // 同步讀取文件 fs.readFile();
當(dāng)我們?cè)噲D同步讀取一個(gè)不存在的文件的時(shí)候,會(huì)拋出以下異常:
fs.readFileSync('something-not-exist.lucifer'); console.log('腦洞前端'); Thrown: Error: ENOENT: no such file or directory, open 'something-not-exist.lucifer' at Object.openSync (fs.js:446:3) at Object.readFileSync (fs.js:348:35) { errno: -2, syscall: 'open', code: 'ENOENT', path: 'something-not-exist.lucifer' }
并且腦洞前端是不會(huì)被打印出來的。這個(gè)比較好理解,我們上面已經(jīng)解釋過了。
而如果以異步方式的話:
fs.readFile('something-not-exist.lucifer', (err, data) => {if(err) {throw err}}); console.log('lucifer') lucifer undefined Thrown: [Error: ENOENT: no such file or directory, open 'something-not-exist.lucifer'] { errno: -2, code: 'ENOENT', syscall: 'open', path: 'something-not-exist.lucifer' } >
腦洞前端是會(huì)被打印出來的。
其本質(zhì)在于 fs.readFile 的函數(shù)調(diào)用已經(jīng)成功,并從調(diào)用棧返回并執(zhí)行到下一行的console.log('lucifer')。因此錯(cuò)誤發(fā)生的時(shí)候,調(diào)用棧是空的,這一點(diǎn)可以從上面的錯(cuò)誤堆棧信息中看出來。
不明白為什么調(diào)用棧是空的同學(xué)可以看下我之前寫的《一文看懂瀏覽器事件循環(huán)》
而 try catch 的作用僅僅是捕獲當(dāng)前調(diào)用棧的錯(cuò)誤(上面異常傳播部分已經(jīng)講過了)。因此異步的錯(cuò)誤是無法捕獲的,比如;
try { fs.readFile("something-not-exist.lucifer", (err, data) => { if (err) { throw err; } }); } catch (err) { console.log("catching an error"); }
上面的 catching an error 不會(huì)被打印。因?yàn)殄e(cuò)誤拋出的時(shí)候, 調(diào)用棧中不包含這個(gè) catch 語句,而僅僅在執(zhí)行fs.readFile的時(shí)候才會(huì)。
如果我們換成同步讀取文件的例子看看:
try { fs.readFileSync("something-not-exist.lucifer"); } catch (err) { console.log("catching an error"); }
上面的代碼會(huì)打印 catching an error。因?yàn)樽x取文件被同步發(fā)起,文件返回之前線程會(huì)被掛起,當(dāng)線程恢復(fù)執(zhí)行的時(shí)候, fs.readFileSync 仍然在函數(shù)調(diào)用棧中,因此 fs.readFileSync 產(chǎn)生的異常會(huì)冒泡到 catch 語句。
簡(jiǎn)單來說就是異步產(chǎn)生的錯(cuò)誤不能用 try catch 捕獲,而要使用回調(diào)捕獲。
可能有人會(huì)問了,我見過用 try catch 捕獲異步異常啊。 比如:
rejectIn = (ms) => new Promise((_, r) => { setTimeout(() => { r(1); }, ms); }); async function t() { try { await rejectIn(0); } catch (err) { console.log("catching an error", err); } } t();
本質(zhì)上這只是一個(gè)語法糖,是 Promise.prototype.catch 的一個(gè)語法糖而已。而這一語法糖能夠成立的原因在于其用了 Promise 這種包裝類型。如果你不用包裝類型,比如上面的 fs.readFile 不用 Promise 等包裝類型包裝,打死都不能用 try catch 捕獲。
而如果我們使用 babel 轉(zhuǎn)義下,會(huì)發(fā)現(xiàn) try catch 不見了,變成了 switch case 語句。這就是 try catch “可以捕獲異步異?!钡脑?,僅此而已,沒有更多。
(babel 轉(zhuǎn)義結(jié)果)
我使用的 babel 轉(zhuǎn)義環(huán)境都記錄在這里,大家可以直接點(diǎn)開鏈接查看.
雖然瀏覽器并不像 babel 轉(zhuǎn)義這般實(shí)現(xiàn),但是至少我們明白了一點(diǎn)。目前的 try catch 的作用機(jī)制是無法捕獲異步異常的。
異步的錯(cuò)誤處理推薦使用容器包裝,比如 Promise。然后使用 catch 進(jìn)行處理。實(shí)際上 Promise 的 catch 和 try catch 的 catch 有很多相似的地方,大家可以類比過去。
和同步處理一樣,很多原則都是通用的。比如異步也不要去吞沒異常。下面的代碼是不好的,因?yàn)樗虥]了它不能處理的異常。
p = Promise.reject(1); p.catch(() => {});
更合適的做法的應(yīng)該是類似這種:
p = Promise.reject(1); p.catch((err) => { if (err == 1) { return console.log("I can handle this"); } throw err; });
徹底消除運(yùn)行時(shí)異常可能么?
我個(gè)人對(duì)目前前端現(xiàn)狀最為頭疼的一點(diǎn)是:大家過分依賴運(yùn)行時(shí),而嚴(yán)重忽略編譯時(shí)。我見過很多程序,你如果不運(yùn)行,根本不知道程序是怎么走的,每個(gè)變量的 shape 是什么。怪不得處處都可以看到 console.log。我相信你一定對(duì)此感同身受。也許你就是那個(gè)寫出這種代碼的人,也許你是給別人擦屁股的人。為什么會(huì)這樣? 就是因?yàn)榇蠹姨蕾囘\(yùn)行時(shí)。TS 的出現(xiàn)很大程度上改善了這一點(diǎn),前提是你用的是 typescript,而不是 anyscript。其實(shí) eslint 以及 stylint 對(duì)此也有貢獻(xiàn),畢竟它們都是靜態(tài)分析工具。
我強(qiáng)烈建議將異常保留在編譯時(shí),而不是運(yùn)行時(shí)。不妨極端一點(diǎn)來看:假如所有的異常都在編譯時(shí)發(fā)生,而一定不會(huì)在運(yùn)行時(shí)發(fā)生。那么我們是不是就可以信心滿滿地對(duì)應(yīng)用進(jìn)行重構(gòu)啦?
幸運(yùn)的是,我們能夠做到。只不過如果當(dāng)前語言做不到的話,則需要對(duì)現(xiàn)有的語言體系進(jìn)行改造。這種改造成本真的很大。不僅僅是 API,編程模型也發(fā)生了翻天覆地的變化,不然函數(shù)式也不會(huì)這么多年沒有得到普及了。
不熟悉函數(shù)編程的可以看看我之前寫的函數(shù)式編程入門篇。
如果才能徹底消除異常呢?在回答這個(gè)問題之前,我們先來看下一門號(hào)稱沒有運(yùn)行時(shí)異常的語言 elm。elm 是一門可以編譯為 JS 的函數(shù)式編程語言,其封裝了諸如網(wǎng)絡(luò) IO 等副作用,是一種聲明式可推導(dǎo)的語言。 有趣的是,elm 也有異常處理。 elm 中關(guān)于異常處理(Error Handling)部分有兩個(gè)小節(jié)的內(nèi)容,分別是:Maybe 和 Result。elm 之所以沒有運(yùn)行時(shí)異常的一個(gè)原因就是它們。 一句話概括“為什么 elm 沒有異?!钡脑?,那就是elm 把異??醋鲾?shù)據(jù)(data)。
舉個(gè)簡(jiǎn)單的例子:
maybeResolveOrNot = (ms) => setTimeout(() => { if (Math.random() > 0.5) { console.log("ok"); } else { throw new Error("error"); } });
上面的代碼有一半的可能報(bào)錯(cuò)。那么在 elm 中就不允許這樣的情況發(fā)生。所有的可能發(fā)生異常的代碼都會(huì)被強(qiáng)制包裝一層容器,這個(gè)容器在這里是 Maybe。
在其他函數(shù)式編程語言名字可能有所不同,但是意義相同。實(shí)際上,不僅僅是異常,正常的數(shù)據(jù)也會(huì)被包裝到容器中,你需要通過容器的接口來獲取數(shù)據(jù)。如果難以理解的話,你可以將其簡(jiǎn)單理解為 Promsie(但并不完全等價(jià))。
Maybe 可能返回正常的數(shù)據(jù) data,也可能會(huì)生成一個(gè)錯(cuò)誤 error。某一個(gè)時(shí)刻只能是其中一個(gè),并且只有運(yùn)行的時(shí)候,我們才真正知道它是什么。從這一點(diǎn)來看,有點(diǎn)像薛定諤的貓。
不過 Maybe 已經(jīng)完全考慮到異常的存在,一切都在它的掌握之中。所有的異常都能夠在編譯時(shí)推導(dǎo)出來。當(dāng)然要想推導(dǎo)出這些東西,你需要對(duì)整個(gè)編程模型做一定的封裝會(huì)抽象,比如 DOM 就不能直接用了,而是需要一個(gè)中間層。
再來看下一個(gè)更普遍的例子 NPE:
null.toString();
elm 也不會(huì)發(fā)生。原因也很簡(jiǎn)單,因?yàn)?null 也會(huì)被包裝起來,當(dāng)你通過這個(gè)包裝類型就行訪問的時(shí)候,容器有能力避免這種情況,因此就可以不會(huì)發(fā)生異常。當(dāng)然這里有一個(gè)很重要的前提就是可推導(dǎo),而這正是函數(shù)式編程語言的特性。這部分內(nèi)容超出了本文的討論范圍,不再這里說了。
運(yùn)行時(shí)異常可以恢復(fù)么?
最后要討論的一個(gè)主題是運(yùn)行時(shí)異常是否可以恢復(fù)。先來解釋一下,什么是運(yùn)行時(shí)異常的恢復(fù)。 還是用上面的例子:
function t() { console.log("start"); throw 1; console.log("end"); } t();
這個(gè)我們已經(jīng)知道了, end 是不會(huì)打印的。 盡管你這么寫也是無濟(jì)于事:
function t() { try { console.log("start"); throw 1; console.log("end"); } catch (err) { console.log("relax, I can handle this"); } } t();
如果我想讓它打印呢?我想讓程序面對(duì)異??梢宰约?recover 怎么辦?我已經(jīng)捕獲這個(gè)錯(cuò)誤, 并且我確信我可以處理,讓流程繼續(xù)走下去吧!如果有能力做到這個(gè),這個(gè)就是運(yùn)行時(shí)異?;謴?fù)。
遺憾地告訴你,據(jù)我所知,目前沒有任何一個(gè)引擎能夠做到這一點(diǎn)。
這個(gè)例子過于簡(jiǎn)單, 只能幫助我們理解什么是運(yùn)行時(shí)異?;謴?fù),但是不足以讓我們看出這有什么用?
我們來看一個(gè)更加復(fù)雜的例子,我們這里直接使用上面實(shí)現(xiàn)過的函數(shù)divide。
function t() { try { const res = divide("foo", "bar"); alert(`you got ${res}`); } catch (err) { if (err.code === 1) { return console.log("被除數(shù)必須是除0之外的數(shù)"); } if (err.code === 2) { return console.log("除數(shù)必須是數(shù)字"); } throw new Error("不可預(yù)知的錯(cuò)誤"); } }
如上代碼,會(huì)進(jìn)入 catch ,而不會(huì) alert。因此對(duì)于用戶來說, 應(yīng)用程序是沒有任何響應(yīng)的。這是不可接受的。
要吐槽一點(diǎn)的是這種事情真的是挺常見的,只不過大家用的不是 alert 罷了。
如果我們的代碼在進(jìn)入 catch 之后還能夠繼續(xù)返回出錯(cuò)位置繼續(xù)執(zhí)行就好了。
如何實(shí)現(xiàn)異常中斷的恢復(fù)呢?我剛剛說了:據(jù)我所知,目前沒有任何一個(gè)引擎能夠做到異常恢復(fù)。那么我就來發(fā)明一個(gè)新的語法解決這個(gè)問題。
function t() { try { const res = divide("foo", "bar"); alert(`you got ${res}`); } catch (err) { console.log("releax, I can handle this"); resume - 1; } } t();
上面的 resume 是我定義的一個(gè)關(guān)鍵字,功能是如果遇到異常,則返回到異常發(fā)生的地方,然后給當(dāng)前發(fā)生異常的函數(shù)一個(gè)返回值 -1,并使得后續(xù)代碼能夠正常運(yùn)行,不受影響。這其實(shí)是一種 fallback。
這絕對(duì)是一個(gè)超前的理念。當(dāng)然挑戰(zhàn)也非常大,對(duì)現(xiàn)有的體系沖擊很大,很多東西都要改。我希望社區(qū)可以考慮把這個(gè)東西加到標(biāo)準(zhǔn)。
最佳實(shí)踐
通過前面的學(xué)習(xí),你已經(jīng)知道了異常是什么,異常是怎么產(chǎn)生的,以及如何正確處理異常(同步和異步)。接下來,我們談一下異常處理的最佳實(shí)踐。
我們平時(shí)開發(fā)一個(gè)應(yīng)用。 如果站在生產(chǎn)者和消費(fèi)者的角度來看的話。當(dāng)我們使用別人封裝的框架,庫,模塊,甚至是函數(shù)的時(shí)候,我們就是消費(fèi)者。而當(dāng)我們寫的東西被他人使用的時(shí)候,我們就是生產(chǎn)者。
實(shí)際上,就算是生產(chǎn)者內(nèi)部也會(huì)有多個(gè)模塊構(gòu)成,多個(gè)模塊之間也會(huì)有生產(chǎn)者和消費(fèi)者的再次身份轉(zhuǎn)化。不過為了簡(jiǎn)單起見,本文不考慮這種關(guān)系。這里的生產(chǎn)者指的就是給他人使用的功能,是純粹的生產(chǎn)者。
從這個(gè)角度出發(fā),來看下異常處理的最佳實(shí)踐。
作為消費(fèi)者
當(dāng)作為消費(fèi)者的時(shí)候,我們關(guān)心的是使用的功能是否會(huì)拋出異常,如果是,他們有哪些異常。比如:
import foo from "lucifer"; try { foo.bar(); } catch (err) { // 有哪些異常? }
當(dāng)然,理論上 foo.bar 可能產(chǎn)生任何異常,而不管它的 API 是這么寫的。但是我們關(guān)心的是可預(yù)期的異常。因此你一定希望這個(gè)時(shí)候有一個(gè) API 文檔,詳細(xì)列舉了這個(gè) API 可能產(chǎn)生的異常有哪些。
比如這個(gè) foo.bar 4 種可能的異常 分別是 A,B,C 和 D。其中 A 和 B 是我可以處理的,而 C 和 D 是我不能處理的。那么我應(yīng)該:
import foo from "lucifer"; try { foo.bar(); } catch (err) { if (err.code === "A") { return console.log("A happened"); } if (err.code === "B") { return console.log("B happened"); } throw err; }
可以看出,不管是 C 和 D,還是 API 中沒有列舉的各種可能異常,我們的做法都是直接拋出。
作為生產(chǎn)者
如果你作為生產(chǎn)者,你要做的就是提供上面提到的詳細(xì)的 API,告訴消費(fèi)者你的可能錯(cuò)誤有哪些。這樣消費(fèi)者就可以在 catch 中進(jìn)行相應(yīng)判斷,處理異常情況。
感謝各位的閱讀,以上就是“有哪些前端異常處理”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對(duì)有哪些前端異常處理這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。