最佳的JavaScript錯(cuò)誤處理實(shí)踐
不管你的技術(shù)水平如何,錯(cuò)誤或異常是應(yīng)用程序開發(fā)者生活的一部分。Web開發(fā)的不連貫性留下了許多錯(cuò)誤能夠發(fā)生并確實(shí)已經(jīng)發(fā)生的地方。解決的關(guān)鍵在于處理任何不可預(yù)見的(或可預(yù)見的錯(cuò)誤),來控制用戶的體驗(yàn)。利用JavaScript,就有多種技術(shù)和語言特色可以用來正確地解決任何問題。
在 JavaScript 中處理錯(cuò)誤很危險(xiǎn)。如果你相信墨菲定律,會(huì)出錯(cuò)的終究會(huì)出錯(cuò)!在這篇文章中,我會(huì)深入研究 JavaScript 中的錯(cuò)誤處理。我會(huì)涉及到一些陷阱和好的實(shí)踐。最后我們會(huì)討論異步代碼處理和 Ajax。
我認(rèn)為 JavaScript 的事件驅(qū)動(dòng)模型給這門語言添加了豐富的含義。我認(rèn)為這種瀏覽器的事件驅(qū)動(dòng)引擎和報(bào)錯(cuò)機(jī)制沒什么區(qū)別。每當(dāng)發(fā)生錯(cuò)誤,就相當(dāng)于在某個(gè)時(shí)間點(diǎn)拋出一個(gè)事件。理論上說,我們?cè)?JavaScript 中可以像處理普通事件一樣去處理拋錯(cuò)事件。如果對(duì)你來說這聽起來很陌生,那請(qǐng)集中注意力開始學(xué)習(xí)下面的旅程。本文只針對(duì)客戶端的 JavaScript。
示例
本文章中用到的代碼示例在 GitHub 上可以得到,目前頁面是這個(gè)樣子的:

