最佳实践

可维护性

什么是可维护的代码

通常,说代码“可维护”就意味着它具备如下特点。

  • 容易理解:无须求助原始开发者,任何人一看代码就知道它是干什么的,以及它是怎么实现的。

  • 符合常识:代码中的一切都显得顺理成章,无论操作有多么复杂。

  • 容易适配:即使数据发生变化也不用完全重写。

  • 容易扩展:代码架构经过认真设计,支持未来扩展核心功能。

  • 容易调试:出问题时,代码可以给出明确的信息,通过它能直接定位问题。

编码规范

可读性

要想让代码容易维护,首先必须使其可读。可读性必须考虑代码是一种文本文件。为此,代码缩进 是保证可读性的重要基础。如果所有人都使用相同的缩进,整个项目的代码就会更容易让人看懂。缩进 通常要使用空格数而不是 Tab(制表符)来定义,因为后者在不同文本编辑器中的显示不同。一般来说, 缩进是 4 个空格,当然具体多少个可以自己定。 可读性的另一方面是代码注释。在大多数编程语言中,广泛接受的做法是为每个方法都编写注释。 因为 JavaScript 可以在代码中的任何地方创建函数,所以这一点经常被忽视。正因为如此,可能给 JavaScript 中的每个函数都写注释才更重要。一般来说,以下这些地方应该写注释。

  • 函数和方法。每个函数和方法都应该有注释来描述其用途,以及完成任务所用的算法。同时, 也写清使用这个函数或方法的前提(假设)、每个参数的含义,以及函数是否返回值(因为通过 函数定义看不出来)。

  • 大型代码块。多行代码但用于完成单一任务的,应该在前面给出注释,把要完成的任务写清楚。

  • 复杂的算法。如果使用了独特的方法解决问题,要通过注释解释明白。这样不仅可以帮助别人查看代码,也可以帮助自己今后查看代码。

  • 使用黑科技。由于浏览器之间的差异,JavaScript 代码中通常包含一些黑科技。不要假设其他人 一看就能明白某个黑科技是为了解决某个浏览器的什么问题。如果某个浏览器不能使用正常方 式达到目的,那要在注释里把黑科技的用途写出来。这样可以避免别人误以为黑科技没有用而 把它“修复”掉,结果你已解决的问题又会出现。 缩进和注释可以让代码更容易理解,将来也更容易维护。

变量和函数命名

以下是关于命名的通用规则。 变量名应该是名词,例如 car 或 person。

  • 函数名应该以动词开始,例如 getName()。返回布尔值的函数通常以 is 开头,比如 isEnabled()。

  • 对变量和函数都使用符合逻辑的名称,不用担心长度。长名字的问题可以通过后处理和压缩解 决(本章稍后会讨论)。

  • 变量、函数和方法应该以小写字母开头,使用驼峰大小写(camelCase)形式,如 getName()和 isPerson。类名应该首字母大写,如 Person、RequestFactory。常量值应该全部大写并以 下划线相接,比如 REQUEST_TIMEOUT。

  • 名称要尽量用描述性和直观的词汇,但不要过于冗长。getName()一看就知道会返回名称,而 PersonFactory 一看就知道会产生某个 Person 对象或实体。 要完全避免没有用的变量名,如不能表示所包含数据的类型的变量名。通过适当命名,代码读起来 就会像故事,因此更容易理解。

使用常量

关键在于把数据从使用它们的逻辑中分离出来。可以使用以下标准检查哪些数据需要提取。

  • 重复出现的值:任何使用超过一次的值都应该提取到常量中,这样可以消除一个值改了而另一 个值没改造成的错误。这里也包括 CSS 的类名。

  • 用户界面字符串:任何会显示给用户的字符串都应该提取出来,以方便实现国际化。

  • URL:Web 应用程序中资源的地址经常会发生变化,因此建议把所有 URL 集中放在一个地方 管理。

  • 任何可能变化的值:任何时候,只要在代码中使用字面值,就问问自己这个值将来是否可能会 变。如果答案是“是”,那么就应该把它提取到常量中。

性能

随着作用域链中作用域数量的 增加,访问当前作用域外部变量所需的时间也会增加。访问全局变量始终比访问局部变量慢,因为必须 遍历作用域链。任何可以缩短遍历作用域链时间的举措都能提升代码性能。

避免全局查找

改进代码性能非常重要的一件事,可能就是要提防全局查询。全局变量和函数相比于局部值始终是 最费时间的,因为需要经历作用域链查找。来看下面的函数:

function updateUI() {
  let imgs = document.getElementsByTagName("img");
  for (let i = 0, len = imgs.length; i < len; i++) {
    imgs[i].title = '${document.title} image ${i}';
  }
  let msg = document.getElementById("msg");
  msg.innerHTML = "Update complete.";
}

这个函数看起来好像没什么问题,但其中三个地方引用了全局 document 对象。如果页面的图片非 常多,那么 for 循环中就需要引用 document 几十甚至上百次,每次都要遍历一次作用域链。通过在局部作用域中保存 document 对象的引用,能够明显提升这个函数的性能,因为只需要作用域链查找。 通过创建一个指向 document 对象的局部变量,可以通过将全局查找的数量限制为一个来提高这个函数 的性能:

function updateUI() {
let doc = document;
let imgs = doc.getElementsByTagName("img");
for (let i = 0, len = imgs.length; i < len; i++) {
    imgs[i].title = '${doc.title} image ${i}';
}
  let msg = doc.getElementById("msg");
  msg.innerHTML = "Update complete.";
}

这里先把 document 对象保存在局部变量 doc 中。然后用 doc 替代了代码中所有的 document。 这样调用这个函数只会查找一次作用域链,相对上一个版本,肯定会快很多。因此,一个经验规则就是,只要函数中有引用超过两次的全局对象,就应该把这个对象保存为一个 局部变量。

选择正确的方法

常量值或 O(1),指字面量和保存在变量中的值,表示读取常量值所需的时间不会因值的多少而变化。 读取常量值是效率极高的操作,因此非常快。来看下面的例子:

let value = 5;
let sum = 10 + value;
console.log(sum);

以上代码查询了 4 次常量值:数值 5、变量 value、数值 10 和变量 sum。整体代码的复杂度可以认 为是 O(1)。 在 JavaScript 中访问数组元素也是 O(1)操作,与简单的变量查找一样。因此,下面的代码与前面的 例子效率一样:

let values = [5, 10];
let sum = values[0] + values[1]; 
console.log(sum);

使用变量和数组相比访问对象属性效率更高,访问对象属性的算法复杂度是 O(n)。访问对象的每个 属性都比访问变量或数组花费的时间长,因为查找属性名要搜索原型链。简单来说,查找的属性越多,执行时间就越长。来看下面的例子:

let values = { first: 5, second: 10 };
let sum = values.first + values.second;
console.log(sum);

这个例子使用两次属性查找来计算 sum 的值。一两次属性查找可能不会有明显的性能问题,但几百 上千次则绝对会拖慢执行速度。

let query = window.location.href.substring(window.location.href.indexOf("?"));

这里有 6 次属性查找:3 次是为查找 window.location.href.substring(),3 次是为查找 window.location.href.indexOf()。通过数代码中出现的点号数量,就可以知道有几次属性查找。 以上代码效率特别低,这是因为使用了两次 window.location.href,即同样的查找执行了两遍。 只要使用某个 object 属性超过一次,就应该将其保存在局部变量中。第一次仍然要用 O(n)的复杂 度去访问这个属性,但后续每次访问就都是 O(1),这样就是质的提升了。例如,前面的代码可以重写为 如下:

let url = window.location.href;
let query = url.substring(url.indexOf("?"));

这个版本的代码只有 4 次属性查找,比之前节省了约 33%。在大型脚本中如果能这样优化,可能就 会明显改进性能。 通常,只要能够降低算法复杂度,就应该尽量通过在局部变量中保存值来替代属性查找。另外,如 果实现某个需求既可以使用数组的数值索引,又可以使用命名属性(比如 NodeList 对象),那就都应该使用数值索引。

switch 语句很快。如果代码中有复杂的 if-else 语句,将其转换成 switch 语句可以变得更快。然后,通过重新组织分支,把最可能的放前面,不太可能的放后面,可以进一步提升性能。

Last updated