用vs2015做网站教程,wordpress主题工具,wordpress opml,传媒公司做网站编辑 如何计算机程序中的缺陷通常被称为 bug。把它们想象成偶然爬进我们工作中的小东西#xff0c;会让程序员感觉良好。当然#xff0c;实际上是我们自己把它们放进去的。 如果程序是思想的结晶#xff0c;我们可以将错误大致分为思想混乱造成的错误和将思想转化为代码时引入错误造成… 计算机程序中的缺陷通常被称为 bug。把它们想象成偶然爬进我们工作中的小东西会让程序员感觉良好。当然实际上是我们自己把它们放进去的。 如果程序是思想的结晶我们可以将错误大致分为思想混乱造成的错误和将思想转化为代码时引入错误造成的错误。前者通常比后者更难诊断和修复。
语言 如果计算机对我们要做的事情有足够的了解那么很多错误都可以由计算机自动指出来。但在这方面JavaScript 的松散性是一个障碍。它对绑定和属性的概念非常模糊以至于在实际运行程序之前很少能发现错误。即便如此它还是允许你做一些明显毫无意义的事情而不会抱怨比如计算 true * “monkey”。 JavaScript 也会抱怨一些事情。编写不符合语言语法的程序会立即引起计算机的抱怨。其他一些事情比如调用一个非函数的东西或者查找一个未定义值的属性都会在程序尝试执行操作时报错。 不过通常情况下你的无意义计算只会产生 NaN不是数字或未定义的值而程序会继续愉快地运行坚信自己在做有意义的事情。只有在假值经过多个函数后错误才会显现出来。它可能根本不会引发错误但会悄无声息地导致程序输出错误。查找此类问题的根源可能很困难。
在程序中查找错误的过程称为调试。
严格模式 通过启用严格模式JavaScript 可以变得更严格一些。这可以通过在文件或函数体的顶部加上 “use strict ”字符串来实现。下面是一个例子 类和模块我们将在第 10 章讨论中的代码自动严格。旧的非严格行为之所以仍然存在只是因为一些旧代码可能依赖于它而语言设计者努力避免破坏任何现有程序。 通常情况下当你忘记在绑定前面加上 let 时如示例中的 counterJavaScript 会悄悄创建一个全局绑定并使用它。而在严格模式下则会报错。这非常有用。不过需要注意的是如果绑定已经存在于作用域中的某个地方那么这个方法就不起作用了。在这种情况下循环仍会悄悄覆盖绑定的值。 严格模式下的另一个变化是在未作为方法调用的函数中该绑定值为未定义。在严格模式之外进行此类调用时this 指的是全局作用域对象而全局作用域对象的属性就是全局绑定。因此如果在严格模式下不小心错误地调用了方法或构造函数JavaScript 在试图从中读取内容时就会产生错误而不是愉快地写入全局作用域。
例如下面的代码在调用构造函数时没有使用 new 关键字这样它的 this 就不会指向一个新构造的对象 对 Person 的假调用成功了但返回了一个未定义的值并创建了全局绑定名称。在严格模式下结果则不同。 我们立即被告知出了问题。这很有帮助。 幸运的是使用类符号创建的构造函数如果在没有 new 的情况下被调用总是会被抱怨因此即使在非严格模式下问题也不大。 严格模式还做了一些事情。它不允许给一个函数提供多个同名参数并完全删除了某些有问题的语言特性如 with 语句这种语句是错误的本书不再讨论。
总之在程序顶端加上 “use strict ”很少有坏处而且可能会帮助你发现问题。
类型 有些语言希望在运行程序之前就知道所有绑定和表达式的类型。当某种类型的使用方式不一致时它们会立即告诉你。JavaScript 只有在实际运行程序时才会考虑类型而且即使在运行程序时也会经常尝试将值隐式地转换为它所期望的类型因此帮不上什么忙。 不过类型还是为讨论程序提供了一个有用的框架。很多错误都是由于对进入函数或从函数中出来的值的类型感到困惑而造成的。如果把这些信息写下来就不容易混淆了。
你可以在上一章的 findRoute 函数前添加类似下面的注释来描述它的类型 用类型注释 JavaScript 程序有许多不同的约定。 关于类型的一个问题是它们需要引入自身的复杂性才能描述出足够有用的代码。你认为从数组中随机返回一个元素的 randomPick 函数的类型是什么你需要引入一个类型变量 T它可以代表任何类型这样你就可以给 randomPick 赋予 (T[]) → T从 Ts 数组到 T 的函数这样的类型。 当程序的类型已知时计算机就有可能为你检查这些类型在程序运行前指出错误。有几种 JavaScript 方言可以在语言中添加类型并对其进行检查。最流行的一种叫做 TypeScript。如果你有兴趣为你的程序增加更多的严谨性我建议你试一试。
在本书中我们将继续使用原始、危险、未键入的 JavaScript 代码。
测试 如果程序语言不能帮助我们发现错误我们就只能通过运行程序看看它是否做对了。 一次又一次地手工操作是个非常糟糕的主意。这不仅令人讨厌而且往往效果不佳因为每次更改都要花费大量时间对所有内容进行详尽测试。 计算机擅长重复性工作而测试正是理想的重复性工作。自动测试是编写一个程序来测试另一个程序的过程。编写测试程序比手动测试要繁琐一些但一旦完成你就会获得一种超能力只需几秒钟就能验证你的程序在你编写测试程序的所有情况下是否仍能正常运行。当你破坏了某些东西时你会立即注意到而不是在以后的某个时间随机遇到。 测试通常采用小标签程序的形式用于验证代码的某些方面。例如toUpperCase 方法标准方法可能已经有人测试过了的测试集可能是这样的 这样编写测试往往会产生相当重复、笨拙的代码。幸运的是有一种软件可以帮助你构建和运行测试集合测试套件它提供了一种适合表达测试的语言以函数和方法的形式并在测试失败时输出信息。这些软件通常被称为测试运行程序。 有些代码比其他代码更容易测试。一般来说与代码交互的外部对象越多设置测试上下文就越难。上一章中展示的编程风格使用自足的持久值而不是不断变化的对象这种风格往往容易测试。
调试 一旦你发现程序出了问题因为它行为不端或产生错误下一步就是找出问题所在。有时问题很明显。错误信息会指向程序中的某一行如果查看错误描述和该行代码通常就能发现问题所在。但并非总是如此。有时引发问题的那行代码只是在其他地方产生的错误值以无效方式被使用的第一个地方。如果你已经解决了前面几章的练习你可能已经经历过这种情况。 下面的示例程序试图将一个整数转换成一个给定基数十进制、二进制等的字符串方法是反复挑出最后一位数字然后除以该数字去掉该数字。但它目前产生的奇怪输出结果表明它存在一个错误。 即使你已经看到了问题所在也请暂时假装看不到。我们知道我们的程序出现了故障我们想找出原因。这时你必须克制住自己的冲动不要随意修改代码看看这样做是否能让程序变得更好。取而代之的是思考。分析正在发生的情况并就可能发生的原因提出理论。然后进行更多的观察来验证这一理论--或者如果你还没有理论也可以进行更多的观察来帮助你提出一个理论。 在程序中调用一些策略性的 console.log 调用是获取关于程序正在做什么的额外信息的好方法。在本例中我们希望 n 取值为 13、1 和 0。 对。13 除以 10 不能得到整数。我们实际上需要的是 n Math.floor(n/base)而不是 n / base这样数字就会正确地向右 “移动”。 除了使用 console.log 来窥探程序的行为外另一种方法是使用浏览器的调试器功能。浏览器具有在特定代码行上设置断点的功能。当程序执行到带有断点的行时程序会暂停你可以检查该点的绑定值。由于不同浏览器的调试器不尽相同我就不详细介绍了但请查看浏览器的开发工具或在网上搜索相关说明。 另一种设置断点的方法是在程序中加入调试器语句仅由该关键字组成。如果浏览器的开发工具处于激活状态程序在运行到该语句时就会暂停。
错误传播 遗憾的是并非所有问题程序员都能避免。如果你的程序以任何方式与外界通信就有可能获得畸形输入、工作负荷过重或网络故障。 如果你只是为自己编程你可以忽略这些问题直到它们发生。但如果你编写的程序将被其他人使用你通常希望程序能做得更好而不仅仅是崩溃。有时正确的做法是接受不良输入并继续运行。在其他情况下最好是向用户报告出错的原因然后放弃。无论在哪种情况下程序都必须积极采取措施来应对问题。 假设有一个 promptNumber 函数向用户询问一个数字并返回该数字。如果用户输入 “orange”它应该返回什么
一种方法是让它返回一个特殊值。这种值的常见选择是 null、undefined 或-1。 现在任何调用 promptNumber 的代码都必须检查是否读取了实际的数字如果没有则必须以某种方式恢复--可能是再次询问也可能是填写默认值。或者它可以再次向调用者返回一个特殊值以表明它未能完成所要求的操作。 在许多情况下主要是当错误很常见并且调用者应该明确考虑到这些错误时返回一个特殊值是一种很好的提示错误的方法。不过它也有缺点。首先如果函数已经可以返回所有可能的值那该怎么办在这样的函数中你就必须像迭代器接口的下一个方法那样将结果封装在一个对象中以便区分成功与失败。 返回特殊值的第二个问题是它可能导致代码笨拙。如果一段代码调用 promptNumber 10 次它就必须检查 10 次是否返回了空值。如果发现空值后的反应是简单地返回空值那么函数的调用者就必须依次检查空值以此类推。
异常 当函数无法正常运行时我们通常希望停止正在进行的工作并立即跳转到知道如何处理问题的地方。这就是异常处理的作用。 异常是一种机制它可以让遇到问题的代码引发或抛出异常。异常可以是任何值。引发异常有点类似于函数的超级返回异常不仅会跳出当前函数还会跳出其调用者一直跳到开始当前执行的第一个调用。这就是所谓的释放堆栈。你可能还记得第 3 章中提到的函数调用栈。异常会沿着堆栈向下缩放丢弃它遇到的所有调用上下文。 如果异常总是直接放大到堆栈底部那么它们就没有什么用处了。它们只是提供了一种炸毁程序的新方法。异常的强大之处在于你可以沿着堆栈设置 “障碍”以便在异常向下放大时捕获它。一旦捕捉到异常你就可以对它进行处理解决问题然后继续运行程序。
这里有一个例子 throw 关键字用于引发异常。捕获异常的方法是将一段代码封装在 try 代码块中然后使用关键字 catch。当 try 代码块中的代码导致异常发生时catch 代码块将被评估括号中的名称将与异常值绑定。在 catch 代码块结束后或者如果 try 代码块结束时没有问题程序将在整个 try/catch 语句下继续执行。 在本例中我们使用 Error 构造函数创建了异常值。这是一个标准的 JavaScript 构造函数用于创建一个带有消息属性的对象。Error 实例还会收集创建异常时存在的调用堆栈信息即所谓的堆栈跟踪。这些信息存储在堆栈属性中在调试问题时很有帮助它会告诉我们问题发生在哪个函数中哪些函数进行了失败调用。 请注意look 函数完全忽略了 promptDirection 可能出错的可能性。这就是异常的最大优势只有在错误发生时和处理错误时才需要错误处理代码。中间的函数可以完全不用考虑。
嗯几乎是...
清理例外情况 异常的影响是另一种控制流。每一个可能导致异常的操作几乎是每一次函数调用和属性访问都可能导致控制权突然离开代码。 这就意味着当代码有多个副作用时即使其 “常规 ”控制流看起来都会发生异常也可能会阻止其中一些副作用的发生。
下面是一些非常糟糕的银行代码 转账函数将一笔钱从一个给定的账户转入另一个账户在此过程中会询问另一个账户的名称。如果给定的账户名称无效getAccount 会抛出异常。 但是转账函数会先将钱从账户中取出然后调用 getAccount再将钱添加到另一个账户中。如果在这个过程中出现异常钱就会消失。 这段代码本可以写得更智能一些例如在开始移动资金之前调用 getAccount。但类似的问题往往以更微妙的方式出现。即使是看起来不会抛出异常的函数在特殊情况下或包含程序员错误时也可能会抛出异常。 解决这一问题的方法之一是减少副作用。同样计算新值而非更改现有数据的编程风格也有帮助。如果一段代码在创建新值的过程中停止运行现有的数据结构不会受到破坏这样就更容易恢复。 由于这并不总是切实可行所以 try 语句还有另一个特点它们后面可能会跟一个 finally 代码块以代替 catch 代码块或作为 catch 代码块的补充。finally 代码块表示 “无论发生什么在尝试运行 try 代码块中的代码后再运行此代码”。 这个版本的函数会跟踪其运行进度如果在离开时发现它在创建了不一致的程序状态时被终止它就会修复所造成的损害。 请注意即使 finally 代码在 try 代码块中抛出异常时运行它也不会干扰异常。在 finally 代码块运行后堆栈会继续展开。 编写即使异常在意想不到的地方出现也能可靠运行的程序是很困难的。很多人根本懒得去做而且由于异常通常只在特殊情况下出现因此问题可能很少发生甚至从未被注意到。这究竟是好事还是坏事取决于软件出现故障时会造成多大的损失。
选择性捕捉 当一个异常没有被捕获而一直到达堆栈底部时它就会被环境处理。这在不同环境中的含义各不相同。在浏览器中错误描述通常会写入 JavaScript 控制台可通过浏览器的 “工具 ”或 “开发人员 ”菜单访问。我们将在第 20 章讨论的无浏览器 JavaScript 环境 Node.js 对数据损坏更为谨慎。当出现无法处理的异常时它会中止整个进程。 对于程序员的错误让错误通过往往是最好的办法。未处理异常是程序崩溃的合理信号在现代浏览器上JavaScript 控制台会提供一些信息告诉你问题发生时堆栈上有哪些函数调用。
对于在日常使用中预计会发生的问题使用未处理异常导致程序崩溃是一种糟糕的策略。 语言的无效使用如引用不存在的绑定、查找 null 属性或调用非函数也会导致异常的产生。这些异常也可以被捕获。 当输入 catch body 时我们只知道 try body 中的某些内容导致了异常。但我们不知道是什么引起了异常也不知道是哪个异常。 JavaScript一个相当明显的疏忽并不直接支持选择性捕获异常要么全部捕获要么一个都不捕获。这就很容易让人认为你捕获到的异常就是你在编写捕获块时考虑到的异常。 但事实可能并非如此。其他一些假设可能被违反或者你可能引入了一个导致异常的错误。下面是一个试图继续调用 promptDirection 直到得到有效答案的示例 for (;;) 结构是一种有意创建不会自行终止的循环的方法。只有在给出有效方向时我们才会跳出循环。不幸的是我们拼错了 promptDirection这将导致 “未定义变量 ”错误。由于 catch 代码块完全忽略了异常值 (e)以为自己知道问题所在因此错误地将绑定错误视为输入错误。这不仅会导致无限循环还会 “掩盖 ”有关拼写错误绑定的有用错误信息。 一般来说除非是为了将异常 “路由 ”到其他地方--例如通过网络告诉其他系统我们的程序崩溃了否则不要一揽子捕获异常。即便如此也要仔细考虑如何隐藏信息。 我们要捕获一种特定的异常。我们可以通过在 catch 块中检查我们得到的异常是否是我们感兴趣的异常如果不是就重新抛出。但如何识别异常呢 我们可以将其信息属性与我们预期的错误信息进行比较。但这种编写代码的方式并不可靠--我们将使用供人类使用的信息信息来做出程序决策。一旦有人更改或翻译了信息代码就会停止工作。
相反让我们定义一种新的错误类型并使用 instanceof 来识别它。 新的错误类扩展了 Error。它没有定义自己的构造函数这意味着它继承了 Error 的构造函数后者需要一个字符串消息作为参数。事实上它根本没有定义任何东西--该类是空的。InputError 对象的行为与 Error 对象类似只是它们有一个不同的类我们可以通过这个类来识别它们。
现在循环可以更仔细地捕捉这些对象了。 这将只捕获 InputError 实例而让无关的异常通过。如果重新引入拼写错误未定义的绑定错误将被正确报告。
断言 断言是程序内部的检查用于验证某些事情是否符合其应有的状态。它们不是用来处理正常运行中可能出现的情况而是用来发现程序员的错误。
例如如果 firstElement 被描述为一个永远不应该在空数组上调用的函数我们可以这样写 现在它不再默默地返回未定义读取不存在的数组属性时会返回未定义而是一旦你误用它程序就会立即爆炸。这样此类错误就不容易被忽视也更容易在错误发生时找到原因。 我不建议为每一种可能的错误输入编写断言。这将是一项很大的工作量而且会导致代码非常嘈杂。你需要为容易犯的错误或你发现自己会犯的错误保留断言。
总结 编程的一个重要部分就是查找、诊断和修复错误。如果有一个自动化测试套件或在程序中添加断言问题就更容易被发现。 对于由程序控制之外的因素导致的问题通常应积极加以规划。有时当问题可以在本地处理时特殊返回值是跟踪问题的好方法。否则异常可能是更好的选择。 抛出异常会导致调用堆栈被释放直到下一个外层 try/catch 块或堆栈底部。异常值将提供给捕获异常的 catch 块该块应验证异常确实是预期的异常类型然后对异常进行处理。为了帮助解决异常导致的控制流不可预测性问题可以使用 finally 块来确保代码在块结束时始终运行。
练习
重试 假设你有一个函数 primitiveMultiply它在 20% 的情况下可以实现两个数字相乘而在另外 80% 的情况下会引发一个类型为 MultiplicatorUnitFailure 的异常。编写一个函数来封装这个笨重的函数并不断尝试直到调用成功然后返回结果。
确保只处理你要处理的异常。
代码
function reliableMultiply(a, b) {try {return primitiveMultiply(a, b);} catch (e) {if (e instanceof MultiplicatorUnitFailure) {console.log(e.message);}}
}
console.log(reliableMultiply(8,2));
上锁的箱子
请看下面这个相当臆造的对象 这是一个带锁的盒子。盒子里有一个数组但只有当盒子被解锁时才能访问它。
编写一个名为 withBoxUnlocked 的函数以函数值作为参数解锁盒子运行函数然后在返回之前确保盒子再次被锁定无论参数函数是正常返回还是抛出异常。 代码
function withBoxUnlocked(body) {box.unlock();try {body();console.log(box.content);} finally {box.lock();}
}
为了获得额外的分数请确保如果在盒子已经解锁的情况下调用 withBoxUnlocked盒子将保持解锁状态。