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.cn/#search=video(您只需在文本框中输入您要搜索的功能)。
  • stackoverflow.com(SO)是一个论坛网站,您可以在其中提问,并让其他开发人员分享他们的解决方案,查找以前的帖子,并帮助其他开发人员。建议您在发布新问题之前,先查看是否已有您问题的答案。例如,我们在 SO 上搜索“禁用 HTML 对话框的自动对焦”,很快就找到了禁用 showModal 自动对焦的 HTML 属性

除此之外,尝试在您喜欢的搜索引擎中搜索您问题的答案。如果您有特定的错误消息,搜索它们通常会很有用——其他开发人员很可能遇到过与您相同的问题。

总结

这就是 JavaScript 调试和错误处理。很简单,是吧?可能没那么简单,但本文至少应该给您一个开端,并提供一些如何解决您将遇到的 JavaScript 相关问题的想法。

JavaScript 动态脚本模块到此结束;恭喜您学完了!在下一个模块中,我们将帮助您探索 JavaScript 框架和库。