如何写出优雅的JavaScript代码 ?
之前总结过一篇《如何写出优雅的css代码?》, 但是前一段时间发现自己的js代码写的真的很任性,没有任何的优雅可言,于是这里总结以下写js时应当注意的问题,注:这篇文章多为参考Nicholas C. Zakas 的《编写可维护的JavaScript》一书。
第一部分:基本的格式化
1.四个空格缩进层级
js中有合适的缩进层级才能使得代码在阅读起来更容易,一般有两种方法:直接使用制表符tab、使用四个空格来缩进。
个人比较推荐使用四个空格,因为制表符的问题在于不同的文本编辑器对制表符的表现是不一致的,所以说如果换了一个文本编辑器, 格式可能就不是自己想要的形式了。
而四个空格可能略显麻烦,但是在sublime中我们可以自行设置tab代表四个空格,在preferrence下面的setting中的个人设置中添加:
"tab_size": ,
"translate_tabs_to_spaces": true
这时就可以发现tab表示四个空格。
2.语句结尾使用分号
我们知道分析器的自动分号插入(Automatic Semicon Insertion,ASI) 机制,这样即使我们不写分号ASI也会帮助我们插入分号,但是缺点在于我们不可能记住自动插入分号的所有规则,写在代码压缩的过程总可能导致混乱,
因此在语句结尾使用分号为最佳实践。
3.行的长度超过80个字符要人为换行
如果一个行的长度过长,就会出现横向滚动条,所以当行的长度超过80个字符时我们最好换行。
注意点一: 换行后保持两个层次的缩进(8个空格) 注意点二: 换行后一般上一行的末尾是符号,下一行的开始没有符号。
如:
一种特殊情况是:如果是在变量声明,那么换行的下一行就要对齐,如下:
4.使用空格提高可读性
对于联系紧密的代码之间我们就可以不使用空行,而对于联系不紧密的代码之间就可以使用空行来提高可读性。
比如在方法和方法之间、 在注释的上一行使用空行、 在方法内的逻辑片段之间插入空行都是不错的主意。
5.正确的命名
对于命名方式,我们不推荐使用_(下划线)的命名方式,很多大公司遵从的都是驼峰式命名方法。
由于变量就是在声明一个事物,用名词; 而函数是在做一件事情,所以用动词;以之作为区分。
5.1 变量的命名如果有前缀则应当是名词(与函数名分开),并尽量使用count、length、size等可以直接体现出值的数据类型的变量名,而i、j、k一般是在循环中使用的。
// 好的写法
var count = ;
var myName = "zzw"; // 不好的写法
var getCount = ;
var getName = "zzw";
可以发现不好的写法中变量看起来像是函数,容易造成误导。
5.2 函数的命名如果有前缀则应当四动词(与变量分开)
5.3 常量的命名使用大写字母+下划线以区分常量
变量的值是可变的,常量的值是不变的,使用不同的命名方式可以更好的区分,如:
5.4 构造函数一般使用大驼峰命名法以和普通的函数区别,且其用名词,因为是创建某个类型的实例。
6.直接量
直接量就是字符串、数字、布尔值、null 和 undefined这些。
6.1 字符串
字符串推荐使用双引号,值得注意的是内部如果还有双引号则需要转义。
6.2 数字
为了避免歧义,尽量不要使用省略小数点前或小数点后的数字的习惯。
6.3 null
使用null的情况多是作为对象的占位符(placeholder)。
6.4 undefined
undefined 和 null是不一样的, 尽管 undefined == null 会返回true,但是我们还是应该严格区分, undefined表示这个变量没有被初始化。 null是对象的占位符。
6.5 对象直接量
我们创建对象的方式就死对象字面量的方式,更加高效和整洁。
6.6 数组直接量
同对象直接量一样,更推荐数组直接量。如下:
var colors = new Array("blue","yellow","red"); 这是不好的做法。
var colors = ["blue", "yellow", "red"]; 这是我们推荐的做法。
第二部分:注释
注释虽然不是执行一个程序的必要部分,但是对于更好地阅读和理解代码,注释是必不可少的。
1. 单行注释
可以看到,在上面的例子中我私用了两种注释的方式。
第一种: 在需要注释的语句上面注释,保持同样的缩进,且在这个注释上面使用空行,以明确表示注释是给下面的语句的。
第二种:在需要注释的语句后面注释,且在语句的最后和//之间使用空格区分开来。
可以看到,在// 之后我们习惯使用空格区分开来。
2. 多行注释
这是多行注释的最好方式,实际上我们只要把多行注释放在 /* */ 之间即可,但是使用上面的做法会更好一些。
注意点一: 注释上方要有空行,必不可少。
注意点二: 注释内容和语句要保持共同的缩进。
注意点三: 在*之后我们希望有一个空格,然后再书写注释的内容。
3. 如何使用注释?
通用的规则是在代码不清晰时需要添加注释,否则就是画蛇添足。
3.1 难于理解的代码添加注释
比如嵌套关系较多,传递参数不明显等等。
3.2 可能被误认为错误的代码
比如在if()中的一个赋值操作,而不是判断是否相等的操作,其他人可能以为是你写错了而修改,导致更为严重的错误,所以,
好的做法就是加上适当的注释。
3.3 hack
一些使用的hack最好用注释的方法来阐述说明。
4. 注释声明
注释有时候也可以用来给一段代码声明额外的信息。这些声明的格式以单个单词打头并紧跟一个冒号。可使用的声明如下所示:
TODO --- 说明代码还没有完成,应当包含下一步要做的事情。
HACK --- 说明代码实现走了一个捷径,应当包含为何使用HACK的原因,这也表明该问题可能会有更好的解决办法。
XXX --- 说明代码是有问题的并应当尽快修复。
FIXME---说明代码是有问题的并应当尽快修复,重要性次于XXX。
REVIEW---说明代码任何可能的改动都需要评审。
上述这些声明可能在一行或多行注释中使用,并且应当遵从同一般注释类型相同的格式规则。如下所示:
第三部分:语句和表达式
var values = [ , , , , , , ],
i,
len;
for (i = , len = values.length; i < len; i++) {
if (i !== ) {
console.log("good");
}
}
这时我们推荐的做法:
注意点一: 声明尽量使用一个var, 将没有初始化的变量往后面放。
注意点二: values 中的值应当和[]有一个空格间隔, 数值和逗号之间不要空格, 逗号和下一个值之间需要一个空格。
注意点三: 无论是 for while 等,我们都建议使用 花括号,在for或if 后面的圆括号中,紧挨着圆括号内部不需要空格,在圆括号的两边需要使用空格,在所有的二元操作符两边都使用空格,一元操作符不使用空格。
第四部分:变量、函数和运算符
1.变量声明
尽量采用一个var来声明,并且将不同的变量置于不同的行之中,且需要缩进相同,看上去才会更加规整。
2.函数声明
函数名和开始圆括号之间不应当有空格,结束的圆括号和右边的花括号之间应该留有一个空格。 右边的花括号应当同function 关键字保持在同一行总。 参数名之间应当在逗号之后保留有一个空格。 如下所示:
function addNumber(num1, num2) {
if (num1 > && num2 < ) {
return num1 + num2;
} else {
return false;
}
}
3.函数调用
函数调用和函数声明是类似的,在函数名和参数外的圆括号之间都不应当有空格,如下所示:
addNumber(, );
4. 立即调用的函数
js中允许使用匿名函数(本身没有命名的函数), 并将匿名函数赋值给变量或属性。如下所示:
var addNumber = function() {
// 这里是函数体
};
这种匿名函数同样可以通过在最后一行加上一对圆括号来立即执行并返回一个值,然后将这个值赋值给变量,如下:
var addNumber = function() {
// 这里是函数体
return ;
}();
这个函数写的很简短当然可以看出来()是在立即调用,但是很多情况下我们是没有办法一眼看出来的,所以最好在外面加一个()来区分,如下:
var addNumber = (function() {
// 这里是函数体
return ;
}());
这样,在第一行中就有了一个标识符(左圆括号), 表明这是一个立即执行的函数,添加一对圆括号并不会改变代码的逻辑。
5. 相等
在使用==和!==操作符的时候,最容易发生的就是强制类型转化 。 比如字符串和数字比较, 那么字符串会首先转换成为数字,再执行比较,实际上这并不是我们想要的,所以,最好使用 === 和 !== 这样的运算符不会发生强制类型转化。
6. 不要使用原始包装类型
我们知道string nuber 和 boolean 都是原始包装类型,比如 var string = "zzw";
string是基本类型,为什么还有方法可以调用呢? 这就是基本包装类型的定义了。 因为,这条语句的表象背后JavaScript创建了String类型的新实例,紧跟着就被销毁了,如下:
我们可以看到 虽然给string添加了属性,但是在这个添加属性的语句一旦执行完毕,这个基本包装对象就会被销毁了,所以后面就没有办法得到。
尽管我们可以使用这些基本包装类型,但是还是强烈推荐大家避免使用他们,开发者的思路会常常在对象和原始值之间跳来跳去, 这样会增加出bug的几率。
第五部分:UI层的松耦合
构建软件设计的方法有两种:一种是把软件做得很简单以至于明显找不到缺陷;另一种是把它做得很复杂以至于找不到明显的缺陷。
我们知道用户界面(User Interface, UI)是由html、css、js定义的。在实际场景中,html和css可以没有js组成一个页面,html和js可以没有css组成一个页面。
松耦合就是说一个组件和另一个组件的直接相关性,直接相关越少,则说明耦合就越松,就越容易调试。
1. 将JavaScript从CSS中抽离
在IE8和更早的版本中有一个特性即css表达式(CSS expression), 它允许在css中插入js,如下所示:
.box {
width: expression(document.body.offsetWidth + "px");
}
这样会严重影响性能,是我们不推荐的。
2.将css从JavaScript中抽离
通过脚本修改样式的最流行的一种方法是:直接修改DOM元素的style属性, 如 ele.style.color = "red";这是非常不好的一种做法,因为样式信息是通过JavaScript而非CSS来承载的,
当出现了样式问题时,你通常会想着去查找css,直到你精疲力尽地排除了所有可能性,才会去JavaScript中查找样式信息。另外我们有时还会用ele.style.cssaText = "color: red; left: 10px;"这样的方法一次性的定义多个属性,但是这样同样具有可维护性问题。 (注:在这方面,阮一峰是推荐的,参照《如何写出高性能网页》)。
将css从JavaScript中抽离意味着所有的样式信息都应该保持在css中,当需要通过js来修改元素样式的时候,最佳方法是操作CSS的className。如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<style>
.light {
color: red;
font-size: 35px;
background-color: blue;
}
</style>
</head>
<body>
<p>这是一段文字</p>
<script>
var ele = document.querySelector("p"); // 好的写法--原生方法
ele.className += " light";
// 好的写法--HTML5
ele.classList.add("light"); // 好的写法--jQuery
$(ele).addClass("light");
</script>
</body>
</html>
所以说: css中的className可以成为css和js之间的桥梁,这就是很好的分离方式。
注:有一种使用style属性的情形是可以接收的,当你给页面的元素做定位,使其相对于另外一个元素或整个页面重新定位,这样在css中实在无法完成的,我们就可以使用js来完成。
3. 将js从html中分离
比如<button onclick="doSomething()">click me </button>这就是典型的没有分离的实践。 问题在于:点击事件发生后,这个doSomething()函数必须存在,这是一个问题,而导致报错。 且如果修改了函数名又会导致一系列的问题。
在html中嵌入js的另一个做法时将<script>放在html中,script标签内又有js代码。
4.将html从js中抽离
当你需要调试一个文本或者是结构性的问题时,你更希望从HTML开始调试,而不是js,所以我们需要将html从js抽离。
在js中添加html往往是使用innerHTML来实现的,如:
var para = document.querySelector("p");
para.innerHTML = "<em>good</em>";
那么如何去解决这种问题呢?比如在我们点击了一个按钮之后,我们希望得到一个弹出框,下面列举了几种做法:
方法一:从服务器端加载
即将模版放在远程服务器,使用XMLHttpRequest对象来获取外部标签,这样就不需要将html嵌入js中,而是向服务器发送请求获取字符串,然后以这种方式注入到页面中。
当我们需要大量的HTML标签到页面中时,使用这种方式加载标签时非常有帮助的。
方法二:简单的客户端模版
方法三: 复杂客户端模版
第六部分:避免使用全局变量
什么全局变量呢?
我们再全局环境下声明的变量实际上就是window对象的属性,在全局环境下声明的函数就是window对象的方法。
var variable = ;
function alertVariable() {
alert(variable);
}
console.log(window.variable); //
window.alertVariable(); // alert 10
虽然没有定义在window上,但是实际上只要是在全局环境中,就是属于window的。
1. 全局变量带来的问题
团队开发的情况下,全局变量的问题是最多的,随着代码量的增长,全局变量越多,引入错误的概率将会因此变得越来越高。为什么这么说呢? 可以从下面几点进行分析
第一: 可能导致命名冲突
全局变量很多时,你就有可能声明了一个别人已经声明过了的变量,由此导致命名冲突。所有的变量都被定义成了局部变量,这样的代码才是最容易维护的。比如将alertVariable函数定义在外部文件中和color分离开来,那么这种依赖关系就很难追踪到了。
第二:代码的脆弱性
一个依赖于全局变量的函数即是深耦合于上下文环境中的,如果环境发生改变,那么这个函数很有可能就失效了!!
如上例中,如果variable不复存在了,我们的alertVariable函数就失效了,但是如果variable被当作参数传入,代码的可维护性就会变得更好。如下所示:
function alertVariable(variable) {
alert(variable);
}
像这样,即使全局变量variable不存在了,我们的这个函数仍然是可用的。修改后的函数不再依赖于全局变量,因此任何对全局环境的修改都不会影响到它。
当定义函数的时候,最好尽可能多地将数据置于局部作用域中。在函数内定义的任何“东西”都应该采取这种写法。
任何来自函数外部的数据都应该以参数的形式传进来,这样做可以将函数和外部环境隔离开来,并且你的修改不会对程序其他部分造成影响。
确保你的函数不会对全局变量有依赖,这将增强你的代码的可测试性。
2.意外的全局变量
如果我们在一个函数中忘记使用var来声明一个变量,这时就会导致一个问题 --- 这个变量成为了一个全局变量。
3. 单全局变量方式
引入全局变量可以使得分隔的代码块之间进行通信。 所以不能说没有全局变量,而是 依赖尽可能少的全局变量。
单全局变量模式在各种流行的JavaScript类库中早已广泛使用了。
-
YUI定义了唯一的一个YUI全局对象
- jQuery 定义了两个全局对象,$和jQuery ,只有在$被其他类库使用了的情况爱,为了避免冲突,我们需要应该使用jQuery。
- Dojo 定义了一个dojo全局对象
- Closure 类库定义了一个goog全局对象。
单全局对象的意思是所创建的这个唯一全局对象时独一无二的,并将你所有的功能代码都挂载到这个全局对象上。 因此每一个全局变量都成为了你唯一全局对象的属性,从而不会创建多个全局变量。
在写代码的时候,尤其是写脚本,最需要注释了。目前脚本、样式的注释格式都有一个已经成文的约定规范(这些约定规范最初是YUI Compressor制定的,详见参考资料)了,如下:
/**
* 这里的注释内容【会】被压缩工具压缩
*/ /*!
* 这里的注释内容【不会】被压缩工具压缩
* 与上面一个注释块不同的是,第2个*换成了!
*/
其中说到这里说到的压缩工具有YUI Compressor 、Google Closure Compiler、gulp-uglify、grunt-contrib-uglify等,这些压缩工具都支持以上的压缩约定。常常把文件的关键信息放在第2种注释内容里,如文件名称、版本号、作者等。
关于这些关键信息,都有一些关键词和一定的格式来书写。关键词书写格式为:
/**
* @author ydr.me
* @version 1.0
*/
使用@key desc
格式来书写,常用的关键词有:
其中,param关键词的格式为:
/**
* @param {String} 参数描述
*/
我们可以安装DocBlockr插件(sublime)。
A、先写完你的函数
function testFunction(a, b, c) { }
B、然后在函数的前面一行,输入
/**
C、然后回车,自动生成
/**
* [testFunction description]
* @param {[type]} a [description]
* @param {[type]} b [description]
* @param {[type]} c [description]
* @return {[type]} [description]
*/
function testFunction(a, b, c) { }
D、并且在注释块中,按@
键可以展开关键词: