前言

这篇文章本应该在上一篇文章:使用更严格的JavaScript编码方式,提高代码质量之前发布,但当时觉得这篇文章太过基础,也就作罢。后来咨询了一些初级的开发者,他们觉得有必要把这篇文章也放上来。尽管这篇文章内容基础,但是很多初中级开发者还是会犯同样的错误,发布出来也算是再一次提醒。

良好的编码习惯,这是每个程序员应具备的最基本素质。无论是前端程序员还是后端程序员,都要遵循基本的规范,减少因代码混乱而造成难以维护的局面。要做到不管有多少人共同参与同一个项目,一定要确保每一行代码都像是同一个人编写的。

提高代码的可读性和可维护性,有一些共同的方法,比如注意代码格式整齐,缩进合理,规范的命名等等。但有一些方式还是和所使用语言本书的特性有关。JavaScript是一种弱类型语言,有着相对松散的限制,这种特点使得开发者可以更灵活更高效地编写JavaScript代码。但同时也存在着一些设计上的缺陷,使得开发者很容易编写带有潜在问题的代码。JavaScript引擎在运行这些有潜在问题的代码时可能并不会报错或者警告,所以发现这些问题就变的很困难。这些形式各异、隐含有逻辑错误的代码,影响着代码整体的可读性和可维护性。因而,需要使用更严格的编码规范,避免出现这些不合规范的代码带来错误。如下是提高JavaScript代码可维护性的一些最佳实践方法

1. 避免定义全局变量或函数

定义全局的变量和函数,会影响代码的可维护性。如果在页面中运行的JavaScript代码是在相同的作用域里面,那这就意味着代码之间的定义存在互相影响的可能。如果在其中一段代码中定义了全局的变量或函数,则这些全局的变量或函数在另一段代码中将会是透明的,意味着在另一段代码中可以操作或者覆盖这些变量或函数。但很多时候,这样的情形并不是设计需要,而是误操作。例如,在项目中的一位开发者定义了如下的全局变量和函数:

var length = 0;
function init(){…}
function action() {…}

如果另外一个开发者在不知道已定义这些变量和函数的情况下,也定义了相同名称的变量或函数,则后定义的函数或者方法会覆盖之前的定义。在代码中出现这样的情形是非常严重的,导致了变量值被重置或者函数逻辑改变,从而发生不可预知的错误。

有很多的手段可以解决因为定义了全局变量而导致代码污染的情况。最简单的方法是把变量和方法封装在一个变量对象上,使其变成对象的属性。例如:

var myCurrentAction = {
     length: 0,
     init: function(){…},
     action: function(){…}
}

这样基本上可以避免全局变量或方法被覆盖的情况。但这种方案也有弊端,所有变量和函数的访问都需要通过主对象来实现了,比如访问如上的length变量,就需要通过myCurrentAction.length来访问。这就增加了代码的重复度和代码编写的繁琐性。另一种改进的方案是把全局的变量包含在一个局部作用域中,然后在这个作用域中完成这些变量的定义以及变量使用的逻辑。比如,可以通过定义一个匿名函数实现:

(function () {
    var length = 0;
    function init(){…}
    function action() {…}
})();

所有的逻辑都包含在了这个立即执行的匿名函数中,形成了一个独立的模块,最大限度地防止了代码之间的污染。当然,在实际的业务中,模块之间会有交互,这时则可以使用return语句,返回需要公开的接口,比如要公开上述代码中的init函数,则如上代码应修改为如下形式:

var myCurrentAction = (function () {
var length = 0;
function init(){…}
function action() {…}
return {
    init: init
}
})();

经过如此调整,外部代码访问init方法时,就可以调用myCurrentAction.init了。此方案既巧妙地做到了代码逻辑的封装,又公开了外部需要访问的接口,是代码模块化的最佳实践方式之一。

另外一个避免定义全局变量的方式是:确保在定义变量时使用var关键字。如果定义变量时没有使用var,浏览器解析时并不会报错,而是自动把这一变量解析为全局变量,比如如下的代码就定义了一个全局的变量length:

(function () {
    length = 0;
    function init(){…}
    function action() {…}
})();

