JavaScript 除錯和錯誤處理
在本課中,我們將回到 JavaScript 除錯的話題(我們在出了什麼問題?中首次探討)。在這裡,我們將深入探討跟蹤錯誤的技術,同時也將學習如何防禦性地編寫程式碼並處理程式碼中的錯誤,從而從一開始就避免問題。
JavaScript 錯誤型別回顧
在本模組的早期,在出了什麼問題?中,我們大致探討了 JavaScript 程式中可能發生的錯誤型別,並指出它們大致可分為兩種型別——語法錯誤和邏輯錯誤。我們還幫助您理解了一些常見的 JavaScript 錯誤訊息,並向您展示瞭如何使用console.log()語句進行簡單的除錯。
在本文中,我們將更深入地探討可用於跟蹤錯誤的工具,並研究如何從一開始就防止錯誤。
程式碼 Linting
在嘗試跟蹤特定錯誤之前,您應該首先確保您的程式碼有效。使用 W3C 的標記驗證服務、CSS 驗證服務和 JavaScript Linter(例如ESLint)來確保您的程式碼有效。這可能會消除一堆錯誤,讓您可以專注於剩餘的錯誤。
程式碼編輯器外掛
一遍又一遍地將程式碼複製貼上到網頁上以檢查其有效性並不方便。我們建議在您的程式碼編輯器上安裝一個 linter 外掛,這樣您就可以在編寫程式碼時收到錯誤報告。嘗試在您的程式碼編輯器的外掛或擴充套件列表中搜索 ESLint 並安裝它。
常見的 JavaScript 問題
您需要注意一些常見的 JavaScript 問題,例如:
- 基本語法和邏輯問題(再次檢視JavaScript 故障排除)。
- 確保變數等在正確的範圍中定義,並且您不會遇到在不同位置宣告的項之間的衝突(請參閱函式範圍和衝突)。
- 關於
this的混淆,包括它適用於哪個作用域,以及因此它的值是否是您預期的。您可以閱讀什麼是“this”?以獲得一個簡單的介紹;您還應該研究像這個這樣的示例,它展示了一種典型的模式:將一個this作用域儲存到單獨的變數中,然後在巢狀函式中使用該變數,這樣您就可以確保將功能應用於正確的this作用域。 - 在使用全域性變數迭代的迴圈中錯誤地使用函式(更普遍地說是“作用域錯誤”)。
例如,在bad-for-loop.html(參閱原始碼)中,我們使用一個用var定義的變數迴圈 10 次,每次建立一個段落併為其新增一個onclick事件處理程式。當點選時,我們希望每個段落顯示一個包含其編號(建立時i的值)的警告訊息。相反,它們都報告i為 11——因為for迴圈在呼叫巢狀函式之前完成了所有迭代。
最簡單的解決方案是用 let 而不是 var 宣告迭代變數——這樣與函式關聯的 i 值對於每次迭代都是唯一的。請參閱 good-for-loop.html(另請參閱 原始碼)以獲取一個可用的版本。
- 確保非同步操作已完成,然後再嘗試使用它們返回的值。這通常意味著理解如何使用Promise:適當使用
await,或在 Promise 的then()處理程式中執行程式碼以處理非同步呼叫的結果。有關此主題的介紹,請參閱如何使用 Promise。
注意:有 Bug 的 JavaScript 程式碼:JavaScript 開發者最常犯的 10 個錯誤對這些常見錯誤及更多內容有一些不錯的討論。
瀏覽器 JavaScript 控制檯
瀏覽器開發者工具具有許多有用的功能,可幫助除錯 JavaScript。首先,JavaScript 控制檯會報告程式碼中的錯誤。
在本地複製我們的fetch-broken示例(另請參閱原始碼)。
如果您檢視控制檯,會看到一條錯誤訊息。確切的措辭因瀏覽器而異,但會類似於:“Uncaught TypeError: heroes is not iterable”,並且引用的行號是 25。如果我們檢視原始碼,相關程式碼段是這樣的:
function showHeroes(jsonObj) {
const heroes = jsonObj["members"];
for (const hero of heroes) {
// …
}
}
因此,一旦我們嘗試使用 jsonObj(您可能期望它是一個 JSON 物件),程式碼就會崩潰。這應該使用以下 fetch() 呼叫從外部 .json 檔案中獲取:
const requestURL =
"https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json";
const response = fetch(requestURL);
populateHeader(response);
showHeroes(response);
但這失敗了。
控制檯 API
您可能已經知道這段程式碼有什麼問題,但讓我們進一步探討一下如何進行調查。我們將從 Console API 開始,它允許 JavaScript 程式碼與瀏覽器的 JavaScript 控制檯進行互動。它有許多可用功能;您已經遇到過 console.log(),它會在控制檯中列印自定義訊息。
嘗試新增一個 console.log() 呼叫來記錄 fetch() 的返回值,像這樣:
const requestURL =
"https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json";
const response = fetch(requestURL);
console.log(`Response value: ${response}`);
populateHeader(response);
showHeroes(response);
在瀏覽器中重新整理頁面。這次,在錯誤訊息之前,您會在控制檯中看到一條新訊息:
Response value: [object Promise]
console.log() 的輸出顯示,fetch() 的返回值不是 JSON 資料,而是一個 Promise。fetch() 函式是非同步的:它返回一個 Promise,只有在實際從網路接收到響應後才會 fulfilled。在使用響應之前,我們必須等待 Promise 被 fulfilled。
console.error() 和呼叫堆疊
簡要離題一下,讓我們嘗試使用不同的控制檯方法來報告錯誤——console.error()。在您的程式碼中,替換:
console.log(`Response value: ${response}`);
with
console.error(`Response value: ${response}`);
儲存程式碼並重新整理瀏覽器,您現在會看到該訊息報告為錯誤,具有與下方未捕獲錯誤相同的顏色和圖示。此外,訊息旁邊現在會有一個展開/摺疊箭頭。如果您按下它,您會看到一行告訴您錯誤源自 JavaScript 檔案中的哪一行。事實上,未捕獲的錯誤行也有這個,但它有兩行:
showHeroes https://:7800/js-debug-test/index.js:25 <anonymous> https://:7800/js-debug-test/index.js:10
這意味著錯誤來自 showHeroes() 函式的第 25 行,正如我們之前所指出的。如果您檢視程式碼,您會看到第 10 行的匿名呼叫是呼叫 showHeroes() 的行。這些行被稱為呼叫堆疊,在嘗試跟蹤涉及程式碼中許多不同位置的錯誤源時非常有用。
在這種情況下,console.error() 呼叫並不是那麼有用,但如果尚無可用呼叫堆疊,它可用於生成呼叫堆疊。
修復錯誤
無論如何,讓我們回到嘗試修復我們的錯誤。我們可以透過將 then() 方法鏈式呼叫到 fetch() 呼叫的末尾來訪問已兌現 Promise 的響應。然後,我們可以將生成的響應值傳遞給接受它的函式,如下所示:
fetch(requestURL).then((response) => {
populateHeader(response);
showHeroes(response);
});
儲存並重新整理,看看您的程式碼是否正常工作。劇透一下——上述更改並未解決問題。不幸的是,我們仍然有相同的錯誤!
注意:總結一下,任何時候出現問題,並且某個值在程式碼的某個點看起來不是它應該有的值,您都可以使用 console.log()、console.error() 或其他類似的函式來打印出該值並檢視發生了什麼。
使用 JavaScript 偵錯程式
讓我們使用瀏覽器開發者工具中更復雜的功能進一步調查這個問題:在 Firefox 中稱為JavaScript 偵錯程式。
注意:其他瀏覽器也提供類似工具;Chrome 中的“Sources”標籤頁、Safari 中的偵錯程式(參閱Safari Web 開發工具)等。
在 Firefox 中,偵錯程式選項卡看起來像這樣:

