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_varregexVar.

变量后可以选择加后缀来标识其用途,如果处于合理的伪命名空间中,也可以不用:

  • 通用 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 )属性和方法不需要下划线开头, 和公共变量保持一致.

想要查阅更多关于 privateprotected 的资料, 可以阅读本指南的 可见性 部分.

方法和函数参数

可缺省函数参数以 opt_ 开头.

如果函数拥有不定参数, 那么应该添加一个名为 var_args 的参数作为最后一个参数. 如果不想在代码中使用 var_args , 也可以使用 arguments 伪数组.

可缺省参数和不定参数可以使用 @param 标记. 尽管是否这样做对解释器来说都可以接受, 但都使用是推荐的做法.

属性访问器 (Getters 和 Setters)

EcmaScript 5 不推荐使用对象属性的 getterssetters.
然而, 如果你已经使用了它们, 请务必注意不要让 getters 去改变显著的对象属性状态.

1
2
3
4
/**
* 错误 -- 不要这样做.
*/

var foo = { get next() { return this.nextId++; } };

函数访问器

也不对函数属性的 getterssetters 做要求. 然而, 如果已经使用来它们, 请将 gettersgetFoo() 的形式命名, 而 setters 则必须命名成 setFoo(value) 的形式. (对布尔变量类型的 getters , isFoo() 形式的命名也可以接受, 而且这样同样也更加自然.)

命名空间

JavaScript 自身并没有对包和命名空间的支持.

全局变量的冲突非常难以调试, 而且当涉及多个项目集成时, 经常造成棘手的难题. 为了减少公共分享 JS 代码时可能出现的冲突问题, 我们应当遵循下列规范.

  • 在全局代码中使用伪命名空间

在全局作用域中, 始终将一个和库或项目关联的前缀标识名作为伪命名空间来使用.
如果你正在开发的项目名为”Porject Sloth”, 则一个合理的伪命名空间应该是 sloth.*.

代码如下:

1
2
3
4
5
var sloth = {};

sloth.sleep = function() {
...
};

一些类似 the Closure Library 和 Dojo toolkit 这样的 JS 库会提供一些高阶方法来声明命名空间.
使用统一的方法来声明命名空间.

代码如下:

1
2
3
4
5
goog.provide('sloth');

sloth.sleep = function() {
/* ... */
};
  • 尊重命名空间的所有权

当选择在一个子命名空间下进行开发, 请确保父命名空间的所有者知道你正在进行的工作. 如果你正在开发一个为 sloth 创建 hats 的项目, 请确保开发 Sloth 的团队 了解你使用了 sloth.hats 作为命名空间.

  • 对外部代码和内部代码使用不同的命名空间

“外部代码”指那些独立于你的代码体系可以独立编译的代码. 外部代码和内部代码的命名空间应该保持严格的隔离. 如果你使用来一个外部库, 其对象都处于 foo.hats. 命名空间下, 你的内部代码就不该定义在 foo.hats. 下定义任何东西, 因为当其他团队试图定义新的对象时就可能会与之发生冲突.

不要像这样写:

1
2
3
4
5
6
7
8
9
foo.require('foo.hats');

/**
* 错误 -- 不要像这样写.
* @constructor
* @extends {foo.hats.RoundHat}
*/

foo.hats.BowlerHat = function() {
};

如果你需要在外部命名空间中定义新的 API, 那你应该显式地导出公共 API 函数, 并且仅仅只导出这些函数. 当你的内部代码需要调用这些 API 时, 可以直接通过内部命名来调用, 这是为了保持一致性, 并且可以让编译器能更好地进行优化.

1
2
3
4
5
6
7
8
9
10
11
12
13
foo.provide('googleyhats.BowlerHat');

foo.require('foo.hats');

/**
* @constructor
* @extends {foo.hats.RoundHat}
*/

googleyhats.BowlerHat = function() {
/* ... */
};

goog.exportSymbol('foo.hats.BowlerHat', googleyhats.BowlerHat);
  • 使用别名引用长名以增强可读性

在局部作用域中可以使用别名来引用完整包名能够增强可读性.
局部作用域中别名的命名应和完整包名的最后一部分相匹配.

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @constructor
*/

some.long.namespace.MyClass = function() {
};

/**
* @param {some.long.namespace.MyClass} a
*/

some.long.namespace.MyClass.staticHelper = function(a) {
...
};

myapp.main = function() {
var MyClass = some.long.namespace.MyClass;
var staticHelper = some.long.namespace.MyClass.staticHelper;
staticHelper(new MyClass());
};

不要在局部作用域中为命名空间创建别名.
仅仅当使用 goog.scope 时才能为命名空间创建别名.

不要像这样写:

