eslint-config-pay
v0.2.0
Published
Pingan Cloud Front End Team Javascript Style Guide
Downloads
3
Readme
平安云前端团队 JavaScript 代码规范 (2017 草案)
1. 介绍
出于提高代码可读性,可维护性,减少难以调试的 bug 的原因,制订本规范。
本规范基于以下几个原则:
- 提高代码可读性
- 提高代码可维护性
- 提高开发效率
- 提高团队风格统一性
- 减少 runtime bug,将错误暴露在编译时
本文档用于规范平安云前端团队的 JavaScript 代码风格。只有完全符合本文档规范的 JavaScript 代码源文件,才能被认为是平安云风格的代码。
本规范处于起草阶段,将分为几个不同的版本:
- ES6 版,用于 Node.js 服务端开发及其他可以使用 ES6 语法的情况(Babel,typescript)
- React 版,基于 ES6 版,针对 React 开发的扩展
本文为 ES6 版内容。
版本:v0.1.0
1.1 术语说明
本文档中,除非另有声明:
注释
均为开发注释,而非文档注释。在 JavaScript 中,也应当使用 JSDoc 而非文档注释,因为 JSDoc 的/** ... */
内容既可以被人阅读,也可以被计算机识别。- 本文档中使用
必须 (Must)
,不允许 (Must not)
,应当 (Should)
,不应当 (Should not)
,和可以 (May)
等表示要求等级的词语时,符合 RFC2119 标准中的规定。
- 必须:表示该要求为硬性要求,在任何时候都应该遵守。在中文语境下可能的替换词有:一定,要求
- 不允许:表示该要求为硬性要求,在任何时候都应该遵守。在中文语境下可能的替换词有:不能,禁止
- 应当:表示在特定情况下可能有合理原因来忽略这条要求,但开发人员需要完全了解该要求的含义,并慎重决定是否选择其他风格。在中文语境下可能的替换词有:推荐,应该
- 不应当:表示在特定情况下可能有合理原因来忽略这条要求,但开发人员需要完全了解该要求的含义,并慎重决定是否选择其他风格,并在同一项目中保持统一。在中文语境下可能的替换词有:不推荐,不应该,避免
- 可以:表示这是一个可选要求,仅出于团队风格统一的考虑来进行制定。在实践中可能有其他标准,开发人员需要了解该要求的含义后作出选择,并在同一项目中保持统一。
其他术语说明会在文档中出现。
1.2 本文档中的代码
本文档中的代码为示例代码,仅作为演示目的,不一定符合本文档所规定的平安云风格。
2. 代码源文件
2.1 命名
命名约定有助于为一目了然地找到内容提供一个统一的方式,在项目和团队中保持统一性是非常必要的。
命名约定应该为代码的检索和沟通提供方便。
2.1.1 通用
代码文件名必须为全小写字符,使用下划线 (_) 或连字符 (-) 进行分隔。文件名称必须使用正确拼写的英文命名,不允许使用未被广泛接受的缩写和拼音。文件名必须使用 .js
作为扩展名。
2.1.2 React
代码文件名必须采用大驼峰命名法,即每个单词首字母大写。其他均与通用规范相同。
2.1.3 Angular 1.x
代码文件名必须使用 Angular 风格命名,即 feature.type.js
的风格,例如 my-feature.controller.js
和 my-feature.service.js
。其他均与通用规范相同。
2.1.4 Angular 2
代码文件可以使用 .ts
或 .tsx
作为扩展名,仅在使用 typescript
作为编译工具时使用。其他均与 Angular 1.x 规范相同。
2.2 编码
源文件必须统一使用 UTF-8
编码。
3 源文件结构
一个源文件应当由以下部分顺序组成
- 许可信息或版权信息(如果有的话)
- require 语句
- 实际开发代码
每部分中间必须有 1-2 个空行用于分隔。
4. 格式
术语说明:块级结构表示一个类 (class), 函数 (function), 方法 (method), 或大括号分隔开的代码块。注意,在本文 5.2 数组字面量 和 5.3 对象字面量中的数组或对象字面量在部分情况下需要被当做块级结构。
4.1 大括号
4.1.1 大括号在所有控制结构中使用
在所有控制结构代码 (例如 if
, else
, for
, do
, while
, try
, catch
等) 中必须使用大括号。即使在该控制结构中只有一行语句。
// 不好的例子
if (someVeryLongCondition())
doSomething();
for (let i = 0; i < foo.length; i++) bar(foo[i]);
// 好的例子
if (someVeryLongCondition()) {
doSomething();
}
for (let i = 0; i < foo.length; i++) {
bar(foo[i]);
}
这是为了编写可读性强的代码
4.1.2 非空代码块:K&R 风格
大括号必须采用 Brian W·Kernighan 和 Dennis M·Ritchie 在 C 程序设计语言 一书中所使用的风格,通常叫做 K&R 风格,也叫做埃及括号风格(因为括号风格和下图中的手的位置类似)。
- 左大括号前不换行
- 左大括号后换行
- 右大括号前换行
- 右大括号如果代表完成了一段语句,或者表示方法和类的结束,必须在其后换行。如果其后还跟随着
else
,catch
,while
或者逗号,分号及右圆括号,则不在其后换行。
// 不好的例子
class SomeClass
{ // 左大括号前不应该换行
constructor() {
} // 空代码块不应该换行
someMethod(foo) {
if (foo === someCondition) {
try {
// 有可能会报错的方法
something();
} // 右括号后如果跟随 catch 不换行
catch (err) { recover(); } // 左大括号后应该换行
} somethingElse(); // 右大括号结束后应该换行
}
otherMethod(array) {
return array.map((element) => {
const newElement = element + 1;
return element * 2;
} // 右大括号跟随右圆括号不应该换行
)
}
}
// 好的例子
class SomeClass {
constructor() {}
someMethod(foo) {
if (foo === someCondition) {
try {
// 有可能会报错的方法
something();
} catch (err) {
recover();
}
}
somethingElse();
}
otherMethod(array) {
return array.map((element) => {
const newElement = element + 1;
return element * 2;
});
}
}
4.1.3 空代码块
空代码块的左大括号可以立刻接上右大括号来关闭,中间没有空格,换行或其他字符。除非这是一个多代码块的控制结构 (例如 if
/else
结构或者 try
/catch
/finally
结构)。
// 不好的例子
function doNothing() {
}
if (condition) {
doSomething();
} else if (otherCondition) {} else {
doSomethingElse();
}
try {
something();
} catch (e) {}
// 好的例子
function doNothing() {}
if (condition) {
doSomething();
} else if (otherCondition) {
// ...
} else {
doSomethingElse();
}
try {
something();
} catch (e) {
// ...
}
在实践中请尽量避免空控制结构,必要时可以用注释填充。
4.2 块级缩进
任何时候开启了一个新块级代码时,其缩进应当增加两个空格。当这块代码结束后,其缩进应当退回到其上一级代码所使用的缩进。
// 不好的例子
function foo() {
∙∙∙∙let name;
}
function bar() {
∙let name;
}
// 好的例子
function baz() {
∙∙let name = 'name';
··if (someValue == 'someValue') {
····for (item of list) {
······item.name = name;
····}
··}
}
4.2.1 数组字面量
任一数组字面量可以表示为块级代码。表示为块级代码时同样要应用缩进。
// 好的例子
const a = [
0, 1, 2,
];
const b = [0, 1, 2];
someMethod(foo, [
0,
1,
2,
], bar);
4.2.2 对象字面量
任一对象字面量可以表示为块级代码。表示为块级代码时同样要应用缩进。
// 好的例子
const a = {
a: 0, b: 1
};
const b = {a: 0, b: 1};
someMethod(foo, {
a: 0,
b: 1,
}, bar);
4.2.3 类字面量
类的字面量 (无论是声明还是表达式) 都必须作为块级代码进行缩进。在类方法后面不能加逗号,在类的声明结尾大括号后面也不能加分号,而表达式声明的类则必须在结尾大括号后加分号。
// 好的例子
class Foo {
constructor() {
this.x = 42;
}
method() {
return this.x;
}
}
Foo.Empty = class {};
foo.Bar = class extends Foo {
/** @override */
method() {
return super.method() / 2;
}
};
/** @interface */
class Frobnicator {
/** @param {string} message */
frobnicate(message) {}
}
4.2.4 函数表达式
当声明一个匿名函数时,函数体应当缩进两个空格。
// 好的例子
prefix.something.reallyLongFunctionName('whatever', (a1, a2) => {
// 缩进 +2 空格
if (a1.equals(a2)) {
someOtherLongFunctionName(a1);
} else {
andNowForSomethingCompletelyDifferent(a2.parrot);
}
});
使用超过 2 个链式调用时,应该把.放在一行的开头。
这样能很明显区分出这是前一个方法的方法调用,而不是一个新的语句。
// 好的例子
some.reallyLongFunctionCall(arg1, arg2, arg3)
.thatsWrapped()
.then((result) => {
// 缩进 +2 空格
if (result) {
result.use();
}
});
链式调用存在多级关系的时候,可以应用额外缩进来区分。
// 不好的例子
$('#items').find('.selected').highlight().end().find('.open').updateCount();
// 不好的例子
$('#items').
find('.selected').
highlight().
end().
find('.open').
updateCount();
// 好的例子
$('#items')
.find('.selected')
.highlight()
.end()
.find('.open')
.updateCount();
// 不好的例子
const leds = stage.selectAll('.led').data(data).enter().append('svg:svg').classed('led', true)
.attr('width', (radius + margin) * 2).append('svg:g')
.attr('transform', `translate(${radius + margin},${radius + margin})`)
.call(tron.led);
// 好的例子
const leds = stage.selectAll('.led')
.data(data)
.enter().append('svg:svg')
.classed('led', true)
.attr('width', (radius + margin) * 2)
.append('svg:g')
.attr('transform', `translate(${radius + margin},${radius + margin})`)
.call(tron.led);
// 好的例子
const leds = stage.selectAll('.led').data(data);
4.2.5 switch 语句
和其他块级控制结构一样,switch
结构内的代码块缩进必须增加两个空格。
在一个 switch
标签后,必须换行,同时,缩进必须增加两个空格,就像开启了一个代码块一样。如果必要(例如块级作用域需要),可以在 switch
标签后显式使用大括号来定义一个代码块。当前标签结束后,其后的标签语句必须回到上一级缩进,就像是关闭了一个代码块一样。
在上一个 switch
标签的 break
语句和下一个 case
语句之间可以加一个空行进行分隔。
// 好的例子
switch (animal) {
case Animal.BANDERSNATCH:
handleBandersnatch();
break;
case Animal.JABBERWOCK:
handleJabberwock();
break;
default:
throw new Error('Unknown animal');
}
4.3 语句
4.3.1 一行只能有一个语句
每条语句后面必须换行。
4.3.2 分号
4.3.2.1 分号结尾
每条语句必须用分号进行结尾,不允许依赖自动分号插入 (ASI) 机制。
大部分情况下,JavaScript 引擎可以确定一个分号应该在什么位置然后自动添加它,这被称为 自动分号插入 (ASI)。
然而这个特性是很有争议的,有可能会引起一些错误。例如:
return
{
name: 'pay'
}
这个看起来像是个 return
语句返回一个对象文本。然而,JavaScript 引擎将代码解释成:
return;
{
name: "pay";
}
一个分号插入到 return 语句之后,导致 (块中的标签文本) 下面的代码不可达。
另一个例子:
var globalCounter = { }
(function () {
var n = 0
globalCounter.increment = function () {
return ++n
}
})()
在这个例子中,分号不会被插入到第一行末尾,导致一个运行时错误(因为一个空的对象被调用,犹如它是个函数)。如下:
var globalCounter = { }(function () {
var n = 0;
globalCounter.increment = function () {
return ++n;
}
})();
因此可以看到很多人写立即执行函数时,会在代码文件最前面加一个分号。
即使 ASI 允许在你的代码风格上提供更多的自由,不论你是否使用分号,它仍可以使你的代码表现的出乎意料。因此,最好是知道 ASI 什么时候插入分号,什么时候不插入分号。正如 Isaac Schlueter 曾经描述的那样:
一个
\n
字符总是一个语句的结尾 (像分号一样),除非是下面情况之一:
- 该语句有一个没有闭合的括号,数组或对象或其它某种方式,不是有效结束一个语句的方式。(比如,以
.
或,
结尾)- 该行是
--
或++
(在这种情况下它将减少或增加下一个标记。)- 它是个
for()
,while()
,do
,if()
或else
,没有{
- 下一行以
[
,(
,+
,*
,/
,-
,,
,.
或一些其他在单个表达式中两个标记之间的二元操作符
因此,在一行语句的结尾始终使用分号,并遵循之前所述的大括号规范。
4.3.2.1 额外分号
不允许使用额外的分号。
额外的分号通常是书写错误和对哪里需要使用分号的误解,会导致出现不必要的分号。虽然在技术上不是个错误,但阅读代码时会引起困惑。
// 不好的例子
var x = 5;;
function foo() {
// 函数声明结尾不需要使用分号,因为它不是一个表达式语句。
};
// 好的例子
var x = 5;
var foo = function() {
// 函数表达式需要使用分号
};
4.4 代码行宽度
每行代码的宽度不允许超过 100。任何超过该限制的代码行应该被折行,参考 4.5.
该规则有以下例外:
- 不可能被换行的代码行,例如一个很长的 URL 地址,或者一个命令行语句
- require 语句
- 很长的字符串
本规则的目的是尽可能让代码能在一个屏幕内显示完毕,而不需要拖动滚动条,提高可读性。同时,由于代码缩进的存在,该规则也有助于减少代码层级,提高可维护性,并提高代码复用程度。
4.5 换行
术语说明: 折行 (Line wrapping) 是指将一句表达式分隔到多行中。换行 (Line break) 是指在表达式结尾结束这行。
目前并没有一个通行的规范表明应该如何折行。通常会有几种合理的方式来折行同一段代码。
注意,尽管折行的目的是不超过代码行宽度的限制,但符合宽度限制要求的代码也可以被折行,取决于代码作者的判断。
很多时候,将一段代码提取为一个独立的方法,或者一个本地变量,有助于减少代码行宽度。不一定非要折行。
4.5.1 折行位置
由于无法准确描述对所有情况下的折行要求,因此这里只列出折行的原则:在高语法等级位置折行,并且:
- 当折行需要在操作符处时,在操作符后折行(点操作符除外)。
// 不好的例子
const someVeryLongVariableName = someVeryLongMethodName()
+ someVeryLongOtherVariableName;
// 好的例子
const someVeryLongVariableName = someVeryLongMethodName() +
someVeryLongOtherVariableName;
- 方法名和其后所跟随的圆括号之间不应该折行
- 逗号前不折行
// 不好的例子
this.foo = foo(firstArg, 1 +
someLongFunctionName());
// 好的例子
this.foo =
foo(
firstArg,
1 + someLongFunctionName());
在上面的例子里,语法级别从高到低是:赋值,外部函数调用,参数,加号,内部函数调用。
请注意,主要的原则是让代码更易读,并没有必要刻意让代码占据较少的行数。
4.5.2 折行缩进
代码折行后的缩进必须增加至少四个空格。除非这和代码块缩进规则冲突。
一般来说,如果在一条语句中有多个折行,由于语法级别的不同,可能会导致多个层级的折行。这时候对更深级别的折行采用更多缩进会更好。使用相同缩进的两行代码,应该在语法层级上是平行的。
if (someCondition() &&
(someOtherCondition() ||
theThirdCondition({
key: value,
}) &&
theForthValue == theForthCondition)) {
doSomething();
}
该例子仅作为演示,通常情况建议把那么多条件包装成一个方法进行调用。在这里的折行中,同一语法层级(&&)的缩进均为四个空格,第二层级(括号内的 || 逻辑符)又增加了四个空格,而调用的代码块参数只增加了两个空格。
4.6 空白
4.6.1 空行
一个空行应该出现在:
- 类或者对象字面量中连续定义的两个方法之间(注意:对于类或对象字面量中定义的属性之间可以没有空行,通常对于多个属性可以使用空行进行逻辑分组)。
- 方法内部,空行可以被用于逻辑分隔。在方法开头和结尾不允许有空行。
- 一个源文件的多个 require 语句之后
允许使用多个空行,但并不推荐。
4.6.2 空格
在代码中使用空格大体可以分为三类:行前,行后,和行中。行前空格的使用请参考缩进部分,行末不允许使用空格。
除去 JavaScript 语言本身要求使用空格,和本文档中其他地方要求外,一个空格仅在以下情况出现:
- 在任何保留字 (例如
if
,for
,catch
等) 和左圆括号 ((
) 之间。 - 在任何保留字 (例如
else
,catch
等) 和右大括号 (}
) 之间(仅在它们出现在一行时) - 在任何左大括号 (
{
) 前- 例外:函数的第一个参数或数组的第一个元素为对象字面量时,例如
foo({a: [{c: d}]})
- 例外:在模板语法中不能在左大括号前加空格,因为语法不允许:
abc${1 + 2}def
- 例外:函数的第一个参数或数组的第一个元素为对象字面量时,例如
- 在任何二元 (例如
+
,-
,=
等) 或三元操作符 (例如a ? b : c
) 两侧 - 在逗号 (
,
) 和分号 (;
) 之后,注意空格不能出现在这两个字符之前 - 在对象字面量中的冒号 (
:
) 后 - 在行尾注释的双斜杠 (
//
) 的两侧,双斜杠前的空格可以使用多个用于对齐 - 在 JSDoc 注释开头字符 (
/**
) 之后以及 JSDoc 结束字符 (*/
) 的两侧 - 在中文字符和数字英文字符之间
汉学家称这个空白字为「盘古之白」,因为它劈开了全角字和半角字之间的混沌。
另有研究显示,打字的时候不喜欢在中文和英文之间加空格的人,感情路都走得很辛苦,有七成的比例会在 34 岁的时候跟自己不爱的人结婚,而其余三成的人最后只能把遗产留给自己的猫。毕竟爱情跟书写都需要适时地留白。
4.6.3 对齐
术语说明: 对齐是指在代码中间加入多个空格使上下多行代码中的某个符号对齐,例如:
{
angular: 2013,
react: 2015,
vue: 2014,
}
// 或
const angular = 2013;
const react = 2015;
const vue = 2014;
// 或
import { React } from 'react';
import { MyComponent } from './MyComponent';
该格式是允许的,但不提倡。因为这在一定程度上提高了代码阅读的舒适度,然而降低了代码的可维护性。假如一个开发者想要修改其中一行,加入了一个更长的名字,那他就不得不修改周围所有对齐的行。这增加了无谓的工作量,而且会导致版本历史受到影响。
4.7 分组圆括号
在语法上可以省略,但为了增强代码可读性,或为了避免歧义而使用圆括号将一部分语句组合起来,以确保该部分语句的优先级,这样的行为是允许的,而且推荐这样做。但如果多位代码审阅人认为这里毫无必要增加括号时,应当省略。
在 delete
, typeof
, void
, return
, throw
, case
, in
或者 of
关键词后面,不允许将整个语句用圆括号括起来。
在显式类型声明时,变量必须用圆括号括起来,例如:/** @type {!Foo} */ (foo)
.
4.8 注释
本部分主要规范开发注释,文档注释在本文后面详述。
术语说明: 开发注释,指用于解释某一段代码的实际功效,开发时所遇到的问题和为什么这样实现或解决的注释。例如在某个循环中设置循环 20 次,此处应该有一个注释解释为什么要循环 20 次,避免谜之数字。 术语说明: 文档注释,指使用指定格式编写的方法说明,包括方法名,变量类型和说明等,用于让 IDE 或编辑器识别,在使用这个方法时能提供完善的代码补全和提示功能。
4.8.1 块级注释
块级注释缩进与所在代码块的缩进一致,可以使用 /* ... */
或 // ...
的风格。
对于采用 /* ... */
风格的多行注释,除第一行外的后几行,每行必须以 *
开头而且 *
符号必须和前一行对齐,在视觉上它们就比较明显,不混杂其他上下文。
参数名注释应当跟在参数后面,且只应当在函数名和参数变量名不足以描述该参数含义的情况下使用。
//
注释符号之后必须要有一个空格。
*
符号之后必须要有一个空格。
使用 /* ... */
风格时,首行和末行必须留空。
/*
* 可以这样注释
* 然后换行
* /
// 这样
// 也可以
/* 这样也是可以的 */
someFunction(obviousParam, true /* shouldRender */, 'hello' /* name */);
禁止 使用 JSDoc 风格 (/** ... */
) 的注释做开发注释,它们仅能被用作文档注释。
4.8.2 行级注释
行级注释必须与代码之间有至少 1 个空格,在注释符号 //
两侧必须要有空格。
使用注释时也要遵循代码列宽度的规范,对于过长的注释,请采用块级注释。
4.9 属性
4.9.1 点操作符
应当使用点操作符来访问对象属性。当属性名中有特殊字符时除外。
const luke = {
jedi: true,
age: 28,
};
// 不好的例子
const isJedi = luke['jedi'];
// 好的例子
const isJedi = luke.jedi;
4.9.2 方括号
当属性名是一个变量时,必须使用方括号来访问属性。
5. 语法
5.1 变量引用
5.1.1 尽量使用 const
应当使用 const
来定义所有引用变量;避免使用 var
。
原因是,这能够确保你的引用值不会被重新赋值,减少项目中的 bug,并提高代码的可读性。
// 不好的例子
var a = 1;
var b = 2;
// 好的例子
const a = 1;
const b = 2;
5.1.2 不要使用 var
如果必须要使用一个可以被重新赋值的引用,必须用 let
来代替 var
。
原因是,let 创建块级作用域的变量,而 var 创建的函数级作用域变量会造成变量提升。
// 不好的例子
var count = 1;
if (true) {
count += 1;
}
// 好的例子
let count = 1;
if (true) {
count += 1;
}
可能产生的问题举例: 参考 examples/no-var.js
。
5.1.3 最小作用域
变量应当在最接近它们被使用的地方被定义,而不是定义在块级作用域的开头。这样能确保它们影响最小的作用域范围。
需要注意的是,let 和 const 都是块级作用域的。
// const 和 let 仅在它们被定义的代码块中存在。
{
let a = 1;
const b = 1;
}
console.log(a); // ReferenceError
console.log(b); // ReferenceError
5.1.4 变量类型声明
可以使用 JSDoc 中的变量类型注解到定义变量那一行的上面,或同一行内。
const /** !Array<number> */ data = [];
/** @type {!Array<number>} */
const data = [];
在很多情况下,IDE 或编辑器可以推断一个模板化参数的类型但不包括参数的类型。当初始化一个字面量但是没有给出任何参数的时候,例如创建了一个空数组,空对象,空 Map 或 Set,或者变量在闭包中被修改了。这些情况下 IDE 或编辑器都无法了解变量的类型,从而无法提供完善的语法错误检查及代码提示功能。这个时候,使用 type 注解就很有帮助。
5.2 对象
5.2.1 使用字面量创建新对象
创建对象时,应当使用字面量创建对象。
由于二者在性能上没有区别,而使用字面量能减少代码文件的大小,且让团队风格统一。因此字面量创建对象是一个更好的方法。
// 不好的例子
const item = new Object();
// 好的例子
const item = {};
5.2.2 在属性结尾使用逗号
定义一个属性后,应当在其后添加逗号,无论是否是最后一个属性。
只有在最后一个属性不换行的情况下可以省略。
// 好的例子
const obj = {
a: 1,
b: 2,
};
const obj = { a: 1, b: 2 };
这么做的原因是,是否在最后一个属性后添加逗号在语法上没有任何区别,但加逗号能提高 git diff 的可读性。例如当开发者以后要加入另一个属性的时候:
// 不好的例子 (不加结尾逗号)
const hero = {
firstName: 'Florence',
- lastName: 'Nightingale'
+ lastName: 'Nightingale',
+ inventorOf: ['coxcomb chart', 'modern nursing']
};
// 好的例子 (加结尾逗号)
const hero = {
firstName: 'Florence',
lastName: 'Nightingale',
+ inventorOf: ['coxcomb chart', 'modern nursing'],
};
5.2.3 动态属性名
对象的动态属性名应当使用计算属性名的方式定义
这能够让对象的所有属性在同一个地方被定义,减少维护工作量
function getKey(k) {
return `a key named ${k}`;
}
// 不好的例子
const obj = {
id: 5,
name: 'San Francisco',
};
obj[getKey('enabled')] = true;
// 好的例子
const obj = {
id: 5,
name: 'San Francisco',
[getKey('enabled')]: true,
};
5.2.4 使用对象方法简写
在对象中定义方法时,必须使用简写。
这是 ES6 提供的语法糖,这个语法可以更简洁地定义复杂对象字面量。
// 不好的例子
const atom = {
value: 1,
addValue: function (value) {
return atom.value + value;
},
};
// 好的例子
const atom = {
value: 1,
addValue(value) {
return atom.value + value;
},
};
5.2.5 使用对象属性简写
在对象中定义属性时,如果属性名和变量名一致,必须使用简写。
这是 ES6 提供的语法糖,这个语法可以更简洁地定义复杂对象字面量。
const lukeSkywalker = 'Luke Skywalker';
// 不好的例子
const obj = {
lukeSkywalker: lukeSkywalker,
};
// 好的例子
const obj = {
lukeSkywalker,
};
5.2.6 简写属性放置在开头
简写过的属性必须放置在一起,可以是对象定义的开头或者结尾。
方便看出在哪里使用了简写
const anakinSkywalker = 'Anakin Skywalker';
const lukeSkywalker = 'Luke Skywalker';
// 不好的例子
const obj = {
episodeOne: 1,
twoJediWalkIntoACantina: 2,
lukeSkywalker,
episodeThree: 3,
mayTheFourth: 4,
anakinSkywalker,
};
// 好的例子
const obj = {
lukeSkywalker,
anakinSkywalker,
episodeOne: 1,
twoJediWalkIntoACantina: 2,
episodeThree: 3,
mayTheFourth: 4,
};
5.2.7 仅在必要时给属性名加上引号
不应当给常规的对象属性名加引号。
在性能上,大多数 JS 引擎对无引号的属性名读取有更高的性能。而且代码编辑器会对此有语法高亮。
// 不好的例子
const bad = {
'foo': 3,
'bar': 4,
'data-blah': 5,
};
// 好的例子
const good = {
foo: 3,
bar: 4,
'data-blah': 5,
};
该规则有一些例外情况:
例如,在 IE8 中(即 ES3 标准的 JS 引擎),如果使用关键字(例如 if
)作为属性名时,会抛出一个异常(ES5 之后没有此限制)。
使用非标识符的字符,比如一个属性中间有一个空格,像 "one two" 这样。
另外有一个例子:
const obj = {
1e2: 1,
100: 2,
};
这可能乍一看起来是没有问题的,但在 ECMAScript 5 严格模式下,这段代码实际上会抛出一个语法错误。因为 1e2
和 100
在作为属性名使用之前被强制转换为字符串。String(1e2)
和 String(100)
正好是等于 "100"
,造成了“严格模式下对象字面量中不允许重复的数据属性”的错误。这样的问题调试起来非常棘手,所以一些人喜欢要求所有的属性名都要有引号。
5.2.8 不直接调用对象原型方法
不应当直接调用对象的原型方法。例如:hasOwnProperty
, propertyIsEnumerable
, and isPrototypeOf
.
因为对象的原型方法可能会被对象的属性覆盖,例如设置了
{ hasOwnProperty: false }
。或者该对象通过Object.create(null)
方法创建
// 不好的例子
console.log(object.hasOwnProperty(key));
// 好的例子
console.log(Object.prototype.hasOwnProperty.call(object, key));
// 最佳用法
const has = Object.prototype.hasOwnProperty; // 将方法缓存
console.log(has.call(object, key));
可能产生的问题举例: 参考 examples/no-prototype-builtins.js
。
5.2.9 使用 spread 操作符而不是 Object.assign 进行浅复制
在浅复制对象时应当使用 spread 操作符。
使用 spread 操作符可以很方便地让指定属性不被复制
// 非常糟糕的例子
const original = { a: 1, b: 2 };
const copy = Object.assign(original, { c: 3 }); // 这样会改变 original
delete copy.a; // 删掉 a 属性
// 不好的例子
const original = { a: 1, b: 2 };
const copy = Object.assign({}, original, { c: 3 }); // copy => { a: 1, b: 2, c: 3 }
// 好的例子
const original = { a: 1, b: 2 };
const copy = { ...original, c: 3 }; // copy => { a: 1, b: 2, c: 3 }
const { a, ...noA } = copy; // noA => { b: 2, c: 3 }
5.3 数组
5.3.1 使用字面量创建数组
必须使用字面量来创建一个数组。
原因是,当 new Array() 的参数只有一个且是个整数时,会返回该整数所代表长度的数组。 例如 new Array(5) 返回 [, , , ,] 而不是 [5]
参见例子 examples/no-array-constructor.js
// 不好的例子
const items = new Array();
// 好的例子
const items = [];
5.3.2 使用 Array#push 来向数组末尾添加元素
应当使用 Array#push 向数组末尾添加元素,这样代码更简单明了,提高易读性。
const someStack = [];
// 不好的例子
someStack[someStack.length] = 'abracadabra';
// 好的例子
someStack.push('abracadabra');
5.3.3 使用 spread 操作符复制数组
// 糟糕的例子
const len = items.length;
const itemsCopy = [];
let i;
for (i = 0; i < len; i += 1) {
itemsCopy[i] = items[i];
}
// 不好的例子
const itemsCopy = Array.prototype.slice.call(foo);
// 好的例子
const itemsCopy = [...items];
5.3.4 使用 Array#from 将一个类数组对象变为数组
const foo = document.querySelectorAll('.foo');
const nodes = Array.from(foo);
5.3.5 在数组方法的回调函数中使用 return
如果回调方法只有一行,可以使用简写而非 return
确保不会忘记应该 return 一个新元素
// 好的例子
[1, 2, 3].map((x) => {
const y = x + 1;
return x * y;
});
// 好的例子
[1, 2, 3].map(x => x + 1);
// 不好的例子
const flat = {};
[[0, 1], [2, 3], [4, 5]].reduce((memo, item, index) => {
const flatten = memo.concat(item);
flat[index] = flatten;
});
// 好的例子
const flat = {};
[[0, 1], [2, 3], [4, 5]].reduce((memo, item, index) => {
const flatten = memo.concat(item);
flat[index] = flatten;
return flatten;
});
// 不好的例子
inbox.filter((msg) => {
const { subject, author } = msg;
if (subject === 'Mockingbird') {
return author === 'Harper Lee';
} else {
return false;
}
});
// 好的例子
inbox.filter((msg) => {
const { subject, author } = msg;
if (subject === 'Mockingbird') {
return author === 'Harper Lee';
}
// else 内部 return 可以直接不写 else
return false;
});
5.4 类
5.4.1 构造方法
对于比较具体的类来说,构造方法可以省略。在使用构造方法时,构造方法必须在类中是第一个定义的方法。继承类的构造方法必须在设置任何属性或调用 this
前调用 super()
。接口不允许定义构造方法。
5.4.2 class 关键词
应该使用 Class 来创建一个类,而不是使用原型方式。不应该使用老式的 class 定义方法,例如 /** @constructor */
。
5.4.3 extend 关键词
应该 使用 extends 来继承一个类,而不是使用原型链或者 apply 方法。
使用语法糖,提高代码可读性
5.4.4 静态方法
静态方法应当只在其被定义的类上调用,而非继承其的类上。
静态方法不应该在类的实例上进行调用。
class Base {
static foo() {}
}
class Sub extends Base {
static bar() {}
}
const cls = new Sub();
// 不好的例子
Sub.foo();
cls.bar();
// 好的例子
Base.foo();
Sub.bar();
5.4.5 原型方法
禁止直接定义或修改原型方法。
例外:在某些框架中可能有必要使用原型方法的,可以使用。
5.4.6 变量和属性访问
5.4.6.1 访问方法
可以不使用访问方法,直接修改对象的属性。
class People {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 这是一个访问 age 的方法
getAge() {
return this.age;
}
setAge(age) {
this.age = age;
}
}
const john = new People('John', 22);
// 可以直接修改属性
john.age += 1;
5.4.6.2 Getters 和 Setters
禁止使用 ES6 中提供的 getter 和 setter 属性。它们有潜在的意外,而且在编译器中的支持度有限。
例外:当使用数据绑定框架时(例如 Angular),可以谨慎的使用 getter 和 setter 方法。请注意,由于编译器的支持度有限,在使用它们时,务必显式定义它们(在类或对象字面量中使用 get foo()
和 set foo(value)
),如果无法在类或对象字面量中定义,可以使用 Object.defineProperties
,但禁止使用 Object.defineProperty
,因为后者可能导致属性更名。使用 getter 时,禁止更改对象的状态。
5.4.6.3 布尔型变量
对于 boolean 型的属性,使用 isVal 或者 hasVal 作为访问方法的名称,而不是 getVal。
5.4.7 复写 toString 方法
可以复写 toString 方法,但必须确保没有任何副作用。
例如,在 toString 里调用其他方法时要小心,在特殊条件下可能会导致死循环。
5.4.8 重名属性
禁止在类中定义重名的方法和属性。这语法上不是个错误,但实际上后定义的会覆盖之前定义的,导致一些难以调试的 bug。
5.5 函数
5.5.1 函数声明
不应当使用函数声明方式来创建函数。直接声明函数会使函数声明提前,导致它在函数定义之前就可以被引用。这会影响可读性和可维护性。
如果一个函数方法太大,或者太复杂,使得它影响了文件其余部分代码的理解,应当将其独立出来作为一个模块。
5.5.2 包裹立即执行函数
立即执行函数理应被当做一个执行语句块,一个独立的单元,因此应当用括号包裹起来。通常来说,使用立即执行函数是为了控制作用域范围。鉴于 ES6 已经拥有了块级作用域,应该已经没有太多的机会来使用立即执行函数了。
5.5.3 不在非函数块中定义函数
不允许在非函数块作用域中(例如 if
, while
等)定义一个函数。应当先把函数分配给一个变量。
5.5.4 参数
5.5.4.1 arguments
不允许将一个参数命名为 arguments
。因为这会覆盖 arguments
对象。
5.5.4.2 不要用 arguments
不应当使用 arguments
对象,应当使用 ...
操作符替代。
...
操作符能够显式的选择哪些参数想被选取出来,而且是个数组,并不像 arguments
是个数组型的对象。
// 不好的例子
function concatenateAll() {
const args = Array.prototype.slice.call(arguments);
return args.join('');
}
// 好的例子
function concatenateAll(...args) {
return args.join('');
}
5.5.4.3 不重新分配参数引用
在任何时候都不允许重新分配参数引用。
5.5.4.4 使用默认参数语法
当参数有默认值时,应当使用默认参数语法,而非修改参数本身。
所有可选参数必须有默认值,即使是 undefined
。
// 非常糟糕的例子
function handleThings(opts) {
// 首先,这未被了 5.5.4.3 中不允许重分配参数的规则
// 其次,如果 opts 是个 falsy 数据,仍会被置为默认的对象
opts = opts || {};
// ...
}
// 不好的例子
function handleThings(opts) {
if (opts === void 0) {
opts = {};
}
// ...
}
// 好的例子
function handleThings(opts = {}) {
// ...
}
5.5.4.5 避免默认参数包含计算
因为降低了代码的可读性,而且会产生副作用。因此不应当使用计算得来的默认参数值。
var b = 1;
// 不好的例子
function count(a = b++) {
console.log(a);
}
count(); // 1
count(); // 2
count(3); // 3
count(); // 3
5.5.4.6 可选参数置后
有默认值的参数必须放置在参数最后。
5.5.4.7 使用解构
使用可选参数时应当考虑是否可以用解构的方式放置参数。解构能创建一个可读性更高的 API,而且不需要记住参数的顺序。
5.5.4.8 参数不可变原则
禁止修改参数。
如果参数以引用的形式传递进来,修改参数会导致函数外部作用域被污染。例如:
const a = { x: 1, y: 2 };
function plusOne(obj) {
obj.x += 1;
return obj;
}
const b = plusOne(a); // a:{ x: 2, y: 2 }
上述代码中,调用函数的人显然是想获得一个新的对象,然而旧的对象同样被污染了。
// 不好的例子
function f1(obj) {
obj.key = 1;
}
// 好的例子
function f2(obj) {
const { key, ...restObj } = obj;
restObj.key = 1;
}
5.5.4.9 参数命名
不允许使用重名的参数。
5.5.5 空格
在 function
和函数名之间必须要有一个空格,如果没有函数名,其与括号之间也必须要有一个空格。
在圆括号和大括号之间必须要有一个空格。
这其实是遵循了 4.6.2 中对空格的要求。只是额外再提一下。
5.5.6 ...操作符调用
可以使用 ... 操作符将参数作为数组调用一个方法。
const dateStr = '2017-4-1';
const dateArr = dateStr.split('-'); // [2017, 4, 1]
// 不好的例子
const date = new Date(dateArr[0], dateArr[1], dateArr[2]);
// 不好的例子
const date = new (Function.prototype.bind.apply(Date, [null, dateArr[0], dateArr[1], dateArr[2]]));
// 好的例子
const date = new Date(...dateArr);
5.5.7 箭头函数
5.5.7.1 回调使用箭头函数
当必须使用一个函数表达式时,例如作为回调函数传递给方法,应当使用箭头函数。
箭头的两侧必须有一个空格。
箭头函数能避免 this 上下文的困扰,并且很简洁。
如果有必须使用独立 this 上下文的情况,应当自行定义函数后再进行传递。
// 不好的例子
[1, 2, 3].map(function (x) {
const y = x + 1;
return x * y;
});
// 好的例子
[1, 2, 3].map((x) => {
const y = x + 1;
return x * y;
});
5.5.7.2 省略括号
当函数体只有一条语句时,应当省略括号并隐式 return
结果。否则,保留括号并使用 return
。
如果该语句超过一行,用括号将其包裹起来。
当参数只有一个时,参数的括号应当省略。
[1, 2, 3].map(x => x * x);
[1, 2, 3].map(number => `A string containing the ${number}.`);
[1, 2, 3].map((number) => {
const nextNumber = number + 1;
return `A string containing the ${nextNumber}.`;
});
[1, 2, 3].map((number, index) => ({
[index]: number,
}));
['get', 'post', 'put'].map(httpMethod => (
Object.prototype.hasOwnProperty.call(
httpMagicObjectWithAVeryLongName,
httpMethod,
)
));
5.5.7.3 避免与比较符号混用
当可能与比较符号 (>=
, <=
) 混用时,应尽量避免这种情况。
// 不好的例子
const itemHeight = item => item.height > 256 ? item.largeSize : item.smallSize;
// 不好的例子
const itemHeight = (item) => item.height > 256 ? item.largeSize : item.smallSize;
// 好的例子
const itemHeight = item => (item.height > 256 ? item.largeSize : item.smallSize);
// 好的例子
const itemHeight = (item) => {
const { height, largeSize, smallSize } = item;
return height > 256 ? largeSize : smallSize;
};
5.5.8 生成器函数
可以使用生成器函数。
当使用生成器函数时,*
符号必须与 function
之间没有空格,与函数名或原括号之间有一个空格。
5.6 字符串
5.6.1 引号
必须使用单引号表示一个字符串。
因为 HTML 中标签属性应该使用双引号,使用单引号表示字符串可以避免转义。
不允许使用 \
符号来表示多行文本。
5.6.2 模板字符串
必须使用模板字符串来表示复杂的经过计算获得的字符串,或多行文本。
function arithmetic(a, b) {
return `Here is a table of arithmetic operations:
${a} + ${b} = ${a + b}
${a} - ${b} = ${a - b}
${a} * ${b} = ${a * b}
${a} / ${b} = ${a / b}`;
}
5.6.3 转义
仅在必要时才使用转义。 例如:
// 不好的例子
const foo = '\'this\' \i\s \"quoted\"';
// 好的例子
const foo = '\'this\' is "quoted"';
const foo = `my name is '${name}'`;
5.6.4 eval()
不允许使用 eval()
方法。
5.7 数字
数字可以使用前缀来表示进制。当使用 0x
, 0o
, 0b
分别表示十六进制,八进制和二进制时,必须使用小写字符。使用数字时不允许以 0
开头除非其后跟着 x
, o
, 或 b
。
5.8 控制结构
5.8.1 循环
在 ES6 中,新增加了一些循环结构。通常来说,应当避免使用 for
进行循环。因为这在一定程度上增加了对外部变量污染的风险。
在需要循环时,应当首先选择数组自带的迭代器方法。例如 map()
/ every()
/ filter()
/ find()
/ findIndex()
/ reduce()
/ some()
等方法进行循环。
const numbers = [1, 2, 3, 4, 5];
// 不好的例子
let sum = 0;
for (let num of numbers) {
sum += num;
}
sum === 15;
// 好的例子
let sum = 0;
numbers.forEach(num => sum += num);
sum === 15;
// 最好的例子
const sum = numbers.reduce((total, num) => total + num, 0);
sum === 15;
// 不好的例子
const increasedByOne = [];
for (let i = 0; i < numbers.length; i++) {
increasedByOne.push(numbers[i] + 1);
}
// 好的例子
const increasedByOne = [];
numbers.forEach(num => increasedByOne.push(num + 1));
// 最好的例子
const increasedByOne = numbers.map(num => num + 1);
在必须使用 for
循环时,禁止使用 for-in
循环方法,只能使用 for
或者 for-of
语句进行循环。因为 for-in
会对对象的原型属性进行循环。
// 糟糕的例子
let count = 0;
const selected = this.state.selected;
for (const wanted in selected) {
// 没有对属性是否是对象的自有属性进行判断
if (selected[wanted] === true) {
count++;
}
}
// 不好的例子
let count = 0;
const selected = this.state.selected;
for (const wanted in selected) {
if (selected.hasOwnProperty(wanted)) {
if (selected[wanted] === true) {
count++;
}
}
}
// 必须使用 for 来循环一个对象时使用 Object.keys() 将其变为数组
// 这比上一个好点,但仍然是一个不好的例子
let count = 0;
const selected = this.state.selected;
for (const wanted of Object.keys(selected)) {
if (selected[wanted] === true) {
count++;
}
}
// 好的例子
const count = Object.values(this.state.selected).filter(x => x === true).length;
5.8.2 异常
异常在程序中是非常重要的部分,在每一个可能出现问题(网络、IO 等)的地方都应该对异常进行处理。
在抛出异常时,必须抛出 Error 或其子类异常,不允许抛出字符串或者对象字面量作为异常。在创建异常对象时必须使用 new
来构造异常。
在原生的错误类型不足时,应当使用自定义的异常来附加更多信息。
5.8.2.1 空捕获块
捕获异常的代码块留空一般都是错误的,因为很少捕获异常后不做任何处理。如果真有这样的情况,在代码块中必须用注释加以说明。
try {
return handleNumericResponse(response);
} catch (ok) {
// 这里出错没关系,因为数字或字符都可以被正常处理
}
return handleTextResponse(response);
5.8.3 Switch 语句
术语说明: 在 switch
语句的大括号中有一组或多组语句,每一组语句包含一个或者多个 switch
标签(case Foo:
或者 default:
),其后跟随一或多条语句。
5.8.3.1 Fall-through 注释
在 switch
语句块中,每组语句应当使用 break
, return
或 throw
结束。否则,应当用注释说明此组语句将有可能运行到下一组语句中。
switch (input) {
case 1:
case 2:
prepareOneOrTwo();
// fall through
case 3:
handleOneTwoOrThree();
break;
default:
handleLargeNumber(input);
}
5.8.3.2 default
每个 switch
语句块中必须要有一个 default
标签,即使其中没有任何代码。
5.8.3.3 case
禁止使用重复的 case
标签。
5.9 this 指针
只允许在类的构造函数和方法,或定义在类的构造函数或方法中的箭头函数里使用 this
指针。
5.10 模块
5.10.1 import/export
应当使用 import/export
来导入模块。
禁止导出一个导入语句。
// 不好的例子
// filename es6.js
export { es6 as default } from './PinganStyleGuide';
// 好的例子
// filename es6.js
import { es6 } from './PinganStyleGuide';
export default es6;
5.10.2 通配符
禁止使用通配符导入模块。
因为这要求对应模块必须有一个默认导出项。
5.10.3 多次导入
禁止从一个文件中多次导入属性。
// 不好的例子
import foo from 'foo';
import { named1, named2 } from 'foo';
// 好的例子
import foo, { named1, named2 } from 'foo';
// 好的例子
import foo, {
named1,
named2,
} from 'foo';
5.10.4 变量
禁止导出一个可被修改的变量。
// 不好的例子
let foo = 3;
export { foo };
// 好的例子
const foo = 3;
export { foo };
5.10.5 默认导出
当只有一个导出项时,应当使用默认导出。
// 不好的例子
export function foo() {}
// 好的例子
export default function foo() {}
5.10.5 导入提升
必须将导入语句放在文件开头。
因为 import 语句本身会被提升,放在开头可以提高可维护性。
5.11 比较
5.11.1 ===/==
必须使用 ===
和 !==
来进行比较。如非必要,不应当使用 ==
和 !=
。
5.11.2 ToBoolean
在 if 语句中,条件部分可以使用变量本身的字面量进行存在性的判断。
- Objects 会得到 true
- Undefined 会得到 false
- Null 会得到 false
- Booleans 会得到 其本身的值
- Numbers 在为 +0, -0, or NaN 时会得到 false,否则都是 true
- Strings 在为空字符串时会得到 false,否则都是 true
if ([0] && []) {
// true
// 数组(即使是空数组)是一个对象,会被认为是 true
}
对于数字或字符串,必须显式地进行比较。
5.11.3 三元表达式
禁止嵌套三元表达式。如非必要不应当折行。
// 不好的例子
const foo = maybe1 > maybe2
? "bar"
: value1 > value2 ? "baz" : null;
// 好一点的例子
const maybeNull = value1 > value2 ? 'baz' : null;
const foo = maybe1 > maybe2
? 'bar'
: maybeNull;
// 好的例子
const maybeNull = value1 > value2 ? 'baz' : null;
const foo = maybe1 > maybe2 ? 'bar' : maybeNull;
应当避免不必要的三元表达式。
// 不好的例子
const foo = a ? a : b;
const bar = c ? true : false;
const baz = c ? false : true;
// 好的例子
const foo = a || b;
const bar = !!c;
const baz = !c;
5.12 jQuery
5.12.1 前缀
在 jQuery 对象变量的变量名前应当加美元符。
这样容易分出哪些是 DOM 元素,哪些是 jQuery 对象。
5.12.2 缓存 jQuery 查询
jQuery 查询到的变量应当被缓存,避免性能消耗。
// 不好的例子
function setSidebar() {
$('.sidebar').hide();
// ...
$('.sidebar').css({
'background-color': 'pink',
});
}
// 好的例子
function setSidebar() {
const $sidebar = $('.sidebar');
$sidebar.hide();
// ...
$sidebar.css({
'background-color': 'pink',
});
}
5.13 不允许的语法
5.13.1 with
不允许使用 with
关键词。
with
关键词在 ES5 的严格模式中已经被禁止。它会使代码变得难以理解。
5.13.2 动态代码执行
不允许使用 eval
或 Function(...string)
构造器来执行动态代码。这些功能有潜在危险,而且在某些环境下不兼容。
5.13.3 自动分号插入
不允许依赖自动分号插入机制,手动插入所有分号。
5.13.4 非标准方法
不允许使用非标准的功能。包括已经被移除的旧功能(WeakMap),或未被标准化的新功能(例如 TC39 草案,提议或 stage)。
编写在特定环境下执行的代码除外(例如 Chrome 插件或 Nodejs 程序)。
6. 命名
6.1 通用规范
变量名必须只使用 ASCII 字母和数字表示。在特定情况下,下划线(第三方库 underscore 或 lodash)和美元符(Angular 框架)可以用于变量名。
变量名应当尽可能具有自描述性。不用为了节省空间来缩短变量名,让别人能一眼看懂这个变量的意义比空间美感重要很多。 不应当使用不被广泛认可的缩写。不允许删节单词中的字母。
// 不好的例子
const n // 无意义字母.
const nErr // 含糊的缩写
const nCompConns // 含糊的缩写
const wgcConnections // 只有项目内部人员可能知道 wgc 的意思
const pcReader // pc 可以指代很多东西
const cstmrId // cstmr 是什么单词删节出来的缩写???
const kSecondsPerDay // 不要使用匈牙利表示法
// 好的例子
const priceCountReader; // 无缩写
const numErrors; // num 大家都了解是什么意思
const numDNSConnections; // 大部分人都了解 DNS 缩写的意义
6.2 特定规范
6.2.1 包名
包名应当使用小驼峰命名法。
6.2.2 类名
类名、接口名应当使用大驼峰命名法。
类名必须使用英语名词或名词短语,例如 Request
, VisibilityMode
等。接口名可以使用形容词或形容词短语,例如 Readable
。
6.2.3 方法名
方法名必须使用小驼峰命名法。
方法名必须使用动词或动词短语,例如 sendMessage
。
6.2.4 常量
常量必须使用全大写字母,用下划线分隔单词。
6.2.5 非常量属性名
非常量属性名必须使用小驼峰命名法。
6.2.6 参数名
参数名必须使用小驼峰命名法。匿名函数中不影响代码阅读的情况下可以使用单个字母表示参数。
const filteredEquipmentList = equipmentList.filter((e) => e.id > 100);
6.2.7 变量名
变量名必须使用小驼峰命名法。
6.2.8 单复数
变量应当正确使用单复数形式。
单个对象应当使用单数形式。数组应当使用复数形式,但如果数组以 List 这类表示多个元素的词结尾时,仍应使用单数。
不可数名词不允许强加复数形式。
7. JSDoc
JSDoc 在所有类、方法上都应当被使用。
7.1 常规格式
基本的 JSDoc 注释格式如下所示
/**
* 多行 JSDoc 注释在这里编写,
* 且应当正常折行
* @param {number} arg 对参数的解释
*/
function doSomething(arg) { … }
或单行例子:
/** @const @private {!Foo} 短小的 JSDoc. */
this.foo_ = foo;
很多编辑器和 IDE 都会对 JSDoc 注释进行解析并做一些校验和优化工作。因此,JSDoc 注释必须严格按照格式编写。
VSCode 下对于有 JSDoc 注释提供的提示:
7.2 Markdown
JSDoc 应当使用 Markdown 格式撰写,必要时可以包括 HTML。
请注意很多工具会自动认为 JSDoc 由 Markdown 编写(例如 JsDossier),将纯文本解析为 Markdown 格式。所以如果你写了下面这样的注释:
/**
* 基于以下三个参数计算
* 发送的包数量
* 接收到的包数量
* 时间戳
*/
会被解析为:基于以下三个参数计算 发送的包数量 接收到的包数量 时间戳
。
所以,用 Markdown 列表来写:
/**
* 基于以下三个参数计算
* - 发送的包数量
* - 接收到的包数量
* - 时间戳
*/
7.3 JSDoc 标签
大多数标签必须独占一行,标签位于该行的开头。
// 不好的例子
/**
* "param" 标签必须位于一行开头,而且不能多个混在一起
* @param {number} left @param {number} right
*/
function add(left, right) { ... }
// 好的例子
/**
* "param" 标签必须位于一行开头,而且不能多个混在一起
* @param {number} left 左数
* @param {number} right 右数
*/
function add(left, right) { ... }
简单的标签,即不需要任何附加信息的标签(例如 @private
, @const
, @final
, @export
等),可以混在同一行内。
7.4 折行
对于超长的描述,可以折行,折行后缩进增加 4 个空格。
7.5 文件级注释
一个文件可以有一个顶级的 @file 标签注释,用于描述文件所含代码的功能,作者和版本。在 @file 注释中折行不应当增加缩进。
/**
* @file 文件基本描述,使用场景和版权信息
* 以及它的依赖
* @author liyifeng796
*/
7.6 类的 JSDoc 注释
类,接口必须用 JSDoc 添加文档注释。描述说明必须能够给读者足够的信息了解应该在何时如何使用该类或接口,以及其他注意事项。构造函数可以不使用 @constructor
标签,除非这个类被用于声明一个接口或继承一个其他类。
7.7 方法 JSDoc 注释
参数和返回的类型必须在文档注释中标注。如果在方法中使用了 this
,应当用 @this
指明 this
的引用。方法、参数和返回的描述可以省略,但类型不允许省略。
方法描述文档应当以第三人称、非主观语气撰写。如果方法覆写了类所继承的超类的方法,其必须包括一个 @override
标签。覆写方法如果修改了参数和返回,则必须包括所有 @param
和 @return
标签,但如果没有修改,则应当省略。
/** 这是一个类 */
class SomeClass extends SomeBaseClass {
/**
* 在 MyClass 对象上进行一些操作并返回一些东西
* @param {!MyClass} obj 一个对象包含了一些需要的信息,并且由于某些原因需要在这里
* 多做一些解释,使得注释超过了一行
* @param {!OtherClass} obviousOtherClass
* @return {boolean} 这个方法成功了没有
*/
someMethod(obj, obviousOtherClass) { ... }
/** @override */
overriddenMethod(param) { ... }
}
/**
* 全局函数应用相同的规则,这里返回一个数组
* @param {TYPE} arg
* @return {!Array<TYPE>}
* @template TYPE
*/
function makeArray(arg) { ... }
匿名函数不需要编写 JSDoc,不过必要情况下,参数类型可以通过行内注释予以说明。
promise.then(
(/** !Array<number|string> */ items) => {
doSomethingWith(items);
return /** @type {string} */ (items[0]);
});
7.8 属性注释
类中的属性可以加以文档注释,也可以省略。省略的前提是属性名本身已经提供了足够的描述性。
7.9 类型注释
变量类型注释在 @param
, @return
, @this
和 @type
标签后必须跟随,在 @const
, @export
后可选。类型标记必须在大括号内。
7.9.1 可空变量
使用 !
和 ?
分别表示不可为空和可为空的变量。基础类型 (undefined
, string
, number
, boolean
, symbol
, 以及 function(...): ...
) 还有对象字面量 ({foo: string, bar: number}
) 默认为非空变量,因此不要显式的加 !
到这些变量的类型标记中。
7.9.2 类型转换
为避免 IDE 或编辑器有时候不太准确的问题,可以在变量前加类型转换,并将变量用圆括号包裹。格式如下:
/** @type {number} */ (x)
7.9.3 模板参数
必须注明模板参数。
// 不好的例子
/** @type {!Object} */ var users;
/** @type {!Array} */ var books;
/** @type {!Promise} */ var response;
// 好的例子
/** @type {!Object<string, User>} */ const users;
/** @type {!Array<string>} */ const books;
/** @type {!Promise<Response>} */ const response;
/** @type {!Promise<undefined>} */ const thisPromiseReturnsNothingButParameterIsStillUseful;
/** @type {!Object<string, *>} */ const mapOfEverything;
例外:当 Object
被用于层次结构而不是 Map 结构的时候。
8. 其他
8.1 未尽事宜:一致性原则
未尽事宜,应当首先以同一文件中的其他类似风格为准,若该文件中没有类似结构,则以项目风格为准。
8.2 非平安云风格
没有必要把已经存在的代码改写为平安云风格。因为风格规范本身可能不断变化,改写时应当考虑修改风格带来的成本。
8.3 项目风格
基于不同的团队和项目,项目本身可能会要求使用一些第三方框架,例如 Angular,React 等,框架本身要求的一些代码风格(例如命名)可能会与平安云风格相冲突。团队应当进行评估选取适当的开发规范,一致性仍然是最重要的。
代码生成器
使用工具生成的代码,例如 webpack 打包压缩后的代码,不要求符合平安云风格。
9. ESLint
采用 ESLint 作为代码风格检查工具,对文中规定的规范进行检查:
必须,应当,禁止,不应当 级别的规范,违反时将会报错。
可以,推荐 级别的规范,违反时将会进行警告。
ESLint 中提供了一些未在本规范中规定的风格检查,此类问题将直接继承 ESLint 推荐风格的设定,并将在后续加入本规范。