深刻理解JavaScript系列1:编写高质量JavaScript代码的基本要点

前言

才华横溢的Stoyan Stefanov,在他写的由O’Reilly第一版的新书《JavaScript Patterns》(JavaScript模式)中,我想要是为咱们的读者贡献其摘要,那会是件很美妙的事情。具体一点就是编写高质量JavaScript的一些要素,例如避免全局变量,使用单变量声明,在循环中预缓存length(长度),遵循代码阅读,以及更多。
此摘要也包括一些与代码不太相关的习惯,但对总体代码的建立息息相关,包括撰写API文档、执行同行评审以及运行JSLint。这些习惯和最佳作法能够帮助你写出更好的,更易于理解和维护的代码,这些代码在几个月或是几年以后再回过头看看也是会以为很自豪的。javascript

书写可维护的代码

软件bug的修复是昂贵的,而且随着时间的推移,这些bug的成本也会增长,尤为当这些bug潜伏并慢慢出如今已经发布的软件中时。当你发现bug 的时候就当即修复它是最好的,此时你代码要解决的问题在你脑中仍是很清晰的。不然,你转移到其余任务,忘了那个特定的代码,一段时间后再去查看这些代码就 须要:php

  • 花时间学习和理解这个问题html

  • 化时间是了解应该解决的问题代码java

还有问题,特别对于大的项目或是公司,修复bug的这位伙计不是写代码的那我的(且发现bug和修复bug的不是同一我的)。所以,必须下降理解代 码花费的时间,不管是一段时间前你本身写的代码仍是团队中的其余成员写的代码。这关系到底线(营业收入)和开发人员的幸福,由于咱们更应该去开发新的激动 人心的事物而不是花几小时几天的时间去维护遗留代码。
另外一个相关软件开发生命的事实是,读代码花费的时间要比写来得多。有时候,当你专一并深刻思考某个问题的时候,你能够坐下来,一个下午写大量的代码。
你的代码很能很快就工做了,可是,随着应用的成熟,还会有不少其余的事情发生,这就要求你的进行进行审查,修改,和调整。例如:web

  • bug是暴露的ajax

  • 新功能被添加到应用程序正则表达式

  • 程序在新的环境下工做(例如,市场上出现新想浏览器)数组

  • 代码改变用途浏览器

  • 代码得彻底从头从新,或移植到另外一个架构上或者甚至使用另外一种语言缓存

因为这些变化,不多人力数小时写的代码最终演变成花数周来阅读这些代码。这就是为何建立可维护的代码对应用程序的成功相当重要。
可维护的代码意味着:

  • 可读的

  • 一致的

  • 可预测的

  • 看上去就像是同一我的写的

  • 已记录

最小全局变量

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库

  • 广告方的脚本代码

  • 第三方用户跟踪和分析脚本代码

  • 不一样类型的小组件,标志和按钮

比方说,该第三方脚本定义了一个全局变量,叫作result;接着,在你的函数中也定义一个名为result的全局变量。其结果就是后面的变量覆盖前面的,第三方脚本就一会儿嗝屁啦!
所以,要想和其余脚本成为好邻居的话,尽量少的使用全局变量是很重要的。在书中后面提到的一些减小全局变量的策略,例如命名空间模式或是函数当即自动执行,可是要想让全局变量少最重要的仍是始终使用var来声明变量。
因为JavaScript的两个特征,不自觉地建立出全局变量是出乎意料的容易。首先,你能够甚至不须要声明就可使用变量;第二,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的反作用

隐式全局变量和明肯定义的全局变量间有些小的差别,就是经过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严格模式下,未声明的变量(如在前面的代码片断中的两个反面教材)工做时会抛出一个错误。

访问全局对象

在浏览器中,全局对象能够经过window属性在代码的任何位置访问(除非你作了些比较出格的事情,像是声明了一个名为window的局部变量)。可是在其余环境下,这个方便的属性可能被叫作其余什么东西(甚至在程序中不可用)。若是你须要在没有硬编码的window标识符下访问全局对象,你能够在任何层级的函数做用域中作以下操做:

var global = (function () {
   return this;
}());

这种方法能够随时得到全局对象,由于其在函数中被当作函数调用了(不是经过new构造),this总 是指向全局对象。实际上这个病不适用于ECMAScript 5严格模式,因此,在严格模式下时,你必须采起不一样的形式。例如,你正在开发一个JavaScript库,你能够将你的代码包裹在一个即时函数中,而后从 全局做用域中,传递一个引用指向this做为你即时函数的参数。

单var形式

在函数顶部使用单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散布的问题

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();

