JavaScript 除錯和錯誤處理

在本課中,我們將回到 JavaScript 除錯的話題(我們在出了什麼問題?中首次探討)。在這裡,我們將深入探討跟蹤錯誤的技術,同時也將學習如何防禦性地編寫程式碼並處理程式碼中的錯誤,從而從一開始就避免問題。

預備知識 瞭解 HTMLCSS 基礎,熟悉前面課程中介紹的 JavaScript 基礎。
學習成果
  • 使用瀏覽器開發者工具檢查頁面上執行的 JavaScript,並檢視它生成了哪些錯誤。
  • 使用 console.log()console.error() 進行除錯。
  • 使用瀏覽器開發者工具進行高階 JavaScript 除錯。
  • 使用 conditionalstry...catchthrow 進行錯誤處理。

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。如果我們檢視原始碼,相關程式碼段是這樣的:

js
function showHeroes(jsonObj) {
  const heroes = jsonObj["members"];

  for (const hero of heroes) {
    // …
  }
}

因此,一旦我們嘗試使用 jsonObj(您可能期望它是一個 JSON 物件),程式碼就會崩潰。這應該使用以下 fetch() 呼叫從外部 .json 檔案中獲取:

js
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() 的返回值,像這樣:

js
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 資料,而是一個 Promisefetch() 函式是非同步的:它返回一個 Promise,只有在實際從網路接收到響應後才會 fulfilled。在使用響應之前,我們必須等待 Promise 被 fulfilled。

console.error() 和呼叫堆疊

簡要離題一下,讓我們嘗試使用不同的控制檯方法來報告錯誤——console.error()。在您的程式碼中,替換:

js
console.log(`Response value: ${response}`);

with

js
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 的響應。然後,我們可以將生成的響應值傳遞給接受它的函式,如下所示:

js
fetch(requestURL).then((response) => {
  populateHeader(response);
  showHeroes(response);
});

儲存並重新整理,看看您的程式碼是否正常工作。劇透一下——上述更改並未解決問題。不幸的是,我們仍然有相同的錯誤

注意:總結一下,任何時候出現問題,並且某個值在程式碼的某個點看起來不是它應該有的值,您都可以使用 console.log()console.error() 或其他類似的函式來打印出該值並檢視發生了什麼。

使用 JavaScript 偵錯程式

讓我們使用瀏覽器開發者工具中更復雜的功能進一步調查這個問題:在 Firefox 中稱為JavaScript 偵錯程式

注意:其他瀏覽器也提供類似工具;Chrome 中的“Sources”標籤頁、Safari 中的偵錯程式(參閱Safari Web 開發工具)等。

在 Firefox 中,偵錯程式選項卡看起來像這樣:

Firefox debugger

  • 在左側,您可以選擇要除錯的指令碼(在此示例中我們只有一個)。
  • 中心面板顯示所選指令碼中的程式碼。
  • 右側面板顯示與當前環境相關的有用詳細資訊——斷點呼叫堆疊和當前活動的作用域

此類工具的主要功能是能夠向程式碼新增斷點——這些是程式碼執行停止的點,此時您可以檢查當前環境的狀態並檢視正在發生的事情。