这种可以不通过var关键字而定义变量的方式也是JavaScript代码灵活的一种体现,但同时也是代码中潜在问题的根源之一。很多时候,开发者并非想通过这种方式定义一个全局变量,只是错误地遗漏了var关键字。规范的制定者也意识到了这种灵活性带来的问题,所以在JavaScript代码的严格模式中,变量定义必须添加var关键字,否则会报编译错误。

2. 使用简化的编码方式

在JavaScript中,提供了很多种简化的编码方式,这些方式保持了代码的简洁性,但同时也提高了可读性。如下示例将使用复杂的方式创建对象和数组,这种方式是在后端语言中惯用的方式:

// 对象创建
var person = new Object();
person.age = 25;
person.name = 'dang';

// 数组创建
var list = new Array();
list[0] = 12;
list[1] = 20;
list[2] = 24;

在JavaScript中,可以使用JSON方式创建对象和数组。如果开发者熟悉JavaScript,则使用这种方式更简洁易读,代码如下:

// 对象创建
person = {age: 25, name: 'dang'};

// 数组创建
list = [12, 20, 24];

3. 使用比较运算符===而不是==

JavaScript有两组相等的运算符:===(严格相等)和!==(严格不等)及==(相等)和!=(不等)。===和!==会比较两个基础类型值是否相等,或者两个复杂对象是否指向同一个地址,而==和!=则会先进行比较值的类型转换,在把两个比较值的类型转换为相同类型后才会进行比较运算,所以只有在两个比较值的类型一致时,它才与第一组相等运算符等同。==和!=在比较时的类型转换规则也很复杂,具体如下:

undefinednull与自己比较时结果为true;它们相互比较时结果也为true;但与其它类型比较时,结果为false;原始类型(数值、布尔和字符类型)进行比较时,会先转换为数值类型再比较;对象和原始类型比较时,会先将对象转换为原始类型,然后再比较。来看看相应的示例:

null == undefined; // true
0 == null; // false

false == '0' // true
false == 'false' // false
'\n  123  \t' == 123 // true

var p = {toString: function(){ return '1'}}
p == 1; // true

要记住如上的这些规则很难,而使用===和!==这两个严格相等运算符进行比较时并不存在类型转换的过程,因此会返回正确的结果。为了避免出现隐含的错误,推荐使用===和!==运算符,不要使用==和!=运算符。

4. 避免使用with语句

在JavaScript中,with语句可用来快捷地访问对象的属性。with语句的格式如下:

with (object) {
    statement
}

with语句的使用原理是:JavaScript解析和运行时,会给with语句单独建立了一个作用域,而和with语句结合的对象上的属性则成为了此作用域的局部变量,因此可以直接访问。比如:

with (Math) {
    a = PI * r * r;
    x = r * cos(PI);
    y = r * sin(PI / 2);
}

上面代码和如下的代码会完成同样的事情:

a = Math.PI * r * r;
x = r * Math.cos(PI);
y = r * Math.sin(PI / 2);

从代码量上看,使用with语句的确简化了代码,但不幸的是,使用with语句可能也会带来不可思议的bug以及兼容问题: 首先,使用with语句,使得代码难以阅读,对于with语句内部的变量引用,只有在运行时才能知道变量属于哪个对象。比如:

function f(x, o) {
	with (o) {
	    print(x);
	}
}

来看一下with语句中的x变量,但从代码分析,x可能是参数上传入的x,也可能是o对象上的属性o.x,这取决于实际运行时的上下文。 当从代码无法确认实际逻辑时,这段代码就可能会有潜在的bug。如上的代码中,可能开发者在代码中使用x的期望是从参数传入x,但如果实际运行时o对象上有x属性,则with语句内部的x会成为o对象上的x属性,这就和开发者预期不同了。

其次,with语句存在兼容问题,如下的示例来自mozilla开发网站:

function f(foo, values) {
    with (foo) {
        console.log(values)
    }
}