为了完整,咱们再提一提执行层面的稍微复杂点的东西。代码处理分两个阶段,第一阶段是变量,函数声明,以及正常格式的参数建立,这是一个解析和进入上下文 的阶段。第二个阶段是代码执行,函数表达式和不合格的标识符(为声明的变量)被建立。可是,出于实用的目的,咱们就采用了”hoisting”这个概念, 这种ECMAScript标准中并未定义,一般用来描述行为。

for循环

for循环中,你能够循环取得数组或是数组相似对象的值,譬如argumentsHTMLCollection对象。一般的循环形式以下:

// 次佳的循环
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形式。不足在于当重构代码的时候,复制和粘贴整个循环有点困难。例如,你从一个函数复制了一个循环到另外一个函数,你不得不去肯定你可以把imax引入新的函数(若是在这里没有用的话,颇有可能你要从原函数中把它们删掉)。

最后一个须要对循环进行调整的是使用下面表达式之一来替换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循环应该用在非数组对象的遍历上,使用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]);
}

(不)扩展内置原型

扩增构造函数的prototype属性是个很强大的增长功能的方法,但有时候它太强大了。

增长内置的构造函数原型(如Object(), Array(), 或Function())挺诱人的,可是这严重下降了可维护性,由于它让你的代码变得难以预测。使用你代码的其余开发人员极可能更指望使用内置的 JavaScript方法来持续不断地工做,而不是你另加的方法。

另外,属性添加到原型中,可能会致使不使用hasOwnProperty属性时在循环中显示出来,这会形成混乱。

所以,不增长内置原型是最好的。你能够指定一个规则,仅当下面的条件均知足时例外:

  • 能够预期未来的ECMAScript版本或是JavaScript实现将一直将此功能看成内置方法来实现。例如,你能够添加ECMAScript 5中描述的方法,一直到各个浏览器都迎头遇上。这种状况下,你只是提早定义了有用的方法。

  • 若是您检查您的自定义属性或方法已不存在——也许已经在代码的其余地方实现或已是你支持的浏览器JavaScript引擎部分。

  • 你清楚地文档记录并和团队交流了变化。

若是这三个条件获得知足,你能够给原型进行自定义的添加,形式以下:

if (typeof Object.protoype.myMethod !== "function") {
   Object.protoype.myMethod = function () {
      // 实现...
   };
}

switch模式

你能够经过相似下面形式的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:确保总有健全的结果,即便无状况匹配。

避免隐式类型转换

JavaScript的变量在比较的时候会隐式类型转换。这就是为何一些诸如:false == 0 或 “” == 0 返回的结果是true。为避免引发混乱的隐含类型转换,在你比较值和表达式类型的时候始终使用===和!==操做符。

var zero = 0;
if (zero === false) {
   // 不执行,由于zero为0, 而不是false
}

// 反面示例
if (zero == false) {
   // 执行了...
}

还有另一种思想观点认为==就足够了===是多余的。例如,当你使用typeof你就知道它会返回一个字符串,因此没有使用严格相等的理由。然而,JSLint要求严格相等,它使代码看上去更有一致性,能够下降代码阅读时的精力消耗。(“==是故意的仍是一个疏漏?”)

避免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()下的数值转换

使用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了结。

编码规范

创建和遵循编码规范是很重要的,这让你的代码保持一致性,可预测,更易于阅读和理解。一个新的开发者加入这个团队能够通读规范,理解其它团队成员书写的代码,更快上手干活。

许多激烈的争论发生会议上或是邮件列表上,问题每每针对某些代码规范的特定方面(例如代码缩进,是Tab制表符键仍是space空格键)。若是你是 你组织中建议采用规范的,准备好面对各类反对的或是听起来不一样但很强烈的观点。要记住,创建和坚决不移地遵循规范要比纠结于规范的细节重要的多。

缩进

代码没有缩进基本上就不能读了。惟一糟糕的事情就是不一致的缩进,由于它看上去像是遵循了规范,可是可能一路上伴随着混乱和惊奇。重要的是规范地使用缩进。

一些开发人员更喜欢用tab制表符缩进,由于任何人均可以调整他们的编辑器以本身喜欢的空格数来显示Tab。有些人喜欢空格——一般四个,这都无所谓,只要团队每一个人都遵循同一个规范就行了。这本书,例如,使用四个空格缩进,这也是JSLint中默认的缩进。

什么应该缩进呢?规则很简单——花括号里面的东西。这就意味着函数体,循环 (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;
}

花括号{}

花括号(亦称大括号,下同)应总被使用,即便在它们为可选的时候。技术上将,在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);
}

左花括号的位置

开发人员对于左大括号的位置有着不一样的偏好——在同一行或是下一行。

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解析器隐式建立。这不只促进更科学和更严格的代码,并且有助于解决存有疑惑的地方,就如前面的例子显示。

空格

