本文转载自https://mp.weixin.qq.com/s/C0rO3YflCgR_TJhb0k8h_w
可维护的代码有几个特点。通常,说代码可维护就意味着它具备如下特点。
-
容易理解:无需求助原始开发者,任何人一看代码就知道是干什么的,怎么实现的。
-
符合常识:代码中的一切都显得自然而然,无论操作有多么复杂。
-
容易适配:即使数据发生变化也不用完全重写。
-
容易扩展:代码架构经过认真设计,支持未来扩展核心功能。
-
容易调试:出问题时,代码可以给出明确的信息,通过它能直接定位问题。
能够写出可维护的JavaScript代码是一项重要的专业技能。这个技能是一个周末就拼凑一个网站的业余爱好者和对自己所做的一切都深思熟虑的专业开发者的重要区别。
(0)Step 0——严格遵守编码规范
编码规范对JavaScript而言非常重要,因为这门语言实在太灵活了。与多数面向对象语言不同,JavaScript并不强迫开发者把任何东西都定义为对象。相反,JavaScript支持任何编程风格,包括传统的面向对象编程和声明式编程,以及函数式编程。简单看几个开源的JavaScript库,就会发现有很多方式可以创建对象、定义方法和管理环境。
(1)Step 1——可读性
可读性必须考虑代码是一种文本文件
a) 缩进(4个空格为单位)
b) 代码注释(每个方法都编写注释)
由于JavaScript可以在代码中任何地方创建函数,所以这一点容易被忽视
需要些注释的地方
-
函数和方法 。每个函数和方法都应该有注释来描述其用途,以及完成任务所用的算法。同时,也写清使用这个函数或方法的前提(假设)、每个参数的含义,以及函数是否返回值(因为通过函数定义看不出来)。
-
大型代码块 。多行代码但用于完成单一任务的,应该在前面给出注释,把要完成的任务写清楚。
-
复杂的算法 。如果使用了不同寻常的手法解决了问题,要通过注释解释明白。这样不仅可以帮到别人,也可以让自己今后再看的时候更快想起来。
-
使用黑科技 。由于浏览器之间的差异,JavaScript代码中通常都会包含一些黑科技。不要假设其他人一看就能明白某个黑科技是为了解决某个浏览器的什么问题。如果对某个浏览器不能使用正常方式达到目的,那要在注释里把黑科技的用途写出来。这样可以避免别人误以为黑科技没有用而把它“修复”掉,结果你已经修好的问题又会复现。
(2)Step 2—— 变量和函数名
-
变量名应该是名词 ,例如car或person。
-
函数名应该以动词开始 ,例如getName()。返回布尔值的函数通常以is开头,比如isEnabled()。
-
对变量和函数都使用符合逻辑的名字,不用担心长度 。长名字的问题可以通过后处理和压缩解决。
-
变量、函数和方法应该以小写字母开头,使用驼峰大小写形式 ,如getName()和isPerson。类名应该首字母大写,比如Person、RequestFactory。常量值应该全部大写并以下划线相接,比如REQUEST_TIMEOUT。
-
名字要尽量用描述性和直观的词汇,但不要过于冗长 。getName()一看就知道会返回名字,而PersonFactory一看就知道会产生某个Person对象或实例。
(3)Step 3—— 变量类型透明化
因为JavaScript是松散类型的语言,所以很容易忘记变量包含的数据类型
方式一: 定义变量时,应该立即将其初始化为一个将来要使用类型的值 。例如,要保存布尔值的变量可以将其初始化为true或false,而要保存数值的变量可以将其初始化为一个数值。
// 通过初始化表明变量类型
let found = false; // Boolean
let count = -1; // number
let name = ""; // string
let person = null; // object
方式二: 使用匈牙利表示法 。匈牙利表示法指的是在变量名前面前缀一个或多个字符表示数据类型 。 对于基本数据类型,用o表示对象(object)、s表示字符串(string),i表示整数(integer),f表示浮点数(float)、b表示布尔值(boolean)。
// 使用匈牙利表示法标明数据类型
let bFound; // Boolean
let iCount; // integer
let sName; // string
let oPerson; // object
缺点是让代码可读性有所下降,不够直观,破坏了类似句子的自然阅读流畅性。为此,匈牙利表示法已经被很多开发者抛弃
方式三: 使用类型注释 。类型注释放在变量名后面,初始化表达式的前面。基本思路是在变量旁边使用注释说明类型
// 使用类型注释标明数据类型
let found /*:Boolean*/= false;
let count /*:int*/= 10;
let name /*:String*/= "Nicholas";
let person /*:Object*/= null;
缺点是不能再使用多行注释把大型代码块注释掉了。因为类型注释也是多行注释,因此会造成干扰 。
(4)Step 4—— 松散耦合
a) 解耦HTML/JavaScript
在网页中,HTML和JavaScript分别代表不同层面的解决方案。HTML是数据,JavaScript是行为。 理想情况下,HTML和JavaScript应该完全分开,通过外部文件引入JavaScript,然后使用DOM添加行为 。
HTML与JavaScript强耦合的情况下,每次分析JavaScript的报错都要首先确定错误来自HTML还是JavaScript。而且,这样也会引入代码可用性的新错误。
b) 解耦CSS/JavaScript
Web应用的另一层是CSS,主要负责页面的外观。JavaScript和CSS是紧密相关的,它们都是建构在HTML之上的,因此也经常一起使用。
因为CSS负责页面外观,任何样式的问题都应该通过CSS文件解决。可是,如果JavaScript直接修改个别样式(比如颜色),就会增加一个排错时要考虑甚至要修改的因素。结果是JavaScript某种程度上承担了页面显示的任务,与CSS搞成了紧密耦合。如果将来有一天要修改样式,那么CSS和JavaScript都需要修改。这对负责维护的开发者来说是一个恶梦。层与层的清晰解耦是必需的。
可以通过动态修改类名而不是样式来实现
// CSS与JavaScript松散耦合
element.className = "edit";
同样,保证层与层之间的适当分离是至关重要的。显示出问题就只应该去CSS里解决,行为出问题就只应该找JavaScript的问题。
(5)Step 5—— 解耦应用逻辑/事件处理程序
这个问题在我看过的代码中很常见,一个方法身兼数职,请不要为了精简代码行数而进行此类神操作,保持单一原则。下面看个例子。
function
handleKeyPress(event) {
if(event.keyCode == 13) {
let target = event.target;
let value = 5* parseInt(target.value);
if(value > 10) {
document.getElementById("error-msg").style.display = "block";
}
}
}
这个事件处理程序除了处理事件,还包含了应用逻辑。这样做的问题是双重的。首先,除了事件没有办法触发应用逻辑,结果造成调试困难。如果没有发生预期的结果怎么办?是因为没有调用事件处理程序,还是因为应用逻辑有错误?其次,如果后续事件也会对应相同的应用逻辑,就会导致代码重复,否则就要把它提取到一个函数中。无论如何,都会导致原本不必要的多余工作 。
更好的做法是将应用逻辑与事件处理程序分开,各自只负责处理各自的事情。事件处理程序应该专注于event对象的相关信息,然后把这些信息传给处理应用逻辑的某些方法,上面的例子重写。
function validateValue(value) {
value = 5* parseInt(value);
if(value > 10) {
document.getElementById("error-msg").style.display = "block";
}
}
function handleKeyPress(event) {
if(event.keyCode == 13) {
let target = event.target;
validateValue(target.value);
}
}
handleKeyPress()函数只负责检查用户是不是按下了回车键(event.keyCode等于13),如果是则取得事件目标,并把目标的值传给validateValue()函数,由该函数处理应用逻辑。注意,validateValue()函数中不包含任何依赖事件处理程序的代码。这个函数只负责接收一个值,然后可以对这个值做任何处理。
把应用逻辑从事件处理程序中分离出来有很多好处。首先,可以方便地修改触发某个流程的事件。如果原来是通过鼠标单击触发流程,而现在又想增加键盘操作来触发,那么修改起来也很简单。其次,可以在不用添加事件的情况下测试代码,这样创建单元测试甚至与应用自动化整合都会更简单 。(说的有点多,请尤其注意这里呃呃呃呃呃)
以下是在解耦应用和业务逻辑时应该注意的几点 :
-
不要把event对象传给其他方法,而是只传递event对象中必要的数据。
-
应用中每个可能的操作都应该无需事件处理程序就可以执行。
-
事件处理程序应该处理事件,而把后续处理交给应用逻辑。
(6)Step 6—— 编码惯例
a) 尊重对象所有权
在其他语言中,在没有源代码的情况下对象和类都是不可修改的。JavaScript则允许在任何时候修改任何对象,因此就可能导致意外地覆盖默认行为 。
不要修改不属于你的对象。简单地讲,如果你不负责创建和维护某个对象,包括它的构造函数或它的方法,就不应该对它进行任何修改。
-
不要给实例或原型添加属性
-
不要给实例或原型添加方法
-
不要重定义已有的方法
以上规则不仅适用于自定义类型和对象,同样适用于原生类型和对象,比如Object、String、document、window,等等。考虑到浏览器厂商也有可能会在不宣布的情况下以非预期方式修改这些对象,那么潜在的风险就更大了。
以前有一个流行的Prototype库就发生过类似事件。当时,这个库在document对象上实现了getElementsByClassName()方法,返回一个Array的实例,而这个实例上还增加了each()方法。jQuery的作者John Resig后来在自己的博客上分析了这个问题造成的影响。他在博客中(https://johnresig.com/blog/getelementsbyclassname-pre-prototype-16/)指出这个问题是由于浏览器也原生实现了相同的getElementsByClassName()方法造成的。但Prototype的同名方法返回的是Array而非NodeList,后者没有each()方法。
我们不能预见浏览器厂商将来会怎么修改原生对象,因此不管怎么修改它们都可能在将来某个时刻出现冲突时导致问题。 可以这样为对象添加新功能:
-
创建包含想要功能的新对象,通过它与别人的对象交互。
-
创建新自定义类型继承本来想要修改的类型,给自定义类型添加新功能。
很多JavaScript库目前都赞同这个开发理念,这样无论浏览器怎样改变都可以发展和适应。
(7)Step 7—— 不声明全局变量
最多可以创建一个全局变量,作为其他对象和函数的命名空间。
// 一个全局变量——推荐
varMyApplication= {
name: "Nicholas",
sayName: function() {
console.log(this.name);
}
};
调用MyApplication.sayName()从逻辑上就会暗示出现任何问题,都可以在MyApplication的代码中找原因。
这样一个全局对象可以扩展为命名空间的概念。命名空间涉及创建一个对象,然后通过这个对象来暴露能力。比如,Google Closure库就利用了这样的命名空间来组织其代码。
只要使用对象以这种方式来组织功能,就可以称该对象为命名空间。整个Google Closure库都构建在这个概念之上,能够在同一个页面上与其他JavaScript库共存。
(8)Step 8—— 不要比较null
JavaScript不会自动做任何类型检查,因此就需要开发者担起这个责任。现实当中,单纯比较null通常是不够的。检查值的类型就要真的检查类型,而不是检查它不能是什么。 如果看到比较null的代码,可以使用下列某种技术替换它。
-
如果值应该是引用类型,使用instanceof操作符检查其构造函数。
-
如果值应该是原始类型,使用typeof检查其类型。
-
如果希望值是有特定方法名的对象,使用typeof操作符确保对象上存在给定名字的对象。
(9)Step 9—— 使用常量
依赖常量的目标是从应用逻辑中分离数据,以便修改数据时不会引发错误。显示在用户界面上的字符串就应该以这种方式提取出来,可以方便实现国际化。URL也应该这样提取出来,因为随着应用越来越复杂,URL也极有可能变化。可以把这些可能会修改的数据提取出来,放在单独定义的常量中,以实现数据与逻辑分离。 可以使用以下标准检查哪些数据需要提取。
-
重复出现的值。任何使用超过一次的值都应该提取到常量中。这样可以消除一个值改了而另一个值没改造成的错误。这里也包括CSS的类名。
-
用户界面字符串。任何会显示给用户的字符串都应该提取出来,以方便实现国际化。
-
URL:Web应用中资源的地址经常会发生变化,因此建议把所有URL集中放在一个地方管理。
-
任何可能变化的值。任何时候,只要在代码中使用字面值,就问问自己这个值将来有没有可能会变。如果答案是有可能,那么就应该把它提取到常量中。