單擊每個(gè)按鈕都會(huì)引發(fā)一個(gè)錯(cuò)誤。它模擬產(chǎn)生一個(gè) TypeError 型的 exception。下面是對(duì)這樣一個(gè)模塊的定義及單元測(cè)試。
function error() {
var foo = {};
return foo.bar();
}
首先,這個(gè)函數(shù)定義了一個(gè)空的對(duì)象 foo。請(qǐng)注意,bar() 方法沒有在任何地方定義。我們用單元測(cè)試來驗(yàn)證這確實(shí)會(huì)引發(fā)報(bào)錯(cuò)。
it('throws a TypeError', function () {
should.throws(target, TypeError);
});
這個(gè)單元測(cè)試使用 Mocha 和 Should.js 庫中的測(cè)試斷言。Mocha 是一個(gè)運(yùn)行測(cè)試框架,should.js 是一個(gè)斷言庫。如果你不太熟悉,可以在線免費(fèi)瀏覽他們的文檔。一個(gè)測(cè)試用例通常以 it('description') 開始,以 should 中斷言的通過或者失敗結(jié)束。用這套框架的好處就是可以在 node 里進(jìn)行單元測(cè)試,而不必非在瀏覽器里。我建議大家認(rèn)真對(duì)待這些測(cè)試,因?yàn)樗鼈凃?yàn)證了 JavaScript 中很多關(guān)鍵的基本概念。
如上所示, error() 定義了一個(gè)空對(duì)象,然后試圖去調(diào)用其中的方法。因?yàn)樵谶@個(gè)對(duì)象中不存在 bar() 這個(gè)方法,它會(huì)拋出一個(gè)異常。相信我,在像 JavaScript 這種動(dòng)態(tài)語言里,任何人都有可能犯這類錯(cuò)誤。
不好的示范
先來看看不佳的錯(cuò)誤處理方式。我處理錯(cuò)誤的動(dòng)作抽象出來,綁定在按鈕上。下面是處理程序的單元測(cè)試的樣子:
function badHandler(fn) {
try {
return fn();
} catch (e) { }
return null;
}
這個(gè)處理函數(shù)接收一個(gè)回調(diào)函數(shù) fn 作為依賴。接著在處理程序的內(nèi)部調(diào)用了這個(gè)函數(shù)。這個(gè)單元測(cè)試示例了如何使用這個(gè)方法。
it('returns a value without errors', function() {
var fn = function() {
return 1;
};
var result = target(fn);
result.should.equal(1);
});
it('returns a null with errors', function() {
var fn = function() {
throw Error('random error');
};
var result = target(fn);
should(result).equal(null);
});
就像你看到的那樣,如果發(fā)生了錯(cuò)誤,這個(gè)詭異的處理方法會(huì)返回一個(gè) null。這個(gè)回調(diào)函數(shù) fn() 會(huì)指向一個(gè)合法的方法或者錯(cuò)誤。下面的單擊處理事件完成了剩下的部分。
(function (handler, bomb) {
var badButton = document.getElementById('bad');
if (badButton) {
badButton.addEventListener('click', function () {
handler(bomb);
console.log('Imagine, getting promoted for hiding mistakes');
});
}
}(badHandler, error));
糟糕的是我剛剛得到的是個(gè) null。這讓我在想確定到底發(fā)生了什么錯(cuò)誤的時(shí)候非常迷茫。這種發(fā)生錯(cuò)誤就沉默的策略覆蓋了從用戶體驗(yàn)設(shè)計(jì)到數(shù)據(jù)損壞的各個(gè)環(huán)節(jié)。隨之而來令人沮喪的一面就是,我必須花費(fèi)好幾個(gè)小時(shí)調(diào)試但是卻看不到 try-catch 代碼塊里的錯(cuò)誤。這種詭異的處理隱藏掉了代碼中所有的報(bào)錯(cuò),它假設(shè)一切都是正常的。這在某些不注重代碼質(zhì)量的團(tuán)隊(duì)中,能夠順利的執(zhí)行。但是,這些被隱藏的錯(cuò)誤最終會(huì)迫使你花幾個(gè)小時(shí)來調(diào)試代碼。在一種依賴于調(diào)用棧的多層解決方案中,有可能可以確定錯(cuò)誤來自于何處??赡茉跇O少數(shù)情況下對(duì) try-catch 做故障靜默處理是合適的。但是如果遇到錯(cuò)誤就去處理,也不是一個(gè)好方案。
這種失敗即沉默的策略會(huì)促使你在代碼中對(duì)錯(cuò)誤做更好的處理。JavaScript 提供了更優(yōu)雅的方式來處理這類問題。
不易讀的方案
繼續(xù),接下來來看看不太好理解的處理方式。我將會(huì)跳過與 DOM 緊耦合的部分。這部分與我們剛剛看過的不好的處理方式?jīng)]什么不同。重點(diǎn)是下面單元測(cè)試中處理異常的部分。
function uglyHandler(fn) {
try {
return fn();
} catch (e) {
throw Error('a new error');
}
}
it('returns a new error with errors', function () {
var fn = function () {
throw new TypeError('type error');
};
should.throws(function () {
target(fn);
}, Error);
});
比起剛剛不好的處理方式,有一個(gè)很好的進(jìn)步。異常在調(diào)用堆棧中被拋出。我喜歡的地方是錯(cuò)誤從堆棧中解放出來,這對(duì)于調(diào)試有巨大的幫助。拋出一個(gè)異常,解釋器就會(huì)在調(diào)用堆棧中一級(jí)級(jí)查看找到下一個(gè)處理函數(shù)。這就提供了很多機(jī)會(huì)在調(diào)用堆棧的頂層去處理錯(cuò)誤。不幸的是,因?yàn)樗且环N不太好理解的錯(cuò)誤,我看不到了原始錯(cuò)誤的信息。所以我必須沿著調(diào)用棧找過去,找到最原始的異常。但是至少我知道拋出異常的地方發(fā)生了一個(gè)錯(cuò)誤。
這種不易讀的錯(cuò)誤處理雖然無傷大雅但是卻使得代碼難以理解。讓我們看看瀏覽器如何處理錯(cuò)誤的。
調(diào)用棧
那么,拋出異常的一種方式就是在調(diào)用堆棧的頂層添加 try...catch 代碼塊。比如說:
function main(bomb) {
try {
bomb();
} catch (e) {
// Handle all the error things
}
}
但是,記得我說過瀏覽器是事件驅(qū)動(dòng)的嗎?是的,JavaScript 中的一個(gè)異常不過就是一個(gè)事件。解釋器會(huì)在發(fā)生異常當(dāng)前的上下文處停止程序,并拋出異常。為了證實(shí)這一點(diǎn),下面寫了一個(gè)我們能夠看到的全局的事件處理函數(shù) onerror。它看上去就是這個(gè)樣子:
window.addEventListener('error', function (e) {
var error = e.error;
console.log(error);
});
這個(gè)事件處理函數(shù)在執(zhí)行環(huán)境中捕獲錯(cuò)誤。錯(cuò)誤事件會(huì)在各種各樣的地方產(chǎn)生各種錯(cuò)誤。這種方式的重點(diǎn)是在代碼中集中處理錯(cuò)誤。就像其他的事件一樣,你可以用一個(gè)全局的處理函數(shù)去處理各種不同的錯(cuò)誤。這使得錯(cuò)誤處理只有一個(gè)單一的目標(biāo),如果你遵守 SOLID (single responsibility 單一職責(zé), open-closed 開閉, Liskov substitution 代換, interface segregation 界面分離 and dependency inversion 依賴倒置) 原則。你可以在任何時(shí)候注冊(cè)錯(cuò)誤處理函數(shù)。解釋器會(huì)循環(huán)執(zhí)行這些函數(shù)。代碼從充滿 try...catch 的語句中解放出來,變得易于調(diào)試。這種做法的關(guān)鍵是像處理 JavaScript 普通事件一樣處理發(fā)生的錯(cuò)誤。
現(xiàn)在,有了一種方法,用全局處理函數(shù)來顯示出調(diào)用棧,我們可以用它來做什么?終究,我們要利用調(diào)用棧。
記錄下調(diào)用棧
調(diào)用棧在處理修復(fù) bug 上非常有用。好消息是瀏覽器提供了這個(gè)信息。就算目前,error 對(duì)象的 stack 屬性并不是標(biāo)準(zhǔn),但是在比較新的瀏覽器里都普遍支持這個(gè)屬性。
所以,我們能夠做的很酷的事情就是把它給服務(wù)器打印出來:
window.addEventListener('error', function (e) {
var stack = e.error.stack;
var message = e.error.toString();
if (stack) {
message += '\n' + stack;
}
var xhr = new XMLHttpRequest();
xhr.open('POST', '/log', true);
xhr.send(message);
});
在代碼示例中可能不太明顯,但這個(gè)事件處理程序會(huì)被前面的錯(cuò)誤代碼觸發(fā)。如上所述,每個(gè)處理程序都有一個(gè)單一的目的,它使代碼 DRY(don't repeat yourself 不重復(fù)制造輪子)。我感興趣的是如何在服務(wù)器上捕獲這些消息。
下面是 node 運(yùn)行時(shí)的截圖:

調(diào)用堆棧對(duì)調(diào)試代碼很有幫助。永遠(yuǎn)不要低估調(diào)用棧的作用。
異步處理
哦,處理異步代碼相當(dāng)危險(xiǎn)!JavaScript 將異步代碼從當(dāng)前的執(zhí)行環(huán)境中帶出來。這意味著下面這種 try...catch 語句有個(gè)問題。
function asyncHandler(fn) {
try {
setTimeout(function () {
fn();
}, 1);
} catch (e) { }
}
這個(gè)單元測(cè)試還有剩下的部分:
it('does not catch exceptions with errors', function () {
var fn = function () {
throw new TypeError('type error');
};
failedPromise(function() {
target(fn);
}).should.be.rejectedWith(TypeError);
});
function failedPromise(fn) {
return new Promise(function(resolve, reject) {
reject(fn);
});
}
我必須用一個(gè) promise 來結(jié)束這個(gè)處理程序,以驗(yàn)證異常。注意,盡管我的代碼都在 try...catch 中,但是還是出現(xiàn)了未處理的異常。是的,try...catch 只在一個(gè)單獨(dú)的執(zhí)行環(huán)境中有作用。當(dāng)異常被拋出時(shí),解釋器的執(zhí)行環(huán)境已經(jīng)不是當(dāng)前的 try-catch 塊了。這一行為的發(fā)生與 Ajax 調(diào)用相似。所以,現(xiàn)在有了兩種選擇。一種可選方案就是在異步回調(diào)中捕捉異常:
setTimeout(function () {
try {
fn();
} catch (e) {
// Handle this async error
}
}, 1);
這種方法雖然有用,但是還有很大的提升空間。首先,try...catch 代碼塊在代碼中處處出現(xiàn)。事實(shí)上,上世紀(jì) 70 年代編程調(diào)用,他們希望他們的代碼能夠回退。另外,V8 引擎不鼓勵(lì) 在函數(shù)中使用 try…catch 代碼塊 (V8 是 Chrome 瀏覽器和 Node 使用的 JavaScript 引擎)。他們推薦在調(diào)用堆棧頂層寫這些捕獲異常的代碼塊。
所以,這告訴我們什么?我上面說過的,在任何執(zhí)行上下文中的全局錯(cuò)誤處理程序是有必要的。如果你將一個(gè)錯(cuò)誤處理程序添加到 window 對(duì)象,那就是說,您已經(jīng)完成了!遵守 DRY 和 SOLID 的原則不是很好嗎?一個(gè)全局錯(cuò)誤處理程序?qū)⒈3帜愕拇a易讀和干凈。
下面就是服務(wù)器端異常處理打印的報(bào)告。注意,如果你使用的示例中的代碼,輸出的內(nèi)容可能會(huì)根據(jù)你使用的瀏覽器不同有少許不同。

這個(gè)處理函數(shù)甚至可以告訴我哪個(gè)錯(cuò)誤是出自于異步代碼。它告訴我錯(cuò)誤來自于 setTimeout() 處理函數(shù)。太酷了!
錯(cuò)誤是每一個(gè)應(yīng)用程序的一部分,但是適當(dāng)?shù)腻e(cuò)誤處理卻不是。在處理錯(cuò)誤這件事上至少有兩種方法。一種是失敗即沉默的方案,即在代碼中忽略錯(cuò)誤。另一種是快速發(fā)現(xiàn)和解決錯(cuò)誤的方法,即在錯(cuò)誤處停止并且重現(xiàn)。我想我已經(jīng)把我贊成哪一種及為什么贊成表達(dá)地很清楚。我的選擇:不要隱藏問題。沒有人會(huì)為你程序中的意外事件去指責(zé)你。這是可以接受的,去打斷點(diǎn)、重現(xiàn)、給用戶一個(gè)嘗試。在一個(gè)并不完美的世界中,給自己一個(gè)機(jī)會(huì)是很重要的。錯(cuò)誤是不可避免的,為了解決錯(cuò)誤你做的事情才是重要的。合理地運(yùn)用JavaScript的錯(cuò)誤處理特色和自動(dòng)靈活的譯碼可以使用戶的體驗(yàn)更順暢,同時(shí)也讓開發(fā)方的診斷工作變得更輕松。
- JavaScript高級(jí)程序設(shè)計(jì) 錯(cuò)誤處理與調(diào)試學(xué)習(xí)筆記
- JavaScript 錯(cuò)誤處理與調(diào)試經(jīng)驗(yàn)總結(jié)
- Javascript 錯(cuò)誤處理的幾種方法
- JavaScript錯(cuò)誤處理
- 深入分析javascript中的錯(cuò)誤處理機(jī)制
- 全面了解javascript中的錯(cuò)誤處理機(jī)制
- Javascript 學(xué)習(xí)筆記 錯(cuò)誤處理
- 使用Chrome調(diào)試JavaScript的斷點(diǎn)設(shè)置和調(diào)試技巧
- js調(diào)試工具Console命令詳解
- js調(diào)試工具console.log()方法查看js代碼的執(zhí)行情況
- javascript代碼調(diào)試之console.log 用法圖文詳解
- JS錯(cuò)誤處理與調(diào)試操作實(shí)例分析
相關(guān)文章
360doc網(wǎng)站不登錄就無法復(fù)制內(nèi)容的解決方法
這篇文章主要介紹了360doc網(wǎng)站不登錄就無法復(fù)制內(nèi)容的解決方法,需要的朋友可以參考下2018-01-01
JavaScript詳解類數(shù)組與可迭代對(duì)象的實(shí)現(xiàn)原理
這篇文章主要介紹了JavaScript詳解類數(shù)組與可迭代對(duì)象的實(shí)現(xiàn)原理,ES6中引入了迭代器與可迭代對(duì)象的概念,并且提供了對(duì)可迭代對(duì)象的相關(guān)支持,如for...of循環(huán),Map(iterable)構(gòu)造器,展開語法...等。讓我們對(duì)數(shù)組外的數(shù)據(jù)集合的遍歷操作也得到極大簡(jiǎn)化2022-06-06
JS 組件系列之Bootstrap Table的凍結(jié)列功能徹底解決高度問題
這篇文章主要介紹了JS 組件系列之Bootstrap Table的凍結(jié)列功能徹底解決高度問題,需要的朋友可以參考下2017-06-06
js自動(dòng)查找select下拉的菜單并選擇(示例代碼)
這篇文章主要介紹了js自動(dòng)查找select下拉的菜單并選擇(示例代碼)需要的朋友可以過來參考下,希望對(duì)大家有所幫助2014-02-02
微信小程序圖片上傳組件實(shí)現(xiàn)圖片拖拽排序
最近在寫小程序過程中遇到一個(gè)拖拽排序需求,上網(wǎng)一頓搜索未果,遂自行實(shí)現(xiàn),下面這篇文章主要給大家介紹了關(guān)于微信小程序圖片上傳組件實(shí)現(xiàn)圖片拖拽排序的相關(guān)資料,需要的朋友可以參考下2022-03-03
一個(gè)不錯(cuò)的可以檢測(cè)多中瀏覽器的函數(shù)和其它功能
一個(gè)不錯(cuò)的可以檢測(cè)多中瀏覽器的函數(shù)和其它功能...2007-04-04
JS圖片自動(dòng)輪換效果實(shí)現(xiàn)思路附截圖
這篇文章主要介紹了JS實(shí)現(xiàn)的圖片自動(dòng)輪換效果,有具體的實(shí)現(xiàn)思路及截圖,希望對(duì)大家學(xué)習(xí)有所幫助2014-04-04