1
2
3
4
myapp.main = function() {
var namespace = some.long.namespace;
namespace.MyClass.staticHelper(new namespace.MyClass());
};

避免访问一个别名的属性, 除非它是枚举类型.

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** @enum {string} */
some.long.namespace.Fruit = {
APPLE: 'a',
BANANA: 'b'
};

myapp.main = function() {
var Fruit = some.long.namespace.Fruit;
switch (fruit) {
case Fruit.APPLE:
...
case Fruit.BANANA:
...
}
};

不要像这样写:

1
2
3
4
myapp.main = function() {
var MyClass = some.long.namespace.MyClass;
MyClass.staticHelper(null);
};

千万不要在全局作用域中使用别名. 仅当处于函数块内才考虑使用它们.

  • 文件命名

文件名应该全部使用小写字母, 避免在某些大小写敏感的平台造成混乱. 文件名应以.js作为后缀, 且文件名中不应该包含 - 或 以外的标点符号(优先使用 - ,接下来考虑使用 ).

自定义 toString() 方法

  • 应该总能调用成功, 且没有副作用.

你可以通过自定义 toString() 方法来控制你的对象属性如何转化成字符串. 这没有什么问题, 但你需要确保你的方法做到 (1) 总是能调用成功且 (2) 不会造成副作用. 如果你的方法不能满足上述条件, 很容易会造成严重问题. 例如, 如果 toString() 调用一个包含 assert 的方法, 当调用失败时 assert 会尝试输出失败的对象名, 结果又会调用 toString().

延迟初始化

  • 没问题

并不是总能在声明变量时初始化变量. 延迟初始化没有什么问题.

明确作用域

  • 始终明确作用域

任何时候都需要明确作用域 - 这样能够提高代码的可移植性和清晰度. 例如, 不应该依赖作用域链中的 window 对象. 有时你会希望在别的应用调用你的函数, 此时使用的 window 对象并非之前的窗口对象.

代码格式

精神上遵从 C++ 格式规范, 做一些额外的补充:

  • 大括号

因为会隐式地插入分号, 所以无论括号是否在一行内闭合, 东欧应当将花括号和前面的代码放在一行中, 如下面的代码所示:

1
2
3
4
5
if (something) {
// ...
} else {
// ...
}
  • 数组和对象初始化

如果放在一行比较合适, 单行的数组和对象初始化可以放在一行内完成:

1
2
var arr = [1, 2, 3];  // 前后皆无空格.
var obj = {a: 1, b: 2, c: 3}; // 前后皆无空格.

多行数组和对象的初始化应当缩进两个空格, 花括号单独成行, 类似代码块.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 对象初始化.
var inset = {
top: 10,
right: 20,
bottom: 15,
left: 12
};

// 数组初始化.
this.rows_ = [
'"Slartibartfast" <fjordmaster@magrathea.com>',
'"Zaphod Beeblebrox" <theprez@universe.gov>',
'"Ford Prefect" <ford@theguide.com>',
'"Arthur Dent" <has.no.tea@gmail.com>',
'"Marvin the Paranoid Android" <marv@googlemail.com>',
'the.mice@magrathea.com'
];

// 用于方法调用.
goog.dom.createDom(goog.dom.TagName.DIV, {
id: 'foo',
className: 'some-css-class',
style: 'display:none'
}, 'Hello, world!');

比较长的标识名或数值会使得对齐的初始化列表出现问题, 因此总是优先使用不对齐的初始化方式. 如下例:

1
2
3
4
5
CORRECT_Object.prototype = {
a: 0,
b: 1,
lengthyName: 2
};

不要这样写:

1
2
3
4
5
WRONG_Object.prototype = {
a : 0,
b : 1,
lengthyName: 2
};
  • 函数参数

尽可能让所有的函数参数处于同一行, 但如果一行内宽度超过了 80 列的限制, 应当以更可读的方式换行处理. 为了节约空间, 你可以尽可能保持行宽接近 80 , 如果为来更好的可都性, 甚至可以让每个参数都单独一行. 既可以缩进4空格, 也可以与函数的圆括号对齐. 下面上参数换行处理的几种通用模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 4 空格缩进, 行宽保持在 80 左右. 适合函数名非常长的情况.
// 在函数重命名后无需重排缩进, 空间占用小.
goog.foo.bar.doThingThatIsVeryDifficultToExplain = function(
veryDescriptiveArgumentNumberOne, veryDescriptiveArgumentTwo,
tableModelEventHandlerProxy, artichokeDescriptorAdapterIterator)
{

// ...
};