如果在ECMAScript 5环境中调用f([1,2,3], obj),则with语句中的values引用的是obj对象。如果在ECMAScript 6环境中调用 f([1,2,3], obj),由于Array.prototype引入了values属性,因此with语句中的values引用的是[1,2,3].values。 此外, with语句的设计方面也有缺陷,在with语句内部修改和with语句结合的对象后,并不能同步到with内部,即不能保证对象数据的一致性。举个例子:

var group = {
    value: {
        node: 1
    }
};
with(group.value) {
    group.value = {
        node: 2
    };
    // 显示错误: 1
    console.log(node);
}
// 显示正确: 2
console.log(group.value.node);

如上的例子中,在with内部修改了group.value对象,设置了group.value.node值为2,但在with语句内部的node值并没用同步修改为2。

基于以上的分析,在使用with语句的过程中,开发者通过阅读代码不能知道它将会做什么,即无法确定代码是否会正确地做期望的事情,并且with语句也存在设计上的缺陷,所以应该在代码中避免使用with语句。

5. 避免使用eval

在JavaScript中,eval函数的用法很简单,它会接受一个字符串参数,把字符串内容作为代码执行,并返回执行结果。典型的用法如下:

eval("x=1;y=2; x*y")

但这个函数存在被滥用的情况。很多新手因为不了解JavaScript语法,所以会在某些不恰当的场合使用eval函数。比如想得到对象上的属性值,但由于属性名是通过变量传入的,所以无法用点操作符,这个时候就可能会想要使用eval,代码类似如下形式:

eval('obj.' + key);

其实可以使用下标法取得属性值:

obj[key]

从eval的功能上看,使用eval函数会让代码难以阅读,影响代码的可维护性。除此之外,eval的使用也存在安全性问题,因为它会执行任意传入的代码,而传入的代码有可能是未知的或者来自不受控制的源,所以尽量避免使用eval。其实在大多数的情况下,都是可以使用其它方案来代替eval的功能。上例便是其中一个典型的例子,使用下标法代替使用eval函数取得了对象的属性。

和eval函数类似的还有setTimeout和setInterval函数,这两个函数也可以接受字符串参数,当传入的参数为字符串时,它们会做类似eval函数的处理,把字符串当作代码执行。所以使用这两个函数时,应该避免使用字符串类型参数。此外,Function构造器也和eval函数的功能类似,所以也应该避免使用。

6. 不要编写检测浏览器的代码

经常在一些老旧的JavaScript代码中存在浏览器判断的逻辑,即根据浏览器的不同做不同的处理。这种判断浏览器的做法在五六年以前还算是有一定合理性的,因为当时浏览器的发展很缓慢,浏览器的功能变化不大。但随着浏览器更新的速度越来越快,并且浏览器之间的差异也越来越小,原来这些判断浏览器版本的代码逻辑就不适时宜了,甚至有可能导致逻辑上的错误。因为浏览器之前不支持的功能有可能在新版本中得到了支持,浏览器的bug也可能在新版本中得到了修正。这样一来,之前那些通过判断浏览器而修正的bug就可能完全没有作用了,开发者不得不重新修改代码来适应新的浏览器。所以,最佳的做法是不要编写检测浏览器的代码,取而代之的是检测浏览器是否支持某个特定功能。开发者可以借助目前流行的Modernizr框架来检测浏览器的特性支持。

当然,也存在某些特定情况需要判断浏览器的版本,尤其是判断IE浏览器。这个时候,最好是把针对特定浏览器的代码逻辑放置在单独的文件中,方便后期的维护和移除。比如,已经知道IE8及以下版本浏览器不支持HTML5的新标签,所以如果要在页面上使用HTML5新标签,则需要针对这些浏览器加入兼容代码。这时,可把兼容代码放在单独的文件中,页面中添加如下代码:

<!--[if lt IE 9]>
<script src="javascript/html5.js"></script>
<![endif]-->

后期如果页面不再支持IE8及以下版本浏览器,则只需移除此代码引用即可。

jQuery 从 1.9 版开始,移除了 $.browser$.browser.version ,取而代之的是$.support。jQuery的做法正是为了让开发者不再借助$.browser$.browser.version来判断浏览器版本,而是使用$.support来判断浏览器的特性支持。