“程序是写给人读的,只是偶尔让计算机执行一下。”——Donald Knuth。

高德纳(Donald Ervin Knuth)是世界顶级计算机科学家之一,被公认为现代计算机科学的鼻祖,著有《计算机程序设计艺术》(The Art of Computer Programming)等经典著作,业余时间里,Knuth不仅写小说,还是一位音乐家、作曲家、管风琴设计师。

有用的工具:JSLint 和 JSHint

JSLint 是由 Douglas Crockford 创建的,这是一个通用的 JavaScript 代码质量检查工具。

Crockford 对 JavaScript 风格的观点分为三个部分:

JSHint 是 JSLint 的一个分支项目,由 Anton Kovalyov 创建并维护。目标是提供更加个性化的JavaScript代码质量和编程风格检查的工具。比如,可以完全自定义消息提示。

第1章 基本的格式化

1.1 缩进层级

  • jQuery核心风格指南(jQuery Core Style Guide)明确规定使用制表符缩进

  • Douglas Crockford 的 JavaScript 代码规范(Douglas Crockford's Code Conventions for the JavaScript Programming Language)规定使用4个空格字符的缩进

  • SproutCore 风格指南(SproutCore Style Guide)规定使用2个空格的缩进

  • Google的JavaScript风格指南(Google JavaScript Style Guide)规定使用2个空格的缩进

  • Dojo编程风格指南(Dojo Style Guide)规定使用制表符缩进

作者推荐使用 4 个空格字符为一个缩进层级。可以在编辑器中配置敲入 Tab 键时插入 4 个空格。

制表符和空格不要混用。

1.2 语句结尾

JS分析器有自动分号插入(Automatic Semicolon Insertion,ASI)机制,省略分号JS也可以正常工作。但其规则非常复杂且很难记住,不推荐省略分号。如:

// 原始代码

function getData() {

   return

   {

       title: "Maintainable JavaScript",

       author: "Nicholas C. Zakas"

   }

}

// 分析器理解成

function getData() {

   return;

   {

       title: ...,

       author: ...

   };

}

可以改为:

// 这段代码工作正常,尽管没有分号

function getData() {

   return {

       title: ...,

       author: ...

   }

}

Douglas Crockford 针对 JavaScript 提炼出的编程规范推荐总是使用分号,jQuery 核心风格指南、Google 的 JavaScript 风格风格以及 Dojo 编程风格指南推荐不要省略分号。如果省略了分号,JSLint 和 JSHint 默认都会有警告。

1.3 行的长度

限定在80个字符。

1.4 换行

在运算符后换行,可以防止ASI自作主张地插入分号。

// 好的做法:在运算符后换行,第二行追加两个缩进

callAFunction(document, element, window, "some string value", true, 123,

       navigator);

// 不好的做法:第二行只有一个缩进

callAFunction(document, element, window, "some string value", true, 123,

   navigator);

// 不好的做法:在运算符之前换行了

callAFunction(document, element, window, "some string value", true, 123

       , navigator);

if (isLeapYear && isFeburary && day == 29 && itsYourBirthday &&

       noPlans) {

   waitAnotherFourYears();

}

例外:给变量赋值,第二行应当和赋值符号对齐

var result = something + anotherThing + yetAnotherThing + somethingElse +

                   anotherSomethingElse;

1.5 空行

有语义关联的代码展现在一起,不相关的用空格隔开。

if (wl && wl.length) {

   for (i = 0, l = wl.length; i < l; i++) {

       p = wl[i];

       type = Y.Lang.type(r[p]);

       if (s.hasOwnProperty(p)) {

           if (merge && type == 'object') {

               Y.mix(r[p], s[p]);

           } else if (ov || !(p in r)) {

               r[p] = s[p];

           }

       }

   }

}

其他添加空行的场景:

  • 方法之间

  • 方法中的局部变量和第一条语句之间

  • 多行或单行注释之前

  • 方法内的逻辑片段之间

1.6 命名

“计算机科学只存在两个难题:缓存失效和命名。”—— Phil Karlton。

变量和函数命名对于增强代码可读性至关重要。

基本遵照(小)驼峰式大小写(Camel Case)命名法,但也不排斥一些其他的命名风格(估计是作者所在的雅虎YUI本身也有其他的命名风格)。

1.6.1 变量和函数

   变量名前缀应当是名词,函数名前缀应当是动词。

   避免使用没有意义的命名。

   函数和方法命名常见约定:

   动词  含义

   can   函数返回布尔值

   has   函数返回布尔值

   is      函数返回布尔值

   get   函数返回非布尔值

   set    函数用来保存一个值

jQuery 没有遵循这种函数命名约定,它的很多方法同时用作 getter 和 setter。

1.6.2 常量

   在ECMAScript 6 之前,JavaScript中并没有真正的常量的概念。

   约定采用C风格:

   var MAX_COUNT = 10;

   var URL = "http://...";

1.6.3 构造函数

// 好的做法

function Person(name) {

   this.name = name;

}

Person.prototype.sayName = function() {

   alert(this.name);

}

var me = new Person("Nicholas");

采用Pascal Case,使用名词。

1.7 直接量

1.7.1 字符串

双引号或单引号都可,但要保持一种风格, 不要混用。

Crockford 编程规范和 jQuery 核心风格指南使用双引号,Google的JavaScript风格指南使用单引号。

作者倾向使用双引号。

1.7.2 数字

// 整数

var count = 10;

// 小数

var price = 10.0;

var price = 10.00;

// 不推荐的写法:没有小数部分

var price = 10.;

// 不推荐的写法:没有整数部分

var price = .1;

// 不推荐的写法:八进制写法现在已经被弃用了

var num = 010;

// 十六进制写法

var num = 0xA2;

// 科学计数法

var num = 1e23;

1.7.3 null

应当使用 null 的场景:

  • 用来初始化一个变量,这个变量可能赋值为一个对象

  • 用来和一个已经初始化的变量比较,这个变量可以是也可以不是一个对象

  • 当函数的参数期望是对象时,用途参数传入

  • 当函数的返回值期望是对象时,用作返回值传出

不应当使用 null 的场景:

  • 不要使用 null 来检测是否传入了某个参数

  • 不要使用 null 来检测一个未初始化的变量

// 好的用法

var person = null;

// 好的用法

function getPerson() {

   if (condition) {

       return new Person("Nicholas");

   } else {

       return null;

   }

}

// 好的用法

var person = getPerson();

if (person !== null) {

   doSomething();

}

// 不好的写法:用来和未初始化的变量比较

var person;

if (person != null) {

   doSomething();

}

// 不好的写法:检测是否传入了参数

function doSomething(arg1, arg2, arg3, arg4) {

   if (arg4 != null) {

       doSomethingElse();

   }

}

理解 null 最好的方式是将它当做对象的占位符。

1.7.4 undefined

undefined 是一个特殊值。没有被初始化的变量都有一个初始化,即 undefined,表示这个变量等待被赋值。

// 不好的写法

var person;

console.log(person === undefined); // true

尽量避免这种用法 。它和 typeof xxx == "undefined" 经常混淆。实际上,typeof 的行为与让人费解,因为不管是值为undefined的变量还是未声明的变量,typeof 的运算结果都是 "undefined"。

var person

console.log(typeof person); // "undefined"

console.log(typeof foo);       // "undefined"

禁止使用特殊值 undefined,如果使用了一个可能会引用对象的变量时,将其初始化为 null。

// 好的做法

var person = null;

console.log(person === null); // true

typeof 运算符运行 null 的类型时返回 "object",这样就可以和 undefined 区分开了。

1.7.5 对象直接量

创建对象最流行的一种做法是使用对象直接量。这种方式可以取代先显式地创建 Object 的实例然后添加属性的做法。

// 不好的做法

var book = new Object();

book.title = "Maintainable JavaScript";

book.author = "Nicholas C. Zakas";

// 好的写法

var book = {

   title: "Maintainable JavaScript",

   author: "Nicholas C. Zakas"

};

第2章 注释

2.1 单行注释

三种用法:

  • 独占一行的注释,用来解释下一行代码。这行注释之前总是有一个空行,且缩进层级和下一行代码保持一致

  • 在代码行尾部的注释。代码结束到注释之间至少有一个缩进

  • 被注释掉的大段代码(很多编辑器都可以批量注释掉多行代码)

2.2 多行注释

作者倾向 Java 风格的多行注释。

2.3 使用注释

当代码不清晰时添加注释。

2.3.1 难于理解的代码

关键是让其他人理容易地读懂这段代码。

2.3.2 可能被误认为错误的代码

防止被别人误解并进行“修复”。

2.3.3 浏览器特性 hack

var ret  = false;

if (!needle || !element || ...) {

   ret = false;

} else if (element[CONTAINS]) {

   // 如果 needle 不是 ELEMENT_NODE 时,IE 和 Safari 下会有错误

   if (Y.UA.opera || needle[NODE_TYPE] === 1) {

       ret = element[CONTAINS](needle);

   } else {

       ...

   }

} else ...

   ...

...

2.4 文档注释

Y.merge = function() {

   ...

};

YUI 类库使用自己的 YUIDoc 工具来根据注释生成文档。它的格式几乎和 JSDoc Toolkit 一模一样,在开源项目中 JSDoc Toolkit 的应用非常广泛,包括 Google 内部的很多开源项目。

确保对如下内容添加注释:

  • 所有的方法,应当对方法、期望的参数和可能的返回值添加注释描述

  • 所有的构造函数,应当对自定义类型和期望的参数添加注释描述

  • 所有包含文档化方法的对象

第3章 语句和表达式

不使用花括号的 if 语句,在 Crockford编程规范、jQuery核心风格指南、Google的JavaScript风格指南以及Dojo编程风格指南中都是明确禁止的。

默认情况下,省略花括号在 JSLint 和 JSHint 中都会报警告。

所有的块语句都应当使用花括号,包括:

  • if

  • for

  • while

  • do...while

  • try...catch...finally

3.1 花括号的对齐方式

两种风格:

// Java 风格

if (condition) {

   doSomething();

} else {

   doSomethingElse();

}

// C# 风格

if (condition)

{

   doSomething();

}

else

{

   doSomethingElse();

}

Google JavaScript 风格指南明确禁止第二种用法,以免导致错误的分号自动插入。

作者也推荐第一种。

3.2 块语句间隔

三种风格:

1. 没有空格

if(condition){

   doSomething();

}

Dojo 编程风格指南推荐。

2. 左右圆括号前后各添加一个空格

if (condition) {

   doSomething();

}

Crockford编程规范和Google JavaScript 风格指南推荐。

3. 左右圆括号内外两侧都添加空格

if ( condition ) {

   doSomething();

}

jQuery 核心风格指南规定了这种风格。

作者倾向于第二种风格。

3.3 switch 语句

JSLint 期望 case 和 switch 具有一致的缩进格式,否则报警。

Crockford规范和Google指南提倡这种格式。

switch (condition) {

// 明显的依次执行

case "first":

case "second":

   // 代码

   break;

case "third":

   // 代码

   

default:

   // 代码

}

对连续执行做了明确的提醒,因此 JSHint 不会给警告。

Crockford规范禁止switch语句中出现连续执行(fall through)的。jQuery核心风格指南允许,Dojo编程风格要添加注释。

Crockford规范和Dojo指南将 default 作为switch语句格式的标准组成部分。

作者倾向于在没有默认行为且写了注释的情况下省略 default:

switch (condition) {

   case ...

   // 没有 default

}

3.4 with 语句

严格模式中,with 语句是被明确禁止的,如果使用则报语法错误。

Crockford规范和Google指南禁止使用 with 。

作者也强烈推荐避免使用 with 语句。

3.5 for 循环

两种:传统for循环,和for-in循环。前者经常用于遍历数组成员,后者用来遍历对象属性。

Crockford 规范不允许使用 continue,使用条件更易理解且不容易出错。Dojo指南明确指出可以使用 continue 和 break。

作者推荐尽可能避免使用 continue,但也没有理由完全禁止使用。

使用 continue 时,JSLint 会给出警告,JSHint 不会给出警告。

3.6 for-in 循环

for-in 不仅遍历对象的实例属性,还遍历从原型继承来的属性。当遍历自定义对象的属性时,往往会因为意外的结果而终止。因此最好使用 hasOwnProperty() 方法来为 for-in 循环过滤出实例属性:

var prop;

for (prop in object) {

   if (object.hasOwnProperty(prop)) {

       console.log("Property name is " + prop);

       console.log("Property value is " + object[prop]);

   }

}

Crockford规范要求所有的 for-in 循环都必须使用 hasOwnProperty()。如果不用,JSLint 和 JSHint 都会给出警告(可以手动关掉)。

for-in 遍历数组成员是错误的用法。Crockford规范、Google 指南中是禁止使用的。

第4章 变量、函数和运算符

4.1 变量声明

不论 var 语句是否真正会被执行,所有的 var 语句都提前到包含这段逻辑的函数的顶部执行。比如:

function doSomething() {

   var result = 10 + value;

   var value = 10;

   return result;

}

计算结果是 NaN。

这段代码被理解为:

function doSomething() {

   var result;

   var value;

   result = 10 + value;

   value = 10;

   return result;

}

在 ECMAScript 5 之前,JavaScript 中并没有块级变量声明。

这意味着,在函数内部任意地方定义变量和在函数顶部定义变量是完全一样的。因此,一种流行的风格是将所有变量声明放在函数顶部而不是散落在各个角落。

作者建议总是将局部变量的定义作为函数内第一条语句。Crockford规范、SproutCore指南和Dojo指南也推荐这样做。

function doSometingWithItems(items) {

   var i, len;

   var value = 10;

   var result = value + 10;

   for (i = 0, len = items.length; i < len; i++) {

       doSomething(items[i]);

   }

}

Crockford还进一步推荐在函数顶部使用单 var 语句。

Dojo指南规定,只有当变量之间有关联性时,才允许使用单 var 语句。

作者倾向于将所有的 var 语句合并为一个语句,每个变量的初始化独占一行。赋值运算符应当对齐。没有初始值的变量放在最后。

4.2 函数声明

函数声明也会被 JavaScript 引擎提前。

推荐先声明函数再使用。Crockford规范还推荐函数内部的局部函数应当紧挨着变量声明之后声明。

函数声明不应当出现在语句块之内。

// 不好的写法

if (condition) {

   function doSomething() {

       alert("Hi!");

   }

} else {

   function doSomething() {

       alert("Yo!");

   }

}

不管 condition 的计算结果如何,大多数浏览器都会自动使用第二个声明。而 Firefox 则根据 condition 的计算结果选用合适的函数声明。这种场景是 ECMAScript 的灰色地带,要尽可能地避免。

这种模式也是 Google 指南明确禁止的。

4.3 函数调用间隔

一般推荐在函数名和左括号之间没有空格。

Crockford规范有明确规定。Google指南、SproutCore指南以及Dojo指南没有明确规定,但都使用了这种风格。

jQuery 核心风格指南中的规定更进一步,它规定应当在左括号之后和右括号之前都加上空格。但有例外情况,特别是和传入单参数的函数相关的场景,这些参数包括对象直接量、数组直接量、函数表达式或者字符串。

// jQuery 例外情况

doSomething(function() {});

doSomething({ item: item });

doSomething([ item ]);

doSomething("Hi!");

有例外的风格是不好的,会给开发者带来困惑。

4.4 立即调用的函数

为了让立即执行的函数能够被一眼看出来,可以把函数用一对圆括号包裹起来,比如:

// 不好的写法

var value = function() {

   // 函数体

   return {

       message: "Hi"

   }

}();

// 好的写法

var value = (function() {

   ...

}());  

Crockford规范推荐。

4.5 严格模式

ECMAScript 5 引入了“严格模式”(strict mode),希望通过这种方式来谨慎地解析执行 JavaScript,以减少错误。

"use strict";

不要用于全局,以防止其他文件中的非严格模式代码出错。

// 不好的写法 - 全局的严格模式

"use strict";

function doSomething() {

   // 代码

}

// 好的写法

function doSomething() {

   "use strict";

   // 代码

}

// 好的写法

(function () {

   "use strict";

   // 代码

})();

4.6 相等

JavaScript 具有强制类型转换机制(type coercion),判断相等操作是很微妙的。

使用 == 和 != 两个运算符都会有强制类型转换。如:

// 比较数字5和字符串5,字符串会先转换为数字,再执行比较

console.log(5 == "5"); // true

// 比较数字25和十六进制的字符串 25

console.log(25 == "0x19"); // true

// 布尔值会先转换为数字,false 值变为 0,true 值变为 1

console.log(1 == true); // true

console.log(0 == false); // true

console.log(2 == true); // false

如果一个值是对象而另一个不是,则会首先调用对象的 valueOf() 方法,得到原始类型值再比较。如果没有定义 valueOf(),则调用 toString()。例如:

var object = {

   toString: function() {

       return "0x19";

};

console.log(object == 25); // true

根据ECMAScript标准规范的描述,null 和 undefined 是相等的。

console.log(null == undefined); // true

推荐不要使用 == 和 !=,而是应当使用 === 和 !== 。

Crockford规范、jQuery指南、SproutCore指南推荐使用 === 和 !==。

jQuery 允许在和 null 比较时使用 ==。

作者推荐毫无例外地总是使用 === 和 !== 。

4.6.1 eval()

eval() 会将传入的字符串当作代码来执行。Function构造函数、setTimeout() 和 setInterval() 也可以做到这一点。

eval("alert('hi')");

var count = 10;

var number = eval("5 + count");

console.log(number); // 15

var myfunc = new Function("alert('Hi!')");

setTimeout("document.body.style.background='red'", 50);

setInterval("document.title='It is now '" + (new Date()), 1000);

一个通用的原则是,严禁使用 Function,并且在别无他法时使用 eval()。setTimeout() 和 setInterval() 也是可以使用的,它不要用字符串而要用函数。

4.6.2 原始包装类型

JavaScript 里有3种原始包装类型:String、Boolean 和 Number。每种类型都代表全局作用域中的一个构造函数,并分别表示各自对应的原始值的对象。原始包装类型的主要作用是让原始值具有对象般的行为。比如:

var name = "Nicholas";

console.log(name.toUpperCase());

这条语句的表象背后,JavaScript引擎创建了String类型的新实例,紧跟着就被销毁了,当再次需要时就会又创建另外一个对象。

var name = "Nicholas";

name.author = true;

console.log(name.author); // undefined

Google指南禁止使用原始包装类型。这样会增加出 bug 的机率,从而使开发人员陷入困惑。