JavaScript 代码风格指南

这份指南列出了编写 JavaScript 时需要遵守的规范, 指明哪些应该提倡, 哪些应该避免.
本文基于 google 的规范翻译整理(JavaScript 是许多 Google 开源项目使用的主要客户端脚本语言).

(1)

JavaScript 语言风格规范

变量

  • 声明变量务必使用 var 关键字.

如果未能使用 var 来声明, 变量就会暴露在全局上下文中, 这样很可能会与已有变量冲突. 此外,如果没有声明,也难以确定变量的作用域, 变量既有可能出现在局部作用域中, 也可能轻易地泄漏到 DocumentWindow 作用域中, 因此务必使用 var 声明变量.

常量

  • 常量的规范命名应该使用这样的形式: NAMES_LIKE_THIS, 即使用大写字符, 并用下划线分隔单词.
  • 也可以使用用 @const 标记来指明它是常量.
  • 千万不要使用 const 关键词.

对于基本类型的常量, 遵循命名规范即可.

1
2
3
4
5
/**
* The number of seconds in a minute.
* @type {number}
*/

goog.example.SECONDS_IN_A_MINUTE = 60;

对于非基本类型, 使用 @const 标记.

1
2
3
4
5
6
7
8
9
10
/**
* The number of seconds in each of the given units.
* @type {Object.<number>}
* @const
*/

goog.example.SECONDS_TABLE = {
minute: 60,
hour: 60 * 60,
day: 60 * 60 * 24
}

使用 @const 标记可以提醒编译器它属于常量.
至于关键词 const, 由于 IE 无法识别, 因此尽量不要使用.

分号

  • 语句结尾总是使用分号.

依赖语句间的隐式分隔, 有时会造成微妙而难以调试的问题.
避免如此。
代码编写者自己应该更能清楚哪里是语句的起止.

在下述情况下, 漏掉分号尤其危险:

1
2
3
4
5
6
7
8
// 例1.
MyClass.prototype.myMethod = function() {
return 42;
} // 这里没有使用分号.

(function() {
// 包裹在函数中的一些初始化代码创建来一个局部作用域.
})();
1
2
3
4
5
6
7
8
9
// 例2.  
var x = {
'i': 1,
'j': 2
} // 这里也没有使用分号.

// 尝试在 IE 上运行一段代码, 而在 firefox 上运行另外一段代码.
// 我知道你从来不会这样写代码, 但我们就不必纠结这个小问题了吧.
[normalVersion, ffVersion][isIE]();
1
2
3
4
5
// 例3.
var THINGS_TO_EAT = [apples, oysters, sprayOnCheese] // 这里也没有加分号.

// 条件执行一段时髦的指令
-1 == resultOfOperation() || die();

这几段代码运行效果如何呢?

  1. 例1会抛出 JavaScript 报错. 这段代码会解释成一个返回值为42的函数附带一个匿名函数作为参数而被调用, 返回值 42 无法以一个匿名函数为参数被”调用”, 从而导致报错.
  2. 例2中, 运行时很可能遇到 ‘no such property in undefined’ 错误, 原因是代码试图执行 x[ffVersion]isIE() .
  3. 例3中 die() 总会被调用并将返回值赋给 THINGS_TO_EAT. 因为一个数组减一的值始终为 NaN, 不会与任何变量相等, 哪怕 resultOfOperation() 返回 NaN 等式也不会成立.

为何会这样呢?

JavaScript 要求语句以分号结尾, 除非当它觉得可以安全地推断语句的结束位置.
在上述几个例子中, 在语句中使用了函数声明, 对象或者数组字面量.
这种情况下,闭合的括号不足以作为语句结束的信号.
JavaScript 从来不会把接下来是中辍运算符括号运算符的地方判断为语句结束位置.

这样会导致令人非常吃惊的后果, 因此请确保使用分号来结束语句.

嵌套函数

  • 可以使用

嵌套函数非常有用, 比如用于创建连续(continuations)或用于隐藏辅助函数. 随意使用它们就好.

代码块内声明函数

  • 不要在代码内声明函数

不要这样:

1
2
3
if (x) {
function foo() {}
}

尽管多数脚本引擎支持代码块内声明函数, 但这一特性并不属于 ECMAScript 规范 (参见 ECMA-262, 条款13与14). 更糟之处在于它们实现方式互不兼容, 与未来的 ECMAScript 建议也相违背. ECMAScript 只允许脚本或函数在顶级语句块中进行函数声明.

1
2
3
if (x) {
var foo = function() {};
}

异常

  • 合理使用

如果试图(使用应用程序开发框架等)写点复杂的东西, 基本上无可避免要使用异常. 尽管用就好.

自定义异常

  • 合理使用

如果不使用自定义异常, 函数返回的错误信息有时会非常复杂, 更不用说不够优雅了.
不太好的解决方案包括传递一个包含错误信息的引用类型, 或者总是返回一个可能包含错误类型的对象. 这些做法基本上都属于原始的 hack 异常处理技巧.
条件合适尽管利用自定义异常的语言特性就好.

标准特性

  • 总是优于非标准特性

