DRAFT 《现代 JavaScript 教程》前端基础系列 05:代码质量——调试、代码规范与自动化测试
约 16092 字大约 54 分钟
2026-04-17
引导段:在编写更复杂的代码前,让我们先来探讨如何保证并提升代码质量。良好的调试技巧与规范的编写习惯,将为你后续的工程化开发打下坚实的基础。
【本篇核心收获】
- 掌握在浏览器中进行断点调试、状态检查与控制台交互的技巧。
- 建立良好的代码风格(包括花括号、缩进、行宽、嵌套及函数位置等)。
- 理解注释的正确书写方式,并学会识别与避免晦涩难懂的“忍者代码”。
- 初步掌握基于 Mocha 的 BDD 自动化测试流程。
- 了解 Polyfill 和转译器在现代 Web 开发中的作用与意义。
3.1 在浏览器中调试
所属章节: 3. 代码质量
所属部分: 第一部分 - JavaScript 编程语言
原始链接: https://zh.javascript.info/debugging-chrome
在编写更复杂的代码前,让我们先来聊聊调试吧。
https://en.wikipedia.org/wiki/Debugging 是指在一个脚本中找出并修复错误的过程。所有的现代浏览器和大多数其他环境都支持调试工具 —— 开发者工具中一个令调试更加容易的特殊用户界面。它也可以让我们一步步地跟踪代码,以查看其当前实际的运行情况。
在这里我们将会使用 Chrome(谷歌浏览器),因为它拥有足够多的功能,且其他大部分浏览器的功能也与之类似。
“资源(Sources)”面板
你的 Chrome 版本可能看起来会有一点不同,但是它应该还是处于很明显的位置。
- 在 Chrome 中打开 article_debugging-chrome_debugging_index。
- 使用快捷键
F12(Mac:Cmd+Opt+I)打开开发者工具。 - 选择
Sources(资源)面板。
如果你是第一次这么做,那你应该会看到下面这个样子:
切换按钮 会打开文件列表的选项卡。
让我们在预览树中点击并选择 hello.js。这里应该会如下图所示:
资源(Sources)面板包含三个主要部分:
- 文件导航(File Navigator) 区域列出了 HTML、JavaScript、CSS 以及包括图片在内的其他依附于此页面的文件。Chrome 扩展程序也会显示在这里。
- 代码编辑(Code Editor) 区域用于展示源码。
- JavaScript 调试(JavaScript Debugging) 区域则是用于调试的,我们很快就会来探索它。
现在你可以再次点击切换按钮,隐藏资源列表来给代码腾出一些空间。
控制台(Console)
如果我们按下 Esc,下面会出现一个控制台,我们可以输入一些命令然后按下 Enter 来执行。
语句执行完毕之后,其执行结果会显示在下面。
例如,1+2 将会返回 3,而 hello("debugger") 函数调用什么也没返回,所以结果是 undefined:
断点(Breakpoints)
我们来看看 article_debugging-chrome_debugging_index 发生了什么。在 hello.js 中,点击第 4 行。是的,就点击数字 "4" 上,不是点击代码。
恭喜你!你已经设置了一个断点。现在,请在第 8 行的数字上也点击一下。
看起来应该是这样的(蓝色是你应该点击的地方):
断点 是调试器会自动暂停 JavaScript 执行的地方。
当代码被暂停时,我们可以检查当前的变量,在控制台执行命令等等。换句话说,我们可以通过它来调试代码。
我们总是可以在右侧的面板中找到断点的列表。当我们在数个文件中有许多断点时,这是非常有用的。它允许我们:
- 快速跳转至代码中的断点(通过点击右侧面板中的对应的断点)。
- 通过取消选中断点来临时禁用对应的断点。
- 通过右键单击并选择移除来删除一个断点。
- ……等等。
条件断点
在行号上 右键单击 允许你创建一个 条件 断点。它只有当给定的表达式(你在创建条件断点时提供的表达式)为真时才会被触发。
当我们需要在特定的变量值或参数的情况下暂停程序执行时,这种调试方法就很有用了。
“debugger” 命令
我们也可以使用 debugger 命令来暂停代码,像这样:
function hello(name) {
let phrase = `Hello, ${name}!`;
debugger; // <-- 调试器会在这停止
say(phrase);
}这样的命令只有在开发者工具打开时才有效,否则浏览器会忽略它。
暂停并查看
在我们的例子中,hello() 函数在页面加载期间被调用,因此激活调试器的最简单的方法(在我们已经设置了断点后)就是 —— 重新加载页面。因此让我们按下 F5(Windows,Linux)或 Cmd+R(Mac)吧。
设置断点之后,程序会在第 4 行暂停执行:
请打开右侧的信息下拉列表(箭头指示出的地方)。这里允许你查看当前的代码状态:
- 察看(Watch)—— 显示任意表达式的当前值。你可以点击加号
+然后输入一个表达式。调试器将显示它的值,并在执行过程中自动重新计算该表达式。 - 调用栈(Call Stack)—— 显示嵌套的调用链。此时,调试器正在
hello()的调用链中,被index.html中的一个脚本调用(这里没有函数,因此显示 "anonymous")。如果你点击了一个堆栈项,调试器将跳到对应的代码处,并且还可以查看其所有变量。 - 作用域(Scope)—— 显示当前的变量。
Local显示当前函数中的局部变量,你还可以在源代码中看到它们的值高亮显示了出来。Global显示全局变量(不在任何函数中)。这里还有一个this关键字,目前我们还没有学到它,不过我们很快就会学习它了。
跟踪执行
现在是 跟踪 脚本的时候了。
在右侧面板的顶部是一些关于跟踪脚本的按钮。让我们来使用它们吧。
- 恢复(Resume):继续执行,快捷键
F8。如果没有其他的断点,那么程序就会继续执行,并且调试器不会再控制程序。我们点击它一下之后,我们会看到这样的情况:
- 执行恢复了,执行到
say()函数中的另外一个断点后,又暂停在了那里。看一下右边的 "Call stack",它已经增加了一个调用信息。我们现在就在say()函数内部。 - 下一步(Step):运行下一条(即当前行)指令,快捷键
F9。如果我们现在点击它,alert会被显示出来。一次接一次地点击此按钮,整个脚本的所有语句会被逐个执行。 - 跨步(Step over):运行下一条(即当前行)指令,但 不会进入到一个函数中,快捷键
F10。这跟上一条命令“下一步(Step)”类似,但如果下一条语句是函数调用则表现不同。这里的函数指的是:不是内建的如alert函数等,而是我们自己写的函数。如果我们对比一下,“下一步(Step)”命令会进入嵌套函数调用并在其第一行暂停执行;而“跨步(Step over)”对我们不可见地执行嵌套函数调用,跳过了函数内部。执行会在该函数调用后立即暂停。如果我们对该函数的内部执行不感兴趣,这个命令会很有用。 - 步入(Step into),快捷键
F11。和“下一步(Step)”类似,但在异步函数调用情况下表现不同。如果你刚刚才开始学 JavaScript,那么你可以先忽略此差异,因为我们还没有用到异步调用。至于之后,只需要记住“下一步(Step)”命令会忽略异步行为,例如setTimeout(计划的函数调用),它会过一段时间再执行。而“步入(Step into)”会进入到代码中并等待(如果需要)。详见 https://developers.google.com/web/updates/2018/01/devtools#async。 - 步出(Step out):继续执行到当前函数的末尾,快捷键
Shift+F11。继续执行当前函数内的剩余代码,并暂停在调用当前函数的下一行代码处。当我们偶然地进入到一个嵌套调用,但是我们又对这个函数不感兴趣,我们想要尽可能地继续执行到最后的时候,它是非常方便的。 - 启用/禁用所有的断点。这个按钮不会影响程序的执行。只是一个批量操作断点的开/关。
- 启用/禁用出现错误时自动暂停脚本执行。当启动此功能,如果开发者工具是打开着的时候,任何脚本执行错误都会导致该脚本执行自动暂停。然后我们可以在调试器中分析变量来看一下什么出错了。因此如果我们的脚本因为错误挂掉的时候,我们可以打开调试器,启用这个选项然后重载页面,查看一下哪里导致它挂掉了以及当时的上下文是什么。
Continue to here
在代码中的某一行上右键,在显示的关联菜单(context menu)中点击一个非常有用的名为 "Continue to here" 的选项。
当你想要向前移动很多步到某一行为止,但是又懒得设置一个断点时,这非常地方便。
日志记录
想要输出一些东西到控制台上?console.log 函数可以满足你。
例如:将从 0 到 4 的值输出到控制台上:
// 打开控制台来查看
for (let i = 0; i < 5; i++) {
console.log("value", i);
}普通用户看不到这个输出,它是在控制台里面的。要想看到它 —— 要么打开开发者工具中的 Console(控制台)选项卡,要么在一个其他的选项卡中按下 Esc:这会在下方打开一个控制台。
如果我们在代码中有足够的日志记录,那么我们可以从记录中看到刚刚发生了什么,而不需要借助调试器。
总结
我们可以看到,这里有 3 种方式来暂停一个脚本:
- 断点。
debugger语句。- error(如果开发者工具是打开状态,并且对应的自动暂停按钮是开启的状态)。
当脚本执行暂停时,我们就可以进行调试:检查变量,跟踪代码来查看执行出错的位置。
开发人员工具中的选项比本文介绍的多得多。完整的手册请点击这个链接查看:https://developers.google.com/web/tools/chrome-devtools。
本章节的内容足够让你上手代码调试了,但是之后,尤其是你做了大量关于浏览器的东西后,推荐你查看上面那个链接中讲的开发者工具更高级的功能。
对了,你也可以点击开发者工具中的其他地方来看一下会显示什么。这可能是你学习开发者工具最快的方式了。不要忘了还有右键单击和关联菜单哟。
3.2 代码风格
所属章节: 3. 代码质量
所属部分: 第一部分 - JavaScript 编程语言
原始链接: https://zh.javascript.info/coding-style
我们的代码必须尽可能的清晰和易读。
这实际上是一种编程艺术 —— 以一种正确并且人们易读的方式编码来完成一个复杂的任务。一个良好的代码风格大大有助于实现这一点。
语法
下面是一个备忘单,其中列出了一些建议的规则(详情请参阅下文):
现在,让我们详细讨论一下这些规则和它们的原因吧。
没有什么规则是“必须”的
没有什么规则是“刻在石头上”的。这些是风格偏好,而不是宗教教条。
花括号
在大多数的 JavaScript 项目中,花括号以 "Egyptian" 风格(译注:"egyptian" 风格又称 K&R 风格 —— 代码段的开括号位于一行的末尾,而不是另起一行的风格)书写,左花括号与相应的关键词在同一行上 —— 而不是新起一行。左括号前还应该有一个空格,如下所示:
if (condition) {
// do this
// ...and that
// ...and that
}单行构造(如 if (condition) doSomething())也是一个重要的用例。我们是否应该使用花括号?如果是,那么在哪里?
下面是这几种情况的注释,你可以自己判断一下它们的可读性:
- 😠 初学者常这样写。非常不好!这里不需要花括号:
if (n < 0) {alert(`Power ${n} is not supported`);}- 😠 拆分为单独的行,不带花括号。永远不要这样做,添加新行很容易出错:
if (n < 0)
alert(`Power ${n} is not supported`);- 😏 写成一行,不带花括号 —— 如果短的话,也是可以的:
if (n < 0) alert(`Power ${n} is not supported`);- 😃 最好的方式:
if (n < 0) {
alert(`Power ${n} is not supported`);
}对于很短的代码,写成一行是可以接受的:例如 if (cond) return null。但是代码块(最后一个示例)通常更具可读性。
行的长度
没有人喜欢读一长串代码,最好将代码分割一下。
例如:
// 回勾引号 ` 允许将字符串拆分为多行
let str = `
ECMA International's TC39 is a group of JavaScript developers,
implementers, academics, and more, collaborating with the community
to maintain and evolve the definition of JavaScript.
`;对于 if 语句:
if (
id === 123 &&
moonPhase === 'Waning Gibbous' &&
zodiacSign === 'Libra'
) {
letTheSorceryBegin();
}一行代码的最大长度应该在团队层面上达成一致。通常是 80 或 120 个字符。
缩进
有两种类型的缩进:
- 水平方向上的缩进:2 或 4 个空格。 一个水平缩进通常由 2 或 4 个空格或者 "Tab" 制表符(
Tab键)构成。选择哪一个方式是一场古老的圣战。如今空格更普遍一点。选择空格而不是 tabs 的优点之一是,这允许你做出比 "Tab" 制表符更加灵活的缩进配置。例如,我们可以将参数与左括号对齐,像下面这样:
show(parameters,
aligned, // 左边有 5 个空格
one,
after,
another
) {
// ...
}- 垂直方向上的缩进:用于将代码拆分成逻辑块的空行。 即使是单个函数通常也被分割为数个逻辑块。在下面的示例中,初始化的变量、主循环结构和返回值都被垂直分割了:
function pow(x, n) {
let result = 1;
// <--
for (let i = 0; i < n; i++) {
result *= x;
}
// <--
return result;
}插入一个额外的空行有助于使代码更具可读性。写代码时,不应该出现连续超过 9 行都没有被垂直分割的代码。
分号
每一个语句后面都应该有一个分号。即使它可以被跳过。
有一些编程语言的分号确实是可选的,那些语言中也很少使用分号。但是在 JavaScript 中,极少数情况下,换行符有时不会被解释为分号,这时代码就容易出错。更多内容请参阅 /structure#semicolon 一章的内容。
如果你是一个有经验的 JavaScript 程序员,你可以选择像 https://standardjs.com/ 这样的无分号代码风格。否则,最好使用分号以避免可能出现的陷阱。大多数开发人员都应该使用分号。
嵌套的层级
尽量避免代码嵌套层级过深。
例如,在循环中,有时候使用 continue 指令以避免额外的嵌套是一个好主意。
例如,不应该像下面这样添加嵌套的 if 条件:
for (let i = 0; i < 10; i++) {
if (cond) {
... // <- 又一层嵌套
}
}我们可以这样写:
for (let i = 0; i < 10; i++) {
if (!cond) continue;
... // <- 没有额外的嵌套
}使用 if/else 和 return 也可以做类似的事情。
例如,下面的两个结构是相同的。
第一个:
function pow(x, n) {
if (n < 0) {
alert("Negative 'n' not supported");
} else {
let result = 1;
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}
}第二个:
function pow(x, n) {
if (n < 0) {
alert("Negative 'n' not supported");
return;
}
let result = 1;
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}但是第二个更具可读性,因为 n < 0 这个“特殊情况”在一开始就被处理了。一旦条件通过检查,代码执行就可以进入到“主”代码流,而不需要额外的嵌套。
函数位置
如果你正在写几个“辅助”函数和一些使用它们的代码,那么有三种方式来组织这些函数。
- 在调用这些函数的代码的 上方 声明这些函数:
// 函数声明
function createElement() {
...
}
function setHandler(elem) {
...
}
function walkAround() {
...
}
// 调用函数的代码
let elem = createElement();
setHandler(elem);
walkAround();- 先写调用代码,再写函数:
// 调用函数的代码
let elem = createElement();
setHandler(elem);
walkAround();
// --- 辅助函数 ---
function createElement() {
...
}
function setHandler(elem) {
...
}
function walkAround() {
...
}- 混合:在第一次使用一个函数时,对该函数进行声明。
大多数情况下,第二种方式更好。
这是因为阅读代码时,我们首先想要知道的是“它做了什么”。如果代码先行,那么在整个程序的最开始就展示出了这些信息。之后,可能我们就不需要阅读这些函数了,尤其是它们的名字清晰地展示出了它们的功能的时候。
风格指南
风格指南包含了“如何编写”代码的通用规则,例如:使用哪个引号、用多少空格来缩进、一行代码最大长度等非常多的细节。
当团队中的所有成员都使用相同的风格指南时,代码看起来将是统一的。无论是团队中谁写的,都是一样的风格。
当然,一个团队可以制定他们自己的风格指南,但是没必要这样做。现在已经有了很多制定好的代码风格指南可供选择。
一些受欢迎的选择:
- https://google.github.io/styleguide/jsguide.html
- https://github.com/airbnb/javascript
- https://github.com/rwaldron/idiomatic.js
- https://standardjs.com/
- 还有很多……
如果你是一个初学者,你可以从本章中上面的内容开始。然后你可以浏览其他风格指南,并选择一个你最喜欢的。
自动检查器
检查器(Linters)是可以自动检查代码样式,并提出改进建议的工具。
它们的妙处在于进行代码风格检查时,还可以发现一些代码错误,例如变量或函数名中的错别字。因此,即使你不想坚持某一种特定的代码风格,我也建议你安装一个检查器。
下面是一些最出名的代码检查工具:
- https://www.jslint.com/ —— 第一批检查器之一。
- https://www.jshint.com/ —— 比 JSLint 多了更多设置。
- https://eslint.org/ —— 应该是最新的一个。
它们都能够做好代码检查。我使用的是 https://eslint.org/。
大多数检查器都可以与编辑器集成在一起:只需在编辑器中启用插件并配置代码风格即可。
例如,要使用 ESLint 你应该这样做:
- 安装 https://nodejs.org/。
- 使用
npm install -g eslint命令(npm 是一个 JavaScript 包安装工具)安装 ESLint。 - 在你的 JavaScript 项目的根目录(包含该项目的所有文件的那个文件夹)创建一个名为
.eslintrc的配置文件。 - 在集成了 ESLint 的编辑器中安装/启用插件。大多数编辑器都有这个选项。
下面是一个 .eslintrc 文件的例子:
{
"extends": "eslint:recommended",
"env": {
"browser": true,
"node": true,
"es6": true
},
"rules": {
"no-console": 0,
"indent": 2
}
}这里的 "extends" 指令表示我们是基于 "eslint:recommended" 的设置项而进行设置的。之后,我们可以制定我们自己的规则。
你也可以从网上下载风格规则集并进行扩展。有关安装的更多详细信息,请参见 https://eslint.org/docs/latest/use/getting-started
此外,某些 IDE 有内建的检查器,这非常方便,但是不像 ESLint 那样可自定义。
总结
本章描述的(和提到的代码风格指南中的)所有语法规则,都旨在帮助你提高代码可读性。它们都是值得商榷的。
当我们思考如何写“更好”的代码的时候,我们应该问自己的问题是:“什么可以让代码可读性更高,更容易被理解?”和“什么可以帮助我们避免错误?”这些是我们讨论和选择代码风格时要牢记的主要原则。
阅读流行的代码风格指南,可以帮助你了解有关代码风格的变化趋势和最佳实践的最新想法。
任务
不好的风格
重要程度: 4
下面的代码风格有什么问题?
function pow(x,n)
{
let result=1;
for(let i=0;i<n;i++) {result*=x;}
return result;
}
let x=prompt("x?",''), n=prompt("n?",'')
if (n<=0)
{
alert(`Power ${n} is not supported, please enter an integer number greater than zero`);
}
else
{
alert(pow(x,n))
}修复它。
解决方案
你可以注意到以下几点:
function pow(x,n) // <- 参数之间没有空格
{ // <- 花括号独占了一行
let result=1; // <- = 号两边没有空格
for(let i=0;i<n;i++) {result*=x;} // <- 没有空格
// { ... } 里面的内容应该在新的一行上
return result;
}
let x=prompt("x?",''), n=prompt("n?",'') // <-- 从技术的角度来看是可以的,
// 但是拆分成 2 行会更好,并且这里也缺了空格和分号 ;
if (n<=0) // <- (n <= 0) 里面没有空格,并且应该在本行上面加一个空行
{ // <- 花括号独占了一行
// 下面的一行代码太长了,可以将其拆分成 2 行以提高可读性
alert(`Power ${n} is not supported, please enter an integer number greater than zero`);
}
else // <- 可以像 "} else {" 这样写在一行上
{
alert(pow(x,n)) // 缺失了空格和分号 ;
}修改后的版本:
function pow(x, n) {
let result = 1;
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}
let x = prompt("x?", "");
let n = prompt("n?", "");
if (n <= 0) {
alert(`Power ${n} is not supported,
please enter an integer number greater than zero`);
} else {
alert( pow(x, n) );
}3.3 注释
所属章节: 3. 代码质量
所属部分: 第一部分 - JavaScript 编程语言
原始链接: https://zh.javascript.info/comments
正如我们在 /structure 一章所了解到的那样,注释可以是以 // 开始的单行注释,也可以是 /* ... */ 结构的多行注释。
我们通常通过注释来描述代码怎样工作和为什么这样工作。
乍一看,写注释可能很简单,但初学者在编程的时候,经常错误地使用注释。
糟糕的注释
新手倾向于使用注释来解释“代码中发生了什么”。就像这样:
// 这里的代码会先做这件事(……)然后做那件事(……)
// ……谁知道还有什么……
very;
complex;
code;但在好的代码中,这种“解释性”注释的数量应该是最少的。严格地说,就算没有它们,代码也应该很容易理解。
关于这一点有一个很棒的原则:“如果代码不够清晰以至于需要一个注释,那么或许它应该被重写。”
配方:分解函数
有时候,用一个函数来代替一个代码片段是更好的,就像这样:
function showPrimes(n) {
nextPrime:
for (let i = 2; i < n; i++) {
// 检测 i 是否是一个质数(素数)
for (let j = 2; j < i; j++) {
if (i % j == 0) continue nextPrime;
}
alert(i);
}
}更好的变体,使用一个分解出来的函数 isPrime:
function showPrimes(n) {
for (let i = 2; i < n; i++) {
if (!isPrime(i)) continue;
alert(i);
}
}
function isPrime(n) {
for (let i = 2; i < n; i++) {
if (n % i == 0) return false;
}
return true;
}现在我们可以很容易地理解代码了。函数自己就变成了一个注释。这种代码被称为 自描述型 代码。
配方:创建函数
如果我们有一个像下面这样很长的代码块:
// 在这里我们添加威士忌(译注:国外的一种酒)
for(let i = 0; i < 10; i++) {
let drop = getWhiskey();
smell(drop);
add(drop, glass);
}
// 在这里我们添加果汁
for(let t = 0; t < 3; t++) {
let tomato = getTomato();
examine(tomato);
let juice = press(tomato);
add(juice, glass);
}
// ...我们像下面这样,将上面的代码重构为函数,可能会是一个更好的变体:
addWhiskey(glass);
addJuice(glass);
function addWhiskey(container) {
for(let i = 0; i < 10; i++) {
let drop = getWhiskey();
//...
}
}
function addJuice(container) {
for(let t = 0; t < 3; t++) {
let tomato = getTomato();
//...
}
}同样,函数本身就可以告诉我们发生了什么。没有什么地方需要注释。并且分割之后代码的结构也更好了。每一个函数做什么、需要什么和返回什么都非常地清晰。
实际上,我们不能完全避免“解释型”注释。例如在一些复杂的算法中,会有一些出于优化的目的而做的一些巧妙的“调整”。但是通常情况下,我们应该尽可能地保持代码的简单和“自我描述”性。
好的注释
所以,解释性注释通常来说都是不好的。那么哪一种注释才是好的呢?
描述架构
对组件进行高层次的整体概括,它们如何相互作用、各种情况下的控制流程是什么样的……简而言之 —— 代码的鸟瞰图。有一个专门用于构建代码的高层次架构图,以对代码进行解释的特殊编程语言 http://wikipedia.org/wiki/Unified_Modeling_Language 。绝对值得学习。
记录函数的参数和用法
有一个专门用于记录函数的语法 http://en.wikipedia.org/wiki/JSDoc :用法、参数和返回值。
例如:
/**
* 返回 x 的 n 次幂的值。
*
* @param {number} x 要改变的值。
* @param {number} n 幂数,必须是一个自然数。
* @return {number} x 的 n 次幂的值。
*/
function pow(x, n) {
...
}这种注释可以帮助我们理解函数的目的,并且不需要研究其内部的实现代码,就可以直接正确地使用它。
顺便说一句,很多诸如 https://www.jetbrains.com/webstorm/ 这样的编辑器,都可以很好地理解和使用这些注释,来提供自动补全和一些自动化代码检查工作。
当然,也有一些像 https://github.com/jsdoc/jsdoc 这样的工具,可以通过注释直接生成 HTML 文档。你可以在 https://jsdoc.app 阅读更多关于 JSDoc 的信息。
为什么任务以这种方式解决?
写了什么代码很重要。但是为什么 不 那样写可能对于理解正在发生什么更重要。为什么任务是通过这种方式解决的?代码并没有给出答案。
如果有很多种方法都可以解决这个问题,为什么偏偏是这一种?尤其当它不是最显而易见的那一种的时候。
没有这样的注释的话,就可能会发生下面的情况:
- 你(或者你的同事)打开了前一段时间写的代码,看到它不是最理想的实现方式。
- 你会想:“我当时是有多蠢啊,现在我真是太聪明了”,然后用“更显而易见且正确的”方式重写了一遍。
- ……重写的这股冲动劲是好的。但是在重写的过程中你发现“更显而易见”的解决方案实际上是有缺陷的。你甚至依稀地记得为什么会这样,因为你很久之前就已经尝试过这样做了。于是你又还原了那个正确的实现,但是时间已经浪费了。
解决方案的注释非常的重要。它们可以帮助你以正确的方式继续开发。
代码有哪些巧妙的特性?它们被用在了什么地方?
如果代码存在任何巧妙和不显而易见的方法,那绝对需要注释。
总结
一个好的开发者的标志之一就是他的注释:它们的存在甚至它们的缺席(译注:在该注释的地方注释,在不需要注释的地方则不注释,甚至写得好的自描述函数本身就是一种注释)。
好的注释可以使我们更好地维护代码,一段时间之后依然可以更高效地回到代码高效开发。
注释这些内容:
- 整体架构,高层次的观点。
- 函数的用法。
- 重要的解决方案,特别是在不是很明显时。
避免注释:
- 描述“代码如何工作”和“代码做了什么”。
- 避免在代码已经足够简单或代码有很好的自描述性而不需要注释的情况下,还写些没必要的注释。
注释也被用于一些如 JSDoc3 等文档自动生成工具:它们读取注释然后生成 HTML 文档(或者其他格式的文档)。
3.4 忍者代码
所属章节: 3. 代码质量
所属部分: 第一部分 - JavaScript 编程语言
原始链接: https://zh.javascript.info/ninja-code
学而不思则罔,思而不学则殆。
孔子《论语》
过去的程序员忍者使用这些技巧,来使代码维护者的头脑更加敏锐。
代码审查大师在测试任务中寻找它们。
一些新入门的开发者有时候甚至比忍者程序员能够更好地使用它们。
仔细阅读本文,找出你是谁 —— 一个忍者、一个新手、或者一个代码审查者?
检测到讽刺意味
许多人试图追随忍者的脚步。只有极少数成功了。
简洁是智慧的灵魂
把代码尽可能写得短。展示出你是多么的聪明啊。
在编程中,多使用一些巧妙的编程语言特性。
例如,看一下这个三元运算符 '?':
// 从一个著名的 JavaScript 库中截取的代码
i = i ? i < 0 ? Math.max(0, len + i) : i : 0;很酷,对吗?如果你这样写了,那些看到这一行代码并尝试去理解 i 的值是什么的开发者们,就会有一个“快活的”的时光了。然后会来找你寻求答案。
告诉他短一点总是更好的。引导他进入忍者之路。
一个字母的变量
道隐无名。夫唯道善贷且成。
老子(道德经)
另一个缩减代码量的方法是,到处使用单字母的变量名。例如 a、b 或 c。
短变量就像森林中真正的忍者一样,一下就找不到了。没有人能够通过编辑器的“搜索”功能找到它。即使有人做到了,他也不能“破译”出变量名 a 或 b 到底是什么意思。
……但是有一个例外情况。一个真正的忍者绝不会在 "for" 循环中使用 i 作为计数器。在任何地方都可以,但是这里不会用。你随便一找,就能找到很多不寻常的字母。例如 x 或 y。
使用一个不寻常的变量多酷啊,尤其是在长达 1-2 页(如果可以的话,你可以写得更长)的循环体中使用的时候。如果某人要研究循环内部实现的时候,他就很难很快地找出变量 x 其实是循环计数器啦。
使用缩写
如果团队规则中禁止使用一个字母和模糊的命名 — 那就缩短命名,使用缩写吧。
像这样:
list→lstuserAgent→uabrowser→brsr- ……等
只有具有真正良好直觉的人,才能够理解这样的命名。尽可能缩短一切。只有真正有价值的人,才能够维护这种代码的开发。
Soar high,抽象化
大方无隅,
大器晚成,
大音希声,
大象无形。老子(道德经)
当选择一个名字时,尽可能尝试使用最抽象的词语。例如 obj、data、value、item 和 elem 等。
- 一个变量的理想名称是
data。 在任何能用的地方都使用它。的确,每个变量都持有 数据(data),对吧?
……但是 data 已经用过了怎么办?可以尝试一下 value,它也很普遍。毕竟,一个变量总会有一个 值(value),对吧?
- 根据变量的类型为变量命名:
str、num……
尝试一下吧。新手可能会诧异 — 这些名字对于忍者来说真的有用吗?事实上,有用的!
一方面,变量名仍然有着一些含义。它说明了变量内是什么:一个字符串、一个数字或是其他的东西。但是当一个局外人试图理解代码时,他会惊讶地发现实际上没有任何有效信息!最终就无法修改你精心思考过的代码。
我们可以通过代码调试,很容易地看出值的类型。但是变量名的含义呢?它存了哪一个字符串或数字?
如果思考的深度不够,是没有办法搞明白的。
- ……但是如果找不到更多这样的名字呢? 可以加一个数字:
data1, item2, elem5……
注意测试
只有一个真正细心的程序员才能理解你的代码。但是怎么检验呢?
方式之一 —— 使用相似的变量名,像 date 和 data。
尽你所能地将它们混合在一起。
想快速阅读这种代码是不可能的。并且如果有一个错别字时……额……我们卡在这儿好长时间了,到饭点了 (⊙v⊙)。
智能同义词
道,可道,非常道。名,可名,非常名。
老子《道德经》
对 同一个 东西使用 类似 的命名,可以使生活更有趣,并且能够展现你的创造力。
例如,函数前缀。如果一个函数的功能是在屏幕上展示一个消息 — 名称可以以 display… 开头,例如 displayMessage。如果另一个函数展示别的东西,比如一个用户名,名称可以以 show… 开始(例如 showName)。
暗示这些函数之间有微妙的差异,实际上并没有。
与团队中的其他忍者们达成一个协议:如果张三在他的代码中以 display... 来开始一个“显示”函数,那么李四可以用 render..,王二可以使用 paint...。你可以发现代码变得多么地有趣多样呀。
……现在是帽子戏法!
对于有非常重要的差异的两个函数 — 使用相同的前缀。
例如,printPage(page) 函数会使用一个打印机(printer)。printText(text) 函数会将文字显示到屏幕上。让一个不熟悉的读者来思考一下:“名字为 printMessage(message) 的函数会将消息放到哪里呢?打印机还是屏幕上?”。为了让代码真正耀眼,printMessage(message) 应该将消息输出到新窗口中!
重用名字
始制有名,
名亦既有,
夫亦将知止,
知止可以不殆。老子(道德经)
仅在绝对必要时才添加新变量。
否则,重用已经存在的名字。直接把新值写进变量即可。
在一个函数中,尝试仅使用作为参数传递的变量。
这样就很难确定这个变量的值现在是什么了。也不知道它是从哪里来的。目的是提高阅读代码的人的直觉和记忆力。一个直觉较弱的人必须逐行分析代码,跟踪每个代码分支中的更改。
这个方法的一个进阶方案是,在循环或函数中偷偷地替换掉它的值。
例如:
function ninjaFunction(elem) {
// 基于变量 elem 进行工作的 20 行代码
elem = clone(elem);
// 又 20 行代码,现在使用的是 clone 后的 elem 变量。
}想要在后半部分中使用 elem 的程序员会感到很诧异……只有在调试期间,检查代码之后,他才会发现他正在使用克隆过的变量!
经常看到这样的代码,即使对经验丰富的忍者来说也是致命的。
下划线的乐趣
在变量名前加上下划线 _ 和 __。例如 _name 和 __value。如果只有你知道它们的含义,那就非常棒了。或者,加这些下划线只是为了好玩儿,没有任何含义,那就更棒了!
加下划线可谓是一箭双雕。首先,代码变得更长,可读性更低;并且,你的开发者小伙伴可能会花费很长时间,来弄清楚下划线是什么意思。
聪明的忍者会在代码的一个地方使用下划线,然后在其他地方刻意避免使用它们。这会使代码变得更加脆弱,并提高了代码未来出现错误的可能性。
展示你的爱
向大家展现一下你那丰富的情感!像 superElement、megaFrame 和 niceItem 这样的名字一定会启发读者。
事实上,从一方面来说,看似写了一些东西:super..、mega..、nice..。但从另一方面来说 — 并没有提供任何细节。阅读代码的人可能需要耗费一到两个小时的带薪工作时间,冥思苦想来寻找一个隐藏的含义。
重叠外部变量
处明者不见暗中一物,
处暗者能见明中区事。关尹子
对函数内部和外部的变量,使用相同的名称。很简单,不用费劲想新的名称。
let user = authenticateUser();
function render() {
let user = anotherValue();
...
...许多行代码...
...
... // <-- 某个程序员想要在这里使用 user 变量……
...
}在研究 render 内部代码的程序员可能不会注意到,有一个内部变量 user 屏蔽了外部的 user 变量。
然后他会假设 user 仍然是外部的变量然后使用它,authenticateUser() 的结果……陷阱出来啦!你好呀,调试器……
无处不在的副作用
有些函数看起来它们不会改变任何东西。例如 isReady(),checkPermission(),findTags()……它们被假定用于执行计算、查找和返回数据,而不会更改任何它们自身之外的数据。这被称为“无副作用”。
一个非常惊喜的技巧就是,除了主要任务之外,给它们添加一个“有用的”行为。
当你的同事看到被命名为 is..、check.. 或 find... 的函数改变了某些东西的时候,他脸上肯定是一脸懵逼的表情 — 这会扩大你的理性界限。
另一个惊喜的方式是,返回非标准的结果。
展示你原来的想法!让调用 checkPermission 时的返回值不是 true/false,而是一个包含检查结果的复杂对象。
那些尝试写 if (checkPermission(..)) 的开发者,会很疑惑为什么它不能工作。告诉他们:“去读文档吧”。然后给出这篇文章。
强大的函数
大道泛兮,
其左可右。老子(道德经)
不要让函数受限于名字中写的内容。拓宽一些。
例如,函数 validateEmail(email) 可以(除了检查邮件的正确性之外)显示一个错误消息并要求重新输入邮件。
额外的行为在函数名称中不应该很明显。一个真正的忍者会使它们在代码中也不明显。
将多个行为合并到一起,可以保护你的代码不被重用。
想象一下,另一个开发者只想检查邮箱而不想输出任何信息。你的函数 validateEmail(email) 对他而言就不合适啦。所以他不会找你问关于这些函数的任何事而打断你的思考。
总结
上面的所有“建议”都是从真实的代码中提炼而来的……有时候,这些代码是由有经验的开发者写的。也许比你更有经验 ;)
- 遵从其中的一丢丢,你的代码就会变得充满惊喜。
- 遵从其中的一大部分,你的代码将真正成为你的代码,没有人会想改变它。
- 遵从所有,你的代码将成为寻求启发的年轻开发者的宝贵案例。
3.5 使用 Mocha 进行自动化测试
所属章节: 3. 代码质量
所属部分: 第一部分 - JavaScript 编程语言
原始链接: https://zh.javascript.info/testing-mocha
自动化测试将被用于进一步的任务中,并且还将被广泛应用在实际项目中。
我们为什么需要测试?
当我们在写一个函数时,我们通常可以想象出它应该做什么:哪些参数会给出哪些结果。
在开发期间,我们可以通过运行程序来检查它并将结果与预期进行比较。例如,我们可以在控制台中这么做。
如果出了问题 —— 那么我们会修复代码,然后再一次运行并检查结果 —— 直到它工作为止。
但这样的手动“重新运行”是不完美的。
当通过手动重新运行来测试代码时,很容易漏掉一些东西。
例如,我们要创建一个函数 f。写一些代码,然后测试:f(1) 可以执行,但是 f(2) 不执行。我们修复了一下代码,现在 f(2) 可以执行了。看起来已经搞定了?但是我们忘了重新测试 f(1)。这样有可能会导致出现错误。
这是非常典型的。当我们在开发一些东西时,我们会保留很多可能需要的用例。但是不要想着程序员在每一次代码修改后都去检查所有的案例。所以这就很容易造成修复了一个问题却造成另一个问题的情况。
自动化测试意味着测试是独立于代码的。它们以各种方式运行我们的函数,并将结果与预期结果进行比较。
行为驱动开发(BDD)
我们来使用一种名为 http://en.wikipedia.org/wiki/Behavior-driven_development 或简言为 BDD 的技术。
BDD 包含了三部分内容:测试、文档和示例。
为了理解 BDD,我们将研究一个实际的开发案例。
开发 “pow”:规范
我们想要创建一个函数 pow(x, n) 来计算 x 的 n 次幂(n 为整数)。我们假设 n≥0。
这个任务只是一个例子:JavaScript 中有一个 ** 运算符可以用于幂运算。但是在这里我们专注于可以应用于更复杂任务的开发流程上。
在创建函数 pow 的代码之前,我们可以想象函数应该做什么并且描述出来。
这样的描述被称作 规范(specification, spec),包含用例的描述以及针对它们的测试,如下所示:
describe("pow", function() {
it("raises to n-th power", function() {
assert.equal(pow(2, 3), 8);
});
});正如你所看到的,一个规范包含三个主要的模块:
describe("title", function() { ... })
表示我们正在描述的功能是什么。在我们的例子中我们正在描述函数 pow。用于组织“工人(workers)” —— it 代码块。
it("use case description", function() { ... })it 里面的描述部分,我们以一种 易于理解 的方式描述特定的用例,第二个参数是用于对其进行测试的函数。
assert.equal(value1, value2)it 块中的代码,如果实现是正确的,它应该在执行的时候不产生任何错误。
assert.* 函数用于检查 pow 函数是否按照预期工作。在这里我们使用了其中之一 —— assert.equal,它会对参数进行比较,如果它们不相等则会抛出一个错误。这里它检查了 pow(2, 3) 的值是否等于 8。还有其他类型的比较和检查,我们将在后面介绍到。
规范可以被执行,它将运行在 it 块中指定的测试。我们稍后会看到。
开发流程
开发流程通常看起来像这样:
- 编写初始规范,测试最基本的功能。
- 创建一个最初始的实现。
- 检查它是否工作,我们运行测试框架 http://mochajs.org/ (很快会有更多细节)来运行测试。当功能未完成时,将显示错误。我们持续修正直到一切都能工作。
- 现在我们有一个带有测试的能工作的初步实现。
- 我们增加更多的用例到规范中,或许目前的程序实现还不支持。无法通过测试。
- 回到第 3 步,更新程序直到测试不会抛出错误。
- 重复第 3 步到第 6 步,直到功能完善。
如此来看,开发就是不断地 迭代。我们写规范,实现它,确保测试通过,然后写更多的测试,确保它们工作等等。最后,我们有了一个能工作的实现和针对它的测试。
让我们在我们的开发案例中看看这个开发流程吧。
在我们的案例中,第一步已经完成了:我们有一个针对 pow 的初始规范。因此让我们来实现它吧。但在此之前,让我们用一些 JavaScript 库来运行测试,就是看看测试是通过了还是失败了。
行为规范
在本教程中,我们将使用以下 JavaScript 库进行测试:
- http://mochajs.org/ —— 核心框架:提供了包括通用型测试函数
describe和it,以及用于运行测试的主函数。 - http://chaijs.com —— 提供很多断言(assertion)支持的库。它提供了很多不同的断言,现在我们只需要用
assert.equal。 - http://sinonjs.org/ —— 用于监视函数、模拟内建函数和其他函数的库,我们在后面才会用到它。
这些库都既适用于浏览器端,也适用于服务器端。这里我们将使用浏览器端的变体。
包含这些框架和 pow 规范的完整的 HTML 页面:
<!DOCTYPE html>
<html>
<head>
<!-- add mocha css, to show results -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css">
<!-- add mocha framework code -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script>
<script>
mocha.setup('bdd'); // minimal setup
</script>
<!-- add chai -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script>
<script>
// chai has a lot of stuff, let's make assert global
let assert = chai.assert;
</script>
</head>
<body>
<script>
function pow(x, n) {
/* function code is to be written, empty now */
}
</script>
<!-- the script with tests (describe, it...) -->
<script src="test.js"></script>
<!-- the element with id="mocha" will contain test results -->
<div id="mocha"></div>
<!-- run tests! -->
<script>
mocha.run();
</script>
</body>
</html>该页面可分为五个部分:
<head>—— 添加用于测试的第三方库和样式文件。<script>包含测试函数,在我们的例子中 —— 和pow相关的代码。- 测试代码 —— 在我们的案例中是名为
test.js的脚本,它包含上面describe("pow", ...)的那些代码。 - HTML 元素
<div id="mocha">将被 Mocha 用来输出结果。 - 可以使用
mocha.run()命令来开始测试。
结果:
到目前为止,测试失败了,出现了一个错误。这是合乎逻辑的:我们的 pow 是一个空函数,因此 pow(2,3) 返回了 undefined 而不是 8。
未来,我们会注意到有更高级的测试工具,像是 https://karma-runner.github.io/ 或其他的,使自动运行许多不同的测试变得更容易。
初始实现
为了可以通过测试,让我们写一个 pow 的简单实现:
function pow() {
return 8; // :) 我们作弊啦!
}哇哦,现在它可以工作了。
改进规范
我们所做的这些绝对是作弊。函数是不起作用的:尝试计算 pow(3,4) 的话就会得到一个不正确的结果,但是测试却通过了。
……但是这种情况却是在实际中相当典型例子。测试通过了,但是函数却是错误的。我们的规范是不完善的。我们需要给它添加更多的测试用例。
这里我们又添加了一个测试来检查 pow(3, 4) = 81。
我们可以选择两种方式中的任意一种来组织测试代码:
- 第一种 —— 在同一个
it中再添加一个assert:
describe("pow", function() {
it("raises to n-th power", function() {
assert.equal(pow(2, 3), 8);
assert.equal(pow(3, 4), 81);
});
});- 第二种 —— 写两个测试:
describe("pow", function() {
it("2 raised to power 3 is 8", function() {
assert.equal(pow(2, 3), 8);
});
it("3 raised to power 4 is 81", function() {
assert.equal(pow(3, 4), 81);
});
});主要的区别是,当 assert 触发一个错误时,it 代码块会立即终止。因此,在第一种方式中,如果第一个 assert 失败了,我们将永远不会看到第二个 assert 的结果。
保持测试之间独立,有助于我们获知代码中正在发生什么,因此第二种方式更好一点。
除此之外,还有一个规范值得遵循。
一个测试检查一个东西。
如果我们在看测试代码的时候,发现在其中有两个相互独立的检查 —— 最好将它拆分成两个更简单的检查。
因此让我们继续使用第二种方式。
结果:
正如我们可以想到的,第二条测试失败了。当然啦,我们的函数总会返回 8,而 assert 期望的是 81。
改进实现
让我们写一些更加实际的代码来通过测试吧:
function pow(x, n) {
let result = 1;
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}为了确保函数可以很好地工作,我们来使用更多值测试它吧。除了手动地编写 it 代码块,我们可以使用 for 循环来生成它们:
describe("pow", function() {
function makeTest(x) {
let expected = x * x * x;
it(`${x} in the power 3 is ${expected}`, function() {
assert.equal(pow(x, 3), expected);
});
}
for (let x = 1; x <= 5; x++) {
makeTest(x);
}
});结果:
嵌套描述
我们继续添加更多的测试。但在此之前,我们需要注意到辅助函数 makeTest 和 for 应该被组合到一起。我们在其他测试中不需要 makeTest,只有在 for 循环中需要它:它们共同的任务就是检查 pow 是如何自乘至给定的幂次方。
使用嵌套的 describe 来进行分组:
describe("pow", function() {
describe("raises x to power 3", function() {
function makeTest(x) {
let expected = x * x * x;
it(`${x} in the power 3 is ${expected}`, function() {
assert.equal(pow(x, 3), expected);
});
}
for (let x = 1; x <= 5; x++) {
makeTest(x);
}
});
// ……可以在这里写更多的测试代码,describe 和 it 都可以添加在这。
});嵌套的 describe 定义了一个新的 “subgroup” 测试。在输出中我们可以看到带有标题的缩进:
将来,我们可以在顶级域中使用 it 和 describe 的辅助函数添加更多的 it 和 describe,它们不会看到 makeTest。
before/after 和 beforeEach/afterEach
我们可以设置 before/after 函数来在运行测试之前/之后执行。也可以使用 beforeEach/afterEach 函数来设置在执行 每一个 it 之前/之后执行。
例如:
describe("test", function() {
before(() => alert("Testing started – before all tests"));
after(() => alert("Testing finished – after all tests"));
beforeEach(() => alert("Before a test – enter a test"));
afterEach(() => alert("After a test – exit a test"));
it('test 1', () => alert(1));
it('test 2', () => alert(2));
});运行顺序将为:
Testing started – before all tests (before)
Before a test – enter a test (beforeEach)
1
After a test – exit a test (afterEach)
Before a test – enter a test (beforeEach)
2
After a test – exit a test (afterEach)
Testing finished – after all tests (after)https://plnkr.co/edit/mmvRd75iRlbC9ASxOJxU?p=preview 通常,beforeEach/afterEach和 before/after 被用于执行初始化,清零计数器或做一些介于每个测试(或测试组)之间的事情。
延伸规范
pow 的基础功能已经完成了。第一次迭代开发完成啦。当我们庆祝和喝完香槟之后,让我们继续改进它吧。
正如前面所说,函数 pow(x, n) 适用于正整数 n。
JavaScript 函数通常会返回 NaN 以表示一个数学错误。接下来我们对无效的 n 值执行相同的操作。
让我们首先将这个行为加到规范中(!):
describe("pow", function() {
// ...
it("for negative n the result is NaN", function() {
assert.isNaN(pow(2, -1));
});
it("for non-integer n the result is NaN", function() {
assert.isNaN(pow(2, 1.5));
});
});新测试的结果:
新加的测试失败了,因为我们的实现方式是不支持它们的。这就是 BDD 的做法:我们首先写一些暂时无法通过的测试,然后去实现它们。
Other assertions
请注意断言语句 assert.isNaN:它用来检查 NaN。
在 http://chaijs.com 中也有其他的断言,例如:
assert.equal(value1, value2)—— 检查相等value1 == value2。assert.strictEqual(value1, value2)—— 检查严格相等value1 === value2。assert.notEqual,assert.notStrictEqual—— 执行和上面相反的检查。assert.isTrue(value)—— 检查value === true。assert.isFalse(value)—— 检查value === false。- ……完整的列表请见 http://chaijs.com/api/assert/
因此我们应该给 pow 再加几行:
function pow(x, n) {
if (n < 0) return NaN;
if (Math.round(n) != n) return NaN;
let result = 1;
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}现在它可以工作了,所有的测试也都通过了:
Open the full final example in the sandbox.总结
在 BDD 中,规范先行,实现在后。最后我们同时拥有了规范和代码。
规范有三种使用方式:
- 作为 测试 —— 保证代码正确工作。
- 作为 文档 ——
describe和it的标题告诉我们函数做了什么。 - 作为 案例 —— 测试实际工作的例子展示了一个函数可以被怎样使用。
有了规范,我们可以安全地改进、修改甚至重写函数,并确保它仍然正确地工作。
这在一个函数会被用在多个地方的大型项目中尤其重要。当我们改变这样一个函数时,没有办法手动检查每个使用它们的地方是否仍旧正确。
如果没有测试,一般有两个办法:
- 展示修改,无论修改了什么。然后我们的用户遇到了 bug,这应该是我们没有手动完成某些检查。
- 如果对出错的惩罚比较严重,并且没有测试,那么大家会很害怕修改这样的函数,然后这些代码就会越来越陈旧,没有人会想接触它。这很不利于发展。
自动化测试则有助于避免这样的问题!
如果这个项目被测试代码覆盖了,就不会出现这种问题。在任何修改之后,我们都可以运行测试,并在几秒钟内看到大量的检查。
另外,一个经过良好测试的代码通常都有更好的架构。
当然,这是因为覆盖了自动化测试的代码更容易修改和改进。但还有另一个原因。
要编写测试,代码的组织方式应确保每个函数都有一个清晰描述的任务、定义良好的输入和输出。这意味着从一开始就有一个好的架构。
在实际开发中有时候可能并不容易,有时很难在写实际代码之前编写规范,因为还不清楚它应该如何表现。但一般来说,编写测试使得开发更快更稳定。
在本教程的后面部分,你将遇到许多包含了测试的任务。所以你会看到更多的实际例子。
编写测试需要良好的 JavaScript 知识。但我们刚刚开始学习它。因此,为了解决所有问题,到目前为止,你不需要编写测试,但是你应该已经能够阅读测试了,即使它们比本章中的内容稍微复杂一些。
任务
测试代码中有什么错误?
重要程度: 5
下面这个 pow 的测试代码有什么错误?
it("Raises x to the power n", function() {
let x = 5;
let result = x;
assert.equal(pow(x, 1), result);
result *= x;
assert.equal(pow(x, 2), result);
result *= x;
assert.equal(pow(x, 3), result);
});附:从语法上来说这些测试代码是正确且通过的。
解决方案
这些测试代码展示了开发人员在编写测试代码时遇到的一些疑惑。
我们这里实际上有三条测试,但是用了一个函数来放置 3 个断言语句。
有时用这种方式编写会更容易,但是如果发生错误,那么到底什么出错了就很不明显。
如果错误发生在一个复杂的执行流的中间,那么我们就必须找出那个点的数据。我们必须 调试测试。
将测试分成多个具有明确输入和输出的 it 代码块会更好。
像是这样:
describe("Raises x to power n", function() {
it("5 in the power of 1 equals 5", function() {
assert.equal(pow(5, 1), 5);
});
it("5 in the power of 2 equals 25", function() {
assert.equal(pow(5, 2), 25);
});
it("5 in the power of 3 equals 125", function() {
assert.equal(pow(5, 3), 125);
});
});我们使用 describe 和一组 it 代码块替换掉了单个的 it。现在,如果某个测试失败了,我们可以清楚地看到数据是什么。
此外,我们可以通过编写 it.only 而不是 it 来隔离单个测试,并以独立模式运行它:
describe("Raises x to power n", function() {
it("5 in the power of 1 equals 5", function() {
assert.equal(pow(5, 1), 5);
});
// Mocha 将只运行这个代码块
it.only("5 in the power of 2 equals 25", function() {
assert.equal(pow(5, 2), 25);
});
it("5 in the power of 3 equals 125", function() {
assert.equal(pow(5, 3), 125);
});
});3.6 Polyfill 和转译器
所属章节: 3. 代码质量
所属部分: 第一部分 - JavaScript 编程语言
原始链接: https://zh.javascript.info/polyfills
JavaScript 语言在稳步发展。也会定期出现一些对语言的新提议,它们会被分析讨论,如果认为有价值,就会被加入到 https://tc39.github.io/ecma262/ 的列表中,然后被加到 http://www.ecma-international.org/publications/standards/Ecma-262.htm 中。
JavaScript 引擎背后的团队关于首先要实现什么有着他们自己想法。他们可能会决定执行草案中的建议,并推迟已经在规范中的内容,因为它们不太有趣或者难以实现。
因此,一个 JavaScript 引擎只能实现标准中的一部分是很常见的情况。
查看语言特性的当前支持状态的一个很好的页面是 https://compat-table.github.io/compat-table/es6/(它很大,我们现在还有很多东西要学)。
作为程序员,我们希望使用最新的特性。好东西越多越好!
另一方面,如何让我们现代的代码在还不支持最新特性的旧引擎上工作?
有两个工作可以做到这一点:
- 转译器(Transpilers)。
- 垫片(Polyfills)。
通过本文,我们一起了解它们的工作原理以及它们在 Web 开发中的位置。
转译器(Transpilers)
https://en.wikipedia.org/wiki/Source-to-source_compiler 是一种可以将源码转译成另一种源码的特殊的软件。它可以解析(“阅读和理解”)现代代码,并使用旧的语法结构对其进行重写,进而使其也可以在旧的引擎中工作。
例如,在 ES2020 之前没有“空值合并运算符” ??。所以,如果访问者使用过时了的浏览器访问我们的网页,那么该浏览器可能就不明白 height = height ?? 100 这段代码的含义。
转译器会分析我们的代码,并将 height ?? 100 重写为 (height !== undefined && height !== null) ? height : 100。
// 在运行转译器之前
height = height ?? 100;
// 在运行转译器之后
height = (height !== undefined && height !== null) ? height : 100;现在,重写了的代码适用于更旧版本的 JavaScript 引擎。
通常,开发者会在自己的计算机上运行转译器,然后将转译后的代码部署到服务器。
说到名字,https://babeljs.io 是最著名的转译器之一。
现代项目构建系统,例如 https://webpack.js.org/ ,提供了在每次代码更改时自动运行转译器的方法,因此很容易将代码转译集成到开发过程中。
垫片(Polyfills)
新的语言特性可能不仅包括语法结构和运算符,还可能包括内建函数。
例如,Math.trunc(n) 是一个“截断”数字小数部分的函数,例如 Math.trunc(1.23) 返回 1。
在一些(非常过时的)JavaScript 引擎中没有 Math.trunc 函数,所以这样的代码会执行失败。
由于我们谈论的是新函数,而不是语法更改,因此无需在此处转译任何内容。我们只需要声明缺失的函数。
更新/添加新函数的脚本被称为“polyfill”。它“填补”了空白并添加了缺失的实现。
对于这种特殊情况,Math.trunc 的 polyfill 是一个实现它的脚本,如下所示:
if (!Math.trunc) { // 如果没有这个函数
// 实现它
Math.trunc = function(number) {
// Math.ceil 和 Math.floor 甚至存在于上古年代的 JavaScript 引擎中
// 在本教程的后续章节中会讲到它们
return number < 0 ? Math.ceil(number) : Math.floor(number);
};
}JavaScript 是一种高度动态的语言。脚本可以添加/修改任何函数,甚至包括内建函数。
两个有趣的 polyfill 库:
- https://github.com/zloirock/core-js 支持了很多特性,允许只包含需要的特性。
总结
在本章中,我们希望激励你学习现代甚至“前沿”的语言特性,即使 JavaScript 引擎还没有很好地支持它们。
只是不要忘记使用转译器(如果使用现代语法或运算符)和 polyfill(添加可能缺少的特性)。它们将确保代码能正常工作。
例如,以后熟悉了 JavaScript,你就可以搭建一个基于 https://webpack.js.org/ 和 https://github.com/babel/babel-loader 插件的代码构建系统。
展示对各种特征的当前支持情况的工具:
- https://compat-table.github.io/compat-table/es6/ —— 对于原生 JavaScript。
- https://caniuse.com/ —— 对于浏览器相关的函数。
P.S. 谷歌的 Chrome 浏览器通常是对最新的语言特性的支持情况最好的浏览器,如果教程的示例运行失败,请尝试使用 Chrome 浏览器。不过,教程中的大多数示例都适用于任意的现代浏览器。
【本篇核心逻辑复盘】
- 在学习并编写任何 JavaScript 代码之前,首先必须掌握如何在浏览器(如 Chrome)中利用开发者工具进行断点调试、日志排查,这是发现并定位问题的前提。
- 其次,无论是单人开发还是团队协作,统一的代码规范、清晰简洁的自描述性代码与高质量的注释不仅能提升可读性,更能避免低级错误和“忍者代码”带来的隐患。
- 最后,通过引入 BDD(行为驱动开发)和 Mocha 自动化测试框架,以及了解 Babel 转译和 Polyfill 机制,能够让你所编写的代码不仅稳健,而且具有更好的向下兼容性,从而顺利跑在不同的环境上。
