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.cn/#search=video(您只需在文本框中输入您要搜索的功能)。
- stackoverflow.com(SO)是一个论坛网站,您可以在其中提问,并让其他开发人员分享他们的解决方案,查找以前的帖子,并帮助其他开发人员。建议您在发布新问题之前,先查看是否已有您问题的答案。例如,我们在 SO 上搜索“禁用 HTML 对话框的自动对焦”,很快就找到了禁用 showModal 自动对焦的 HTML 属性。
除此之外,尝试在您喜欢的搜索引擎中搜索您问题的答案。如果您有特定的错误消息,搜索它们通常会很有用——其他开发人员很可能遇到过与您相同的问题。
总结
这就是 JavaScript 调试和错误处理。很简单,是吧?可能没那么简单,但本文至少应该给您一个开端,并提供一些如何解决您将遇到的 JavaScript 相关问题的想法。
JavaScript 动态脚本模块到此结束;恭喜您学完了!在下一个模块中,我们将帮助您探索 JavaScript 框架和库。