编写可维护的 JavaScript 很重要,因为大部分的开发者都花费了大量的时间来维护他人编写的代码,因为一般情况下,我们都是以他人的工作成果为基础,开始开发新代码的。
这里的很多概念也适用于其他编程语言哦O(∩_∩)O~
1 什么是可维护代码?
可维护代码有这些特点:
- 可理解性——其他开发者可以接手源代码,并无需原代码者的完整解释的情况下,理解它的意图。
- 直观性——代码一看就能明白。
- 可适应性——能够适用数据上的变化,而无须完全重写原方法。
- 可扩展性——架构上保证,可以在未来对核心功能进行扩展。
- 可调试性——出错时,代码记录了足够的信息可以直接确定问题的所在。
2 代码约定
杰出的开放源代码项目都有着严格的代码约定要求,下面将会一一说明这些代码约定要求。
2.1 可读性
可读性一般与代码缩进相关,当所有的人都使用一样的缩进方式时,整个项目的代码就会变得更易于阅读,一般使用 4 个空格作为缩进大小。在 IDE 编辑器中一般都能保证缩进要求。
另一个方面是注释,这些地方需要加注释:
- 函数和方法——用于描述目的以及完成任务所可能使用的算法。如果有事先的假设也要列出,还有参数的含义、是否有返回值以及返回值的含义。
- 大段代码——如果使用某种独特的方式解决某个问题,就需要写出这种方式的含义和算法,因为这不仅可以帮助那些将来维护你的代码的未来开发者,也能够在你再次阅读这段代码时,帮助理解。
- Hack——应付浏览器差异的代码。这些代码一般比较奇葩,所以最好要加上注释加以说明。
2.2 变量和函数命名
给变量和函数起个好名字,对于增加代码的可理解性以及可维护性是很重要的!命名规则如下:
- 变量名是名词,诸如 car 或者 person。
- 函数名应该以动词开头,比如 getName(),如果是返回布尔类型,函数名一般以 is 开头,比如 isEnable()。
- 变量和函数都应该使用合乎逻辑的名字,不要担心长度。长度问题可以通过后期压缩来缓解。
2.3 变量的数据类型命名
合适的命名方式可以缓解松散类型带来的问题,有三种命名方式。
2.3.1 初始化
定义变量后,就初始化为一个值,来表示这个变量的数据类型:
var isOk = false;//布尔型
var count = -1;//数字
var name = "";//字符串
var person = null;//对象
2.3.2 匈牙利标记法
匈牙利标记法指的是在变量名之前加一个或多个字符来表示变量的数据类型,这种方法在脚本语言中很流行,在 JavaScript 中使用单个字符表示基本类型:
字符 | 说明 |
---|---|
o | 对象 |
s | 字符串 |
i | 整数 |
f | 浮点数 |
b | 布尔型 |
var bFound;//布尔型
var iCount;//整数
var sName;//字符串
var oPerson;//对象
匈牙利标记法的缺点是:它在某种程度上会造成代码难以阅读。
2.3.3 类型注释
类型注释可以放在初始化之前,变量名的右边:
var found /*:Boolean*/ = false;
var count /*:int*/ = 10;
var name /*:String*/ = "Deniro";
var person /*:Object*/ = null;
类型注释维持了整体代码的可读性,但它的缺点是不能使用多行注释一次性注释大块的代码,因为它们会发生冲突!这可以使用单行注释予以解决。
以上这三种指定变量数据类型的方式,各有优劣势,所以要在使用之前先进行评估,最重要的是确定哪一种方式最适合你当前的项目哦O(∩_∩)O~
3 松散耦合
紧密耦合的系统很难维护,可能需要经常重写!所以我们要尽可能地保持弱耦合的代码。
3.1 解耦 HTML/JavaScript
HTML 用于表示数据,而 JavaScript 处理行为,所以它们肯定需要交互。但有时候耦合的过于紧密!
使用 script
的紧密耦合:
<script type=text/javascript>
document.write("Hello world!");
</script>
使用事件处理程序的属性值的紧密耦合:
<input type="button" value="Click Me" onclick="doSomething()"/>
理想情况是,HTML 与 JavaScript 完全分离,HTML 只是通过外部文件和 DOM 行为来包含 JavaScript。
有时候是 JavaScript 包含了 HTML 的紧密耦合:
function insertMessage(msg){
var container = document.getElementById("container");
container.innerHTML = "<div class=\"msg\">...</div>";
}
应该避免在 JavaScript 中创建大量的 HTML。我们要保持层次的分离,因为这样可以很容易地确定错误的来源。
HTML 的呈现应该尽可能地与 JavaScript 保持分离。可以在页面中直接包含标记并隐藏它,然后在需要的时候再显示这些标记。
3.2 解耦 CSS/JavaScript
CSS 负责页面的显示,有时候它也会与 JavaScript 过于紧密地耦合在一起:
element.style.color = "red";
element.style.backgroundColor = "blue";
如果未来要修改样式表,那么 CSS 与 JavaScript 都需要修改,这会造成维护上的噩梦!
在 JavaScript 中,可以通过动态来修改样式:
element.className = "edit";
把样式信息都放在 CSS 中,这样只要应用了正确的类,那么如果出现显示问题,我们就可以直接把问题定位到 CSS 中,是不是很棒O(∩_∩)O~
注意,显示问题的来源应该只是 CSS,行为问题的来源只是 JavaScript。在这些层次之间保持松散的耦合关系,可以让整个应用更易于维护。
3.3 解耦应用逻辑/事件处理程序
一个 web 应用都会有很多的事件处理程序,它们监听着很多事件,但一般都是把应用逻辑直接写在事件处理程序中:
/**
* 应用逻辑与事件处理程序紧密耦合(避免)
* @param event
*/
function handleKeyPress(event) {
event = EventUtil.getEvent(event);
if (event.keyCode == 13) {
var target = EventUtil.getTarget(event);
var value = 5 * parseInt(target.value);
if (value > 10) {
document.getElementById("error-msg").style.display = "block";
}
}
}
这样做的结果是,除了通过事件之外就再没有别的方法可以执行应用逻辑咯,这会让调试变得困难。其次,如果其他某个事件也会触发同样的应用逻辑时,就必须复制代码或者将代码抽象到一个独立的函数中,而这都要做出更多的改动。
所以我们要 解耦应用逻辑与事件处理程序,让它们分离。一个事件处理程序从事件对象中提取出相关的信息,然后把这些信息传送到处理应用逻辑的某个方法中:
function validateValue(value) {
var value = 5 * parseInt(target.value);
if (value > 10) {
document.getElementById("error-msg").style.display = "block";
}
}
function handleKeyPress(event) {
event = EventUtil.getEvent(event);
if (event.keyCode == 13) {
var target = EventUtil.getTarget(event);
validateValue(target);
}
}
这样做的好处是:
- 更容易更改触发特定过程的事件。
- 更容易创建单元测试或自动化应用流程。
记住下面这些原则:
- 不要将整个 event 对象传给其他方法,而是只传递 event 对象中所需要的数据。
- 任何在应用层面可以做的事情,都应该可以在不执行事件处理程序的情况下进行。
- 事件处理程序应该只处理事件,然后把剩下的工作转交给应用逻辑。
只要记住了这几条,就可以在代码可维护性上获得极大的改进啦O(∩_∩)O~
4 编程实践
企业环境下创建的 web 应用往往是由许多开发人员共同编写的,所以最好遵循这些编程实践:
4.1 尊重对象所有权
默认情况下,所有的对象都是可以修改的。
尊重对象所有权的意思是:不修改不属于你的对象,这些对象不是你创建的,所以:
- 不要为这些实例或原型添加属性;
- 不要为这些实例或原型添加方法;
- 不要重新定义那些已经存在的方法。
这些规则不仅适用于自定义类型和对象,对于原生类型和对象(Object、String、document、window)也适用。
我们可以通过以下方式来创建对象:
- 创建包含所需功能的新对象,然后让它与相关对象进行交互。
- 创建自定义类型,继承需要修改的类型,然后再为它添加额外的功能。
4.2 避免全局变量
避免全局变量和函数,这是为了保证脚本执行一致而且是可维护的环境。最多创建一个全局变量,然后把其他对象和函数放在这个变量中统一管理。
//两个全局变量(避免)
var name = "deniro";
function sayName() {
console.log(name);
}
推荐这样做:
//一个全局变量(推荐)
var MyApplication = {
name: "deniro",
sayName: function () {
console.log(this.name);
}
};
“一个全局变量”延伸后的概念其实就是“命名空间”。大家都同意使用全局变量的名字,并尽可能唯一,一般使用公司的名字。我们可以使用命名空间来组合功能:
//创建全局对象
var Deniro = {};
//创建对象
Deniro.EventUtil = {};
虽然命名空间可能需要多写一些代码,但对于可维护性而言是值得的。命名空间的使用有助于确保同一个页面中的代码可以与其他代码以互不干扰的方式正常工作。
4.3 避免与 null 进行比较
function sortArray(values) {
var comparator = null;
if (values != null) {//避免
values.sort(comparator);
}
}
上面的代码仅仅检查了 values 是否是 null,检测的并不充分。如果 values 是字符串、数字就会抛出错误!
必须按照所期望的值进行实际的检查!比如上面的函数,就要 values 是否是数组:
function sortArray(values) {
var comparator = null;
if (values instanceof Array) {//推荐
values.sort(comparator);
}
}
注意: 上面验证数组的方法在多框架的页面中不能正确工作,因为每个框架(frame)都有自己的全局对象,也有自己的 Array 构造函数。所以如果是从一个框架将数组传送到另一个框架,就要再检查是否存在 sort() 方法。
4.4 使用常量
//避免
function validate(value) {
if (!value) {
console.log("Invalid value!");
location.href = "/errors/invalid.php";
}
}
这个函数中,显示用的字符串应该抽取出来以应对国际化要求,URL 也要被抽取出来,因为随着应用的增长可能会发生改变。把数据抽取出来定义为常量,可以把应用逻辑与数据修改分离开来:
//推荐
var Constants = {
INVALID_VALUE_MSG: "Invalid value!",
INVALID_VALUE_URL: "/errors/invalid.php"
};
function validate(value) {
if (!value) {
console.log(Constants.INVALID_VALUE_MSG);
location.href = Constants.INVALID_VALUE_URL;
}
}
这样设计后,我们就可以在无须接触使用这些数据的函数的基础上,对这些数据进行变更咯。Constants 对象甚至可以在完全独立的文件中进行定义,也可以灵活应对国际化的要求。
使用常量的原则如下:
- 重复值——多处用到的值都要抽取为常量。
- 用户界面字符串——如果需要国际化,就需要把显示给用户的这些字符串,都抽取出来。
- URLs——web 应用的资源位置经常性地发生改变,所有最好把所有的 URL 都放到一个公共的地方统一管理。
- 可能会改变的值——未来会发生变化的值都应该被提取为一个常量。
使用常量,会让代码更容易维护,而且在数据更改的同时还保护了代码,一举两得呀O(∩_∩)O~