[toc]
1 概要
本规范限于在浏览器中使用的javascript开发规范
本规范目前适用Amd方式模块定义和调用方式,后续ES6模式补充或另外规范。
术语
RFC 2119.
指南
代码规范约束或强制了开发人员的思想,可能会影响或导致观念上和行为上的不一致,也可能阻碍新方法,新思维的引进。开发团队在实施规范前应充分考虑团队中每个成员现有习惯,以及语言本身发展计划和新模式的情况,综合考虑代码规范、一致性和创新发展间的平衡。
本规范在有些代码书写的分歧上给出2种可选方案,以便不同的习惯在一定的规范下逐渐融合。
代码格式化工具
本规范建议使用代码工具格式化代码,而不是单纯手工代码调整格式。
格式化代码的方法:
- sublime text 3(需要插件): ctrl + shift + h ( mac: cmd + shift + h)
- web storm: 稍后补充
代码注释和文档
本规范认可代码本身的描述性,认可代码就是文档(code is the document),也认可注释和文档可以辅助描述代码,使代码更具可读性。
运行的系统是代码的直接输出,是准确全面描述系统的事实标准,而文档无法直接影响系统运行。
代码的描述比文档弱,但随着开发语言和工具、库的进步,代码的说明能力越来越强,代码结构、命名、流程都直接描述代码。
文档因为涉及到外部管理问题,在多版本迭代后,可能会失效甚至误导代码可读。任何时候都应以实际代码为准,文档代码结合阅读,尤其当代码运行出现同文档预期不同,首先怀疑文档代码是否一致!
注释是代码内嵌的文档,作辅助阅读之用。jsdoc,jsduck等工具可以导出单独文档,建议使用。
优先级指南
代码的可读性和运行效率经常成为如何书写一段代码的争论。到底是可读性重要,还是运行效率重要?其实都不是绝对的,从多个角度解决开发问题都是可行的。
但是回归到代码规范,如果开发团队确定应用代码规范,则以代码规范的目标为指南。
代码规范的目标,是以其部署的生产环境长期正常稳定运行为检验,在长期运行中,维护阶段的变化变为主要工作,修改和升级对代码的可读性、文档建设要求大。
所以,以代码维护的代价是根本,我们很容易排出以下代码书写的优先级顺序
- 可读性
- 运行效率
- 语法糖
固然,一段运行效率高但可读性不好的代码,可以通过文档方式说明,但是维护、修改的代价可能更大,文档的失效(文档管理可能延迟,不同步),也会导致代码无法维护。
2 文件规范
2.1 文件名
文件名必须小写,除了可以包括下划线或减号,不能使用其他标点符号。
文件名扩展名必须为.js
2.2 文件编码
源文件使用UTF-8编码
2.3 特殊字符
2.3.1 空格
空格用于语句和行内分割,可以使用多种字符如空格(0x20)和tab(\t),为了避免格式不确定,仅使用空格字符(0x20),禁止使用tab
2.3.2 转义字符
使用反斜线的转义符(', ", \, \b, \f, \n, \r, \t, \v),禁止使用16进制数字或8进制方式如 (e.g \x0a, \u000a, or \u{a}).
由于使用UTF-8编码,可以解决中文字符问题,所以禁止中文字符串以\u000a\u221e...形式存储。
2.3.3 非ASCII字符和编码
对于遗留的非ASCII字符,以及一些Unicode字符,需要通过编码方式存储。
//Example
const units = 'μs'; //Best: perfectly clear even without a comment.
const units = '\u03bcs'; // 'μs' Allowed
const units = '\u03bcs'; // Greek letter mu, 's' Allowed, but awkward and prone to mistakes.
const units = '\u03bcs'; Poor: the reader has no idea what this is.
return '\ufeff' + content; // byte order mark Good: use escapes for non-printable characters, and comment if necessary.
由于字符编码方式不易阅读,可能包含不合规内容而没有被检查。为了避免违规,以下原则执行:
- 中文字符串禁止使用编码方式
- 字符串以最可读方式存储
- 如果必须以编码方式存储,必须提供可验证的解码说明或双向工具。
2.4 文件打包
源文件通过webpack打包工具合并成输出文件。
输出文件保留原始方式和压缩方式两个文件。
//原始打包目标文件
projectname.js
//压缩后的目标文件
projectname.min.js
2.4 文件大小和行数
每个源代码文件建议不超过100K,行数不超过2000行。
超出规格的代码,通过解耦(拆分或分层),重构到新的类中,建立新的代码文件
每个项目的文件数量不超过1000个
3 源文件内容
一个源文件的结构,顺序为
- 许可和版权信息,根据需要加入
- @fileoverview,用于jsdoc,根据需要加入
- 模块定义
- 外部引用
- 代码实现
以上内容通过空行分割。
3.1 软件许可或版权信息
根据统一格式文本复制。
3.3 模块导出定义
模块必须通过定义,才能被外部模块调用
目前2种模块方法,AMD方式模块导出定义
- amd: define
- cmd, es6: exports
// amd:
define(function(require)
{
let vov = {};
vov.utility = require('../vov/vovutility');
return vov.cfg;
});
define(function(require)
{
let vov = {};
vov.utility = require('../vov/vovutility');
return vov.cfg;
});
3.4 引用外部模块
//amd:
vov.utility = require('../vov/vovutility');
//google mode:
const MyClass = goog.require('some.package.MyClass');
const NsMyClass = goog.require('other.ns.MyClass');
const googAsserts = goog.require('goog.asserts');
const testingAsserts = goog.require('goog.testing.asserts');
const than80columns = goog.require('pretend.this.is.longer.than80columns');
const {clear, forEach, map} = goog.require('goog.array');
/** @suppress {extraRequire} Initializes MyFramework. */
goog.require('my.framework.initialization');
3.5 代码实现
源文件结构的最后是代码实现,包括本地变量声明,以及代码逻辑实现。
constants, variables, classes, functions, etc
3.6 目录结构
项目的目录结构建议2级模式或3级固定级数,可以称为平行结构,不包含逻辑意义(平行结构的好处是代码中的相对引用路径是简单统一的)。
- 目录结构采用平行结构
- 仅在最末层建立源代码文件
//平行结构
- projectdir //顶级目录
- biz //只有一层子目录
- simmodels
- simmodelsde.js //最末级存储文件
- simmodelscn.js
- simrunner
- roadeditor
- roadrender
- ui
- controllers
不建议以多级树形方式,更不建议多种逻辑分类方法。多级树方式,子目录和文件移动后,需要调整相对路径,以及其他缺点。
//不建议的目录结构
- biz
- sim
- models
- bus //不推荐不同的逻辑分层
- busmodel.js
- car
- carmodel.js
- bike
- modelsde.js //不推荐每级都有文件,bad
- rendner
- simbase.js //每级都有文件,bad
- road
- roadrender
- roadediter
4 代码格式
代码块是指一段代码集合,类似类主体,功能、方法或者大括号包含的代码区域
由于Lamda表达式可能会有单独的处理,本规范暂不明确规定。
4.1 大括号
大括号的结束标记单独成行基本上大部分规范都是统一的,而开始标记是否单独成行,成为2种代码风格的焦点。
本规范遵循大括号单行模式。
如果
4.1.1 大括号单独成行
大括号的开始标记和结束标记
4.1.2 非空块
class InnerClass {
constructor() {}
/** @param {number} foo */
method(foo) {
if (condition(foo)) {
try {
// Note: this might fail.
something();
} catch (err) {
recover();
}
}
}
}
class InnerClass
{
constructor()
{
}
/** @param {number} foo */
method(foo)
{
if (condition(foo))
{
try
{
// Note: this might fail.
something();
}
catch (err)
{
recover();
}
}
}
}
4.2 块缩进:4个空格
代码和类似块结构缩进采用4个空格。
在开发环境中,当开发者按tab键,或者编辑器自动缩进,编辑器存储的字符转化为4个空格字符,同开发者手动按4下空格键的存储是一样的。这是我们期望的。
但是有的开发环境编辑器或选择的风格不同,存储可能会是一个tab字符,不是我们期望的,所以规定统一使用4个空格,避免不同环境的表现差异。
这两种代码风格在有些编辑器中看到的效果可能一致,是因为编辑器将tab字符转换为4个空格,但是另外一些编辑器,效果就不一样,1个tab可能用2个空格。
请根据开发工具和选项,重新设置tab的实际存储。
4.2.1 数组块
数组可以格式化为块结构,如下列都是允许的。
const a =
[
0,
1,
2,
];
const b =
[0, 1, 2];
const c = [0, 1, 2];
someMethod(foo, [
0, 1, 2,
], bar);
4.2.2 对象块
对象也可以采用块结构声明,以下都是允许的
const a =
{
a: 0,
b: 1,
};
const b =
{a: 0, b: 1};
const c = {a: 0, b: 1};
someMethod(foo, {
a: 0, b: 1,
}, bar);
4.2.3 类
class Foo
{
constructor()
{
/** @type {number} */
this.x = 42;
}
/** @return {number} */
method()
{
return this.x;
}
}
Foo.Empty = class {};
/** @extends {Foo} */
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) => {
// Indent the function body +2 relative to indentation depth
// of the 'prefix' statement one line above.
if (a1.equals(a2))
{
someOtherLongFunctionName(a1);
}
else
{
andNowForSomethingCompletelyDifferent(a2.parrot);
}
});
some.reallyLongFunctionCall(arg1, arg2, arg3)
.thatsWrapped()
.then((result) => {
// Indent the function body +2 relative to the indentation depth
// of the '.then()' call.
if (result)
{
result.use();
}
});
4.2.5 switch 块
case 条件必须使用枚举值,如Animal.BANDERSNATCH
在break和下一个case之间可以加入1个空行
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.4 列宽限制
列宽限制为80字符
4.5 手动换行
一个代码语句,如果超出列宽,则需手动加入换行。
4.5.1 手动换行原则
在换行的一行中保持逻辑连续
//good
currentEstimate =
calc(currentEstimate + x * currentEstimate) /
2.0f;
//bad
currentEstimate = calc(currentEstimate + x *
currentEstimate) / 2.0f;
When a line is broken at an operator the break comes after the symbol. (Note that this is not the same practice used in Google style for Java.)
This does not apply to the dot (.), which is not actually an operator.
A method or constructor name stays attached to the open parenthesis (() that follows it.
A comma (,) stays attached to the token that precedes it.
4.5.2 换行缩进
如果手动换行,首行后的续行以4个空格缩进
4.6 空格
4.6.1 空行
空行出现在:
- 两个相邻函数之间
- 其他对象、属性和函数相邻处
- 类的第一个属性或方法前
不应出现空行:
- 不允许连续2个空行
- 师徒空行进行逻辑分组
4.6.2 空格
空格出现在三类位置,行首、尾和中间
不能出现空格
- 行尾
- 逗号和分号之前
出现空格:
- 行首如果符合缩进原则,则加入响应缩进空格
- 保留字和圆括号开始标记后,加入一个空格
if (typeof ol != 'undefined')
{
vov.base._pixelRatio = ol.has.DEVICE_PIXEL_RATIO; // 1;
}
else
{}
- 运算符两端各加入一个空格, 三元操作符也是两端加入空格
sum = 1 + 2;
isOK = person == 'xi' ? true : false;
- 逗号和分号、冒号之后如果有语句,加入一个空格
- 行尾注释 (//),前后各加入一空格
4.7 圆括号
4.8 注释
5 语言特性
6 命名规范
6.1 标识命名规则
标识名称仅使用ASCII字符和数字组合,可以包括下划线和少数使用特殊符号,如$
- 尽量使用描述性、自然语言单词
- 使用动词 + 名词 + 补足
- 禁止使用缩写
- 从变量自身描述,而不是外部视角,混合模式
如,
6.2 标识命名规则
标识名称仅使用ASCII字符和数字组合,可以包括下划线和少数使用特殊符号,如$
- 尽量使用描述性、自然语言单词
- 使用动词 + 名词 + 补足
- 禁止使用缩写
- 从变量自身描述,而不是外部视角,混合模式
如,
6.3 驼峰格式
7 JSDoc
JSDoc|JSDuck应用在全部类,字段和方法
7.1 注释格式
JSDoc采用基本格式化注释,使用/**方式开始注释
/** 多行示例
* Multiple lines of JSDoc text are written here,
* wrapped normally.
* @param {number} arg A number to do something to.
*/
function doSomething(arg)
{
…
}
单行示例
/** @const @private {!Foo} A short bit of JSDoc. */
this.foo_ = foo;
7.2 Markdown
JSDoc用Markdown 输出,为了保证预期格式,如段落、换行和缩进等,可以使用Markdown 格式表示。
/**
* Computes weight based on three factors:
* - items sent
* - items received
* - last timestamp
*/
因为标记及HTML代码中的空白字符,可能被忽略。
如
/**
* Computes weight based on three factors:
* items sent
* items received
* last timestamp
*/
可能被输出为:
Computes weight based on three factors: items sent items received last timestamp
7.3 JSDoc标签
/**
* Place more complex annotations (like "implements" and "template")
* on their own lines. Multiple simple tags (like "export" and "final")
* may be combined in one line.
* @export @final
* @implements {Iterable}
* @template TYPE
*/
class MyClass
{
/**
* @param {!ObjType} obj Some object.
* @param {number=} num An optional number.
*/
constructor(obj, num = 42)
{
/** @private @const {!Array} */
this.data_ = [obj, num];
}
}