// 4 空格缩进, 单参数独立成行. 适合函数名非常长的情况.
// 在函数重命名后无需重排, 强调来每个参数.
goog.foo.bar.doThingThatIsVeryDifficultToExplain = function(
veryDescriptiveArgumentNumberOne,
veryDescriptiveArgumentTwo,
tableModelEventHandlerProxy,
artichokeDescriptorAdapterIterator)
{

// ...
};

// 小括号对齐缩进, 行宽保持在 80 左右.
// 可视的分组参数, 空间占用小.
function foo(veryDescriptiveArgumentNumberOne, veryDescriptiveArgumentTwo,
tableModelEventHandlerProxy, artichokeDescriptorAdapterIterator)
{

// ...
}

// 小括号对齐, 单参数独立成行.
// 强调来每个单独的参数.
function bar(veryDescriptiveArgumentNumberOne,
veryDescriptiveArgumentTwo,
tableModelEventHandlerProxy,
artichokeDescriptorAdapterIterator)
{

// ...
}

如果函数自身是缩进的, 你可以自由选择到底是根据初始语句的缩进位置再缩进 4 格还是依据当前所处函数调用的起始位置来缩进. 下面几种缩进风格都是接受的.

1
2
3
4
5
6
7
8
if (veryLongFunctionNameA(
veryLongArgumentName) ||
veryLongFunctionNameB(
veryLongArgumentName)) {
veryLongFunctionNameC(veryLongFunctionNameD(
veryLongFunctioNameE(
veryLongFunctionNameF)));
}
  • 传递匿名函数

当传入参数的列表中包含匿名函数的声明时, 匿名函数的函数体应该相对于该函数调用从左侧缩进 2 格, 或者也可以选择从该匿名函数关键字的左侧处缩进 2 格. 这样可以让匿名函数的函数体更容易阅读(例如, 不需要把内容主题全部挤到屏幕右侧去).

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
prefix.something.reallyLongFunctionName('whatever', function(a1, a2) {
if (a1.equals(a2)) {
someOtherLongFunctionName(a1);
} else {
andNowForSomethingCompletelyDifferent(a2.parrot);
}
});

var names = prefix.something.myExcellentMapFunction(
verboselyNamedCollectionOfItems,
function(item) {
return item.name;
});
  • 配合 goog.scope 使用别名

当使用 the Closure Library 库时, 可以使用 goog.scope 来缩短对命名空间的引用.

每个文件内只能使用一次 goog.scope 调用, 并且始终放置在全局作用域中.

在打开 goog.scope(fuchuntion() { 代码块前, 需要保留且只保留一个空行, 上接 goog.provide 语句, goog.require 语句, 或者高层级的注释. 该代码块必须在文件最后一行处关闭, 即把 }); 放在文件末行处. 在代码块闭合处附加注释 // goog.scope, 并且将代码块与注释用分号和两个空格分隔.

类似 C++ 命名空间, 不要在 goog.scope 内缩进, 直接顶格开始.

只使用那些不会给其他对象使用的别名(例如多数构造函数, 枚举变量或者命名空间).

不要像这样写(参看更下面的代码来了解如何对构造函数使用别名):

1
2
3
4
5
// goog.scope 内
var Button = goog.ui.Button;

Button = function() { ... };
/*...*/

别名应该和全局下调用的最后一个属性名相一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
goog.provide('my.module.SomeType');

goog.require('goog.dom');
goog.require('goog.ui.Button');

goog.scope(function() {
var Button = goog.ui.Button;
var dom = goog.dom;

// Alias new types after the constructor declaration.
my.module.SomeType = function() { ... };
var SomeType = my.module.SomeType;

// Declare methods on the prototype as usual:
SomeType.prototype.findButton = function() {
// Button as aliased above.
this.button = new Button(dom.getElement('my-button'));
};
...
}); // goog.scope
  • 换行缩进

除了对数组字面量, 对象字面量和匿名函数外, 换行都应该和上方同级表达式左对齐, 或则从父级表达式左侧处缩进 4 格(这里的父级和同级指的是括号嵌套的层级).

代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
someWonderfulHtml = '' +
getEvenMoreHtml(someReallyInterestingValues, moreValues,
evenMoreParams, 'a duck', true, 72,
slightlyMoreMonkeys(0xfff)) +
'';

thisIsAVeryLongVariableName =
hereIsAnEvenLongerOtherFunctionNameThatWillNotFitOnPrevLine();

thisIsAVeryLongVariableName = siblingOne + siblingTwo + siblingThree +
siblingFour + siblingFive + siblingSix + siblingSeven +
moreSiblingExpressions + allAtTheSameIndentationLevel;

thisIsAVeryLongVariableName = operandOne + operandTwo + operandThree +
operandFour + operandFive * (
aNestedChildExpression + shouldBeIndentedMore);

someValue = this.foo(
shortArg,
'非常非常非常非常长的字符串参数, 实际上这种情况经常出现.',
shorty2,
this.bar());

