W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗值獎勵
不管你多么精通編程,有時我們的腳本總還是會出現(xiàn)錯誤??赡苁且驗槲覀兊木帉懗鲥e,或是與預(yù)期不同的用戶輸入,或是錯誤的服務(wù)端響應(yīng)以及其他數(shù)千種原因。
通常,如果發(fā)生錯誤,腳本就會“死亡”(立即停止),并在控制臺將錯誤打印出來。
但是有一種語法結(jié)構(gòu) try...catch
,它使我們可以“捕獲(catch)”錯誤,因此腳本可以執(zhí)行更合理的操作,而不是死掉。
try...catch
結(jié)構(gòu)由兩部分組成:try
和 catch
:
try {
// 代碼...
} catch (err) {
// 錯誤捕獲
}
它按照以下步驟執(zhí)行:
try {...}
? 中的代碼。catch (err)
?:執(zhí)行到 ?try
? 的末尾并跳過 ?catch
? 繼續(xù)執(zhí)行。try
? 執(zhí)行停止,控制流轉(zhuǎn)向 ?catch (err)
? 的開頭。變量 ?err
?(我們可以使用任何名稱)將包含一個 error 對象,該對象包含了所發(fā)生事件的詳細(xì)信息。
所以,try {...}
塊內(nèi)的 error 不會殺死腳本 —— 我們有機(jī)會在 catch
中處理它。
讓我們來看一些例子。
alert (1)
? 和 ?(2)
?:try {
alert('開始執(zhí)行 try 中的內(nèi)容'); // (1) <--
// ...這里沒有 error
alert('try 中的內(nèi)容執(zhí)行完畢'); // (2) <--
} catch (err) {
alert('catch 被忽略,因為沒有 error'); // (3)
}
(1)
和 (3)
行的 alert
中的內(nèi)容:try {
alert('開始執(zhí)行 try 中的內(nèi)容'); // (1) <--
lalala; // error,變量未定義!
alert('try 的末尾(未執(zhí)行到此處)'); // (2)
} catch (err) {
alert(`出現(xiàn)了 error!`); // (3) <--
}
?
try...catch
? 僅對運行時的 error 有效要使得
try...catch
能工作,代碼必須是可執(zhí)行的。換句話說,它必須是有效的 JavaScript 代碼。
如果代碼包含語法錯誤,那么
try..catch
將無法正常工作,例如含有不匹配的花括號:
try { {{{{{{{{{{{{ } catch (err) { alert("引擎無法理解這段代碼,它是無效的"); }
JavaScript 引擎首先會讀取代碼,然后運行它。在讀取階段發(fā)生的錯誤被稱為“解析時間(parse-time)”錯誤,并且無法恢復(fù)(從該代碼內(nèi)部)。這是因為引擎無法理解該代碼。
所以,
try...catch
只能處理有效代碼中出現(xiàn)的錯誤。這類錯誤被稱為“運行時的錯誤(runtime errors)”,有時被稱為“異常(exceptions)”。
?
try...catch
? 同步執(zhí)行如果在“計劃的(scheduled)”代碼中發(fā)生異常,例如在
setTimeout
中,則try...catch
不會捕獲到異常:
try { setTimeout(function() { noSuchVariable; // 腳本將在這里停止運行 }, 1000); } catch (err) { alert( "不工作" ); }
因為
try...catch
包裹了計劃要執(zhí)行的函數(shù),該函數(shù)本身要稍后才執(zhí)行,這時引擎已經(jīng)離開了try...catch
結(jié)構(gòu)。
為了捕獲到計劃的(scheduled)函數(shù)中的異常,那么
try...catch
必須在這個函數(shù)內(nèi):
setTimeout(function() { try { noSuchVariable; // try...catch 處理 error 了! } catch { alert( "error 被在這里捕獲了!" ); } }, 1000);
發(fā)生錯誤時,JavaScript 會生成一個包含有關(guān)此 error 詳細(xì)信息的對象。然后將該對象作為參數(shù)傳遞給 catch
:
try {
// ...
} catch (err) { // <-- “error 對象”,也可以用其他參數(shù)名代替 err
// ...
}
對于所有內(nèi)建的 error,error 對象具有兩個主要屬性:
?name
?
Error 名稱。例如,對于一個未定義的變量,名稱是 ?"ReferenceError"
?。
?message
?
關(guān)于 error 的詳細(xì)文字描述。
還有其他非標(biāo)準(zhǔn)的屬性在大多數(shù)環(huán)境中可用。其中被最廣泛使用和支持的是:
?stack
?
當(dāng)前的調(diào)用棧:用于調(diào)試目的的一個字符串,其中包含有關(guān)導(dǎo)致 error 的嵌套調(diào)用序列的信息。
例如:
try {
lalala; // error, variable is not defined!
} catch (err) {
alert(err.name); // ReferenceError
alert(err.message); // lalala is not defined
alert(err.stack); // ReferenceError: lalala is not defined at (...call stack)
// 也可以將一個 error 作為整體顯示出來
// error 信息被轉(zhuǎn)換為像 "name: message" 這樣的字符串
alert(err); // ReferenceError: lalala is not defined
}
最近新增的特性
這是一個最近添加到 JavaScript 的特性。 舊式瀏覽器可能需要 polyfills.
如果我們不需要 error 的詳細(xì)信息,catch
也可以忽略它:
try {
// ...
} catch { // <-- 沒有 (err)
// ...
}
讓我們一起探究一下真實場景中 try...catch
的用例。
正如我們所知道的,JavaScript 支持 JSON.parse(str) 方法來解析 JSON 編碼的值。
通常,它被用來解析從網(wǎng)絡(luò)、服務(wù)器或是其他來源接收到的數(shù)據(jù)。
我們收到數(shù)據(jù)后,然后像下面這樣調(diào)用 JSON.parse
:
let json = '{"name":"John", "age": 30}'; // 來自服務(wù)器的數(shù)據(jù)
let user = JSON.parse(json); // 將文本表示轉(zhuǎn)換成 JavaScript 對象
// 現(xiàn)在 user 是一個解析自 json 字符串的有自己屬性的對象
alert( user.name ); // John
alert( user.age ); // 30
你可以在 JSON 方法,toJSON 一章中找到更多關(guān)于 JSON 的詳細(xì)內(nèi)容。
如果 json
格式錯誤,JSON.parse
就會生成一個 error,因此腳本就會“死亡”。
我們對此滿意嗎?當(dāng)然不!
如果這樣做,當(dāng)拿到的數(shù)據(jù)出了問題,那么訪問者永遠(yuǎn)都不會知道原因(除非他們打開開發(fā)者控制臺)。代碼執(zhí)行失敗卻沒有提示信息,這真的是很糟糕的用戶體驗。
讓我們用 try...catch
來處理這個 error:
let json = "{ bad json }";
try {
let user = JSON.parse(json); // <-- 當(dāng)出現(xiàn) error 時...
alert( user.name ); // 不工作
} catch (err) {
// ...執(zhí)行會跳轉(zhuǎn)到這里并繼續(xù)執(zhí)行
alert( "很抱歉,數(shù)據(jù)有錯誤,我們會嘗試再請求一次。" );
alert( err.name );
alert( err.message );
}
在這兒,我們將 catch
塊僅僅用于顯示信息,但我們可以做更多的事:發(fā)送一個新的網(wǎng)絡(luò)請求,向訪問者建議一個替代方案,將有關(guān)錯誤的信息發(fā)送給記錄日志的設(shè)備,……。所有這些都比代碼“死掉”好得多。
如果這個 json
在語法上是正確的,但是沒有所必須的 name
屬性該怎么辦?
像這樣:
let json = '{ "age": 30 }'; // 不完整的數(shù)據(jù)
try {
let user = JSON.parse(json); // <-- 沒有 error
alert( user.name ); // 沒有 name!
} catch (err) {
alert( "doesn't execute" );
}
這里 JSON.parse
正常執(zhí)行,但缺少 name
屬性對我們來說確實是個 error。
為了統(tǒng)一進(jìn)行 error 處理,我們將使用 throw
操作符。
throw
操作符會生成一個 error 對象。
語法如下:
throw <error object>
技術(shù)上講,我們可以將任何東西用作 error 對象。甚至可以是一個原始類型數(shù)據(jù),例如數(shù)字或字符串,但最好使用對象,最好使用具有 name
和 message
屬性的對象(某種程度上保持與內(nèi)建 error 的兼容性)。
JavaScript 中有很多內(nèi)建的標(biāo)準(zhǔn) error 的構(gòu)造器:Error
,SyntaxError
,ReferenceError
,TypeError
等。我們也可以使用它們來創(chuàng)建 error 對象。
它們的語法是:
let error = new Error(message);
// 或
let error = new SyntaxError(message);
let error = new ReferenceError(message);
// ...
對于內(nèi)建的 error(不是對于其他任何對象,僅僅是對于 error),name
屬性剛好就是構(gòu)造器的名字。message
則來自于參數(shù)(argument)。
例如:
let error = new Error("Things happen o_O");
alert(error.name); // Error
alert(error.message); // Things happen o_O
讓我們來看看 JSON.parse
會生成什么樣的 error:
try {
JSON.parse("{ bad json o_O }");
} catch(err) {
alert(err.name); // SyntaxError
alert(err.message); // Unexpected token b in JSON at position 2
}
正如我們所看到的, 那是一個 SyntaxError
。
在我們的示例中,缺少 name
屬性就是一個 error,因為用戶必須有一個 name
。
所以,讓我們拋出這個 error。
let json = '{ "age": 30 }'; // 不完整的數(shù)據(jù)
try {
let user = JSON.parse(json); // <-- 沒有 error
if (!user.name) {
throw new SyntaxError("數(shù)據(jù)不全:沒有 name"); // (*)
}
alert( user.name );
} catch(err) {
alert( "JSON Error: " + err.message ); // JSON Error: 數(shù)據(jù)不全:沒有 name
}
在 (*)
標(biāo)記的這一行,throw
操作符生成了包含著我們所給定的 message
的 SyntaxError
,與 JavaScript 自己生成的方式相同。try
的執(zhí)行立即停止,控制流轉(zhuǎn)向 catch
塊。
現(xiàn)在,catch
成為了所有 error 處理的唯一場所:對于 JSON.parse
和其他情況都適用。
在上面的例子中,我們使用 try...catch
來處理不正確的數(shù)據(jù)。但是在 try {...}
塊中是否可能發(fā)生 另一個預(yù)料之外的 error?例如編程錯誤(未定義變量)或其他錯誤,而不僅僅是這種“不正確的數(shù)據(jù)”。
例如:
let json = '{ "age": 30 }'; // 不完整的數(shù)據(jù)
try {
user = JSON.parse(json); // <-- 忘記在 user 前放置 "let"
// ...
} catch (err) {
alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
// (實際上并沒有 JSON Error)
}
當(dāng)然,一切皆有可能!程序員也會犯錯。即使是被數(shù)百萬人使用了幾十年的開源項目中,也可能突然被發(fā)現(xiàn)了一個漏洞,并導(dǎo)致可怕的黑客入侵。
在我們的例子中,try...catch
旨在捕獲“數(shù)據(jù)不正確”的 error。但實際上,catch 會捕獲到 所有 來自于 try
的 error。在這兒,它捕獲到了一個預(yù)料之外的 error,但仍然拋出的是同樣的 "JSON Error"
信息。這是不正確的,并且也會使代碼變得更難以調(diào)試。
為了避免此類問題,我們可以采用“重新拋出”技術(shù)。規(guī)則很簡單:
catch
應(yīng)該只處理它知道的 error,并“拋出”所有其他 error。
“再次拋出(rethrowing)”技術(shù)可以被更詳細(xì)地解釋為:
catch (err) {...}
? 塊中,我們對 error 對象 ?err
? 進(jìn)行分析。throw err
?。通常,我們可以使用 ?instanceof
? 操作符判斷錯誤類型:
try {
user = { /*...*/ };
} catch (err) {
if (err instanceof ReferenceError) {
alert('ReferenceError'); // 訪問一個未定義(undefined)的變量產(chǎn)生了 "ReferenceError"
}
}
我們還可以從 err.name
屬性中獲取錯誤的類名。所有原生的錯誤都有這個屬性。另一種方式是讀取 err.constructor.name
。
在下面的代碼中,我們使用“再次拋出”,以達(dá)到在 catch
中只處理 SyntaxError
的目的:
let json = '{ "age": 30 }'; // 不完整的數(shù)據(jù)
try {
let user = JSON.parse(json);
if (!user.name) {
throw new SyntaxError("數(shù)據(jù)不全:沒有 name");
}
blabla(); // 預(yù)料之外的 error
alert( user.name );
} catch (err) {
if (err instanceof SyntaxError) {
alert( "JSON Error: " + err.message );
} else {
throw err; // 再次拋出 (*)
}
}
如果 (*)
標(biāo)記的這行 catch
塊中的 error 從 try...catch
中“掉了出來”,那么它也可以被外部的 try...catch
結(jié)構(gòu)(如果存在)捕獲到,如果外部不存在這種結(jié)構(gòu),那么腳本就會被殺死。
所以,catch
塊實際上只處理它知道該如何處理的 error,并“跳過”所有其他的 error。
下面這個示例演示了這種類型的 error 是如何被另外一級 try...catch
捕獲的:
function readData() {
let json = '{ "age": 30 }';
try {
// ...
blabla(); // error!
} catch (err) {
// ...
if (!(err instanceof SyntaxError)) {
throw err; // 再次拋出(不知道如何處理它)
}
}
}
try {
readData();
} catch (err) {
alert( "External catch got: " + err ); // 捕獲了它!
}
上面這個例子中的 readData
只知道如何處理 SyntaxError
,而外部的 try...catch
知道如何處理任意的 error。
等一下,以上并不是所有內(nèi)容。
try...catch
結(jié)構(gòu)可能還有一個代碼子句(clause):finally
。
如果它存在,它在所有情況下都會被執(zhí)行:
try
? 之后,如果沒有 error,catch
? 之后,如果有 error。該擴(kuò)展語法如下所示:
try {
... 嘗試執(zhí)行的代碼 ...
} catch (err) {
... 處理 error ...
} finally {
... 總是會執(zhí)行的代碼 ...
}
試試運行這段代碼:
try {
alert( 'try' );
if (confirm('Make an error?')) BAD_CODE();
} catch (err) {
alert( 'catch' );
} finally {
alert( 'finally' );
}
這段代碼有兩種執(zhí)行方式:
try -> catch -> finally
?。try -> finally
?。finally
子句(clause)通常用在:當(dāng)我們開始做某事的時候,希望無論出現(xiàn)什么情況都要完成完成某個任務(wù)。
例如,我們想要測量一個斐波那契數(shù)字函數(shù) fib(n)
執(zhí)行所需要花費的時間。通常,我們可以在運行它之前開始測量,并在運行完成時結(jié)束測量。但是,如果在該函數(shù)調(diào)用期間出現(xiàn) error 該怎么辦?特別是,下面這段 fib(n)
的實現(xiàn)代碼在遇到負(fù)數(shù)或非整數(shù)數(shù)字時會返回一個 error。
無論如何,finally
子句都是一個結(jié)束測量的好地方。
在這兒,finally
能夠保證在兩種情況下都能正確地測量時間 —— 成功執(zhí)行 fib
以及 fib
中出現(xiàn) error 時:
let num = +prompt("輸入一個正整數(shù)?", 35)
let diff, result;
function fib(n) {
if (n < 0 || Math.trunc(n) != n) {
throw new Error("不能是負(fù)數(shù),并且必須是整數(shù)。");
}
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}
let start = Date.now();
try {
result = fib(num);
} catch (err) {
result = 0;
} finally {
diff = Date.now() - start;
}
alert(result || "出現(xiàn)了 error");
alert( `執(zhí)行花費了 ${diff}ms` );
你可以通過運行上面這段代碼并在 prompt
彈窗中輸入 35
來進(jìn)行檢查 —— 代碼運行正常,先執(zhí)行 try
然后是 finally
。如果你輸入的是 -1
—— 將立即出現(xiàn) error,執(zhí)行將只花費 0ms
。以上兩種情況下的時間測量都正確地完成了。
換句話說,函數(shù) fib
以 return
還是 throw
完成都無關(guān)緊要。在這兩種情況下都會執(zhí)行 finally
子句。
變量和 ?
try...catch...finally
? 中的局部變量請注意,上面代碼中的
result
和diff
變量都是在try...catch
之前 聲明的。
否則,如果我們使用
let
在try
塊中聲明變量,那么該變量將只在try
塊中可見。
?
finally
? 和 ?return
?
finally
子句適用于try...catch
的 任何 出口。這包括顯式的return
。
在下面這個例子中,在
try
中有一個return
。在這種情況下,finally
會在控制轉(zhuǎn)向外部代碼前被執(zhí)行。
function func() { try { return 1; } catch (err) { /* ... */ } finally { alert( 'finally' ); } } alert( func() ); // 先執(zhí)行 finally 中的 alert,然后執(zhí)行這個 alert
?
try...finally
?沒有
catch
子句的try...finally
結(jié)構(gòu)也很有用。當(dāng)我們不想在這兒處理 error(讓它們 fall through),但是需要確保我們啟動的處理需要被完成。
function func() { // 開始執(zhí)行需要被完成的操作(比如測量) try { // ... } finally { // 完成前面我們需要完成的那件事,即使 try 中的執(zhí)行失敗了 } }
上面的代碼中,由于沒有
catch
,所以try
中的 error 總是會使代碼執(zhí)行跳轉(zhuǎn)至函數(shù)func()
外。但是,在跳出之前需要執(zhí)行finally
中的代碼。
環(huán)境特定
這個部分的內(nèi)容并不是 JavaScript 核心的一部分。
設(shè)想一下,在 try...catch
結(jié)構(gòu)外有一個致命的 error,然后腳本死亡了。這個 error 就像編程錯誤或其他可怕的事兒那樣。
有什么辦法可以用來應(yīng)對這種情況嗎?我們可能想要記錄這個 error,并向用戶顯示某些內(nèi)容(通常用戶看不到錯誤信息)等。
規(guī)范中沒有相關(guān)內(nèi)容,但是代碼的執(zhí)行環(huán)境一般會提供這種機(jī)制,因為它確實很有用。例如,Node.JS 有 process.on("uncaughtException")
。在瀏覽器中,我們可以將一個函數(shù)賦值給特殊的 window.onerror 屬性,該函數(shù)將在發(fā)生未捕獲的 error 時執(zhí)行。
語法如下:
window.onerror = function(message, url, line, col, error) {
// ...
};
?message
?
error 信息。
?url
?
發(fā)生 error 的腳本的 URL。
?line
?,?col
?
發(fā)生 error 處的代碼的行號和列號。
?error
?
error 對象。
例如:
<script>
window.onerror = function(message, url, line, col, error) {
alert(`${message}\n At ${line}:${col} of ${url}`);
};
function readData() {
badFunc(); // 啊,出問題了!
}
readData();
</script>
全局錯誤處理程序 window.onerror
的作用通常不是恢復(fù)腳本的執(zhí)行 —— 如果發(fā)生編程錯誤,恢復(fù)腳本的執(zhí)行幾乎是不可能的,它的作用是將錯誤信息發(fā)送給開發(fā)者。
也有針對這種情況提供 error 日志的 Web 服務(wù),例如 https://errorception.com 或 http://www.muscula.com。
它們會像這樣運行:
window.onerror
? 函數(shù)。?try...catch
? 結(jié)構(gòu)允許我們處理執(zhí)行過程中出現(xiàn)的 error。從字面上看,它允許“嘗試”運行代碼并“捕獲”其中可能發(fā)生的 error。
語法如下:
try {
// 執(zhí)行此處代碼
} catch (err) {
// 如果發(fā)生 error,跳轉(zhuǎn)至此處
// err 是一個 error 對象
} finally {
// 無論怎樣都會在 try/catch 之后執(zhí)行
}
這兒可能會沒有 catch
或者沒有 finally
,所以 try...catch
或 try...finally
都是可用的。
Error 對象包含下列屬性:
message
? _ 人類可讀的 error 信息。name
? —— 具有 error 名稱的字符串(Error 構(gòu)造器的名稱)。stack
?(沒有標(biāo)準(zhǔn),但得到了很好的支持)—— Error 發(fā)生時的調(diào)用棧。如果我們不需要 error 對象,我們可以通過使用 catch {
而不是 catch (err) {
來省略它。
我們也可以使用 throw
操作符來生成自定義的 error。從技術(shù)上講,throw
的參數(shù)可以是任何東西,但通常是繼承自內(nèi)建的 Error
類的 error 對象。下一章我們會詳細(xì)介紹擴(kuò)展 error。
再次拋出(rethrowing)是一種錯誤處理的重要模式:catch
塊通常期望并知道如何處理特定的 error 類型,因此它應(yīng)該再次拋出它不知道的 error。
即使我們沒有 try...catch
,大多數(shù)執(zhí)行環(huán)境也允許我們設(shè)置“全局” error 處理程序來捕獲“掉出(fall out)”的 error。在瀏覽器中,就是 window.onerror
。
比較下面兩個代碼片段。
finally
在 try..catch
之后執(zhí)行代碼:try {
// 工作
} catch (err) {
// 處理 error
} finally {
// 清理工作空間
}
try...catch
之后:try {
// 工作
} catch (err) {
// 處理 error
}
// 清理工作空間
我們肯定需要在工作后進(jìn)行清理,無論工作過程中是否有 error 都不影響。
在這兒使用 ?finally
? 更有優(yōu)勢,還是說兩個代碼片段效果一樣?如果在這有這樣的優(yōu)勢,如果需要,請舉例說明。
當(dāng)我們看函數(shù)中的代碼時,差異就變得很明顯了。
如果在這有“跳出” try..catch
的行為,那么這兩種方式的表現(xiàn)就不同了。
例如,當(dāng) try...catch
中有 return
時。finally
子句會在 try...catch
的 任意 出口處起作用,即使是通過 return
語句退出的也是如此:在 try...catch
剛剛執(zhí)行完成后,但在調(diào)用代碼獲得控制權(quán)之前。
function f() {
try {
alert('start');
return "result";
} catch (err) {
/// ...
} finally {
alert('cleanup!');
}
}
f(); // cleanup!
……或者當(dāng)有 throw
時,如下所示:
function f() {
try {
alert('start');
throw new Error("一個 error");
} catch (err) {
// ...
if("無法處理此 error") {
throw err;
}
} finally {
alert('cleanup!')
}
}
f(); // cleanup!
正是這里的 finally
保證了 cleanup。如果我們只是將代碼放在函數(shù) f
的末尾,則在這些情況下它不會運行。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: