mirror of
https://github.com/apachecn/eloquent-js-3e-zh.git
synced 2025-05-24 04:22:20 +00:00
8.
This commit is contained in:
parent
b657a0b6aa
commit
acbef990e5
179
8.md
179
8.md
@ -1,20 +1,20 @@
|
|||||||
## 八、处理缺陷与错误
|
## 八、Bug 和错误
|
||||||
|
|
||||||
程序是开发人员智慧的结晶。而程序中出现的缺陷或错误则是设计本身,或者是在设计转化成程序代码的过程中引入的。
|
> 调试的难度是开始编写代码的两倍。 因此,如果您尽可能巧妙地编写代码,那么根据定义,您的智慧不足以进行调试。
|
||||||
|
>
|
||||||
|
> Brian Kernighan 和 P.J. Plauger,《The Elements of Programming Style》
|
||||||
|
|
||||||
我们通常将程序中的缺陷称为bug。而bug则有可能是开发人员编写的代码导致的,也有可能是程序与其他系统交互时产生的。有些程序中的bug很容易被发现,而有些则会在系统中隐藏很久,难以发现。
|
计算机程序中的缺陷通常称为 bug。 它让程序员觉得很好,将它们想象成小事,只是碰巧进入我们的作品。 实际上,我们当然会把它们放在那里。
|
||||||
|
|
||||||
通常来说,只有在程序执行的代码逻辑不符合开发人员意图的时候,问题才比较容易被发现。有时这种情况难以避免。当程序要求用户输入年龄的时候,用户却将“橘子”作为输入,我们的程序可能就很难处理这种情况了。我们必须采用某种方法来预备并处理这种情况的发生。
|
如果一个程序是思想的结晶,你可以粗略地将错误分为因为思想混乱引起的错误,以及思想转换为代码时引入的错误。 前者通常比后者更难诊断和修复。
|
||||||
|
|
||||||
### 8.1 开发人员造成的问题
|
### 语言
|
||||||
|
|
||||||
当问题出现在开发人员编写的代码中时,我们的目标十分简单,就是找到问题的所在并修复它们。这类问题十分广泛,简单到代码的拼写错误,细微到一些特殊情况下计算机会将代码错误地理解成另一种含义并执行,然后导致错误的发生。而后者往往需要数周的时间才能被发现。
|
计算机能够自动地向我们指出许多错误,如果它足够了解我们正在尝试做什么。 但是这里 JavaScript 的宽松是一个障碍。 它的绑定和属性概念很模糊,在实际运行程序之前很少会发现拼写错误。 即使这样,它也允许你做一些不会报错的无意义的事情,比如计算`true *'monkey'`。
|
||||||
|
|
||||||
不同的编程语言为开发人员寻找错误提供了不同程度的支持。不幸的是,JavaScript几乎没有为开发人员提供任何帮助。一些语言在执行程序之前需要确定程序中所有变量和表达式的类型,在类型不一致时也会立即向开发者报告。JavaScript只会在实际执行程序时判断类型,即便如此,JavaScript也允许开发人员编写一些明显不对的代码,却不发出任何警告,比如x=true*“monkey”。
|
JavaScript 有一些报错的事情。 编写不符合语言语法的程序会立即使计算机报错。 其他的东西,比如调用不是函数的东西,或者在未定义的值上查找属性,会导致在程序尝试执行操作时报告错误。
|
||||||
|
|
||||||
虽说如此,JavaScript还是会在遇到某些问题时报告错误。当JavaScript遇到程序中的语法错误时,会立即报告错误。此外,当程序执行到一些明显不对的代码逻辑,比如调用的值根本不是函数,或尝试从undefined中获取属性时,都会引发错误并将错误通知给开发人员。
|
不过,JavaScript 在处理无意义的计算时,会仅仅返回NaN(表示不是数字)或undefined这样的结果。程序会认为其执行的代码毫无问题并顺利运行下去,要等到随后的运行过程中才会出现问题,而此时已经有许多函数使用了这个无意义的值。程序执行中也可能不会遇到任何错误,只会产生错误的程序输出。找出这类错误的源头是非常困难的。
|
||||||
|
|
||||||
不过,JavaScript在处理无意义的计算时,会直接返回NaN(表示不是数字)或undefined这样的结果。程序会认为其执行的代码毫无问题并顺利运行下去,要等到随后的运行过程中才会出现问题,而此时已经有许多函数使用了这个无意义的值。程序执行中也可能不会遇到任何错误,只会产生错误的程序输出。找出这类错误的源头是非常困难的。
|
|
||||||
|
|
||||||
我们将查找程序中的错误或者bug的过程称为调试(debug)。
|
我们将查找程序中的错误或者bug的过程称为调试(debug)。
|
||||||
|
|
||||||
@ -25,95 +25,111 @@
|
|||||||
```
|
```
|
||||||
function canYouSpotTheProblem() {
|
function canYouSpotTheProblem() {
|
||||||
"use strict";
|
"use strict";
|
||||||
for (counter = 0; counter < 10; counter++)
|
for (counter = 0; counter < 10; counter++) {
|
||||||
console.log("Happy happy");
|
console.log("Happy happy");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canYouSpotTheProblem();
|
canYouSpotTheProblem();
|
||||||
// → ReferenceError: counter is not defined
|
// → ReferenceError: counter is not defined
|
||||||
```
|
```
|
||||||
|
|
||||||
当你忘记在变量前添加var时,比如示例中的counter。在默认情况下,JavaScript会悄悄创建一个全局变量,并使用这个全局变量。但在严格模式中这种行为就会引发错误。这种特性非常有用。不过需要注意的是如果代码中已经将该变量定义为全局变量,那么这里就不会发生任何错误,而是直接将值赋予已经创建的全局变量。
|
通常,当您忘记在绑定前面放置`let`时,就像在示例中的`counter`一样,JavaScript 静静地创建一个全局绑定并使用它。 在严格模式下,它会报告错误。 这非常有帮助。 但是,应该指出的是,当绑定已经作为全局绑定存在时,这是行不通的。 在这种情况下,循环仍然会悄悄地覆盖绑定的值。
|
||||||
|
|
||||||
严格模式中还有另一个变化,如果我们没有将函数作为方法调用,函数中的this会绑定undefined。而如果没有在严格模式中,函数的this会引用一个全局对象。因此如果你在严格模式中不小心错误调用了一个方法或构造函数,JavaScript会立即报告错误(因为会尝试读取this),而不是创建一个全局对象并读取全局对象。
|
严格模式中的另一个变化是,在未被作为方法而调用的函数中,`this`绑定持有值`undefined`。 当在严格模式之外进行这样的调用时,`this`引用全局作用域对象,该对象的属性是全局绑定。 因此,如果您在严格模式下不小心错误地调用方法或构造函数,JavaScript 会在尝试从`this`读取某些内容时产生错误,而不是愉快地写入全局作用域。
|
||||||
|
|
||||||
思考下面的代码,代码中调用构造函数时并未使用关键字new,因此this不会引用新创建的对象。
|
例如,考虑下面的代码,该代码不带`new`关键字调用构造函数,以便其`this`不会引用新构造的对象:
|
||||||
|
|
||||||
```
|
```
|
||||||
function Person(name) { this.name = name; }
|
function Person(name) { this.name = name; }
|
||||||
var ferdinand = Person("Ferdinand"); // oops
|
let ferdinand = Person("Ferdinand"); // oops
|
||||||
console.log(name);
|
console.log(name);
|
||||||
// → Ferdinand
|
// → Ferdinand
|
||||||
```
|
```
|
||||||
|
|
||||||
虽然我们错误调用了Person,代码也可以执行成功,但会返回一个未定义值,并创建名为name的全局变量。而在严格模式中,结果就不同了。
|
虽然我们错误调用了Person,代码也可以执行成功,但会返回一个未定义值,并创建名为name的全局绑定。而在严格模式中,结果就不同了。
|
||||||
|
|
||||||
```
|
```
|
||||||
"use strict";
|
"use strict";
|
||||||
function Person(name) { this.name = name; }
|
function Person(name) { this.name = name; }
|
||||||
// Oops, forgot 'new'
|
let ferdinand = Person("Ferdinand");
|
||||||
var ferdinand = Person("Ferdinand");
|
|
||||||
// → TypeError: Cannot set property 'name' of undefined
|
// → TypeError: Cannot set property 'name' of undefined
|
||||||
```
|
```
|
||||||
|
|
||||||
JavaScript会立即告知我们代码中包含错误。这种特性十分有用。
|
JavaScript会立即告知我们代码中包含错误。这种特性十分有用。
|
||||||
|
|
||||||
严格模式还有很多其他特性。该模式下,不允许将多个同名参数赋予函数。完全移除一些本身就有问题的语言特性(比如with语句,该功能太具误导性,因此本书不会对其进行讨论)。
|
幸运的是,使用`class`符号创建的构造函数,如果在不使用`new`来调用,则始终会抱怨,即使在非严格模式下也不会产生问题。
|
||||||
|
|
||||||
简而言之,在程序顶部放置“use strict”无伤大雅,而且能够帮助你发现问题。
|
严格模式做了更多的事情。 它不允许使用同一名称给函数赋多个参数,并且完全删除某些有问题的语言特性(例如`with`语句,这是错误的,本书不会进一步讨论)。
|
||||||
|
|
||||||
### 8.3 测试
|
简而言之,在程序顶部放置`"use strict"`很少会有问题,并且可能会帮助您发现问题。
|
||||||
|
|
||||||
如果语言本身不能帮助我们找出错误,我们也可以通过运行程序来找出错误,并观察是否能得到我们期望的结果,只不过这种方法不是很好操作。
|
### 类型
|
||||||
|
|
||||||
重复进行手动测试明显会令人抓狂。所幸的是我们可以编写第二个程序来自动测试实际的程序。
|
有些语言甚至在运行程序之前想要知道,所有绑定和表达式的类型。 当类型以不一致的方式使用时,他们会马上告诉你。 JavaScript 只在实际运行程序时考虑类型,即使经常尝试将值隐式转换为它预期的类型,所以它没有多大帮助。
|
||||||
|
|
||||||
作为示例,我们再次使用Vector类型。
|
尽管如此,类型为讨论程序提供了一个有用的框架。 许多错误来自于值的类型的困惑,它们进入或来自一个函数。 如果你把这些信息写下来,你不太可能会感到困惑。
|
||||||
|
|
||||||
|
你可以在上一章的`goalOrientedRobot`函数上面,添加一个像这样的注释来描述它的类型。
|
||||||
|
|
||||||
```
|
```
|
||||||
function Vector(x, y) {
|
// (WorldState, Array) → {direction: string, memory: Array}
|
||||||
this.x = x;
|
function goalOrientedRobot(state, memory) {
|
||||||
this.y = y;
|
// ...
|
||||||
}
|
}
|
||||||
Vector.prototype.plus = function(other) {
|
|
||||||
return new Vector(this.x + other.x, this.y + other.y);
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
我们将会编写一个程序来检查Vector的实现是否符合我们的预期。接着,在每次修改实现时,我们都执行这个测试程序,以确保我们没有破坏原有的代码逻辑。当添加额外功能时(比如添加新的方法),我们也可以为新添加的功能编写测试。
|
有许多不同的约定,用于标注 JavaScript 程序的类型。
|
||||||
|
|
||||||
|
关于类型的一点是,他们需要引入自己的复杂性,以便能够描述足够有用的代码。 你认为从数组中返回一个随机元素的`randomPick`函数的类型是什么? 你需要引入一个变量类型`T`,它可以代表任何类型,这样你就可以给予`randomPick`一个像`([T])->T`的类型(从`T`到`T`的数组的函数)。
|
||||||
|
|
||||||
|
当程序的类型已知时,计算机可以为你检查它们,在程序运行之前指出错误。 有几种 JavaScript 语言为语言添加类型并检查它们。 最流行的称为 [TypeScript](https://www.typescriptlang.org/)。 如果您有兴趣为您的程序添加更多的严谨性,我建议您尝试一下。
|
||||||
|
|
||||||
|
在本书中,我们将继续使用原始的,危险的,非类型化的 JavaScript 代码。
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
|
||||||
|
如果语言不会帮助我们发现错误,我们将不得不努力找到它们:通过运行程序并查看它是否正确执行。
|
||||||
|
|
||||||
|
一次又一次地手动操作,是一个非常糟糕的主意。 这不仅令人讨厌,而且也往往是无效的,因为每次改变时都需要花费太多时间来详尽地测试所有内容。
|
||||||
|
|
||||||
|
计算机擅长重复性任务,测试是理想的重复性任务。 自动化测试是编写测试另一个程序的程序的过程。 编写测试比手工测试有更多的工作,但是一旦你完成了它,你就会获得一种超能力:它只需要几秒钟就可以验证,你的程序在你编写为其测试的所有情况下都能正常运行。 当你破坏某些东西时,你会立即注意到,而不是在稍后的时间里随机地碰到它。
|
||||||
|
|
||||||
|
测试通常采用小标签程序的形式来验证代码的某些方面。 例如,一组(标准的,可能已经由其他人测试过)`toUpperCase`方法的测试可能如下:
|
||||||
|
|
||||||
```
|
```
|
||||||
function testVector() {
|
function test(label, body) {
|
||||||
var p1 = new Vector(10, 20);
|
if (!body()) console.log(`Failed: ${label}`);
|
||||||
var p2 = new Vector(-10, 5);
|
|
||||||
var p3 = p1.plus(p2);
|
|
||||||
|
|
||||||
if (p1.x !== 10) return "fail: x property";
|
|
||||||
if (p1.y !== 20) return "fail: y property";
|
|
||||||
if (p2.x !== -10) return "fail: negative x property";
|
|
||||||
if (p3.x !== 0) return "fail: x from plus";
|
|
||||||
if (p3.y !== 25) return "fail: y from plus";
|
|
||||||
return "everything ok";
|
|
||||||
}
|
}
|
||||||
console.log(testVector());
|
|
||||||
// → everything ok
|
test("convert Latin text to uppercase", () => {
|
||||||
|
return "hello".toUpperCase() == "HELLO";
|
||||||
|
});
|
||||||
|
test("convert Greek text to uppercase", () => {
|
||||||
|
return "Χαίρετε".toUpperCase() == "ΧΑΊΡΕΤΕ";
|
||||||
|
});
|
||||||
|
test("don't convert case-less characters", () => {
|
||||||
|
return "مرحبا".toUpperCase() == "مرحبا";
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
这样编写测试容易产生重复糟糕的代码。幸运的是有些软件可以帮助你构建并运行测试集(测试套件),并提供了一种适合描述测试的语言(以函数和方法的形式描述),当测试失败时会输出有用的信息。我们将这类软件称为测试框架。
|
像这样写测试往往会产生很多重复,笨拙的代码。 幸运的是,有些软件通过提供适合于表达测试的语言(以函数和方法的形式),并在测试失败时输出丰富的信息来帮助您构建和运行测试集合(测试套件,test suite)。 这些通常被称为测试运行器(test runner)。
|
||||||
|
|
||||||
### 8.4 调试
|
一些代码比其他代码更容易测试。 通常,代码与外部交互的对象越多,建立用于测试它的上下文就越困难。 上一章中显示的编程风格,使用自包含的持久值而不是更改对象,通常很容易测试。
|
||||||
|
|
||||||
|
### 调试
|
||||||
|
|
||||||
当程序的运行结果不符合预期或在运行过程中产生错误时,你就会注意到程序出现问题了,下一步就是要推断问题出在什么地方。
|
当程序的运行结果不符合预期或在运行过程中产生错误时,你就会注意到程序出现问题了,下一步就是要推断问题出在什么地方。
|
||||||
|
|
||||||
有时错误很明显。错误消息会指出错误出现在程序的哪一行,只要稍加阅读错误描述及出错的那行代码,你一般就知道如何修正错误了。
|
有时错误很明显。错误消息会指出错误出现在程序的哪一行,只要稍加阅读错误描述及出错的那行代码,你一般就知道如何修正错误了。
|
||||||
|
|
||||||
但问题往往不会这么简单。触发问题的代码行有时只不过是第一个非法使用错误值的位置,而那个错误值则是在其他位置产生的。有时甚至没有任何错误消息,只有一个无效结果。如果读者编写了前面章节中的习题,就可能已经非常熟悉这种情况了。
|
但不总是这样。 有时触发问题的行,只是第一个地方,它以无效方式使用其他地方产生的奇怪的值。 如果您在前几章中已经解决了练习,您可能已经遇到过这种情况。
|
||||||
|
|
||||||
下面的示例代码尝试将一个整数转换成任意进制表示的字符串(十进制、二进制等),其原理是:不断循环取出最后一位数字,并将其除以基数(将最后一位数从数字中除去)。但该程序目前的输出表明程序中是存在bug的。
|
下面的示例代码尝试将一个整数转换成给定进制表示的字符串(十进制、二进制等),其原理是:不断循环取出最后一位数字,并将其除以基数(将最后一位数从数字中除去)。但该程序目前的输出表明程序中是存在bug的。
|
||||||
|
|
||||||
```
|
```
|
||||||
function numberToString(n, base) {
|
function numberToString(n, base = 10) {
|
||||||
var result = "", sign = "";
|
let result = "", sign = "";
|
||||||
if (n < 0) {
|
if (n < 0) {
|
||||||
sign = "-";
|
sign = "-";
|
||||||
n = -n;
|
n = -n;
|
||||||
@ -130,7 +146,7 @@ console.log(numberToString(13, 10));
|
|||||||
|
|
||||||
你可能已经发现程序运行结果不对了,不过先暂时装作不知道。我们知道程序运行出了问题,试图找出其原因。
|
你可能已经发现程序运行结果不对了,不过先暂时装作不知道。我们知道程序运行出了问题,试图找出其原因。
|
||||||
|
|
||||||
我们必须遏制住随意修改代码进行调试的冲动,思考才是最重要的。分析一下程序运行出错的原因,并推测出一个理论。接着,针对这个推测理论再对程序做些观察。抑或你手上还没有一个现实可行的理论,再多做些观察,也许就可以发现一些蛛丝马迹了。
|
这是一个地方,你必须抵制随机更改代码来查看它是否变得更好的冲动。 相反,要思考。 分析正在发生的事情,并提出为什么可能发生的理论。 然后,再做一些观察来检验这个理论 - 或者,如果你还没有理论,可以进一步观察来帮助你想出一个理论。
|
||||||
|
|
||||||
有目的地在程序中使用console.log来查看程序当前的运行状态,是一种不错的获取额外信息的方法。在本例中,我们希望n的值依次变为13、1,然后是0。让我们先在循环起始处输出n的值。
|
有目的地在程序中使用console.log来查看程序当前的运行状态,是一种不错的获取额外信息的方法。在本例中,我们希望n的值依次变为13、1,然后是0。让我们先在循环起始处输出n的值。
|
||||||
|
|
||||||
@ -145,55 +161,68 @@ console.log(numberToString(13, 10));
|
|||||||
|
|
||||||
没错。13除以10并不会产生整数。我们不应该使用n/=base,而应该使用n=Math.floor(n/base),使数字“右移”,这才是我们实际想要的结果。
|
没错。13除以10并不会产生整数。我们不应该使用n/=base,而应该使用n=Math.floor(n/base),使数字“右移”,这才是我们实际想要的结果。
|
||||||
|
|
||||||
另一种替代console.log的方案是使用浏览器的调试器功能。现代浏览器可以在特定代码行上设置断点。这使得程序每次执行到断点所在行时会暂停执行,这样你就可以查看那一刻变量的值。由于不同浏览器之间调试器差异很大,因此笔者不会深入介绍其细节,但读者可以顺便看看浏览器开发者工具,并在网络上搜索更多信息。另一个设置断点的方式是在程序中包含debugger语句(直接使用该关键字即可)。如果浏览器开发者工具处于激活状态,程序遇到该语句后会暂停执行,并在这时查看程序的状态。
|
使用`console.log`来查看程序行为的替代方法,是使用浏览器的调试器(debugger)功能。 浏览器可以在代码的特定行上设置断点(breakpoint)。 当程序执行到带有断点的行时,它会暂停,并且您可以检查该点的绑定值。 我不会详细讨论,因为调试器在不同浏览器上有所不同,但请查看浏览器的开发人员工具或在 Web 上搜索来获取更多信息。
|
||||||
|
|
||||||
### 8.5 错误传播
|
设置断点的另一种方法,是在程序中包含一个`debugger`语句(仅由该关键字组成)。 如果您的浏览器的开发人员工具是激活的,则只要程序达到这个语句,程序就会暂停。
|
||||||
|
|
||||||
不过开发人员不可能避免所有的问题。只要你的程序使用了某种方式与外界进行通信,就有可能会获取到无效的输入,与之通信的其他系统也有可能损坏或无法访问。
|
### 错误传播
|
||||||
|
|
||||||
对于简单的程序和在你观察下运行的程序来说,我们可以在程序运行出现问题的时候直接停止程序的执行。你可以实时地研究并解决问题,然后重新运行。不过对于真实环境下的应用程序来说,我们不能直接让程序崩溃。有时正确的做法反而是接受错误的输入并继续执行程序。另外,最好在应用程序出现错误的时候通知用户,然后再退出。而还有一些情况,我们需要确保程序在出现错误的时候作出积极响应,并执行一些处理逻辑。
|
不幸的是,程序员不可能避免所有问题。 如果您的程序以任何方式与外部世界进行通信,则可能会导致输入格式错误,工作负荷过重或网络故障。
|
||||||
|
|
||||||
假设有一个函数promptInteger,该函数向用户请求一个整数,并将其返回。如果用户输入orange,程序该如何处理呢?
|
如果你只为自己编程,那么你就可以忽略这些问题直到它们发生。 但是如果你创建了一些将被其他人使用的东西,你通常希望程序比只是崩溃做得更好。 有时候,正确的做法是不择手段地继续运行。 在其他情况下,最好向用户报告出了什么问题然后放弃。 但无论在哪种情况下,该程序都必须积极采取措施来回应问题。
|
||||||
|
|
||||||
一种办法是返回一个特殊值,通常会使用null或undefined。
|
假设你有一个函数`promptInteger`,要求用户输入一个整数并返回它。 如果用户输入“橙色”,它应该返回什么?
|
||||||
|
|
||||||
|
一种办法是返回一个特殊值,通常会使用`null`,`undefined`或 -1。
|
||||||
|
|
||||||
```
|
```
|
||||||
function promptNumber(question) {
|
function promptNumber(question) {
|
||||||
var result = Number(prompt(question, ""));
|
let result = Number(prompt(question, ""));
|
||||||
if (isNaN(result)) return null;
|
if (Number.isNaN(result)) return null;
|
||||||
else return result;
|
else return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(promptNumber("How many trees do you see?"));
|
console.log(promptNumber("How many trees do you see?"));
|
||||||
```
|
```
|
||||||
|
|
||||||
这是一种合理的处理方式。现在任何代码调用promptNumber时,都需要检查读取的值是否是一个实际的数字,如果失败,必须以某种方法恢复程序执行——再次询问输入或使用一个默认值。
|
现在,调用`promptNumber`的任何代码都必须检查是否实际读取了数字,否则必须以某种方式恢复 - 也许再次询问或填充默认值。 或者它可能会再次向它的调用者返回一个特殊值,表示它未能完成所要求的操作。
|
||||||
|
|
||||||
在许多情况中,当错误很普遍时,调用方通常应该显式地处理无效的返回值,返回一个特殊值是指出错误的极佳方式。但这种方式也有其不足之处。如果函数本身就可以返回任何类型的值,该如何处理?对于这类函数,我们难以找到一个特殊值,可以区分有效的结果和无效的结果。
|
在很多情况下,当错误很常见并且调用者应该明确地考虑它们时,返回特殊值是表示错误的好方法。 但它确实有其不利之处。 首先,如果函数已经可能返回每一种可能的值呢? 在这样的函数中,你必须做一些事情,比如将结果包装在一个对象中,以便能够区分成功与失败。
|
||||||
|
|
||||||
返回特殊值还会让代码看起来非常凌乱。如果代码中调用了10次promptNumber,就需要检查10次返回值是否为null。如果当发现null时,处理方式也是简单地返回null,那么函数调用方需要轮流检查特殊值。
|
```
|
||||||
|
function lastElement(array) {
|
||||||
|
if (array.length == 0) {
|
||||||
|
return {failed: true};
|
||||||
|
} else {
|
||||||
|
return {element: array[array.length - 1]};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 8.6 异常
|
返回特殊值的第二个问题是它可能产生非常笨拙的代码。 如果一段代码调用`promptNumber` 10 次,则必须检查是否返回`null` 10 次。 如果它对`null`的回应是简单地返回`null`本身,函数的调用者将不得不去检查它,以此类推。
|
||||||
|
|
||||||
当函数无法正常工作时,我们只希望停止当前任务,并立即跳转回负责处理问题的位置。这就是异常处理的功能。
|
### 异常
|
||||||
|
|
||||||
|
当函数无法正常工作时,我们只希望停止当前任务,并立即跳转到负责处理问题的位置。这就是异常处理的功能。
|
||||||
|
|
||||||
异常是一种当代码执行中遇到问题时,可以触发(或抛出)异常的机制,异常只是一个普通的值。触发异常类似于从函数中强制返回:异常不只跳出到当前函数中,还会跳出函数调用方,直到当前执行流初次调用函数的位置。这种方式被称为“堆栈展开(Unwinding the Stack)”。你可能还记得我们在第3章中介绍的函数调用栈,异常会减小堆栈的尺寸,并丢弃所有在缩减程序栈尺寸过程中遇到的函数调用上下文。
|
异常是一种当代码执行中遇到问题时,可以触发(或抛出)异常的机制,异常只是一个普通的值。触发异常类似于从函数中强制返回:异常不只跳出到当前函数中,还会跳出函数调用方,直到当前执行流初次调用函数的位置。这种方式被称为“堆栈展开(Unwinding the Stack)”。你可能还记得我们在第3章中介绍的函数调用栈,异常会减小堆栈的尺寸,并丢弃所有在缩减程序栈尺寸过程中遇到的函数调用上下文。
|
||||||
|
|
||||||
如果异常总是会将堆栈尺寸缩减到栈底,那么异常也就毫无用处了。它只不过是换了一种方式来彻底破坏你的程序罢了。异常真正强大的地方在于你可以在堆栈上设置一个“障碍物”,当异常缩减堆栈到达这个位置时会被捕获。接着你就可以对异常进行一些处理,并使得程序从异常捕获点开始继续执行。这里给出一个示例。
|
如果异常总是会将堆栈尺寸缩减到栈底,那么异常也就毫无用处了。它只不过是换了一种方式来彻底破坏你的程序罢了。异常真正强大的地方在于你可以在堆栈上设置一个“障碍物”,当异常缩减堆栈到达这个位置时会被捕获。一旦发现异常,您可以使用它来解决问题,然后继续运行该程序。
|
||||||
|
|
||||||
```
|
```
|
||||||
function promptDirection(question) {
|
function promptDirection(question) {
|
||||||
var result = prompt(question, "");
|
let result = prompt(question, "");
|
||||||
if (result.toLowerCase() == "left") return "L";
|
if (result.toLowerCase() == "left") return "L";
|
||||||
if (result.toLowerCase() == "right") return "R";
|
if (result.toLowerCase() == "right") return "R";
|
||||||
throw new Error("Invalid direction: " + result);
|
throw new Error("Invalid direction: " + result);
|
||||||
}
|
}
|
||||||
|
|
||||||
function look() {
|
function look() {
|
||||||
if (promptDirection("Which way?") == "L")
|
if (promptDirection("Which way?") == "L") {
|
||||||
return "a house";
|
return "a house";
|
||||||
else
|
} else {
|
||||||
return "two angry bears";
|
return "two angry bears";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -203,7 +232,7 @@ try {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
throw关键字用于触发异常。将throw语句填写在try块中,紧跟catch关键字,就可以捕获代码片段中的异常。当try块中的代码引发异常,程序就会执行catch块。catch关键字后的变量名(在括号中)会绑定到异常值。如果在catch块结束或try块正常结束后,未引发任何问题,那么控制流将会跳转到try/catch语句下方。
|
throw关键字用于触发异常。将throw语句填写在try块中,紧跟catch关键字,就可以捕获代码片段中的异常。当try块中的代码引发异常,程序就会执行catch块。catch关键字后的绑定名(在括号中)会绑定到异常值。如果在catch块结束或try块正常结束后,未引发任何问题,那么控制流将会跳转到try/catch语句下方。
|
||||||
|
|
||||||
在本例中,我们使用Error构造函数来创建异常值。这是一个标准的JavaScript构造函数,用于创建一个对象,包含message属性。在现代JavaScript环境中,构造函数实例也会收集异常创建时的调用栈信息,即堆栈跟踪信息(Stack Trace)。该信息存储在stack属性中,对于调用问题有很大的帮助,我们可以从堆栈跟踪信息中得知问题发生的精确位置,即问题具体出现在哪个函数中,以及执行失败为止调用的其他函数链。
|
在本例中,我们使用Error构造函数来创建异常值。这是一个标准的JavaScript构造函数,用于创建一个对象,包含message属性。在现代JavaScript环境中,构造函数实例也会收集异常创建时的调用栈信息,即堆栈跟踪信息(Stack Trace)。该信息存储在stack属性中,对于调用问题有很大的帮助,我们可以从堆栈跟踪信息中得知问题发生的精确位置,即问题具体出现在哪个函数中,以及执行失败为止调用的其他函数链。
|
||||||
|
|
||||||
@ -213,7 +242,7 @@ throw关键字用于触发异常。将throw语句填写在try块中,紧跟catc
|
|||||||
|
|
||||||
### 8.7 异常后清理
|
### 8.7 异常后清理
|
||||||
|
|
||||||
考虑下面一种情况:withContext函数执行过程中,会赋予全局变量context一个特殊值。需要确保执行完成后将变量还原为其原始值。
|
考虑下面一种情况:withContext函数执行过程中,会赋予全局绑定context一个特殊值。需要确保执行完成后将绑定还原为其原始值。
|
||||||
|
|
||||||
```
|
```
|
||||||
var context = null;
|
var context = null;
|
||||||
@ -243,7 +272,7 @@ function withContext(newContext, body) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
我们再也不需要将body结果(希望返回的值)存储在变量中了。即使我们从try块直接返回,finally块也必然会执行。现在我们可以安全完成任务:
|
我们再也不需要将body结果(希望返回的值)存储在绑定中了。即使我们从try块直接返回,finally块也必然会执行。现在我们可以安全完成任务:
|
||||||
|
|
||||||
```
|
```
|
||||||
try {
|
try {
|
||||||
@ -260,7 +289,7 @@ console.log(context);
|
|||||||
// → null
|
// → null
|
||||||
```
|
```
|
||||||
|
|
||||||
即使withContext中的函数调用引发异常,witchContext自身依然可以正确清理context变量。
|
即使withContext中的函数调用引发异常,witchContext自身依然可以正确清理context绑定。
|
||||||
|
|
||||||
### 8.8 选择性捕获
|
### 8.8 选择性捕获
|
||||||
|
|
||||||
@ -270,7 +299,7 @@ console.log(context);
|
|||||||
|
|
||||||
但对于已知的程序运行错误来说,如果我们还去使用未处理异常的方式进行处理,这种方式就不大友好了。
|
但对于已知的程序运行错误来说,如果我们还去使用未处理异常的方式进行处理,这种方式就不大友好了。
|
||||||
|
|
||||||
语言的非法使用方式,比如引用一个不存在的变量在null中查询属性或调用的对象不是函数最终都会引发异常。你可以像自己的异常一样捕获这些异常。
|
语言的非法使用方式,比如引用一个不存在的绑定在null中查询属性或调用的对象不是函数最终都会引发异常。你可以像自己的异常一样捕获这些异常。
|
||||||
|
|
||||||
进入catch语句块时,我们只知道try体中引发了异常,但不知道引发了哪一类或哪一个异常。
|
进入catch语句块时,我们只知道try体中引发了异常,但不知道引发了哪一类或哪一个异常。
|
||||||
|
|
||||||
@ -290,7 +319,7 @@ for (;;) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
我们可以使用for(;;)循环体来创建一个无限循环,其自身永远不会停止运行。我们在用户给出有效的方向之后会跳出循环。但我们拼写错了promptDirection,因此会引发一个“未定义值”错误。由于catch块完全忽略了异常值,假定其知道问题所在,错将变量错误信息当成错误输入。这样不仅会引发无限循环,而且会掩盖掉真正的错误消息——变量名拼写错误。
|
我们可以使用for(;;)循环体来创建一个无限循环,其自身永远不会停止运行。我们在用户给出有效的方向之后会跳出循环。但我们拼写错了promptDirection,因此会引发一个“未定义值”错误。由于catch块完全忽略了异常值,假定其知道问题所在,错将绑定错误信息当成错误输入。这样不仅会引发无限循环,而且会掩盖掉真正的错误消息——绑定名拼写错误。
|
||||||
|
|
||||||
一般而言,只有将抛出的异常重定位到其他地方进行处理时,我们才会捕获所有异常。比如说通过网络传输通知其他系统当前应用程序的崩溃信息。即便如此,我们也要注意编写的代码是否会将错误信息掩盖起来。
|
一般而言,只有将抛出的异常重定位到其他地方进行处理时,我们才会捕获所有异常。比如说通过网络传输通知其他系统当前应用程序的崩溃信息。即便如此,我们也要注意编写的代码是否会将错误信息掩盖起来。
|
||||||
|
|
||||||
@ -341,7 +370,7 @@ for (;;) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
这里的catch代码只会捕获InputError类型的异常,而其他类型的异常则不会在这里进行处理。如果又输入了不正确的值,那么系统会向用户准确报告错误——“变量未定义”。
|
这里的catch代码只会捕获InputError类型的异常,而其他类型的异常则不会在这里进行处理。如果又输入了不正确的值,那么系统会向用户准确报告错误——“绑定未定义”。
|
||||||
|
|
||||||
### 8.9 断言
|
### 8.9 断言
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user