if (searchableCollection(allYourStuff).contains(theStuffYouWant) &&
!ambientNotification.isActive() && (client.isAmbientSupported() ||
client.alwaysTryAmbientAnyways())) {
ambientNotification.activate();
}
  • 空行

对逻辑相关的一段代码开新行来写, 如下例所示:

1
2
3
4
5
6
7
doSomethingTo(x);
doSomethingElseTo(x);
andThen(x);

nowDoSomethingWith(y);

andNowWith(z);
  • 二元和三元操作符

总是将操作符保留在前行. 其他方面的换行和缩进规则遵循指南中提到的其他规范即可. 这种事先约定的方式可以无需顾虑分号隐式插入造成的问题. 实际上, 分号不可能被隐式地插入到二元操作符之前, 但所有的代码都应该遵循一致的风格规范.

1
2
3
4
5
6
7
8
9
10
var x = a ? b : c;  // All on one line if it will fit.

// Indentation +4 is OK.
var y = a ?
longButSimpleOperandB : longButSimpleOperandC;

// Indenting to the line position of the first operand is also OK.
var z = a ?
moreComplicatedB :
moreComplicatedC;

包括点操作符.

1
2
3
var x = foo.bar().
doSomething().
doSomethingElse();
  • 小括号

仅用于需要处. 成对使用, 一般情况下只在语法和语义要求处用括号.

在一元操作符(如 delete, typeofvoid)或关键字(如 return, throw, case, innew)后永远不要使用括号.

  • 字符串

单引号’比双引号”好.

基于一致性的考虑, 优先使用单引号(‘)而不是双引号(“). 在创建包含 HTML 的字符串中这非常有用.

1
var msg = 'This is some HTML';
  • 可见性(privateprotected 字段)

鼓励, 使用 JSDoc 中的 @private@protected 来标注.

我们推荐使用 JSDoc 中的 @private@protected 来标注类, 函数, 属性的可见级别.

使用编译器标识 –jscomp_warning=visibility 可以开启对代码违背可见性的警告. 可以参考 Closure Compiler Warnings.

私有全局变量和函数( @private )只能在同一文件中访问.

标注了 @private 的构造函数只能在同一文件中实例化或者访问公共静态属性. 私有构造函数还可以在同一文件的任意位置使用 instanceof 操作符使用公共静态属性.

全局变量, 全局函数和全局构造函数任何时候都不要用 @protected 标注.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 文件 1.
// AA_PrivateClass_ 和 AA_init_ 可以访问因为它们是全局的并且在同一文件中.

/**
* @private
* @constructor
*/

AA_PrivateClass_ = function() {
};

/** @private */
function AA_init_() {
return new AA_PrivateClass_();
}

AA_init_();

标注了 @private 的私有属性在同一文件以及它所属类的静态方法和实例方法中都可以访问. 但处于不同文件的所属类的子类则无法访问和重载该属性.

标注了 @protected 的受限属性在同一文件, 以及包含这条属性的类及其任意子类的任意静态方法和实例方法中, 都可以访问.

注意这些语义与 C++ 和 Java 有所区别, 它们允许在同一文件中访问私有和受限属性, 而非限制在同一个类和类继承中. 另外, 不像 C++, 私有属性不允许被子类重载.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 文件 1.

/** @constructor */
AA_PublicClass = function() {
/** @private */
this.privateProp_ = 2;

/** @protected */
this.protectedProp = 4;
};

/** @private */
AA_PublicClass.staticPrivateProp_ = 1;

/** @protected */
AA_PublicClass.staticProtectedProp = 31;

/** @private */
AA_PublicClass.prototype.privateMethod_ = function() {};

/** @protected */
AA_PublicClass.prototype.protectedMethod = function() {};

// 文件 2.

/**
* @return {number} The number of ducks we've arranged in a row.
*/

AA_PublicClass.prototype.method = function() {
// 合法访问这两条属性.
return this.privateProp_ + AA_PublicClass.staticPrivateProp_;
};

// 文件 3.

/**
* @constructor
* @extends {AA_PublicClass}
*/

AA_SubClass = function() {
// 合法访问受限静态属性.
AA_PublicClass.staticProtectedProp = this.method();
};
goog.inherits(AA_SubClass, AA_PublicClass);

/**
* @return {number} The number of ducks we've arranged in a row.
*/

AA_SubClass.prototype.method = function() {
// 合法访问静态实例属性.
return this.protectedProp;
};

请注意在 JS 中, 类型(比如 AA_PrivateClass_)和类型的构造函数的可见性是没有区别的. 并没有办法让一个类型共有而令其构造函数为私有(因为很容易对构造函数使用别名从而破坏私有检查).