Skip to main content

代码质量

包括语法、注释、规范。

调试

有 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.不使用同义词 如displayTimeprintTime可能并无区别 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 是一种高度动态的语言。脚本可以添加/修改任何函数,甚至包括内建函数。