28.1.1 什么是可维护的代码

通常,说代码“可维护”就意味着它具备如下特点:
❑ 容易理解:无须求助原始开发者,任何人一看代码就知道它是干什么的,以及它是怎么实现的。
❑ 符合常识:代码中的一切都显得顺理成章,无论操作有多么复杂。
❑ 容易适配:即使数据发生变化也不用完全重写。
❑ 容易扩展:代码架构经过认真设计,支持未来扩展核心功能。
❑ 容易调试:出问题时,代码可以给出明确的信息,通过它能直接定位问题。

28.1.2 编码规范

编写可维护代码的第一步是认真考虑编码规范。

1.可读性

要想让代码容易维护,首先必须使其可读。可读性必须考虑代码是一种文本文件。为此,代码缩进是保证可读性的重要基础。
可读性的另一方面是代码注释。一般来说,以下这些地方应该写注释:
❑ 函数和方法。每个函数和方法都应该有注释来描述其用途,以及完成任务所用的算法。同时,也写清使用这个函数或方法的前提(假设)、每个参数的含义,以及函数是否返回值(因为通过函数定义看不出来)。
❑ 大型代码块。多行代码但用于完成单一任务的,应该在前面给出注释,把要完成的任务写清楚。
❑ 复杂的算法。如果使用了独特的方法解决问题,要通过注释解释明白。这样不仅可以帮助别人查看代码,也可以帮助自己今后查看代码。
❑ 使用黑科技。由于浏览器之间的差异,JavaScript代码中通常包含一些黑科技。不要假设其他人一看就能明白某个黑科技是为了解决某个浏览器的什么问题。如果某个浏览器不能使用正常方式达到目的,那要在注释里把黑科技的用途写出来。这样可以避免别人误以为黑科技没有用而把它“修复”掉,结果你已解决的问题又会出现。

2.变量和函数命名

关于命名的通用规则:
❑ 变量名应该是名词,例如car或person。
❑ 函数名应该以动词开始,例如getName()。返回布尔值的函数通常以is开头,比如isEnabled()。
❑ 对变量和函数都使用符合逻辑的名称,不用担心长度。长名字的问题可以通过后处理和压缩解决(本章稍后会讨论)。
❑ 变量、函数和方法应该以小写字母开头,使用驼峰大小写(camelCase)形式,如getName()和isPerson。类名应该首字母大写,如Person、RequestFactory。常量值应该全部大写并以下划线相接,比如REQUEST_TIMEOUT。
❑ 名称要尽量用描述性和直观的词汇,但不要过于冗长。getName()一看就知道会返回名称,而PersonFactory一看就知道会产生某个Person对象或实体。

3.变量类型透明化

三种方式可以标明变量的数据类型:
① 通过初始化。
定义变量时,应该立即将其初始化为一个将来要使用的类型值。
② 使用匈牙利表示法。
匈牙利表示法指的是:在变量名前面前缀一个或多个字符表示数据类型。对于基本数据类型,JavaScript传统的匈牙利表示法用o表示对象,s表示字符串,i表示整数,f表示浮点数,b表示布尔值。已不流行
③ 使用类型注释。
类型注释放在变量名后面、初始化表达式的前面。基本思路是在变量旁边使用注释说明类型

28.1.3 松散耦合

只要应用程序的某个部分对另一个部分依赖得过于紧密,代码就会变成紧密耦合,因而难以维护。典型的问题是在一个对象中直接引用另一个对象,这样,修改其中一个,可能必须还得修改另一个。紧密耦合的软件难于维护,肯定需要频繁地重写。

1.解耦HTML/JavaScriptWeb

开发中最常见的耦合是HTML/JavaScript耦合。在网页中,HTML和JavaScript分别代表不同层面的解决方案。HTML是数据,JavaScript是行为。
解耦HTML和JavaScript可以节省排错时间,因为更容易定位错误来源。同样解耦也有助于保证可维护性。修改行为只涉及JavaScript,修改标记只涉及要渲染的文件。

2.解耦CSS/JavaScript

Web应用程序的另一层是CSS,主要负责页面显示。CSS也可能与JavaScript产生紧密耦合。最常见的例子就是使用JavaScript修改个别样式

  1. // css紧耦合到了javascript
  2. element.style.color = 'red';
  3. element.style.backgroundColor = 'blue';

解决:
可以通过动态修改类名而不是样式来实现
比如:

  1. // css与javascript松散耦合
  2. element.className = 'edit';

3.解耦应用程序逻辑/事件处理程序

每个Web应用程序中都会有大量事件处理程序在监听各种事件。其中很少能真正做到应用程序逻辑与事件处理程序分离

  1. function handleKeyPress(event) {
  2. if (event.keyCode === 13) {
  3. let target = event.target;
  4. let value = 5 * parseInt(target.value);
  5. if (value > 10) {
  6. document.getElementById('error-msg').style.display = 'block';
  7. }
  8. }
  9. }

