原文:The Essentials of Writing High Quality JavaScript
才华横溢的Stoyan Stefanov,在他写的由O’Reilly初版的新书《JavaScript Patterns》(JavaScript模式)中。我想要是为我们的读者贡献其摘要,那会是件非常美妙的事情。详细一点就是编写高质量JavaScript的一些要素,比如避免全局变量,使用单变量声明,在循环中预缓存length(长度),遵循代码阅读,以及很多其它。
此摘要也包含一些与代码不太相关的习惯,但对总体代码的创建息息相关。包含撰写API文档、运行同行评审以及运行JSLint。这些习惯和最佳做法能够帮助你写出更好的,更易于理解和维护的代码。这些代码在几个月或是几年之后再回过头看看也是会觉得非常自豪的。
书写可维护的代码(Writing Maintainable Code )
软件bug的修复是昂贵的。而且随着时间的推移,这些bug的成本也会添加。尤其当这些bug潜伏并慢慢出如今已经公布的软件中时。当你发现bug的时候就马上修复它是最好的,此时你代码要解决的问题在你脑中还是非常清楚的。否则。你转移到其它任务,忘了那个特定的代码。一段时间后再去查看这些代码就须要:
- 花时间学习和理解这个问题
- 化时间是了解应该解决的问题代码
还有问题。特别对于大的项目或是公司,修复bug的这位伙计不是写代码的那个人(且发现bug和修复bug的不是同一个人)。因此。必须减少理解代码花费的时间,不管是一段时间前你自己写的代码还是团队中的其它成员写的代码。这关系究竟线(营业收入)和开发者的幸福,由于我们更应该去开发新的激动人心的事物而不是花几小时几天的时间去维护遗留代码。
还有一个相关软件开发生命的事实是,读代码花费的时间要比写来得多。
有时候,当你专注并深入思考某个问题的时候,你能够坐下来,一个下午写大量的代码。
你的代码非常能非常快就工作了,可是。随着应用的成熟,还会有非常多其它的事情发生,这就要求你的进行进行审查,改动。和调整。比如:
- bug是暴露的
- 新功能被加入到应用程序
- 程序在新的环境下工作(比如。市场上出现新想浏览器)
- 代码改变用途
- 代码得全然从头又一次,或移植到还有一个架构上或者甚至使用还有一种语言
由于这些变化,非常少人力数小时写的代码终于演变成花数周来阅读这些代码。这就是为什么创建可维护的代码对应用程序的成功至关重要。
可维护的代码意味着:
- 可读的
- 一致的
- 可预測的
- 看上去就像是同一个人写的
- 已记录
最小全局变量(Minimizing Globals)
JavaScript通过函数管理作用域。在函数内部声明的变量仅仅在这个函数内部。函数外面不可用。
还有一方面,全局变量就是在不论什么函数外面声明的或是未声明直接简单使用的。
每一个JavaScript环境有一个全局对象,当你在随意的函数外面使用this的时候能够訪问到。
你创建的每一个全部变量都成了这个全局对象的属性。在浏览器中,方便起见。该全局对象有个附加属性叫做window。此window(通常)指向该全局对象本身。
以下的代码片段显示了怎样在浏览器环境中创建和訪问的全局变量:
myglobal = "hello"; // 不推荐写法 console.log(myglobal); // "hello" console.log(window.myglobal); // "hello" console.log(window["myglobal"]); // "hello" console.log(this.myglobal); // "hello"
全局变量的问题
全局变量的问题在于,你的JavaScript应用程序和web页面上的全部代码都共享了这些全局变量,他们住在同一个全局命名空间,所以当程序的两个不同部分定义同名但不同作用的全局变量的时候,命名冲突在所难免。
web页面包含不是该页面开发者所写的代码也是比較常见的,比如:
- 第三方的JavaScript库
- 广告方的脚本代码
- 第三方用户跟踪和分析脚本代码
- 不同类型的小组件,标志和button
比如说,该第三方脚本定义了一个全局变量,叫做result。接着。在你的函数中也定义一个名为result的全局变量。其结果就是后面的变量覆盖前面的。第三方脚本就一下子嗝屁啦!
因此,要想和其它脚本成为好邻居的话,尽可能少的使用全局变量是非常重要的。在书中后面提到的一些减少全局变量的策略,比如命名空间模式或是函数马上自己主动运行。可是要想让全局变量少最重要的还是始终使用var来声明变量。
由于JavaScript的两个特征,不自觉地创建出全局变量是出乎意料的easy。
首先,你能够甚至不须要声明就能够使用变量。第二,JavaScript有隐含的全局概念,意味着你不声明的不论什么变量都会成为一个全局对象属性。參考以下的代码:
function sum(x, y) {
// 不推荐写法: 隐式全局变量
result = x + y;
return result;
}
此段代码中的result
没有声明。
代码照样运作正常,但在调用函数后你最后的结果就多一个全局命名空间,这能够是一个问题的根源。
经验法则是始终使用var声明变量,正如改进版的sum()函数所演示的:
function sum(x, y) { var result = x + y; return result; }
还有一个创建隐式全局变量的反例就是使用任务链进行部分var声明。
以下的片段中。a
是本地变量可是b
确实全局变量,这可能不是你希望发生的:
// 反例,勿使用 function foo() { var a = b = 0; // ... }
此现象发生的原因在于这个从右到左的赋值。首先。是赋值表达式b = 0
,此情况下b是未声明的。这个表达式的返回值是0,然后这个0就分配给了通过var定义的这个局部变量a。换句话说,就好比你输入了:
var a = (b = 0);
假设你已经准备好声明变量,使用链分配是比較好的做法,不会产生不论什么意料之外的全局变量。如:
function foo() {
var a, b;
// ... a = b = 0; // 两个均局部变量
}
然而。另外一个避免全局变量的原因是可移植性。假设你想你的代码在不同的环境下(主机下)运行,使用全局变量如履薄冰,由于你会无意中覆盖你最初环境下不存在的主机对象(所以你原以为名称能够放心大胆地使用,实际上对于有些情况并不适用)。
忘记var的副作用(Side Effects When Forgetting var)
隐式全局变量和明白定义的全局变量间有些小的差异,就是通过delete
操作符让变量没有定义的能力。
- 通过var创建的全局变量(不论什么函数之外的程序中创建)是不能被删除的。
- 无var创建的隐式全局变量(无视是否在函数中创建)是能被删除的。
这表明,在技术上,隐式全局变量并非真正的全局变量,但它们是全局对象的属性。
属性是能够通过delete
操作符删除的,而变量是不能的:
// 定义三个全局变量 var global_var = 1; global_novar = 2; // 反面教材 (function () { global_fromfunc = 3; // 反面教材 }()); // 试图删除 delete global_var; // false delete global_novar; // true delete global_fromfunc; // true // 測试该删除 typeof global_var; // "number" typeof global_novar; // "undefined" typeof global_fromfunc; // "undefined"
在ES5严格模式下,未声明的变量(如在前面的代码片段中的两个反面教材)工作时会抛出一个错误。
訪问全局对象(Access to the Global Object)
在浏览器中,全局对象能够通过window
属性在代码的不论什么位置訪问(除非你做了些比較出格的事情,像是声明了一个名为window的局部变量)。
可是在其它环境下,这个方便的属性可能被叫做其它什么东西(甚至在程序中不可用)。假设你须要在没有硬编码的window
标识符下訪问全局对象。你能够在不论什么层级的函数作用域中做例如以下操作:
var global = (function () { return this; }());
这样的方法能够随时获得全局对象。由于其在函数中被当做函数调用了(不是通过new
构造),this
总是指向全局对象。
实际上这个病不适用于ECMAScript 5严格模式。所以,在严格模式下时。你必须採取不同的形式。
比如,你正在开发一个JavaScript库,你能够将你的代码包裹在一个即时函数中,然后从全局作用域中,传递一个引用指向this作为你即时函数的參数。
单var形式(Single var Pattern)
在函数顶部使用单var语句是比較实用的一种形式,其优点在于:
- 提供了一个单一的地方去寻找功能所须要的全部局部变量
- 防止变量在定义之前使用的逻辑错误
- 帮助你记住声明的全局变量,因此较少了全局变量//zxx:此处我自己是有点晕乎的…
- 少代码(类型啊传值啊单线完毕)
单var形式长得就像以下这个样子:
function func() {
var a = 1,
b = 2,
sum = a + b,
myobject = {},
i,
j;
// function body...
}
您能够使用一个var语句声明多个变量,并以逗号分隔。像这样的初始化变量同一时候初始化值的做法是非常好的。这样子能够防止逻辑错误(全部未初始化但声明的变量的初始值是undefined
)和添加代码的可读性。
在你看到代码后,你能够依据初始化的值知道这些变量大致的用途,比如是要当作对象呢还是当作整数来使。
你也能够在声明的时候做一些实际的工作。比如前面代码中的sum = a + b
这个情况,另外一个样例就是当你使用DOM(文档对象模型)引用时。你能够使用单一的var把DOM引用一起指定为局部变量,就如以下代码所看到的的:
function updateElement() {
var el = document.getElementById("result"),
style = el.style;
// 使用el和style干点其它什么事...
}
预解析:var散布的问题(Hoisting: A Problem with Scattered vars)
JavaScript中,你能够在函数的不论什么位置声明多个var语句,而且它们就好像是在函数顶部声明一样发挥作用,这样的行为称为hoisting(悬置/置顶解析/预解析)。当你使用了一个变量,然后不久在函数中又又一次声明的话,就可能产生逻辑错误。
对于JavaScript,仅仅要你的变量是在同一个作用域中(同一函数),它都被当做是声明的。即使是它在var声明前使用的时候。看以下这个样例:
// 反例 myname = "global"; // 全局变量 function func() { alert(myname); // "undefined" var myname = "local"; alert(myname); // "local" } func();
在这个样例中,你可能会以为第一个alert弹出的是”global”,第二个弹出”loacl”。这样的期许是能够理解的,由于在第一个alert的时候。myname未声明,此时函数肯定非常自然而然地看全局变量myname,可是,实际上并非这么工作的。第一个alert会弹出”undefined”是由于myname被当做了函数的局部变量(虽然是之后声明的),全部的变量声明当被悬置到函数的顶部了。因此,为了避免这样的混乱。最好是预先声明你想使用的全部变量。
上面的代码片段运行的行为可能就像以下这样:
myname = "global"; // global variable function func() { var myname; // 等同于 -> var myname = undefined; alert(myname); // "undefined" myname = "local"; alert(myname); // "local"} func();
//zxx:关于JavaScript的置顶解析。我上周专门翻译了篇文章,您有兴趣能够看看:“翻译 – 解释JavaScript的置顶解析”。
为了完整。我们再提一提运行层面的略微复杂点的东西。代码处理分两个阶段,第一阶段是变量。函数声明。以及正常格式的參数创建,这是一个解析和进入上下文的阶段。
第二个阶段是代码运行。函数表达式和不合格的标识符(为声明的变量)被创建。可是。出于实用的目的。我们就採用了”hoisting”这个概念。这样的ECMAScript标准中并没有定义,通常常使用来描写叙述行为。
for循环(for Loops)
在for
循环中,你能够循环取得数组或是数组相似对象的值。譬如arguments
和HTMLCollection
对象。通常的循环形式例如以下:
// 次佳的循环 for (var i = 0; i < myarray.length; i++) { // 使用myarray[i]做点什么 }
这样的形式的循环的不足在于每次循环的时候数组的长度都要去获取下。
这回减少你的代码。尤其当myarray
不是数组,而是一个HTMLCollection
对象的时候。
HTMLCollections
指的是DOM方法返回的对象,比如:
document.getElementsByName() document.getElementsByClassName() document.getElementsByTagName()
还有其它一些HTMLCollections
。这些是在DOM标准之前引进而且如今还在使用的。有:
document.images: 页面上全部的图片元素 document.links : 全部a标签元素 document.forms : 全部表单 document.forms[0].elements : 页面上第一个表单中的全部域
集合的麻烦在于它们实时查询基本文档(HTML页面)。这意味着每次你訪问不论什么集合的长度,你要实时查询DOM。而DOM操作一般都是比較昂贵的。
这就是为什么当你循环获取值时,缓存数组(或集合)的长度是比較好的形式,正如以下代码显示的:
for (var i = 0, max = myarray.length; i < max; i++) {
// 使用myarray[i]做点什么
}
这样,在这个循环过程中,你仅仅检索了一次长度值。
在全部浏览器下,循环获取内容时缓存HTMLCollections
的长度是更快的,2倍(Safari3)到190倍(IE7)之间。
//zxx:此数据貌似非常老,仅供參考
注意到,当你明白想要改动循环中的集合的时候(比如。加入很多其它的DOM元素)。你可能更喜欢长度更新而不是常量。
伴随着单var形式,你能够把变量从循环中提出来,就像以下这样:
function looper() { var i = 0, max, myarray = []; // ... for (i = 0, max = myarray.length; i < max; i++) { // 使用myarray[i]做点什么 } }
这样的形式具有一致性的优点,由于你坚持了单一var形式。不足在于当重构代码的时候,复制和粘贴整个循环有点困难。比如。你从一个函数复制了一个循环到还有一个函数。你不得不去确定你能够把i
和max
引入新的函数(假设在这里没实用的话,非常有可能你要从原函数中把它们删掉)。
最后一个须要对循环进行调整的是使用以下表达式之中的一个来替换i++
。
i = i + 1 i += 1
JSLint提示您这样做,原因是++
和–-
促进了“过分棘手(excessive trickiness)”。//zxx:这里比較难翻译,我想本意应该是让代码变得更加的棘手
假设你直接无视它。JSLint的plusplus
选项会是false
(默认是default)。
还有两种变化的形式。其又有了些微改进。由于:
- 少了一个变量(无max)
- 向下数到0。通常更快,由于和0做比較要比和数组长度或是其它不是0的东西作比較更有效率
第一种变化的形式:
var i, myarray = [];
for (i = myarray.length; i–-;) {
// 使用myarray[i]做点什么
}
第二种使用while
循环:
var myarray = [],
i = myarray.length;
while (i–-) {
// 使用myarray[i]做点什么
}
这些小的改进仅仅体如今性能上,此外JSLint会对使用i–-加以抱怨。
for-in循环(for-in Loops)
for-in
循环应该用在非数组对象的遍历上,使用for-in
进行循环也被称为“枚举”。
从技术上将,你能够使用for-in
循环数组(由于JavaScript中数组也是对象),但这是不推荐的。
由于假设数组对象已被自己定义的功能增强,就可能发生逻辑错误。另外,在for-in中,属性列表的顺序(序列)是不能保证的。所以最好数组使用正常的for循环。对象使用for-in循环。
有个非常重要的hasOwnProperty()
方法,当遍历对象属性的时候能够过滤掉从原型链上下来的属性。
思考以下一段代码:
// 对象 var man = { hands: 2, legs: 2, heads: 1 }; // 在代码的某个地方 // 一个方法加入给了全部对象 if (typeof Object.prototype.clone === "undefined") { Object.prototype.clone = function () {}; }
在这个样例中。我们有一个使用对象字面量定义的名叫man的对象。在man定义完毕后的某个地方。在对象原型上添加了一个非常实用的名叫clone()的方法。此原型链是实时的,这就意味着全部的对象自己主动能够訪问新的方法。为了避免枚举man的时候出现clone()方法,你须要应用hasOwnProperty()
方法过滤原型属性。假设不做过滤。会导致clone()函数显示出来。在大多数情况下这是不希望出现的。
// 1. // for-in 循环 for (var i in man) { if (man.hasOwnProperty(i)) { // 过滤 console.log(i, ":", man[i]); } } /* 控制台显示结果 hands : 2 legs : 2 heads : 1 */ // 2. // 反面样例: // for-in loop without checking hasOwnProperty() for (var i in man) { console.log(i, ":", man[i]); } /* 控制台显示结果 hands : 2 legs : 2 heads : 1 clone: function() */
第二种使用hasOwnProperty()
的形式是取消Object.prototype上的方法。像是:
for (var i in man) {
if (Object.prototype.hasOwnProperty.call(man, i)) { // 过滤
console.log(i, ":", man[i]);
}
}
其优点在于在man对象又一次定义hasOwnProperty情况下避免命名冲突。也避免了长属性查找对象的全部方法。你能够使用局部变量“缓存”它。
var i, hasOwn = Object.prototype.hasOwnProperty;
for (i in man) {
if (hasOwn.call(man, i)) { // 过滤
console.log(i, ":", man[i]);
}
}
严格来说。不使用
hasOwnProperty()
并非一个错误。依据任务以及你对代码的自信程度。你能够跳过它以提高些许的循环速度。可是当你对当前对象内容(和其原型链)不确定的时候。加入hasOwnProperty()
更加保险些。
格式化的变化(通只是JSLint)会直接忽略掉花括号,把if语句放到同一行上。其优点在于循环语句读起来就像一个完整的想法(每一个元素都有一个自己的属性”X”,使用”X”干点什么):
// 警告: 通只是JSLint检測 var i, hasOwn = Object.prototype.hasOwnProperty; for (i in man) if (hasOwn.call(man, i)) { // 过滤 console.log(i, ":", man[i]); }
(不)扩展内置原型((Not) Augmenting Built-in Prototypes)
扩增构造函数的prototype属性是个非常强大的添加功能的方法。但有时候它太强大了。
添加内置的构造函数原型(如Object(), Array(), 或Function())挺诱人的。可是这严重减少了可维护性,由于它让你的代码变得难以预測。
使用你代码的其它开发者非常可能更期望使用内置的JavaScript方法来持续不断地工作。而不是你另加的方法。
另外,属性加入到原型中,可能会导致不使用hasOwnProperty属性时在循环中显示出来,这会造成混乱。
因此,不添加内置原型是最好的。
你能够指定一个规则,仅当以下的条件均满足时例外:
- 能够预期将来的ECMAScript版本号或是JavaScript实现将一直将此功能当作内置方法来实现。比如,你能够加入ECMAScript 5中描写叙述的方法。一直到各个浏览器都迎头赶上。这样的情况下,你仅仅是预定义了实用的方法。
- 假设您检查您的自己定义属性或方法已不存在——或许已经在代码的其它地方实现或已经是你支持的浏览器JavaScript引擎部分。
- 你清楚地文档记录并和团队交流了变化。
假设这三个条件得到满足,你能够给原型进行自己定义的加入。形式例如以下:
if (typeof Object.protoype.myMethod !== "function") {
Object.protoype.myMethod = function () {
// 实现...
};
}
switch形式(switch Pattern )
你能够通过相似以下形式的switch语句增强可读性和健壮性:
var inspect_me = 0, result = ''; switch (inspect_me) { case 0: result = "zero"; break; case 1: result = "one"; break; default: result = "unknown"; }
这个简单的样例中所遵循的风格约定例如以下:
- 每一个case和switch对齐(花括号缩进规则除外)
- 每一个case中代码缩进
- 每一个case以break清除结束
- 避免贯穿(有益忽略break)。假设你非常确信贯穿是最好的方法。务必记录此情况,由于对于有些阅读人而言,它们可能看起来是错误的。
- 以default结束switch:确保总有健全的结果,即使无情况匹配。
避免隐式类型转换(Avoiding Implied Typecasting )
JavaScript的变量在比較的时候会隐式类型转换。这就是为什么一些诸如:false == 0 或 “” == 0 返回的结果是true。
为避免引起混乱的隐含类型转换,在你比較值和表达式类型的时候始终使用===和!==操作符。
var zero = 0; if (zero === false) { // 不运行。由于zero为0, 而不是false } // 反面演示样例 if (zero == false) { // 运行了... }
还有第二种思想观点觉得==就足够了===是多余的。比如。当你使用typeof你就知道它会返回一个字符串,所以没有使用严格相等的理由。
然而,JSLint要求严格相等。它使代码看上去更有一致性,能够减少代码阅读时的精力消耗。(“==是有益的还是一个疏漏?”)
避免(Avoiding) eval()
假设你如今的代码中使用了eval(),记住该咒语“eval()是魔鬼”。此方法接受随意的字符串,并当作JavaScript代码来处理。当有问题的代码是事先知道的(不是运行时确定的),没有理由使用eval()。
假设代码是在运行时动态生成,有一个更好的方式不使用eval而达到相同的目标。比如,用方括号表示法来訪问动态属性会更好更简单:
// 反面演示样例 var property = "name"; alert(eval("obj." + property)); // 更好的 var property = "name"; alert(obj[property]);
使用eval()也带来了安全隐患。由于被运行的代码(比如从网络来)可能已被篡改。这是个非常常见的反面教材。当处理Ajax请求得到的JSON 对应的时候。
在这些情况下,最好使用JavaScript内置方法来解析JSON对应,以确保安全和有效。若浏览器不支持JSON.parse(),你能够使用来自JSON.org的库。
相同重要的是要记住。给setInterval(), setTimeout()和Function()构造函数传递字符串。大部分情况下,与使用eval()是相似的,因此要避免。在幕后。JavaScript仍须要评估和运行你给程序传递的字符串:
// 反面演示样例 setTimeout("myFunc()", 1000); setTimeout("myFunc(1, 2, 3)", 1000); // 更好的 setTimeout(myFunc, 1000); setTimeout(function () { myFunc(1, 2, 3); }, 1000);
使用新的Function()构造就相似于eval(),应小心接近。
这可能是一个强大的构造,但往往被误用。假设你绝对必须使用eval()。你能够考虑使用new Function()取代。有一个小的潜在优点。由于在新Function()中作代码评估是在局部函数作用域中运行,所以代码中不论什么被评估的通过var定义的变量都不会自己主动变成全局变量。还有一种方法来阻止自己主动全局变量是封装eval()调用到一个即时函数中。
考虑以下这个样例。这里仅un
作为全局变量污染了命名空间。
console.log(typeof un); // "undefined" console.log(typeof deux); // "undefined" console.log(typeof trois); // "undefined" var jsstring = "var un = 1; console.log(un);"; eval(jsstring); // logs "1" jsstring = "var deux = 2; console.log(deux);"; new Function(jsstring)(); // logs "2" jsstring = "var trois = 3; console.log(trois);"; (function () { eval(jsstring); }()); // logs "3" console.log(typeof un); // number console.log(typeof deux); // "undefined" console.log(typeof trois); // "undefined"
还有一间eval()和Function构造不同的是eval()能够干扰作用域链,而Function()更安分守己些。
不管你在哪里运行Function(),它仅仅看到全局作用域。
所以其能非常好的避免本地变量污染。
在以下这个样例中,eval()能够訪问和改动它外部作用域中的变量,这是Function做不来的(注意到使用Function和new Function是相同的)。
(function () { var local = 1; eval("local = 3; console.log(local)"); // logs "3" console.log(local); // logs "3" }()); (function () { var local = 1; Function("console.log(typeof local);")(); // logs undefined }());
parseInt()下的数值转换(Number Conversions with parseInt())
使用parseInt()你能够从字符串中获取数值,该方法接受还有一个基数參数,这常常省略。但不应该。当字符串以”0″开头的时候就有可能会出问题。比如,部分时间进入表单域。在ECMAScript 3中,开头为”0″的字符串被当做8进制处理了,但这已在ECMAScript 5中改变了。
为了避免矛盾和意外的结果,总是指定基数參数。
var month = "06", year = "09"; month = parseInt(month, 10); year = parseInt(year, 10);
此例中,假设你忽略了基数參数,如parseInt(year)。返回的值将是0,由于“09”被当做8进制(好比运行 parseInt( year, 8 )),而09在8进制中不是个有效数字。
替换方法是将字符串转换成数字,包含:
+"08" // 结果是 8 Number("08") // 8
这些通常快于parseInt(),由于parseInt()方法,顾名思意,不是简单地解析与转换。可是,假设你想输入比如“08 hello”,parseInt()将返回数字,而其它以NaN告终。
编码规范(Coding Conventions)
建立和遵循编码规范是非常重要的,这让你的代码保持一致性,可预測,更易于阅读和理解。一个新的开发者加入这个团队能够通读规范,理解其它团队成员书写的代码,更快上手干活。
很多激烈的争论发生会议上或是邮件列表上,问题往往针对某些代码规范的特定方面(比如代码缩进,是Tab制表符键还是space空格键)。假设你是你组织中建议採用规范的,准备好面对各种反对的或是听起来不同但非常强烈的观点。要记住。建立和坚定不移地遵循规范要比纠结于规范的细节重要的多。
缩进(Indentation)
代码没有缩进基本上就不能读了。唯一糟糕的事情就是不一致的缩进,由于它看上去像是遵循了规范,可是可能一路上伴随着混乱和惊奇。重要的是规范地使用缩进。
一些开发者更喜欢用tab制表符缩进,由于不论什么人都能够调整他们的编辑器以自己喜欢的空格数来显示Tab。
有些人喜欢空格——通常四个。这都无所谓,仅仅要团队每一个人都遵循同一个规范就好了。这本书。比如,使用四个空格缩进。这也是JSLint中默认的缩进。
什么应该缩进呢?规则非常easy——花括号中面的东西。这就意味着函数体。循环 (do, while, for, for-in),if,switch,以及对象字面量中的对象属性。以下的代码就是使用缩进的演示样例:
function outer(a, b) { var c = 1, d = 2, inner; if (a > b) { inner = function () { return { r: c - d }; }; } else { inner = function () { return { r: c + d }; }; } return inner; }
花括号{}(Curly Braces)
花括号(亦称大括号。下同)应总被使用,即使在它们为可选的时候。技术上将,在in或是for中假设语句仅一条,花括号是不须要的,可是你还是应该总是使用它们,这会让代码更有持续性和易于更新。
想象下你有一个仅仅有一条语句的for循环,你能够忽略花括号。而没有解析的错误。
// 糟糕的实例
for (var i = 0; i < 10; i += 1)
alert(i);
可是,假设。后来。主体循环部分又添加了行代码?
// 糟糕的实例 for (var i = 0; i < 10; i += 1) alert(i); alert(i + " is " + (i % 2 ?"odd" : "even"));
第二个alert已经在循环之外,缩进可能欺骗了你。
为了长远打算,最好总是使用花括号,即时值一行代码:
// 好的实例
for (var i = 0; i < 10; i += 1) {
alert(i);
}
if条件相似:
// 坏 if (true) alert(1); else alert(2); // 好 if (true) { alert(1); } else { alert(2); }
左花括号的位置(Opening Brace Location)
开发者对于左大括号的位置有着不同的偏好——在同一行或是下一行。
if (true) { alert("It's TRUE!"); }
或
if (true) { alert("It's TRUE!"); }
这个实例中。仁者见仁智者见智。但也有个案,括号位置不同会有不同的行为表现。
这是由于分号插入机制(semicolon insertion mechanism)——JavaScript是不挑剔的。当你选择不使用分号结束一行代码时JavaScript会自己帮你补上。这样的行为可能会导致麻烦。如当你返回对象字面量,而左括号却在下一行的时候:
// 警告: 意外的返回值 function func() { return // 以下代码不运行 { name : "Batman" } }
假设你希望函数返回一个含有name属性的对象。你会吃惊。由于隐含分号,函数返回undefined。
前面的代码等价于:
// 警告: 意外的返回值 function func() { return undefined; // 以下代码不运行 { name : "Batman" } }
总之。总是使用花括号,并始终把在与之前的语句放在同一行:
function func() { return { name : "Batman" }; }
关于分号注:就像使用花括号,你应该总是使用分号,即使他们可由JavaScript解析器隐式创建。这不仅促进更科学和更严格的代码,而且有助于解决存有疑惑的地方。就如前面的样例显示。
空格(White Space)
空格的使用相同有助于改善代码的可读性和一致性。在写英文句子的时候,在逗号和句号后面会使用间隔。在JavaScript中。你能够依照相同的逻辑在列表模样表达式(相当于逗号)和结束语句(相对于完毕了“想法”)后面加入间隔。
适合使用空格的地方包含:
- for循环分号分开后的的部分:如
for (var i = 0; i < 10; i += 1) {...}
- for循环中初始化的多变量(i和max):
for (var i = 0, max = 10; i < max; i += 1) {...}
- 分隔数组项的逗号的后面:
var a = [1, 2, 3];
- 对象属性逗号的后面以及分隔属性名和属性值的冒号的后面:
var o = {a: 1, b: 2};
- 限定函数參数:
myFunc(a, b, c)
- 函数声明的花括号的前面:
function myFunc() {}
- 匿名函数表达式function的后面:
var myFunc = function () {};
使用空格分开全部的操作符和操作对象是还有一个不错的使用,这意味着在+, -, *, =, <, >, <=, >=, ===, !==, &&, ||, +=
等前后都须要空格。
// 宽松一致的间距 // 使代码更易读 // 使得更加“透气” var d = 0, a = b + 1; if (a && b && c) { d = a % c; a += d; } // 反面样例 // 缺失或间距不一 // 使代码变得疑惑 var d = 0, a = b + 1; if (a && b && c) { d = a % c; a += d; }
//zxx:我就琢磨着这正面和反面样例不长得一样吗...原文就是如此。我也不好擅自改动。
最后须要注意的一个空格——花括号间距。
最好使用空格:
- 函数、if-else语句、循环、对象字面量的左花括号的前面({)
- else或while之间的右花括号(})
空格使用的一点不足就是添加了文件的大小,可是压缩无此问题。
有一个常常被忽略的代码可读性方面是垂直空格的使用。你能够使用空行来分隔代码单元。就像是文学作品中使用段落分隔一样。
命名规范(Naming Conventions)
还有一种方法让你的代码更具可预測性和可维护性是採用命名规范。这就意味着你须要用同一种形式给你的变量和函数命名。
以下是建议的一些命名规范,你能够原样採用。也能够依据自己的喜好作调整。相同,遵循规范要比规范是什么更重要。
以大写字母写构造函数(Capitalizing Constructors)
JavaScript并没有类,但有new调用的构造函数:
var adam = new Person();
由于构造函数仍仅仅是函数,仅看函数名就能够帮助告诉你这应该是一个构造函数还是一个正常的函数。
命名构造函数时首字母大写具有暗示作用,使用小写命名的函数和方法不应该使用new调用:
function MyConstructor() {...} function myFunction() {...}
分隔单词(Separating Words)
当你的变量或是函数名有多个单词的时候,最好单词的分离遵循统一的规范,有一个常见的做法被称作“驼峰(Camel)命名法”,就是单词小写。每一个单词的首字母大写。
对于构造函数。能够使用大驼峰式命名法(upper camel case)。如MyConstructor()
。
对于函数和方法名称,你能够使用小驼峰式命名法(lower camel case),像是myFunction(), calculateArea()
和getFirstName()
。
要是变量不是函数呢?开发者通常使用小驼峰式命名法。但还有第二种做法就是全部单词小写以下划线连接:比如,first_name, favorite_bands,
和 old_company_name
,这样的标记法帮你直观地区分函数和其它标识——原型和对象。
ECMAScript的属性和方法均使用Camel标记法,虽然多字的属性名称是罕见的(正則表達式对象的lastIndex和ignoreCase属性)。
其它命名形式(Other Naming Patterns)
有时。开发者使用命名规范来弥补或替代语言特性。
比如。JavaScript中没有定义常量的方法(虽然有些内置的像Number, MAX_VALUE),所以开发者都採用全部单词大写的规范来命名这个程序生命周期中都不会改变的变量。如:
// 珍贵常数,仅仅可远观
var PI = 3.14,
MAX_WIDTH = 800;
还有另外一个全然大写的惯例:全局变量名字全部大写。
全部大写命名全局变量能够加强减小全局变量数量的实践,同一时候让它们易于区分。
第二种使用规范来模拟功能的是私有成员。
虽然能够在JavaScript中实现真正的私有,可是开发者发现仅仅使用一个下划线前缀来表示一个私有属性或方法会更easy些。
考虑以下的样例:
var person = { getName: function () { return this._getFirst() + ' ' + this._getLast(); }, _getFirst: function () { // ... }, _getLast: function () { // ... } };
在此例中,getName()
就表示公共方法。部分稳定的API。而_getFirst()
和_getLast()
则表明了私有。
它们仍然是正常的公共方法。可是使用下划线前缀来警告person对象的使用者这些方法在下一个版本号中时不能保证工作的,是不能直接使用的。注意,JSLint有些不鸟下划线前缀。除非你设置了noman选项为:false。
以下是一些常见的_private规范:
- 使用尾下划线表示私有,如name_和getElements_()
- 使用一个下划线前缀表_protected(保护)属性,两个下划线前缀表示__private (私有)属性
- Firefox中一些内置的变量属性不属于该语言的技术部分,使用两个前下划线和两个后下划线表示,如:__proto__和__parent__。
凝视(Writing Comments)
你必须凝视你的代码,即使不会有其它人向你一样接触它。通常。当你深入研究一个问题。你会非常清楚的知道这个代码是干嘛用的,可是,当你一周之后再回来看的时候。想必也要耗掉不少脑细胞去搞明白究竟怎么工作的。
非常显然,凝视不能走极端:每一个单独变量或是单独一行。可是。你通常应该记录全部的函数。它们的參数和返回值,或是不论什么不平常的技术和方法。要想到凝视能够给你代码未来的阅读者以诸多提示;阅读者须要的是(不要读太多的东西)仅凝视和函数属性名来理解你的代码。比如。当你有五六行程序运行特定的任务。假设你提供了一行代码目的以及为什么在这里的描写叙述的话。阅读者就能够直接跳过这段细节。没有硬性规定凝视代码比。代码的某些部分(如正則表達式)可能凝视要比代码多。
最重要的习惯,然而也是最难遵守的,就是保持凝视的及时更新。由于过时的凝视比没有凝视更加的误导人。
关于作者(About the Author )
Stoyan Stefanov是Yahoo!web开发者,多个O'Reilly书籍的作者、投稿者和技术评审。他常常在会议和他的博客www.phpied.com发布时间web言语发展主题。
Stoyan仍是smush.it图像优化创作工具,YUI贡献者,雅虎性能优化工具YSlow 2.0建筑师。