前言

这篇文章本应该在上一篇文章:使用更严格的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来判断浏览器的特性支持。

Web前端开发最佳实践(10):JavaScript代码不好读,不好维护?你需要改变写代码的习惯的更多相关文章

  1. 【WEB前端开发最佳实践系列】高可读的HTML

    一.HTML语义化 HTML5中增加了很多标签都是基于此类原则设计的(article   nav  header  footer).页面标签语义化的优点是使得搜索引擎以及第三方抓包工具等更容易读懂页面 ...

  2. 【社区公益】送《Web前端开发最佳实践》给需要的人

    算起来至今,我进入软件开发行业已经有11年之久.从最初的研究人工智能,到后来的Web开发,控件开发,直到现在纯粹的Web前端开发.虽然没有大的作品问世,但也是勤勤恳恳,踏实做事,低调做人.从来不吹牛逼 ...

  3. Web前端开发最佳实践(3):前端代码和资源的压缩与合并

    一般在网站发布时,会压缩前端HTML.CSS.JavaScript代码及用到的资源文件(主要是图片文件),目的是加快文件在网络中的传输,让网页更快的展现.当然,CDN分发.缓存等方式也是加快代码或资源 ...

  4. Web前端开发最佳实践(9):CSS代码太太乱,重复代码太多?你需要精简CSS代码

    前言 提高网站整体加载速度的一个重要手段就是提高代码文件的网络传输速度.之前提到过,所有的代码文件都应该是经过压缩了的,这可提高网络传输速度,提高性能.除了压缩代码之外,精简代码也是一种减小代码文件大 ...

  5. Web前端开发最佳实践(2):前端代码重构

    前言 代码重构是业内经常讨论的一个热门话题,重构指的是在不改变代码外部行为的情况下进行源代码修改,所以重构之前需要考虑的是重构后如何才能保证外部行为不改变.对于后端代码来说,可以通过大量的自动化测试来 ...

  6. Web前端开发最佳实践系列文章汇总

    Web前端开发最佳实践(1):前端开发概述 Web前端开发最佳实践(2):前端代码重构 Web前端开发最佳实践(3):前端代码和资源的压缩与合并 Web前端开发最佳实践(4):在页面中添加必要的met ...

  7. Web前端开发最佳实践(1):前端开发概述

    引言 我从07年开始进入博客园,从最开始阅读别人的文章到自己开始尝试表达一些自己对技术的看法.可以说,博客园是我参与技术讨论的一个主要的平台.在这其间,随着接触技术的广度和深度的增加,也写了一些得到了 ...

  8. Web前端开发最佳实践(5):正确闭合HTML标签,停止使用不标准的标签和属性

    正确闭合HTML标签 HTML元素的内容模型定义了元素的结构,表明元素可以包含哪些内容以及元素可以有哪些属性.元素可以包含的内容包括其他元素和字符,但是也有一些元素是空元素,即不能包含任何内容,这些元 ...

  9. web前端开发最佳实践笔记

    一.文章开篇 由于最近也比较忙,一方面是忙着公司的事情,另外一方面也是忙着看书和学习,所以没有时间来和大家一起分享知识,现在好了,终于回归博客园的大家庭了,今天我打算来分享一下关于<web前端开 ...

  10. Web前端开发最佳实践(8):还没有给CSS样式排序?其实你可以更专业一些

    前言 CSS样式排序是指按照一定的规则排列CSS样式属性的定义,排序并不会影响CSS样式的功能和性能,只是让代码看起来更加整洁.CSS代码的逻辑性并不强,一般的开发者写CSS样式也很随意,所以如果不借 ...

随机推荐

  1. select()函数

    select(),确定一个或多个套接口的状态,本函数用于确定一个或多个套接口的状态,对每一个套接口,调用者可查询它的可读性.可写性及错误状态信息,用fd_set结构来表示一组等待检查的套接口,在调用返 ...

  2. vue 项目代码初始化

    1. <meta>补充 <head> <meta charset="utf-8"> <meta name="viewport&q ...

  3. Mac下配置环境变量(转)

    说明:Mac下一般使用bash作为默认shell 一.Mac系统的环境变量,加载顺序为: /etc/profile /etc/paths ~/.bash_profile ~/.bash_login ~ ...

  4. Java并发编程原理与实战四十五:问题定位总结

    背景   “线下没问题的”. “代码不可能有问题 是系统原因”.“能在线上远程debug么”    线上问题不同于开发期间的bug,与运行时环境.压力.并发情况.具体的业务相关.对于线上的问题利用线上 ...

  5. [整理]C语言中字符常量与ASCII码

    所有的ASCII码都可以用“\”加数字(一般是8进制数字)来表示.而C中定义了一些字母前加"\"来表示常见的那些不能显示的ASCII字符,如\0,\t,\n等,就称为转义字符,因为 ...

  6. 为FreeBSD安装adobe flash插件

    参考 FreeBSD官方手册浏览器一章. pkg install nspluginwrapper nspluginwrapper 是一个辅助安装配置 NetScape Plugin的工具. 可以为Ne ...

  7. 当python模式遇见cedet

    TAG: emacs, python, cedet, semantic, ctags DATE: 2013-08-20 我用Emacs 24写python程序. 发现屏幕不时有些闪动,MiniBuff ...

  8. 20155233 2016-2017-2 《Java程序设计》第7周学习总结

    20155233 2016-2017-2 <Java程序设计>第7周学习总结 学习目标 了解Lambda语法 了解方法引用 了解Fucntional与Stream API 掌握Date与C ...

  9. Python练习-函数版-锁定三次登陆失败的用户

    代码如下: # 编辑者:闫龙 if __name__ == '__main__': import UserLoginFuncation LoclCount=[]; while True: UserNa ...

  10. CMD命令利用tasklist与taskkill关闭程序

    昨天远程服务器后,服务器无故卡住了,鼠标各种延迟与无反应,想在进程管理器里关闭程序也卡住,想点击重启系统也卡死无反应.纠结后win+R打开了cmd用shutdown重启才算搞定.重启期间思考了下,如何 ...