为了尽可能提高移植性和兼容性,应该总是优先使用标准特性而不是非标准特性(例如优先使用 string.charAt(3) 而不是 string[3] , 再比如说优先使用 DOM 原生方法去访问节点元素, 而非使用某个特定框架封装好的快捷引用).

封装基本类型

  • 一般情况下, 不要封装基本类型

没有理由去封装基本类型, 而且这样做还存在一些风险:

1
2
3
4
5
6
var x = new Boolean(false);   
if (x) {
alert('hi');
// 运行结果会显示 'hi'.
// 因为基本类型封装的实例调用 typeof 时会返回 "object", 在进行判断时又会被转换为布尔量类型 true .
}

不要这样写!

不过在需要进行类型转换时下面的写法是可行的:

1
2
3
4
5
6
var x = Boolean(0);
if (x) {
alert('hi'); // 永远不会被显示.
}
typeof Boolean(0) == 'boolean';
typeof new Boolean(0) == 'object';

这种写法在需要将变量转换成数字,字符串或者布尔量类型时非常方便.

多级原型结构

  • 不推荐

多级原型结构是 JavaScript 中实现继承的方式. 当你自定义了一个D类, 并且把另一个自定义的B类作为其原型, 就得到了一个多级原型结构.
这种原型结构会变得越来越复杂, 越来越难以维护.

基于上述原因, 可以考虑使用 the Closure 库 中的 goog.inherits() 或其他类似库函数.

1
2
3
4
5
6
7
8
function D() {
goog.base(this)
}
goog.inherits(D, B);

D.prototype.method = function() {
/* ... */
};

方法和成员变量定义

  • /** @constructor */ function SomeConstructor() { this.someProperty = 1; } Foo.prototype.someMethod = function() { ... };

通过 new 有许多种方法为已经创建的对象添加方法和成员变量, 但推荐使用下面的方法:

1
2
3
Foo.prototype.bar = function() {
/* ... */
};

而其他属性则推荐在构造器中初始化:

1
2
3
4
/** @constructor */
function Foo() {
this.bar = value;
}

为何呢?

现在的 JavaScript 引擎会基于对象的”形状”来优化, 向对象添加属性(包括覆写原型中设定的值)会改变对象的”形状”并导致性能降低.

删除

  • 优先使用 this.foo = null 而非关键字 delete

使用:

1
2
3
Foo.prototype.dispose = function() {
this.property_ = null;
};

而非:

1
2
3
Foo.prototype.dispose = function() {
delete this.property_;
};

在现代的 Javascript 引擎中, 改变对象属性个数的速度远远比为其重新赋值慢.
除非真有必要从对象的迭代列表中移除一个键, 或者想要改变 if (key in obj) 的结果, 否则应当尽量避免使用关键字 delete

闭包

  • 可以, 但务必谨慎.

创建闭包的能力可能是 JS 最有用但却经常被忽略的特性了. 参阅 a good description of how closures work.

有一件事需要牢记, 闭包会保持一个指向它封闭作用域的指针. 因此在为 DOM 元素附加闭包时, 可能会产生循环引用,进而导致内存泄漏. 例如下面的代码:

1
2
3
function foo(element, a, b) {
element.onclick = function() { /* uses a and b */ };
}

这个函数闭包会保持对 element, ab 的引用, 即使它从未使用 element. 而 element 也保持了对闭包的引用, 因此导致了循环引用, 无法被 GC 回收. 如果遇到这种情况, 可以按照下面代码结构优化一下:

1
2
3
4
5
6
7
function foo(element, a, b) {
element.onclick = bar(a, b);
}

function bar(a, b) {
return function() { /* uses a and b */ }
}

eval()

  • 仅用于代码加载器和 REPL (交互式解释器) 中

eval() 会导致混乱的语义. 当 eval() 中包含用户输入内容, 使用时还可能造成安全隐患. 还可以通过更好, 更清晰, 更安全的方式编写代码, 因此一般情况下不要使用 eval().

解析 RPC 响应时应该总是使用 JSON 并且使用 JSON.parse() 而非 eval() 来读取结果.

假设我们有一台服务器会返回类似下面的内容:

1
2
3
4
5
{
"name": "Alice",
"id": 31502,
"email": "looking_glass@example.com"
}

使用 eval() 的写法如下:

1
2
var userInfo = eval(feed);
var email = userInfo['email'];

feed 中可能包含的恶意 JS 代码会被 eval() 执行.

采用如下写法:

1
2
var userInfo = JSON.parse(feed);
var email = userInfo['email'];

使用 JSON.parse, 无效的 JSON (也包括所有可执行的 JS 代码) 都会抛出异常.

with() {}

  • 不要使用

使用 with 会导致程序的语义含混不清. 因为 with 添加过来的对象包含的属性很可能会与局部变量冲突, 进而彻底地改变程序的原有含义.

下面这段代码做了什么呢?

1
2
3
4
with (foo) {
var x = 3;
return x;
}

回答是: 一切都有可能发生. 局部变量 x 可能会被 foo 对象中的属性覆盖. 它甚至可能会有一个 setter, 从而导致在赋值3时执行许多其他代码. 别用 with.

this

  • 仅用于构造函数, 方法以及构造闭包.

this 的语义非常棘手. 有时它引用全局对象(多数情况下), 有时属于调用者的作用域(使用 eval() 时), 有时属于 DOM 树的某个节点(当为 HTML 属性绑定事件时), 有时属于某个新建的对象(使用构造函数时), 或者还有可能属于其他对象(当对函数使用 call() 或者 apply() 时).

由于在使用上很容易出错, 因此应当限制它在下列的情况下按需使用:

  • 构造函数中;
  • 对象的方法中(包括构造闭包时);

for-in 循环

  • 只用于 object/map/hash 的遍历

for-in 循环经常被错误地用于遍历数组元素. 然而这是非常容易导致错误的做法, 因为它并不会按 0length - 1 的顺序去进行遍历, 而是会遍历这个对象包括其原型链上所有的键值. 下面是几个失败时的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function printArray(arr) {
for (var key in arr) {
print(arr[key]);
}
}

printArray([0,1,2,3]); // 这里没有问题.

var a = new Array(10);
printArray(a); // 出错了.

a = document.getElementsByTagName('*');
printArray(a); // 出错了.

a = [0,1,2,3];
a.buhu = 'wine';
printArray(a); // 又出错了.

a = new Array;
a[3] = 3;
printArray(a); // 又出错了.

遍历数组用最普通的 for 循环即可.

1
2
3
4
5
6
function printArray(arr) {
var l = arr.length;
for (var i = 0; i < l; i++) {
print(arr[i]);
}
}

关联数组

  • 永远不要将 Array 作为 map/hash/associative 数组使用.

不应该允许将 Array 作为关联数组使用, 换一个更准确的说法则是, 数组中不允许使用非整型作为索引值. 如果你需要一个 map/hash 类型, 请使用 Object 而非 Array, 因为在这种情况下, 你真正需要的也只是 Object 的特性而非 Array 的特性. Array 也只是扩展自 Object 类型 (就像 Date, RegExp 或者 String 一样).

多行字符串字面量

  • 不要使用

不要像这样写长字符串:

1
2
3
4
5
6
var myString = 'A rather long string of English text, an error message \
actually that just keeps going and going -- an error \
message to make the Energizer bunny blush (right through \
those Schwarzenegger shades)! Where was I? Oh yes, \
you\'ve got an error and all the extraneous whitespace is \
just gravy. Have a nice day.';

使用这种写法时, 每行开始处的空白字符无法在编译时被安全跳过; 而反斜划线后面的空白字符也常常会导致棘手的错误; 尽管多数脚本引擎支持这种写法, 但它并非 ECMAScript 的标准规范.

应当使用这种写法:

1
2
3
4
5
6
var myString = 'A rather long string of English text, an error message ' +
'actually that just keeps going and going -- an error ' +
'message to make the Energizer bunny blush (right through ' +
'those Schwarzenegger shades)! Where was I? Oh yes, ' +
'you\'ve got an error and all the extraneous whitespace is ' +
'just gravy. Have a nice day.';

Array 和 Object 字面量

  • 可以使用

使用 ArrayObject 字面量而不是 ArrayObject 构造器.

Array 的构造函数很容易因为传参不当而造成错误.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 长度为3.
var a1 = new Array(x1, x2, x3);

// 长度为2.
var a2 = new Array(x1, x2);

// 如果x1是一个自然数, 则数组长度会变成 x1.
// 如果x1是一个数字, 但不是自然数, 这里则会抛出一个异常.
// 否则数组会拥有一个值为x1的元素.
var a3 = new Array(x1);

// 长度为0.
var a4 = new Array();

基于上述原因, 当有人重构代码将传入参数由两个变为一个时, 数组就可能变成非预期的长度.

为了避免出现这种奇怪的情况, 总是使用更加可读的数组字面量来声明数组.

1
2
3
4
var a = [x1, x2, x3];
var a2 = [x1, x2];
var a3 = [x1];
var a4 = [];

虽然 Object 构造器并不存在类似问题, 但鉴于可读性和一致性考虑, 最好也使用字面量来声明.

不要使用:

1
2
3
4
5
6
7
var o = new Object();

var o2 = new Object();
o2.a = 0;
o2.b = 1;
o2.c = 2;
o2['strange key'] = 3;

应当写成:

1
2
3
4
5
6
7
8
var o = {};

var o2 = {
a: 0,
b: 1,
c: 2,
'strange key': 3
};

修改内置对象的原型

  • 不要这样做

尝试修改内建对象, 诸如 Object.prototype 或者 Array.prototype 的行为应该被严格禁止. 修改其他内建对象, 比如 Function.prototype 虽然没有那么危险, 但在生产环境中依然会带来调试问题, 应当尽量避免.

IE下的条件注释

  • 不要使用

不要像这样写:

1
2
3
var f = function () {
/*@cc_on if (@_jscript) { return 2* @*/ 3; /*@ } @*/
};

条件注释会干扰自动化工具的使用, 因为它们会在运行时改变 JavaScript 的语义树.