- 在左側,您可以選擇要除錯的指令碼(在此示例中我們只有一個)。
- 中心面板顯示所選指令碼中的程式碼。
- 右側面板顯示與當前環境相關的有用詳細資訊——斷點、呼叫堆疊和當前活動的作用域。
此類工具的主要功能是能夠向程式碼新增斷點——這些是程式碼執行停止的點,此時您可以檢查當前環境的狀態並檢視正在發生的事情。
我們來探索一下斷點的使用
- 錯誤仍然在與之前相同的行丟擲——
for (const hero of heroes) {——在下面的截圖中是第 26 行。單擊中心面板中的行號以新增斷點(您會看到一個藍色箭頭出現在其上方)。 - 現在重新整理頁面(Cmd/Ctrl + R)——瀏覽器將暫停在該行執行程式碼。此時,右側將更新顯示以下內容:

- 在斷點下,您將看到已設定斷點的詳細資訊。
- 在呼叫堆疊下,您會看到幾個條目——這基本上與我們在
console.error()部分中看到的呼叫堆疊相同。呼叫堆疊顯示了導致當前函式被呼叫的函式列表。最上面是showHeroes(),我們當前所在的函式,其次是onload,它儲存了包含對showHeroes()呼叫的事件處理函式。 - 在作用域下,您將看到我們正在檢視的函式的當前活動作用域。我們只有三個——
showHeroes、block和Window(全域性作用域)。每個作用域都可以展開以顯示程式碼執行停止時作用域內變數的值。
我們可以在這裡找到一些非常有用的資訊
- 展開
showHeroes作用域——您可以從中看到heroes變數是undefined,這表明訪問jsonObj的members屬性(函式的第 一行)沒有成功。 - 您還可以看到
jsonObj變數儲存的是一個Response物件,而不是一個 JSON 物件。
showHeroes() 的引數是 fetch() promise 成功完成後的值。所以這個 promise 不是 JSON 格式的:它是一個 Response 物件。還需要額外一步才能將響應內容作為 JSON 物件檢索。
我們希望您自己嘗試解決這個問題。為了幫助您入門,請參閱 Response 物件的文件。如果您遇到困難,可以在 https://github.com/mdn/learning-area/tree/main/tools-testing/cross-browser-testing/javascript/fetch-fixed 找到修復後的原始碼。
注意:偵錯程式選項卡還有許多其他有用的功能,我們在這裡沒有討論,例如條件斷點和監視表示式。有關更多資訊,請參閱偵錯程式頁面。
在程式碼中處理 JavaScript 錯誤
HTML 和 CSS 是寬容的——由於語言的性質,錯誤和未識別的功能通常可以被處理。例如,CSS 會忽略未識別的屬性,而其餘程式碼通常仍能正常工作。然而,JavaScript 不像 HTML 和 CSS 那樣寬容——如果 JavaScript 引擎遇到錯誤或無法識別的語法,它通常會丟擲錯誤。
讓我們探討一種在程式碼中處理 JavaScript 錯誤的常見策略。以下部分旨在透過將以下模板檔案複製為本地計算機上的 handling-errors.html,在開頭和結尾的 <script> 和 </script> 標籤之間新增程式碼片段,然後在瀏覽器中開啟檔案並檢視開發工具 JavaScript 控制檯中的輸出來進行操作。
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Handling JS errors</title>
</head>
<body>
<script>
// Code goes below this line
</script>
</body>
</html>
條件請求
JavaScript 條件語句的一個常見用途是處理錯誤。條件語句允許您根據變數的值執行不同的程式碼。通常,您會希望防禦性地使用它,以避免在值不存在或型別錯誤時丟擲錯誤,或在值導致返回不正確結果(這可能會在以後導致問題)時捕獲錯誤。
我們來看一個例子。假設我們有一個函式,它接受一個等於使用者身高(英寸)的引數,並以米為單位返回他們的身高,精確到小數點後兩位。這可能看起來像這樣:
function inchesToMeters(num) {
const mVal = (num * 2.54) / 100;
const m2dp = mVal.toFixed(2);
return m2dp;
}
-
在您的示例檔案的
<script>元素中,宣告一個名為height的const併為其分配值70:jsconst height = 70; -
將上述函式複製到前一行下方。
-
呼叫該函式,將
height常量作為其引數傳遞,並將返回值記錄到控制檯:jsconsole.log(inchesToMeters(height)); -
在瀏覽器中載入示例並檢視 devtools JavaScript 控制檯。您應該會看到一個值
1.78被記錄。 -
所以這在隔離情況下執行良好。但是如果提供的資料缺失或不正確怎麼辦?嘗試以下場景:
- 如果將
height值更改為"70"(即以字串形式表示的70),則示例應該……仍然正常工作。這是因為字串第一行上的計算將值強制轉換為數字資料型別。在這樣的簡單情況下,這沒有問題,但在更復雜的程式碼中,錯誤的資料可能導致各種 bug,其中一些細微且難以檢測! - 如果將
height更改為無法強制轉換為數字的值,例如"70 inches"或["Bob", 70],或者NaN,則示例應將結果返回為NaN。這可能導致各種問題,例如如果您想在網站使用者介面中包含使用者的身高。 - 如果你完全刪除
height值(透過在行首新增//來註釋掉它),控制檯會顯示一個類似於 "Uncaught ReferenceError: height is not defined" 的錯誤,這種錯誤可能會導致你的應用程式徹底停止執行。
顯然,這些結果都不理想。我們如何防禦不良資料?
- 如果將
-
讓我們在函式內部新增一個條件判斷,在進行計算之前測試資料是否良好。嘗試用以下程式碼替換您當前的函式:
jsfunction inchesToMeters(num) { if (typeof num !== "number" || Number.isNaN(num)) { console.log("A number was not provided. Please correct the input."); return undefined; } const mVal = (num * 2.54) / 100; const m2dp = mVal.toFixed(2); return m2dp; } -
現在,如果您再次嘗試前兩種情況,您會看到我們返回了稍微更有用的訊息,讓您瞭解需要做什麼來解決問題。您可以在其中放置任何您喜歡的內容,包括嘗試執行程式碼來糾正
num的值,但不建議這樣做——此函式有一個簡單的目的,您應該在系統的其他地方處理糾正值。注意:在
if()語句中,我們首先使用typeof運算子測試num的資料型別是否為"number",但我們還測試Number.isNaN(num)是否返回false。我們必須這樣做以防止num被設定為NaN的特定情況,因為typeof NaN仍然返回"number"! -
但是,如果您再次嘗試第三種情況,您仍然會收到 "Uncaught ReferenceError: height is not defined" 錯誤。您無法從正在嘗試使用該值的函式內部修復值不可用的事實。
我們如何處理這個?好吧,讓我們的函式在沒有收到正確資料時返回自定義錯誤可能更好。我們首先看看如何做到這一點,然後我們將一起處理所有錯誤。
丟擲自定義錯誤
您可以使用 throw 語句,結合 Error() 建構函式,在程式碼中的任何位置丟擲自定義錯誤。讓我們看看它的實際應用。
-
在你的函式中,將
else塊中console.log()那一行替換為以下程式碼:jsthrow new Error("A number was not provided. Please correct the input."); -
再次執行您的示例,但確保
num設定為一個錯誤(即非數字)的值。這次,您應該會看到丟擲了您的自定義錯誤,以及一個有用的呼叫堆疊來幫助您定位錯誤源(儘管請注意,訊息仍然告訴我們錯誤是“uncaught”或“unhandled”)。好的,所以錯誤很煩人,但這比成功執行函式並返回一個非數字值(可能在以後導致問題)要有用得多。
那麼,我們如何處理所有這些錯誤呢?
try...catch
try...catch 語句是專門為處理錯誤而設計的。它具有以下結構:
try {
// Run some code
} catch (error) {
// Handle any errors
}
在 try 塊內部,你嘗試執行一些程式碼。如果這段程式碼執行沒有丟擲錯誤,一切正常,catch 塊會被忽略。但是,如果丟擲錯誤,catch 塊會被執行,它提供了對錶示錯誤的 Error 物件的訪問,並允許你執行程式碼來處理它。
讓我們在程式碼中使用 try...catch。
-
將指令碼末尾呼叫
inchesToMeters()函式的console.log()行替換為以下程式碼塊。我們現在在try塊中執行console.log()行,並在相應的catch塊中處理它返回的任何錯誤。jstry { console.log(inchesToMeters(height)); } catch (error) { console.error(error); console.log("Insert code to handle the error"); } -
儲存並重新整理,您現在應該會看到兩件事:
- 錯誤訊息和呼叫堆疊與之前相同,但這次沒有“未捕獲”或“未處理”的標籤。
- 記錄的訊息“插入程式碼來處理錯誤”。
-
現在嘗試將
num更新為一個良好(數字)值,您將看到計算結果被記錄,並且沒有錯誤訊息。
這很重要——任何丟擲的錯誤都不再是未處理的,所以它們不會導致應用程式崩潰。您可以執行任何您喜歡的程式碼來處理錯誤。上面我們只是記錄一條訊息,但例如,您可以呼叫之前執行的任何函式,要求使用者輸入他們的身高,這次要求他們糾正輸入錯誤。您甚至可以使用 if...else 語句來根據返回的錯誤型別執行不同的錯誤處理程式碼。
特性檢測
當您計劃使用可能並非所有瀏覽器都支援的新 JavaScript 功能時,功能檢測非常有用。測試該功能,然後有條件地執行程式碼,以便在支援和不支援該功能的瀏覽器中都提供可接受的體驗。舉個簡單的例子,地理位置 API(它公開了執行 Web 瀏覽器的裝置可用的位置資料)有一個主要的入口點——全域性 Navigator 物件上可用的 geolocation 屬性。因此,您可以透過使用類似於我們之前看到的 if() 結構來檢測瀏覽器是否支援地理位置:
if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition((position) => {
// show the location on a map, perhaps using the Google Maps API
});
} else {
// Give the user a choice of static maps instead
}
您可以在替代使用者代理嗅探中找到更多功能檢測示例。
尋求幫助
您還會遇到許多其他 JavaScript(以及 HTML 和 CSS!)問題,因此瞭解如何在網上找到答案是無價的。
最佳支援資訊來源包括 MDN(您現在就在這裡!)、stackoverflow.com 和 caniuse.com。
- 要使用 Mozilla 開發者網路 (MDN),大多數人會搜尋他們想要查詢資訊的科技詞條,加上“mdn”一詞,例如“mdn HTML video”。
- caniuse.com 提供支援資訊,以及一些有用的外部資源連結。例如,請參閱https://caniuse.com/#search=video(您只需在文字框中輸入您要搜尋的功能)。
- stackoverflow.com(SO)是一個論壇網站,您可以在其中提問,並讓其他開發人員分享他們的解決方案,查詢以前的帖子,並幫助其他開發人員。建議您在釋出新問題之前,先檢視是否已有您問題的答案。例如,我們在 SO 上搜索“停用 HTML 對話方塊的自動對焦”,很快就找到了停用 showModal 自動對焦的 HTML 屬性。
除此之外,嘗試在您喜歡的搜尋引擎中搜索您問題的答案。如果您有特定的錯誤訊息,搜尋它們通常會很有用——其他開發人員很可能遇到過與您相同的問題。
總結
這就是 JavaScript 除錯和錯誤處理。很簡單,是吧?可能沒那麼簡單,但本文至少應該給您一個開端,並提供一些如何解決您將遇到的 JavaScript 相關問題的想法。
JavaScript 動態指令碼模組到此結束;恭喜您學完了!在下一個模組中,我們將幫助您探索 JavaScript 框架和庫。