除了处理事件,还包含了应用程序逻辑。这样做的问题是双重的。
首先,除了事件没有办法触发应用程序逻辑,结果造成调试困难。
其次,如果后续事件也会对应相同的应用程序逻辑,则会导致代码重复,或者把它提取到单独的函数中。
更好的做法是:
将应用程序逻辑与事件处理程序分开,各自负责处理各自的事情。
事件处理程序应该专注于event对象的相关信息,然后把这些信息传给处理应用程序逻辑的某些方法。

  1. function validateValue(value) {
  2. value = 5 * parseInt(value);
  3. if (value > 10) {
  4. document.getElementById('error-msg').style.display = 'block';
  5. }
  6. }
  7. function handleKeyPress(event) {
  8. if (event.keyCode === 13) {
  9. let target = event.target;
  10. validateValue(target.value);
  11. }
  12. }

应用程序逻辑跟事件处理程序就分开了
handleKeyPress()函数只负责检查用户是不是按下了回车键(event.keyCode等于13),如果是则取得事件目标,并把目标值传给validateValue()函数,该函数包含应用程序逻辑。
注意,validateValue()函数中不包含任何依赖事件处理程序的代码。这个函数只负责接收一个值,并根据该值执行其他所有操作。

28.1.4 编码惯例

1.尊重对象所有权

简单来讲,如果你不负责创建和维护某个对象及其构造函数或方法,就不应该对其进行任何修改。更具体一点说,就是如下惯例:
❑ 不要给实例或原型添加属性。❑ 不要给实例或原型添加方法。❑ 不要重定义已有的方法。
可以按如下这样为对象添加新功能:
❑ 创建包含想要功能的新对象,通过它与别人的对象交互。
❑ 创建新自定义类型继承本来想要修改的类型,可以给自定义类型添加新功能。

2.不声明全局变量

这样一个全局对象可以扩展为命名空间的概念。
命名空间涉及创建一个对象,然后通过这个对象来暴露能力。
比如,Google Closure库就利用了这样的命名空间来组织其代码。
下面是几个例子。
❑ goog.string:用于操作字符串的方法。
❑ goog.html.utils:与HTML相关的方法。
❑ goog.i18n:与国际化(i18n)相关的方法。
对象goog就相当于一个容器,其他对象包含在这里面。只要使用对象以这种方式来组织功能,就可以称该对象为命名空间。整个Google Closure库都构建在这个概念之上,能够在同一个页面上与其他JavaScript库共存。
下面的例子演示了使用Wrox作为命名空间来组织功能:

  1. // 创建全局对象
  2. var Wrox = {};
  3. // 为本书创建命名空间
  4. Wrox.ProJS = {};
  5. // 添加本书用到的其他对象
  6. Wrox.ProJS.EventUtil = { ... };
  7. Wrox.ProJS.CookieUtil = { ... };

在这个例子中,Wrox是全局变量,然后在它的下面又创建了命名空间。如果本书所有代码都保存在Wrox.ProJS命名空间中,那么其他作者的代码就可以使用自己的对象来保存。只要每个人都遵循这个模式,就不必担心有人会覆盖这里的EventUtil或CookieUtil,因为即使重名它们也只会出现在不同的命名空间中。

3.不要比较null

现实当中,单纯比较null通常是不够的。检查值的类型就要真的检查类型,而不是检查它不能是什么。
如果看到比较null的代码,可以使用下列某种技术替换它:
❑ 如果值应该是引用类型,则使用instanceof操作符检查其构造函数。
❑ 如果值应该是原始类型,则使用typeof检查其类型。
❑ 如果希望值是有特定方法名的对象,则使用typeof操作符确保对象上存在给定名字的方法。
代码中比较null的地方越少,就越容易明确类型检查的目的,从而消除不必要的错误。

4.使用常量

关键在于把数据从使用它们的逻辑中分离出来。可以使用以下标准检查哪些数据需要提取:
❑ 重复出现的值:任何使用超过一次的值都应该提取到常量中,这样可以消除一个值改了而另一个值没改造成的错误。这里也包括CSS的类名。
❑ 用户界面字符串:任何会显示给用户的字符串都应该提取出来,以方便实现国际化。
❑ URL:Web应用程序中资源的地址经常会发生变化,因此建议把所有URL集中放在一个地方管理。
❑ 任何可能变化的值:任何时候,只要在代码中使用字面值,就问问自己这个值将来是否可能会变。如果答案是“是”,那么就应该把它提取到常量中。
使用常量是企业级JavaScript开发的重要技术,因为它可以让代码更容易维护,同时可以让代码免受数据变化的影响。