代码质量
包括语法、注释、规范。
调试
有 3 种方式来暂停一个脚本:
- 断点。
- debugger 语句。
- error(开发者工具打开+控制台开启)。
语法
如下是一些需要遵守的代码规范(建议)。
花括号
if (n < 0) {
alert(`Power ${n} is not supported`);
}
行的长度
没有人喜欢读一长串代码,最好将代码分割一下。
// 回勾引号 ` 允许将字符串拆分为多行
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 个空格。
- 垂直方向上的缩进:用于将代码拆分成逻辑块的空行。
插入一个额外的空行有助于使代码更具可读性。 写代码时,不应该出现连续超过 9 行都没有被垂直分割的代码。
分号
每一个语句后面都应该有一个分号。即使它可以被跳过。
嵌套的层级
尽量避免代码嵌套层级过深。
例如,在循环中,有时候使用 continue 指令以避免额外的嵌套是一个好主意。
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
这个“特殊情况”在一开始就被处理了。一旦条件通过检查,代码执行就可以进入到“主”代码流,而不需要额外的嵌套。
函数位置
如果你正在写几个“辅助”函数和一些使用它们的代码,那么 先写调用代码,再写函数。
// 调用函数的代码
let elem = createElement();
setHandler(elem);
walkAround();
// --- 辅助函数 ---
function createElement() {
...
}
function setHandler(elem) {
...
}
function walkAround() {
...
}
自动检查器
检查器(Linters)是可以自动检查代码样式,并提出改进建议的工具。
它们的妙处在于进行代码风格检查时,还可以发现一些代码错误,例如变量或函数名中的错别字。因此,即使你不想坚持某一种特定的代码风格,我也建议你安装一个检查器。
下面是一些最出名的代码检查工具: JSLint —— 第一批检查器之一。 JSHint —— 比 JSLint 多了更多设置。 ESLint —— 应该是最新的一个。
大多数检查器都可以与编辑器集成在一起:只需在编辑器中启用插件并配置代码风格即可。
例如,要使用 ESLint 你应该这样做:
安装 Node.JS。
使用 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” 的设置项而进行设置的。之后,我们制定我们自己的规则。
注释
注释可以是以 //
开始的单行注释,也可以是 /* ... */
结构的多行注释。
糟糕的注释: 使用注释来解释“代码中发生了什么”,就像这样:
// 这里的代码会先做这件事(……)然后做那件事(……)
// ……谁知道还有什么……
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;
}
现在我们可以很容易地理解代码了。函数自己就变成了一个注释。这种代码被称为 自描述型 代码。
实际上,我们不能完全避免“解释型”注释。例如在一些复杂的算法中,会有一些出于优化的目的而做的一些巧妙的“调整”。但是通常情况下,我们应该尽可能地保持代码的简单和“自我描述”性。
好的注释
1.描述架构 对组件进行高层次的整体概括,它们如何相互作用、各种情况下的控制流程是什么样的……简而言之 —— 代码的鸟瞰图。有一个专门用于构建代码的高层次架构图,以对代码进行解释的特殊编程语言 UML。 2.记录函数的参数和用法 有一个专门用于记录函数的语法 JSDoc:用法、参数和返回值。 例如:
/**
* 返回 x 的 n 次幂的值。
*
* @param {number} x 要改变的值。
* @param {number} n 幂数,必须是一个自然数。
* @return {number} x 的 n 次幂的值。
*/
function pow(x, n) {
...
}
这种注释可以帮助我们理解函数的目的,并且不需要研究其内部的实现代码,就可以直接正确地使用它。
规范
不要过分简洁
// 从一个著名的 JavaScript 库中截取的代码
i = i ? i < 0 ? Math.max(0, len + i) : i : 0;
变量规范
1.不使用单字母变量
2.不使用缩写
3.不抽象命名,功能描述要具体
抽象词: obj、data、value、item、elem 等。
笼统词: str、num等。
无意义描述词: nice、good等。
4.不使用同义词
如displayTime
与printTime
可能并无区别
5.不重用变量
包括内部变量和外部变量,都不要重复赋值使用。
6.仅在必要时使用下划线
下划线词代表特殊意义,不能无序使用。
7.遵守无副作用函数规定
有些函数名称代表了校验、检查等功能,无实际的数据或业务处理,如:isNum
checkTime
findStr
那么,在这些函数中就不应该存在其他额外的代码执行。
8.遵守命名代表的函数功能
不做函数名称代表含义之外的事情。
自动化测试
自动化测试意味着测试是独立于代码的。它们以各种方式运行我们的函数,并将结果与预期结果进行比较。
测试准备
Mocha —— 核心框架:提供了包括通用型测试函数 describe 和 it,以及用于运行测试的主函数。 Chai —— 提供很多断言(assertion)支持的库。它提供了很多不同的断言,现在我们只需要用 assert.equal。 Sinon —— 用于监视函数、模拟内建函数和其他函数的库,我们在后面才会用到它。
步骤1:搭建本地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>
步骤2:编写测试代码
在test.js
中写入如下内容:
// 测试语句
// 1.使用describe描述功能
describe("pow", function() {
// 2.使用it描述用例内容
it("2 raised to power 3 is 8", function() {
// 3.使用断言执行预设的用例
assert.equal(pow(2, 3), 8);
});
it("3 raised to power 4 is 81", function() {
assert.equal(pow(3, 4), 81);
});
});
// 具体函数
function pow(x, n) {
let result = 1;
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}
测试规范
1.一个测试检查一个东西
2.测试用例批量生成
// 引入循环
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);
}
});
3.嵌套描述
// 引入多个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 都可以添加在这。
});
4.引入预处理/后处理逻辑
// 设置 before/after 函数来在运行测试之前/之后执行。
// 可以使用 beforeEach/afterEach 函数来设置在执行 每一个 it 之前/之后执行。
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"));
5.延伸规范 除函数的目标范围入参外,还需要考虑到非正常值的入参
// 使用isNaN断言测试异常入参的返回值
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));
});
});
BBD规范
行为驱动开发(BBD),包含三部分:测试、文档和示例。 在BBD中,规范先行,实现在后。首先写一些暂时无法通过的测试,然后去实现它们。
规范有三种使用方式: 作为 测试 —— 保证代码正确工作。 作为 文档 —— describe 和 it 的标题告诉我们函数做了什么。 作为 案例 —— 测试实际工作的例子展示了一个函数可以被怎样使用。
Polyfill 和转译器
JavaScript 语言在稳步发展。也会定期出现一些对语言的新提议,它们会被分析讨论,如果认为有价值,就会被加入到 https://tc39.github.io/ecma262/ 的列表中,然后被加到 规范 中。
但JavaScript 引擎背后的团队关于首先要实现什么有着他们自己想法。他们可能会决定执行草案中的建议,并推迟已经在规范中的内容,因为它们不太有趣或者难以实现。
因此,一个 JavaScript 引擎只能实现标准中的一部分是很常见的情况。
作为程序员,我们希望使用最新的特性。好东西越多越好!
另一方面,如何让我们现代的代码在还不支持最新特性的旧引擎上工作?
有两个工作可以做到这一点:转译器(Transpilers)和 垫片(Polyfills)。
转译器(Transpilers)
转译器 是一种可以将源码转译成另一种源码的特殊的软件。它可以解析(“阅读和理解”)现代代码,并使用旧的语法结构对其进行重写,进而使其也可以在旧的引擎中工作。
例如,在 ES2020 之前没有“空值合并运算符” ??。所以,如果访问者使用过时了的浏览器访问我们的网页,那么该浏览器可能就不明白 height = height ?? 100 这段代码的含义。
转译器会分析我们的代码,并将 height ?? 100 重写为 (height !== undefined && height !== null) ? height : 100。
// 在运行转译器之前
height = height ?? 100;
// 在运行转译器之后
height = (height !== undefined && height !== null) ? height : 100;
现在,重写了的代码适用于更旧版本的 JavaScript 引擎。
通常,开发者会在自己的计算机上运行转译器 ,然后将转译后的代码部署到服务器。
说到名字,Babel 是最著名的转译器之一。
现代项目构建系统,例如 webpack,提供了在每次代码更改时自动运行转译器的方法,因此很容易将代码转译集成到开发过程中。
垫片(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 是一种高度动态的语言。脚本可以添加/修改任何函数,甚至包括内建函数。