我們來探索一下斷點的使用

  1. 錯誤仍然在與之前相同的行丟擲——for (const hero of heroes) {——在下面的截圖中是第 26 行。單擊中心面板中的行號以新增斷點(您會看到一個藍色箭頭出現在其上方)。
  2. 現在重新整理頁面(Cmd/Ctrl + R)——瀏覽器將暫停在該行執行程式碼。此時,右側將更新顯示以下內容:

Firefox debugger with a breakpoint

  • 斷點下,您將看到已設定斷點的詳細資訊。
  • 呼叫堆疊下,您會看到幾個條目——這基本上與我們在 console.error() 部分中看到的呼叫堆疊相同。呼叫堆疊顯示了導致當前函式被呼叫的函式列表。最上面是 showHeroes(),我們當前所在的函式,其次是 onload,它儲存了包含對 showHeroes() 呼叫的事件處理函式。
  • 作用域下,您將看到我們正在檢視的函式的當前活動作用域。我們只有三個——showHeroesblockWindow(全域性作用域)。每個作用域都可以展開以顯示程式碼執行停止時作用域內變數的值。

我們可以在這裡找到一些非常有用的資訊

  1. 展開 showHeroes 作用域——您可以從中看到 heroes 變數是 undefined,這表明訪問 jsonObjmembers 屬性(函式的第 一行)沒有成功。
  2. 您還可以看到 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 控制檯中的輸出來進行操作。

html
<!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 條件語句的一個常見用途是處理錯誤。條件語句允許您根據變數的值執行不同的程式碼。通常,您會希望防禦性地使用它,以避免在值不存在或型別錯誤時丟擲錯誤,或在值導致返回不正確結果(這可能會在以後導致問題)時捕獲錯誤。

我們來看一個例子。假設我們有一個函式,它接受一個等於使用者身高(英寸)的引數,並以米為單位返回他們的身高,精確到小數點後兩位。這可能看起來像這樣:

js
function inchesToMeters(num) {
  const mVal = (num * 2.54) / 100;
  const m2dp = mVal.toFixed(2);
  return m2dp;
}
  1. 在您的示例檔案的 <script> 元素中,宣告一個名為 heightconst 併為其分配值 70

    js
    const height = 70;
    
  2. 將上述函式複製到前一行下方。

  3. 呼叫該函式,將 height 常量作為其引數傳遞,並將返回值記錄到控制檯:

    js
    console.log(inchesToMeters(height));
    
  4. 在瀏覽器中載入示例並檢視 devtools JavaScript 控制檯。您應該會看到一個值 1.78 被記錄。

  5. 所以這在隔離情況下執行良好。但是如果提供的資料缺失或不正確怎麼辦?嘗試以下場景:

    • 如果將 height 值更改為 "70"(即以字串形式表示的 70),則示例應該……仍然正常工作。這是因為字串第一行上的計算將值強制轉換為數字資料型別。在這樣的簡單情況下,這沒有問題,但在更復雜的程式碼中,錯誤的資料可能導致各種 bug,其中一些細微且難以檢測!
    • 如果將 height 更改為無法強制轉換為數字的值,例如 "70 inches"["Bob", 70],或者 NaN,則示例應將結果返回為 NaN。這可能導致各種問題,例如如果您想在網站使用者介面中包含使用者的身高。
    • 如果你完全刪除 height 值(透過在行首新增 // 來註釋掉它),控制檯會顯示一個類似於 "Uncaught ReferenceError: height is not defined" 的錯誤,這種錯誤可能會導致你的應用程式徹底停止執行。

    顯然,這些結果都不理想。我們如何防禦不良資料?

  6. 讓我們在函式內部新增一個條件判斷,在進行計算之前測試資料是否良好。嘗試用以下程式碼替換您當前的函式:

    js
    function 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;
    }
    
  7. 現在,如果您再次嘗試前兩種情況,您會看到我們返回了稍微更有用的訊息,讓您瞭解需要做什麼來解決問題。您可以在其中放置任何您喜歡的內容,包括嘗試執行程式碼來糾正 num 的值,但不建議這樣做——此函式有一個簡單的目的,您應該在系統的其他地方處理糾正值。

    注意:if() 語句中,我們首先使用 typeof 運算子測試 num 的資料型別是否為 "number",但我們還測試 Number.isNaN(num) 是否返回 false。我們必須這樣做以防止 num 被設定為 NaN 的特定情況,因為 typeof NaN 仍然返回 "number"

  8. 但是,如果您再次嘗試第三種情況,您仍然會收到 "Uncaught ReferenceError: height is not defined" 錯誤。您無法從正在嘗試使用該值的函式內部修復值不可用的事實。

我們如何處理這個?好吧,讓我們的函式在沒有收到正確資料時返回自定義錯誤可能更好。我們首先看看如何做到這一點,然後我們將一起處理所有錯誤。

丟擲自定義錯誤

您可以使用 throw 語句,結合 Error() 建構函式,在程式碼中的任何位置丟擲自定義錯誤。讓我們看看它的實際應用。

  1. 在你的函式中,將 else 塊中 console.log() 那一行替換為以下程式碼:

    js
    throw new Error("A number was not provided. Please correct the input.");
    
  2. 再次執行您的示例,但確保 num 設定為一個錯誤(即非數字)的值。這次,您應該會看到丟擲了您的自定義錯誤,以及一個有用的呼叫堆疊來幫助您定位錯誤源(儘管請注意,訊息仍然告訴我們錯誤是“uncaught”或“unhandled”)。好的,所以錯誤很煩人,但這比成功執行函式並返回一個非數字值(可能在以後導致問題)要有用得多。

那麼,我們如何處理所有這些錯誤呢?

try...catch

try...catch 語句是專門為處理錯誤而設計的。它具有以下結構:

js
try {
  // Run some code
} catch (error) {
  // Handle any errors
}

try 塊內部,你嘗試執行一些程式碼。如果這段程式碼執行沒有丟擲錯誤,一切正常,catch 塊會被忽略。但是,如果丟擲錯誤,catch 塊會被執行,它提供了對錶示錯誤的 Error 物件的訪問,並允許你執行程式碼來處理它。

讓我們在程式碼中使用 try...catch

  1. 將指令碼末尾呼叫 inchesToMeters() 函式的 console.log() 行替換為以下程式碼塊。我們現在在 try 塊中執行 console.log() 行,並在相應的 catch 塊中處理它返回的任何錯誤。

    js
    try {
      console.log(inchesToMeters(height));
    } catch (error) {
      console.error(error);
      console.log("Insert code to handle the error");
    }
    
  2. 儲存並重新整理,您現在應該會看到兩件事:

    • 錯誤訊息和呼叫堆疊與之前相同,但這次沒有“未捕獲”或“未處理”的標籤。
    • 記錄的訊息“插入程式碼來處理錯誤”。
  3. 現在嘗試將 num 更新為一個良好(數字)值,您將看到計算結果被記錄,並且沒有錯誤訊息。

這很重要——任何丟擲的錯誤都不再是未處理的,所以它們不會導致應用程式崩潰。您可以執行任何您喜歡的程式碼來處理錯誤。上面我們只是記錄一條訊息,但例如,您可以呼叫之前執行的任何函式,要求使用者輸入他們的身高,這次要求他們糾正輸入錯誤。您甚至可以使用 if...else 語句來根據返回的錯誤型別執行不同的錯誤處理程式碼。

特性檢測

當您計劃使用可能並非所有瀏覽器都支援的新 JavaScript 功能時,功能檢測非常有用。測試該功能,然後有條件地執行程式碼,以便在支援和不支援該功能的瀏覽器中都提供可接受的體驗。舉個簡單的例子,地理位置 API(它公開了執行 Web 瀏覽器的裝置可用的位置資料)有一個主要的入口點——全域性 Navigator 物件上可用的 geolocation 屬性。因此,您可以透過使用類似於我們之前看到的 if() 結構來檢測瀏覽器是否支援地理位置:

js
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.comcaniuse.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 框架和庫。