JavaScript 代码风格指南
这份指南列出了编写 JavaScript 时需要遵守的规范, 指明哪些应该提倡, 哪些应该避免.
本文基于 google 的规范翻译整理(JavaScript 是许多 Google 开源项目使用的主要客户端脚本语言).
(2)
JavaScript 代码风格规范(1)
命名规范
- 一般情况下,按下列命名规范即可:
- 函数命名:
functionNamesLikeThis
(首字母小写驼峰式) - 变量命名:
variableNamesLikeThis
(首字母小写驼峰式) - 类命名:
ClassNamesLikeThis
(首字母大写驼峰式) - 枚举量命名:
EnumNamesLikeThis
(首字母大写驼峰式) - 方法命名:
methodNamesLikeThis
(首字母小写驼峰式) - 常量命名:
CONSTANT_VALUES_LIKE_THIS
(全字母大写下划线式) - 命名空间命名:
foo.namespaceNamesLikeThis.bar
(首字母小写驼峰式) - 文件命名:
filenameslikethis.js
(全小写)
译者附注:
在 google 规范的基础上, 我们还可以作如下调整:
变量命名在局部作用域使用 variable_names_like_this
全小写下划线分隔
变量命名在模块作用域使用 variableNamesLikeThis
首字母小写驼峰式
变量命名的基本规范:
- 变量名只使用 a~z, A~Z, 0~9, 下划线, 中划线和 $ 符号.
- 变量名不能数字开头.
应该在布尔量前加上前缀来区别于普通变量
- 通用
bool_var
boolVar
- 请求
do_verb
doVerb
- 包含
has_var
hasVar
- 状态
is_var
isVar
- 绑定事件 ‘on_var’ ‘onVar’
对正则变量加上前缀 regex
, 如 regex_var
和 regexVar
.
变量后可以选择加后缀来标识其用途,如果处于合理的伪命名空间中,也可以不用:
- 通用
var_str
varStr
- 标识
var_id
varId
- HTML
var_html
varHtml
- 消息
var_msg
varMsg
- 名字
var_name
varName
- 文本
var_text
varText
- 类型
var_type
var_Type
- 图片
var_img
varImg
- 声音
var_snd
varSnd
- 列表
var_list
varList
- 数据
var_data
varData
- 映射
var_map
varMap
属性和方法
- 私有(
private
)属性和方法命名应该以下划线 _ 开头; - 受限(
protected
)属性和方法不需要下划线开头, 和公共变量保持一致.
想要查阅更多关于 private
和 protected
的资料, 可以阅读本指南的 可见性 部分.
方法和函数参数
可缺省函数参数以 opt_
开头.
如果函数拥有不定参数, 那么应该添加一个名为 var_args
的参数作为最后一个参数. 如果不想在代码中使用 var_args
, 也可以使用 arguments
伪数组.
可缺省参数和不定参数可以使用 @param
标记. 尽管是否这样做对解释器来说都可以接受, 但都使用是推荐的做法.
属性访问器 (Getters 和 Setters)
EcmaScript 5 不推荐使用对象属性的 getters
和 setters
.
然而, 如果你已经使用了它们, 请务必注意不要让 getters
去改变显著的对象属性状态.
1 | /** |
函数访问器
也不对函数属性的 getters
和 setters
做要求. 然而, 如果已经使用来它们, 请将 getters
按 getFoo()
的形式命名, 而 setters
则必须命名成 setFoo(value)
的形式. (对布尔变量类型的 getters
, isFoo()
形式的命名也可以接受, 而且这样同样也更加自然.)
命名空间
JavaScript 自身并没有对包和命名空间的支持.
全局变量的冲突非常难以调试, 而且当涉及多个项目集成时, 经常造成棘手的难题. 为了减少公共分享 JS 代码时可能出现的冲突问题, 我们应当遵循下列规范.
- 在全局代码中使用伪命名空间
在全局作用域中, 始终将一个和库或项目关联的前缀标识名作为伪命名空间来使用.
如果你正在开发的项目名为”Porject Sloth”, 则一个合理的伪命名空间应该是 sloth.*.
代码如下:
1 | var sloth = {}; |
一些类似 the Closure Library 和 Dojo toolkit 这样的 JS 库会提供一些高阶方法来声明命名空间.
使用统一的方法来声明命名空间.
代码如下:
1 | goog.provide('sloth'); |
- 尊重命名空间的所有权
当选择在一个子命名空间下进行开发, 请确保父命名空间的所有者知道你正在进行的工作. 如果你正在开发一个为 sloth 创建 hats 的项目, 请确保开发 Sloth 的团队 了解你使用了 sloth.hats 作为命名空间.
- 对外部代码和内部代码使用不同的命名空间
“外部代码”指那些独立于你的代码体系可以独立编译的代码. 外部代码和内部代码的命名空间应该保持严格的隔离. 如果你使用来一个外部库, 其对象都处于 foo.hats. 命名空间下, 你的内部代码就不该定义在 foo.hats. 下定义任何东西, 因为当其他团队试图定义新的对象时就可能会与之发生冲突.
不要像这样写:
1 | foo.require('foo.hats'); |
如果你需要在外部命名空间中定义新的 API, 那你应该显式地导出公共 API 函数, 并且仅仅只导出这些函数. 当你的内部代码需要调用这些 API 时, 可以直接通过内部命名来调用, 这是为了保持一致性, 并且可以让编译器能更好地进行优化.
1 | foo.provide('googleyhats.BowlerHat'); |
- 使用别名引用长名以增强可读性
在局部作用域中可以使用别名来引用完整包名能够增强可读性.
局部作用域中别名的命名应和完整包名的最后一部分相匹配.
代码如下:
1 | /** |
不要在局部作用域中为命名空间创建别名.
仅仅当使用 goog.scope
时才能为命名空间创建别名.
不要像这样写:
1 | myapp.main = function() { |
避免访问一个别名的属性, 除非它是枚举类型.
代码如下:
1 | /** @enum {string} */ |
不要像这样写:
1 | myapp.main = function() { |
千万不要在全局作用域中使用别名. 仅当处于函数块内才考虑使用它们.
- 文件命名
文件名应该全部使用小写字母, 避免在某些大小写敏感的平台造成混乱. 文件名应以.js作为后缀, 且文件名中不应该包含 - 或 以外的标点符号(优先使用 - ,接下来考虑使用 ).
自定义 toString() 方法
- 应该总能调用成功, 且没有副作用.
你可以通过自定义 toString() 方法来控制你的对象属性如何转化成字符串. 这没有什么问题, 但你需要确保你的方法做到 (1) 总是能调用成功且 (2) 不会造成副作用. 如果你的方法不能满足上述条件, 很容易会造成严重问题. 例如, 如果 toString() 调用一个包含 assert 的方法, 当调用失败时 assert 会尝试输出失败的对象名, 结果又会调用 toString().
延迟初始化
- 没问题
并不是总能在声明变量时初始化变量. 延迟初始化没有什么问题.
明确作用域
- 始终明确作用域
任何时候都需要明确作用域 - 这样能够提高代码的可移植性和清晰度. 例如, 不应该依赖作用域链中的 window 对象. 有时你会希望在别的应用调用你的函数, 此时使用的 window 对象并非之前的窗口对象.
代码格式
精神上遵从 C++ 格式规范, 做一些额外的补充:
- 大括号
因为会隐式地插入分号, 所以无论括号是否在一行内闭合, 东欧应当将花括号和前面的代码放在一行中, 如下面的代码所示:
1 | if (something) { |
- 数组和对象初始化
如果放在一行比较合适, 单行的数组和对象初始化可以放在一行内完成:
1 | var arr = [1, 2, 3]; // 前后皆无空格. |
多行数组和对象的初始化应当缩进两个空格, 花括号单独成行, 类似代码块.
1 | // 对象初始化. |
比较长的标识名或数值会使得对齐的初始化列表出现问题, 因此总是优先使用不对齐的初始化方式. 如下例:
1 | CORRECT_Object.prototype = { |
不要这样写:
1 | WRONG_Object.prototype = { |
- 函数参数
尽可能让所有的函数参数处于同一行, 但如果一行内宽度超过了 80 列的限制, 应当以更可读的方式换行处理. 为了节约空间, 你可以尽可能保持行宽接近 80 , 如果为来更好的可都性, 甚至可以让每个参数都单独一行. 既可以缩进4空格, 也可以与函数的圆括号对齐. 下面上参数换行处理的几种通用模式:
1 | // 4 空格缩进, 行宽保持在 80 左右. 适合函数名非常长的情况. |
如果函数自身是缩进的, 你可以自由选择到底是根据初始语句的缩进位置再缩进 4 格还是依据当前所处函数调用的起始位置来缩进. 下面几种缩进风格都是接受的.
1 | if (veryLongFunctionNameA( |
- 传递匿名函数
当传入参数的列表中包含匿名函数的声明时, 匿名函数的函数体应该相对于该函数调用从左侧缩进 2 格, 或者也可以选择从该匿名函数关键字的左侧处缩进 2 格. 这样可以让匿名函数的函数体更容易阅读(例如, 不需要把内容主题全部挤到屏幕右侧去).
示例代码如下:
1 | prefix.something.reallyLongFunctionName('whatever', function(a1, a2) { |
- 配合 goog.scope 使用别名
当使用 the Closure Library 库时, 可以使用 goog.scope
来缩短对命名空间的引用.
每个文件内只能使用一次 goog.scope
调用, 并且始终放置在全局作用域中.
在打开 goog.scope(fuchuntion() {
代码块前, 需要保留且只保留一个空行, 上接 goog.provide
语句, goog.require
语句, 或者高层级的注释. 该代码块必须在文件最后一行处关闭, 即把 }); 放在文件末行处. 在代码块闭合处附加注释 // goog.scope
, 并且将代码块与注释用分号和两个空格分隔.
类似 C++ 命名空间, 不要在 goog.scope
内缩进, 直接顶格开始.
只使用那些不会给其他对象使用的别名(例如多数构造函数, 枚举变量或者命名空间).
不要像这样写(参看更下面的代码来了解如何对构造函数使用别名):
1 | // goog.scope 内 |
别名应该和全局下调用的最后一个属性名相一致:
1 | goog.provide('my.module.SomeType'); |
- 换行缩进
除了对数组字面量, 对象字面量和匿名函数外, 换行都应该和上方同级表达式左对齐, 或则从父级表达式左侧处缩进 4 格(这里的父级和同级指的是括号嵌套的层级).
代码示例如下:
1 | someWonderfulHtml = '' + |
- 空行
对逻辑相关的一段代码开新行来写, 如下例所示:
1 | doSomethingTo(x); |
- 二元和三元操作符
总是将操作符保留在前行. 其他方面的换行和缩进规则遵循指南中提到的其他规范即可. 这种事先约定的方式可以无需顾虑分号隐式插入造成的问题. 实际上, 分号不可能被隐式地插入到二元操作符之前, 但所有的代码都应该遵循一致的风格规范.
1 | var x = a ? b : c; // All on one line if it will fit. |
包括点操作符.
1 | var x = foo.bar(). |
- 小括号
仅用于需要处. 成对使用, 一般情况下只在语法和语义要求处用括号.
在一元操作符(如 delete
, typeof
和 void
)或关键字(如 return
, throw
, case
, in
或 new
)后永远不要使用括号.
- 字符串
单引号’比双引号”好.
基于一致性的考虑, 优先使用单引号(‘)而不是双引号(“). 在创建包含 HTML 的字符串中这非常有用.
1 | var msg = 'This is some HTML'; |
- 可见性(
private
和protected
字段)
鼓励, 使用 JSDoc 中的 @private
和 @protected
来标注.
我们推荐使用 JSDoc 中的 @private
和 @protected
来标注类, 函数, 属性的可见级别.
使用编译器标识 –jscomp_warning=visibility 可以开启对代码违背可见性的警告. 可以参考 Closure Compiler Warnings.
私有全局变量和函数( @private
)只能在同一文件中访问.
标注了 @private
的构造函数只能在同一文件中实例化或者访问公共静态属性. 私有构造函数还可以在同一文件的任意位置使用 instanceof
操作符使用公共静态属性.
全局变量, 全局函数和全局构造函数任何时候都不要用 @protected
标注.
1 | // 文件 1. |
标注了 @private
的私有属性在同一文件以及它所属类的静态方法和实例方法中都可以访问. 但处于不同文件的所属类的子类则无法访问和重载该属性.
标注了 @protected
的受限属性在同一文件, 以及包含这条属性的类及其任意子类的任意静态方法和实例方法中, 都可以访问.
注意这些语义与 C++ 和 Java 有所区别, 它们允许在同一文件中访问私有和受限属性, 而非限制在同一个类和类继承中. 另外, 不像 C++, 私有属性不允许被子类重载.
1 | // 文件 1. |
请注意在 JS 中, 类型(比如 AA_PrivateClass_
)和类型的构造函数的可见性是没有区别的. 并没有办法让一个类型共有而令其构造函数为私有(因为很容易对构造函数使用别名从而破坏私有检查).