空格的使用一样有助于改善代码的可读性和一致性。在写英文句子的时候,在逗号和句号后面会使用间隔。在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;
}

最后须要注意的一个空格——花括号间距。最好使用空格:

  • 函数、if-else语句、循环、对象字面量的左花括号的前面({)

  • else或while之间的右花括号(})

空格使用的一点不足就是增长了文件的大小,可是压缩无此问题。

有一个常常被忽略的代码可读性方面是垂直空格的使用。你可使用空行来分隔代码单元,就像是文学做品中使用段落分隔同样。

命名规范

另外一种方法让你的代码更具可预测性和可维护性是采用命名规范。这就意味着你须要用同一种形式给你的变量和函数命名。

下面是建议的一些命名规范,你能够原样采用,也能够根据本身的喜爱做调整。一样,遵循规范要比规范是什么更重要。

以大写字母写构造函数

JavaScript并无类,但有new调用的构造函数:

var adam = new Person();

由于构造函数仍仅仅是函数,仅看函数名就能够帮助告诉你这应该是一个构造函数仍是一个正常的函数。

命名构造函数时首字母大写具备暗示做用,使用小写命名的函数和方法不该该使用new调用:

function MyConstructor() {...}
function myFunction() {...}

分隔单词

当你的变量或是函数名有多个单词的时候,最好单词的分离遵循统一的规范,有一个常见的作法被称做“驼峰(Camel)命名法”,就是单词小写,每一个单词的首字母大写。

对于构造函数,可使用大驼峰式命名法(upper camel case),如MyConstructor()。对于函数和方法名称,你可使用小驼峰式命名法(lower camel case),像是myFunction(), calculateArea()getFirstName()

要是变量不是函数呢?开发者一般使用小驼峰式命名法,但还有另一种作法就是全部单词小写如下划线链接:例如,first_name, favorite_bands,old_company_name,这种标记法帮你直观地区分函数和其余标识——原型和对象。

ECMAScript的属性和方法均使用Camel标记法,尽管多字的属性名称是罕见的(正则表达式对象的lastIndex和ignoreCase属性)。

其它命名形式

有时,开发人员使用命名规范来弥补或替代语言特性。

例如,JavaScript中没有定义常量的方法(尽管有些内置的像Number, MAX_VALUE),因此开发者都采用所有单词大写的规范来命名这个程序生命周期中都不会改变的变量,如:

// 珍贵常数,只可远观
var PI = 3.14,
    MAX_WIDTH = 800;

还有另一个彻底大写的惯例:全局变量名字所有大写。所有大写命名全局变量能够增强减少全局变量数量的实践,同时让它们易于区分。

另一种使用规范来模拟功能的是私有成员。虽然能够在JavaScript中实现真正的私有,可是开发者发现仅仅使用一个下划线前缀来表示一个私有属性或方法会更容易些。考虑下面的例子:

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__

注释

你必须注释你的代码,即便不会有其余人向你同样接触它。一般,当你深刻研究一个问题,你会很清楚的知道这个代码是干吗用的,可是,当你一周以后再回来看的时候,想必也要耗掉很多脑细胞去搞明白到底怎么工做的。

很显然,注释不能走极端:每一个单独变量或是单独一行。可是,你一般应该记录全部的函数,它们的参数和返回值,或是任何不寻常的技术和方法。要想到注 释能够给你代码将来的阅读者以诸多提示;阅读者须要的是(不要读太多的东西)仅注释和函数属性名来理解你的代码。例如,当你有五六行程序执行特定的任务, 若是你提供了一行代码目的以及为何在这里的描述的话,阅读者就能够直接跳过这段细节。没有硬性规定注释代码比,代码的某些部分(如正则表达式)可能注释 要比代码多。

最重要的习惯,然而也是最难遵照的,就是保持注释的及时更新,由于过期的注释比没有注释更加的误导人。

关于做者

Stoyan Stefanov是Yahoo!web开发人员,多个O'Reilly书籍的做者、投稿者和技术评审。他常常在会议和他的博客www.phpied.com上发表web开发主题的演讲。Stoyan仍是smush.it图片优化工具的创造者,YUI贡献者,雅虎性能优化工具YSlow 2.0的架构设计师。

本文转自:张鑫旭-翻译-高质量JavaScript代码书写基本要点

英文原文:http://net.tutsplus.com/tutorials/javascript-ajax/the-essentials-of-writing-high-quality-javascript/

关于本文

本文转自TOM大叔深刻理解JavaScript系列

【深刻理解JavaScript系列】文章,包括了原创,翻译,转载,整理等各种型文章,原文是TOM大叔的一个很是不错的专题,现将其从新整理发布。谢谢大叔。

相关文章
相关标签/搜索