JavaScript 代码风格指南
这份指南列出了编写 JavaScript 时需要遵守的规范, 指明哪些应该提倡, 哪些应该避免.
本文基于 google 的规范翻译整理(JavaScript 是许多 Google 开源项目使用的主要客户端脚本语言).
(1)
JavaScript 语言风格规范
变量
- 声明变量务必使用 var 关键字.
如果未能使用 var
来声明, 变量就会暴露在全局上下文中, 这样很可能会与已有变量冲突. 此外,如果没有声明,也难以确定变量的作用域, 变量既有可能出现在局部作用域中, 也可能轻易地泄漏到 Document
或 Window
作用域中, 因此务必使用 var
声明变量.
常量
- 常量的规范命名应该使用这样的形式:
NAMES_LIKE_THIS
, 即使用大写字符, 并用下划线分隔单词. - 也可以使用用
@const
标记来指明它是常量. - 千万不要使用
const
关键词.
对于基本类型的常量, 遵循命名规范即可.
1 | /** |
对于非基本类型, 使用 @const
标记.
1 | /** |
使用 @const
标记可以提醒编译器它属于常量.
至于关键词 const
, 由于 IE 无法识别, 因此尽量不要使用.
分号
- 语句结尾总是使用分号.
依赖语句间的隐式分隔, 有时会造成微妙而难以调试的问题.
避免如此。
代码编写者自己应该更能清楚哪里是语句的起止.
在下述情况下, 漏掉分号尤其危险:
1 | // 例1. |
1 | // 例2. |
1 | // 例3. |
这几段代码运行效果如何呢?
- 例1会抛出 JavaScript 报错. 这段代码会解释成一个返回值为42的函数附带一个匿名函数作为参数而被调用, 返回值 42 无法以一个匿名函数为参数被”调用”, 从而导致报错.
- 例2中, 运行时很可能遇到
‘no such property in undefined’
错误, 原因是代码试图执行x[ffVersion]isIE()
. - 例3中
die()
总会被调用并将返回值赋给THINGS_TO_EAT
. 因为一个数组减一的值始终为NaN
, 不会与任何变量相等, 哪怕resultOfOperation()
返回NaN
等式也不会成立.
为何会这样呢?
JavaScript 要求语句以分号结尾, 除非当它觉得可以安全地推断语句的结束位置.
在上述几个例子中, 在语句中使用了函数声明, 对象或者数组字面量.
这种情况下,闭合的括号不足以作为语句结束的信号.
JavaScript 从来不会把接下来是中辍运算符或括号运算符的地方判断为语句结束位置.
这样会导致令人非常吃惊的后果, 因此请确保使用分号来结束语句.
嵌套函数
- 可以使用
嵌套函数非常有用, 比如用于创建连续(continuations)或用于隐藏辅助函数. 随意使用它们就好.
代码块内声明函数
- 不要在代码内声明函数
不要这样:
1 | if (x) { |
尽管多数脚本引擎支持代码块内声明函数, 但这一特性并不属于 ECMAScript 规范 (参见 ECMA-262, 条款13与14). 更糟之处在于它们实现方式互不兼容, 与未来的 ECMAScript 建议也相违背. ECMAScript 只允许脚本或函数在顶级语句块中进行函数声明.
1 | if (x) { |
异常
- 合理使用
如果试图(使用应用程序开发框架等)写点复杂的东西, 基本上无可避免要使用异常. 尽管用就好.
自定义异常
- 合理使用
如果不使用自定义异常, 函数返回的错误信息有时会非常复杂, 更不用说不够优雅了.
不太好的解决方案包括传递一个包含错误信息的引用类型, 或者总是返回一个可能包含错误类型的对象. 这些做法基本上都属于原始的 hack 异常处理技巧.
条件合适尽管利用自定义异常的语言特性就好.
标准特性
- 总是优于非标准特性
为了尽可能提高移植性和兼容性,应该总是优先使用标准特性而不是非标准特性(例如优先使用 string.charAt(3)
而不是 string[3]
, 再比如说优先使用 DOM 原生方法去访问节点元素, 而非使用某个特定框架封装好的快捷引用).
封装基本类型
- 一般情况下, 不要封装基本类型
没有理由去封装基本类型, 而且这样做还存在一些风险:
1 | var x = new Boolean(false); |
不要这样写!
不过在需要进行类型转换时下面的写法是可行的:
1 | var x = Boolean(0); |
这种写法在需要将变量转换成数字,字符串或者布尔量类型时非常方便.
多级原型结构
- 不推荐
多级原型结构是 JavaScript 中实现继承的方式. 当你自定义了一个D类, 并且把另一个自定义的B类作为其原型, 就得到了一个多级原型结构.
这种原型结构会变得越来越复杂, 越来越难以维护.
基于上述原因, 可以考虑使用 the Closure 库 中的 goog.inherits()
或其他类似库函数.
1 | function D() { |
方法和成员变量定义
/** @constructor */ function SomeConstructor() { this.someProperty = 1; } Foo.prototype.someMethod = function() { ... };
通过 new
有许多种方法为已经创建的对象添加方法和成员变量, 但推荐使用下面的方法:
1 | Foo.prototype.bar = function() { |
而其他属性则推荐在构造器中初始化:
1 | /** @constructor */ |
为何呢?
现在的 JavaScript 引擎会基于对象的”形状”来优化, 向对象添加属性(包括覆写原型中设定的值)会改变对象的”形状”并导致性能降低.
删除
- 优先使用
this.foo = null
而非关键字delete
使用:
1 | Foo.prototype.dispose = function() { |
而非:
1 | Foo.prototype.dispose = function() { |
在现代的 Javascript 引擎中, 改变对象属性个数的速度远远比为其重新赋值慢.
除非真有必要从对象的迭代列表中移除一个键, 或者想要改变 if (key in obj)
的结果, 否则应当尽量避免使用关键字 delete
闭包
- 可以, 但务必谨慎.
创建闭包的能力可能是 JS 最有用但却经常被忽略的特性了. 参阅 a good description of how closures work.
有一件事需要牢记, 闭包会保持一个指向它封闭作用域的指针. 因此在为 DOM 元素附加闭包时, 可能会产生循环引用,进而导致内存泄漏. 例如下面的代码:
1 | function foo(element, a, b) { |
这个函数闭包会保持对 element
, a
和 b
的引用, 即使它从未使用 element
. 而 element
也保持了对闭包的引用, 因此导致了循环引用, 无法被 GC 回收. 如果遇到这种情况, 可以按照下面代码结构优化一下:
1 | function foo(element, a, b) { |
eval()
- 仅用于代码加载器和 REPL (交互式解释器) 中
eval()
会导致混乱的语义. 当 eval()
中包含用户输入内容, 使用时还可能造成安全隐患. 还可以通过更好, 更清晰, 更安全的方式编写代码, 因此一般情况下不要使用 eval()
.
解析 RPC 响应时应该总是使用 JSON 并且使用 JSON.parse()
而非 eval()
来读取结果.
假设我们有一台服务器会返回类似下面的内容:
1 | { |
使用 eval() 的写法如下:
1 | var userInfo = eval(feed); |
feed 中可能包含的恶意 JS 代码会被 eval()
执行.
采用如下写法:
1 | var userInfo = JSON.parse(feed); |
使用 JSON.parse
, 无效的 JSON (也包括所有可执行的 JS 代码) 都会抛出异常.
with() {}
- 不要使用
使用 with
会导致程序的语义含混不清. 因为 with
添加过来的对象包含的属性很可能会与局部变量冲突, 进而彻底地改变程序的原有含义.
下面这段代码做了什么呢?
1 | with (foo) { |
回答是: 一切都有可能发生. 局部变量 x
可能会被 foo
对象中的属性覆盖. 它甚至可能会有一个 setter, 从而导致在赋值3时执行许多其他代码. 别用 with
.
this
- 仅用于构造函数, 方法以及构造闭包.
this
的语义非常棘手. 有时它引用全局对象(多数情况下), 有时属于调用者的作用域(使用 eval()
时), 有时属于 DOM 树的某个节点(当为 HTML 属性绑定事件时), 有时属于某个新建的对象(使用构造函数时), 或者还有可能属于其他对象(当对函数使用 call()
或者 apply()
时).
由于在使用上很容易出错, 因此应当限制它在下列的情况下按需使用:
- 构造函数中;
- 对象的方法中(包括构造闭包时);
for-in 循环
- 只用于 object/map/hash 的遍历
for-in
循环经常被错误地用于遍历数组元素. 然而这是非常容易导致错误的做法, 因为它并不会按 0
到 length - 1
的顺序去进行遍历, 而是会遍历这个对象包括其原型链上所有的键值. 下面是几个失败时的例子:
1 | function printArray(arr) { |
遍历数组用最普通的 for 循环即可.
1 | function printArray(arr) { |
关联数组
- 永远不要将
Array
作为 map/hash/associative 数组使用.
不应该允许将 Array
作为关联数组使用, 换一个更准确的说法则是, 数组中不允许使用非整型作为索引值. 如果你需要一个 map/hash 类型, 请使用 Object
而非 Array
, 因为在这种情况下, 你真正需要的也只是 Object
的特性而非 Array
的特性. Array
也只是扩展自 Object
类型 (就像 Date
, RegExp
或者 String
一样).
多行字符串字面量
- 不要使用
不要像这样写长字符串:
1 | var myString = 'A rather long string of English text, an error message \ |
使用这种写法时, 每行开始处的空白字符无法在编译时被安全跳过; 而反斜划线后面的空白字符也常常会导致棘手的错误; 尽管多数脚本引擎支持这种写法, 但它并非 ECMAScript 的标准规范.
应当使用这种写法:
1 | var myString = 'A rather long string of English text, an error message ' + |
Array 和 Object 字面量
- 可以使用
使用 Array
和 Object
字面量而不是 Array
和 Object
构造器.
Array
的构造函数很容易因为传参不当而造成错误.
1 | // 长度为3. |
基于上述原因, 当有人重构代码将传入参数由两个变为一个时, 数组就可能变成非预期的长度.
为了避免出现这种奇怪的情况, 总是使用更加可读的数组字面量来声明数组.
1 | var a = [x1, x2, x3]; |
虽然 Object
构造器并不存在类似问题, 但鉴于可读性和一致性考虑, 最好也使用字面量来声明.
不要使用:
1 | var o = new Object(); |
应当写成:
1 | var o = {}; |
修改内置对象的原型
- 不要这样做
尝试修改内建对象, 诸如 Object.prototype
或者 Array.prototype
的行为应该被严格禁止. 修改其他内建对象, 比如 Function.prototype
虽然没有那么危险, 但在生产环境中依然会带来调试问题, 应当尽量避免.
IE下的条件注释
- 不要使用
不要像这样写:
1 | var f = function () { |
条件注释会干扰自动化工具的使用, 因为它们会在运行时改变 JavaScript 的语义树.