1. 前言
本文档的目标是使JavaScript代码风格保持一致,容易被理解和被维护。
虽然本文档是针对JavaScript设计的,但是在使用各种JavaScript的预编译语言时(如TypeScript等)时,适用的部分也应尽量遵循本文档的约定。
2. 代码风格
2.1. 文件
[建议] JavaScript 文件使用无 BOM 的 UTF-8 编码。
解释
UTF-8 编码具有更广泛的适应性。BOM 在使用程序或工具处理文件时可能造成不必要的干扰。
[建议] 在文件结尾处,保留一个空行。
2.2. 结构
2.2.1. 缩进
[强制] 使用 4 个空格做为一个缩进层级,不允许使用 2 个空格 或 tab 字符。
[强制] switch 下的 case 和 default 必须增加一个缩进层级。
示例
|
|
2.2.2. 空格
[强制] 二元运算符两侧必须有一个空格,一元运算符与操作对象之间不允许有空格。
示例
|
|
[强制] 用作代码块起始的左花括号 { 前必须有一个空格。
示例
|
|
[强制] if / else / for / while / function / switch / do / try / catch / finally
关键字后,必须有一个空格。
示例
|
|
[强制] 在对象创建时,属性中的 : 之后必须有空格,: 之前不允许有空格。
示例
|
|
[强制] 函数声明、具名函数表达式、函数调用中,函数名和 ( 之间不允许有空格。
示例
|
|
[强制] , 和 ; 前不允许有空格。
示例
|
|
[强制] 在函数调用、函数声明、括号表达式、属性访问 if / for / while / switch / catch
等语句中,()
和 []
内紧贴括号部分不允许有空格。
示例
|
|
[强制] 单行声明的数组与对象,如果包含元素,{}
和 []
内紧贴括号部分不允许包含空格。
解释
声明包含元素的数组与对象,只有当内部元素的形式较为简单时,才允许写在一行。元素复杂的情况,还是应该换行书写。
示例
|
|
[强制] 行尾不得有多余的空格。
2.2.3. 换行
[强制] 每个独立语句结束后必须换行。
[强制] 每行不得超过 120 个字符。
解释
超长的不可分割的代码允许例外,比如复杂的正则表达式。长字符串不在例外之列。
[强制] 运算符处换行时,运算符必须在新行的行首。
示例
|
|
[强制] 在函数声明、函数表达式、函数调用、对象创建、数组创建、for语句等场景中,不允许在 , 或 ; 前换行。
示例
|
|
[建议] 不同行为或逻辑的语句集,使用空行隔开,更易阅读。
示例
|
|
[建议] 在语句的行长度超过 120 时,根据逻辑条件合理缩进。
示例
|
|
[建议] 对于 if...else...
、 try...catch...finally
等语句,推荐使用在 } 号后添加一个换行 的风格,使代码层次结构更清晰,阅读性更好。
示例
|
|
2.2.4. 语句
[强制] 不得省略语句结束的分号。
[强制] 在 if / else / for / do / while
语句中,即使只有一行,也不得省略块 {…}。
示例
|
|
[强制] IIFE立即执行函数表达式 必须在函数表达式外添加 (,非 IIFE 不得在函数表达式外添加 ( 。
解释
额外的 ( 能够让代码在阅读的一开始就能判断函数是否立即被调用,进而明白接下来代码的用途。而不是一直拖到底部才恍然大悟。
示例
|
|
2.3. 命名
[强制] 变量 使用 Camel命名法 。
示例
[强制] 函数 使用 Camel命名法 。
示例
|
|
[强制] 函数的 参数 使用 Camel命名法 。
示例
|
|
[强制] 类 使用 Pascal命名法 。
示例
|
|
[强制] 类的 方法 / 属性 使用 Camel命名法 。
示例
|
|
[强制] 枚举变量 使用 Pascal命名法 ,枚举的属性 使用 全部字母大写,单词间下划线分隔 的命名方式。
示例
|
|
[强制] 命名空间 使用 Camel命名法 。
示例
function XMLParser() {
}
function insertHTML(element, html) {
}
var httpRequest = new HTTPRequest();
function Engine(options) {
}
function getStyle(element) {
}
var isReady = false;
var hasMoreCommands = false;
var loadingData = ajax.get(‘url’);
loadingData.then(callback);
/**
- @file Describe the file
*/12345678910[建议] 文件注释中可以用 `@author` 标识开发者信息。解释开发者信息能够体现开发人员对文件的贡献,并且能够让遇到问题或希望了解相关信息的人找到维护人。通常情况文件在被创建时标识的是创建者。随着项目的进展,越来越多的人加入,参与这个文件的开发,新的作者应该被加入 `@author` 标识。`@author` 标识具有多人时,原则是按照 责任 进行排序。通常的说就是如果有问题,就是找第一个人应该比找第二个人有效。比如文件的创建者由于各种原因,模块移交给了其他人或其他团队,后来因为新增需求,其他人在新增代码时,添加 `@author` 标识应该把自己的名字添加在创建人的前面。`@author` 中的名字不允许被删除。任何劳动成果都应该被尊重。业务项目中,一个文件可能被多人频繁修改,并且每个人的维护时间都可能不会很长,不建议为文件增加 `@author` 标识。通过版本控制系统追踪变更,按业务逻辑单元确定模块的维护责任人,通过文档与wiki跟踪和查询,是更好的责任管理方式。对于业务逻辑无关的技术型基础项目,特别是开源的公共项目,应使用 `@author` 标识。示例
/**
- @file Describe the file
- @author author-name(mail-name@domain.com)
- author-name2(mail-name2@domain.com)
*/12342.4.6. 命名空间注释---[建议] 命名空间使用 `@namespace` 标识。示例
/**
- @namespace
*/
var util = {};1234562.4.7. 类注释---[建议] 使用 `@class` 标记类或构造函数。解释对于使用对象 `constructor` 属性来定义的构造函数,可以使用 `@constructor` 来标记。示例
/**
- 描述
* - @class
*/
function Developer() {
// constructor body
}12[建议] 使用 `@extends` 标记类的继承信息。示例
/**
- 描述
* - @class
- @extends Developer
*/
function Fronteer() {
Developer.call(this);
// constructor body
}
util.inherits(Fronteer, Developer);1234[强制] 使用包装方式扩展类成员时, 必须通过 `@lends` 进行重新指向。解释没有 `@lends` 标记将无法为该类生成包含扩展类成员的文档。示例
/**
- 类描述
* - @class
- @extends Developer
*/
function Fronteer() {
Developer.call(this);
// constructor body
}
util.extend(
Fronteer.prototype,
/* @lends Fronteer.prototype /{
_getLevel: function () {
// TODO
}
}
);
/**
- 类描述
* - @class
@extends Developer
*/
var Fronteer = function () {
Developer.call(this);/**
- 属性描述
* - @type {string}
@private
*/
this._level = ‘T12’;// constructor body
};
util.inherits(Fronteer, Developer);
- 属性描述
/**
- 方法描述
* - @private
- @return {string} 返回值描述
*/
Fronteer.prototype._getLevel = function () {
};1234562.4.8. 函数/方法注释---[强制] 函数/方法注释必须包含函数说明,有参数和返回值时必须使用注释标识。[强制] 参数和返回值注释必须包含类型信息和说明。[建议] 当函数是内部函数,外部不可访问时,可以使用 `@inner` 标识。示例
/**
- 函数描述
* - @param {string} p1 参数1的说明
- @param {string} p2 参数2的说明,比较长
- 那就换行了.
- @param {number=} p3 参数3的说明(可选)
- @return {Object} 返回值描述
*/
function foo(p1, p2, p3) {
var p3 = p3 || 10;
return {
};p1: p1, p2: p2, p3: p3
}12[强制] 对 Object 中各项的描述, 必须使用 `@param` 标识。示例
/**
- 函数描述
* - @param {Object} option 参数描述
- @param {string} option.url option项描述
- @param {string=} option.method option项描述,可选参数
*/
function foo(option) {
// TODO
}1234567[建议] 重写父类方法时, 应当添加 `@override` 标识。如果重写的形参个数、类型、顺序和返回值类型均未发生变化,可省略 `@param 、@return` ,仅用 `@override` 标识,否则仍应作完整注释。解释简而言之,当子类重写的方法能直接套用父类的方法注释时可省略对参数与返回值的注释。2.4.9. 事件注释---[强制] 必须使用 `@event` 标识事件,事件参数的标识与方法描述的参数标识相同。示例
/**
- 值变更时触发
* - @event
- @param {Object} e e描述
- @param {string} e.before before描述
- @param {string} e.after after描述
*/
onchange: function (e) {
}123[强制] 在会广播事件的函数前使用 `@fires` 标识广播的事件,在广播事件代码前使用 `@event` 标识事件。[建议] 对于事件对象的注释,使用 `@param` 标识,生成文档时可读性更好。示例
/**
- 点击处理
* - @fires Select#change
- @private
/
Select.prototype.clickHandler = function () {
/*- 值变更时触发
* - @event Select#change
- @param {Object} e e描述
- @param {string} e.before before描述
- @param {string} e.after after描述
*/
this.fire(
‘change’,
{
}before: 'foo', after: 'bar'
);
};12342.4.10. 常量注释---[强制] 常量必须使用 `@const` 标记,并包含说明和类型信息。示例
- 值变更时触发
/**
- 常量说明
* - @const
- @type {string}
*/
var REQUEST_URL = ‘myurl.do’;12342.4.11. 复杂类型注释---[建议] 对于类型未定义的复杂结构的注释,可以使用 `@typedef ` 标识来定义。示例
// namespaceA~
可以换成其它 namepaths 前缀,目的是为了生成文档中能显示 @typedef
定义的类型和链接。
/**
- 服务器
* - @typedef {Object} namespaceA~Server
- @property {string} host 主机
- @property {number} port 端口
*/
/**
- 服务器列表
* - @type {Array.
}
*/
var servers = [
{
},host: '1.2.3.4', port: 8080
{
}host: '1.2.3.5', port: 8081
];1234562.4.12. AMD 模块注释---[强制] AMD 模块使用 `@module` 或 `@exports` 标识。解释`@exports` 与 `@module` 都可以用来标识模块,区别在于 `@module` 可以省略模块名称。而只使用 `@exports` 时在 namepaths 中可以省略 module: 前缀。示例
define(
function (require) {
/**
* foo description
*
* @exports Foo
*/
var foo = {
// TODO
};
/**
* baz description
*
* @return {boolean} return description
*/
foo.baz = function () {
// TODO
};
return foo;
}
);
define(
function (require) {
/**
* module description.
*
* @module foo
*/
var exports = {};
/**
* bar description
*
*/
exports.bar = function () {
// TODO
};
return exports;
}
);
/**
- module description.
* @module
*/
define(
function (require, exports) {/** * bar description * */ exports.bar = function () { // TODO }; return exports;
}
);1234[强制] 对于已使用 `@module` 标识为 AMD模块 的引用,在 namepaths 中必须增加 module: 作前缀。解释namepaths 没有 module: 前缀时,生成的文档中将无法正确生成链接。示例
/**
- 点击处理
* - @fires module:Select#change
- @private
/
Select.prototype.clickHandler = function () {
/*- 值变更时触发
* - @event module:Select#change
- @param {Object} e e描述
- @param {string} e.before before描述
- @param {string} e.after after描述
*/
this.fire(
‘change’,
{
}before: 'foo', after: 'bar'
);
};12[建议] 对于类定义的模块,可以使用 `@alias` 标识构建函数。示例
- 值变更时触发
/**
- A module representing a jacket.
@module jacket
*/
define(
function () {/** * @class * @alias module:jacket */ var Jacket = function () { }; return Jacket;
}
);12[建议] 多模块定义时,可以使用 `@exports` 标识各个模块。示例
// one module
define(‘html/utils’,
/**
* Utility functions to ease working with DOM elements.
* @exports html/utils
*/
function () {
var exports = {
};
return exports;
}
);
// another module
define(‘tag’,
/* @exports tag /
function () {
var exports = {
};
return exports;
}
);
// 只使用 @class Bar 时,类方法和属性都必须增加 @name Bar#methodName 来标识,与 @exports 配合可以免除这一麻烦,并且在引用时可以省去 module: 前缀。
// 另外需要注意类名需要使用 var 定义的方式。
/**
- Bar description
* - @see foo
- @exports Bar
- @class
*/
var Bar = function () {
// TODO
};
/**
- baz description
* - @return {(string|Array)} return description
*/
Bar.prototype.baz = function () {
// TODO
};123452.4.13. 细节注释---对于内部实现、不容易理解的逻辑说明、摘要信息等,我们可能需要编写细节注释。[建议] 细节注释遵循单行注释的格式。说明必须换行时,每行是一个单行注释的起始。示例
function foo(p1, p2) {
// 这里对具体内部逻辑进行说明
// 说明太长需要换行
for (…) {
….
}
}
// good
var name = ‘MyName’;
// bad
name = ‘MyName’;
// good
var hangModules = [];
var missModules = [];
var visited = {};
// bad
var hangModules = [],
missModules = [],
visited = {};
// good
function kv2List(source) {
var list = [];
for (var key in source) {
if (source.hasOwnProperty(key)) {
var item = {
k: key,
v: source[key]
};
list.push(item);
}
}
return list;
}
// bad
function kv2List(source) {
var list = [];
var key;
var item;
for (key in source) {
if (source.hasOwnProperty(key)) {
item = {
k: key,
v: source[key]
};
list.push(item);
}
}
return list;
}
// good
if (age === 30) {
// ……
}
// bad
if (age == 30) {
// ……
}
// 字符串为空
// good
if (!name) {
// ……
}
// bad
if (name === ‘’) {
// ……
}
// 字符串非空
// good
if (name) {
// ……
}
// bad
if (name !== ‘’) {
// ……
}
// 数组非空
// good
if (collection.length) {
// ……
}
// bad
if (collection.length > 0) {
// ……
}
// 布尔不成立
// good
if (!notTrue) {
// ……
}
// bad
if (notTrue === false) {
// ……
}
// null 或 undefined
// good
if (noValue == null) {
// ……
}
// bad
if (noValue === null || typeof noValue === ‘undefined’) {
// ……
}
// good
switch (typeof variable) {
case ‘object’:
// ……
break;
case ‘number’:
case ‘boolean’:
case ‘string’:
// ……
break;
}
// bad
var type = typeof variable;
if (type === ‘object’) {
// ……
}
else if (type === ‘number’ || type === ‘boolean’ || type === ‘string’) {
// ……
}
// good
function getName() {
if (name) {
return name;
}
return 'unnamed';
}
// bad
function getName() {
if (name) {
return name;
}
else {
return ‘unnamed’;
}
}
// good
function clicker() {
// ……
}
for (var i = 0, len = elements.length; i < len; i++) {
var element = elements[i];
addListener(element, ‘click’, clicker);
}
// bad
for (var i = 0, len = elements.length; i < len; i++) {
var element = elements[i];
addListener(element, ‘click’, function () {});
}
// good
var width = wrap.offsetWidth + ‘px’;
for (var i = 0, len = elements.length; i < len; i++) {
var element = elements[i];
element.style.width = width;
// ……
}
// bad
for (var i = 0, len = elements.length; i < len; i++) {
var element = elements[i];
element.style.width = wrap.offsetWidth + ‘px’;
// ……
}
for (var i = 0, len = elements.length; i < len; i++) {
var element = elements[i];
// ……
}
var len = elements.length;
while (len–) {
var element = elements[len];
// ……
}
// string
typeof variable === ‘string’
// number
typeof variable === ‘number’
// boolean
typeof variable === ‘boolean’
// Function
typeof variable === ‘function’
// Object
typeof variable === ‘object’
// RegExp
variable instanceof RegExp
// Array
variable instanceof Array
// null
variable === null
// null or undefined
variable == null
// undefined
typeof variable === ‘undefined’
// good
num + ‘’;
// bad
new String(num);
num.toString();
String(num);
// good
+str;
// bad
Number(str);
var width = ‘200px’;
parseInt(width, 10);
// good
parseInt(str, 10);
// bad
parseInt(str);
var num = 3.14;
!!num;
// good
var num = 3.14;
Math.ceil(num);
// bad
var num = 3.14;
parseInt(num, 10);
var str = ‘我是一个字符串’;
var html = ‘
|
|
// 使用数组拼接字符串
var str = [
// 推荐换行开始并缩进开始第一个字符串, 对齐代码, 方便阅读.
‘
- ‘,
- 第一项 ‘,
- 第二项 ‘,
‘
‘
‘
].join(‘’);
// 使用 + 拼接字符串
var str2 = ‘’ // 建议第一个为空字符串, 第二个换行开始并缩进开始, 对齐代码, 方便阅读
+ '<ul>',
+ '<li>第一项</li>',
+ '<li>第二项</li>',
+ '</ul>';
|
|
// good
var obj = {};
// bad
var obj = new Object();
var info = {
name: ‘someone’,
age: 28
};
// good
var info = {
‘name’: ‘someone’,
‘age’: 28,
‘more-info’: ‘…’
};
// bad
var info = {
name: ‘someone’,
age: 28,
‘more-info’: ‘…’
};
// 以下行为绝对禁止
String.prototype.trim = function () {
};
info.age;
info[‘more-info’];
var newInfo = {};
for (var key in info) {
if (info.hasOwnProperty(key)) {
newInfo[key] = info[key];
}
}
// good
var arr = [];
// bad
var arr = new Array();
var arr = [‘a’, ‘b’, ‘c’];
arr.other = ‘other things’; // 这里仅作演示, 实际中应使用Object类型
// 正确的遍历方式
for (var i = 0, len = arr.length; i < len; i++) {
console.log(i);
}
// 错误的遍历方式
for (i in arr) {
console.log(i);
}
function syncViewStateOnUserAction() {
if (x.checked) {
y.checked = true;
z.value = ‘’;
}
else {
y.checked = false;
}
if (!a.value) {
warning.innerText = 'Please enter it';
submitButton.disabled = true;
}
else {
warning.innerText = '';
submitButton.disabled = false;
}
}
// 直接阅读该函数会难以明确其主线逻辑,因此下方是一种更合理的表达方式:
function syncViewStateOnUserAction() {
syncXStateToView();
checkAAvailability();
}
function syncXStateToView() {
if (x.checked) {
y.checked = true;
z.value = ‘’;
}
else {
y.checked = false;
}
}
function checkAAvailability() {
if (!a.value) {
displayWarningForAMissing();
}
else {
clearWarnignForA();
}
}
/**
- 移除某个元素
* - @param {Node} element 需要移除的元素
- @param {boolean} removeEventListeners 是否同时将所有注册在元素上的事件移除
*/
function removeElement(element, removeEventListeners) {
element.parent.removeChild(element);
if (removeEventListeners) {
}element.clearEventListeners();
}
可以转换为下面的签名:
/** - 移除某个元素
* - @param {Node} element 需要移除的元素
- @param {Object} options 相关的逻辑配置
- @param {boolean} options.removeEventListeners 是否同时将所有注册在元素上的事件移除
*/
function removeElement(element, options) {
element.parent.removeChild(element);
if (options.removeEventListeners) {
}element.clearEventListeners();
}1234567891011121314151617181920212223242526272829303132333435363738这种模式有几个显著的优势:boolean 型的配置项具备名称,从调用的代码上更易理解其表达的逻辑意义。当配置项有增长时,无需无休止地增加参数个数,不会出现 removeElement(element, true, false, false, 3) 这样难以理解的调用代码。当部分配置参数可选时,多个参数的形式非常难处理重载逻辑,而使用一个 options 对象只需判断属性是否存在,实现得以简化。3.8.3. 闭包---[建议] 在适当的时候将闭包内大对象置为 null 。解释在 JavaScript 中,无需特别的关键词就可以使用闭包,一个函数可以任意访问在其定义的作用域外的变量。需要注意的是,函数的作用域是静态的,即在定义时决定,与调用的时机和方式没有任何关系。闭包会阻止一些变量的垃圾回收,对于较老旧的JavaScript引擎,可能导致外部所有变量均无法回收。首先一个较为明确的结论是,以下内容会影响到闭包内变量的回收:> 嵌套的函数中是否有使用该变量。> 嵌套的函数中是否有直接调用eval 。> 是否使用了 with 表达式。Chakra、V8 和 SpiderMonkey 将受以上因素的影响,表现出不尽相同又较为相似的回收策略,而JScript.dll和Carakan则完全没有这方面的优化,会完整保留整个 LexicalEnvironment 中的所有变量绑定,造成一定的内存消耗。由于对闭包内变量有回收优化策略的 Chakra、V8 和 SpiderMonkey 引擎的行为较为相似,因此可以总结如下,当返回一个函数 fn 时:> 如果 fn 的 [[Scope]] 是ObjectEnvironment(with 表达式生成 ObjectEnvironment,函数和 catch 表达式生成 DeclarativeEnvironment),则:1. 如果是 V8 引擎,则退出全过程。2. 如果是 SpiderMonkey,则处理该 ObjectEnvironment 的外层 LexicalEnvironment。> 获取当前 LexicalEnvironment 下的所有类型为 Function 的对象,对于每一个 Function 对象,分析其 FunctionBody:1.如果 FunctionBody 中含有直接调用eval ,则退出全过程。2.否则得到所有的 Identifier。3.对于每一个 Identifier,设其为 name,根据查找变量引用的规则,从 LexicalEnvironment 中找出名称为 name 的绑定 binding。4.对 binding 添加 notSwap 属性,其值为 true。> 检查当前 LexicalEnvironment 中的每一个变量绑定,如果该绑定有 notSwap 属性且值为 true,则:1.如果是V8引擎,删除该绑定。2.如果是SpiderMonkey,将该绑定的值设为 undefined,将删除 notSwap 属性。对于Chakra引擎,暂无法得知是按 V8 的模式还是按 SpiderMonkey 的模式进行。如果有 非常庞大 的对象,且预计会在老旧的引擎 中执行,则使用闭包时,注意将闭包不需要的对象置为空引用。[建议] 使用 IIFE 避免 Lift 效应 。解释在引用函数外部变量时,函数执行时外部变量的值由运行时决定而非定义时,最典型的场景如下:
var tasks = [];
for (var i = 0; i < 5; i++) {
tasks[tasks.length] = function () {
console.log(‘Current cursor is at ‘ + i);
};
}
var len = tasks.length;
while (len–) {
taskslen;
}
var tasks = [];
for (var i = 0; i < 5; i++) {
// 注意有一层额外的闭包
tasks[tasks.length] = (function (i) {
return function () {
console.log(‘Current cursor is at ‘ + i);
};
})(i);
}
var len = tasks.length;
while (len–) {
taskslen;
}
var emptyFunction = function () {};
``
[建议] 对于性能有高要求的场合,建议存在一个空函数的常量,供多处使用共享。
示例
|
|
3.9. 面向对象
[强制] 类的继承方案,实现时需要修正 constructor。
解释
通常使用其他 library 的类继承方案都会进行 constructor 修正。如果是自己实现的类继承方案,需要进行 constructor 修正。
示例
|
|
[建议] 声明类时,保证 constructor 的正确性。
示例
|
|
[建议] 属性在构造函数中声明,方法在原型中声明。
解释
原型对象的成员被所有实例共享,能节约内存占用。所以编码时我们应该遵守这样的原则:原型对象包含程序不会修改的成员,如方法函数或配置项。
|
|
[强制] 自定义事件的 事件名 必须全小写。
解释
在 JavaScript 广泛应用的浏览器环境,绝大多数 DOM 事件名称都是全小写的。为了遵循大多数 JavaScript 开发者的习惯,在设计自定义事件时,事件名也应该全小写。
[强制] 自定义事件只能有一个 event 参数。如果事件需要传递较多信息,应仔细设计事件对象。
解释
一个事件对象的好处有:
顺序无关,避免事件监听者需要记忆参数顺序。
每个事件信息都可以根据需要提供或者不提供,更自由。
扩展方便,未来添加事件信息时,无需考虑会破坏监听器参数形式而无法向后兼容。
[建议] 设计自定义事件时,应考虑禁止默认行为。
解释
常见禁止默认行为的方式有两种:
事件监听函数中 return false。
事件对象中包含禁止默认行为的方法,如 preventDefault。
3.10. 动态特性
3.10.1. eval
[强制] 避免使用直接 eval 函数。
解释
直接 eval,指的是以函数方式调用 eval 的调用方法。直接 eval 调用执行代码的作用域为本地作用域,应当避免。
如果有特殊情况需要使用直接 eval ,需在代码中用详细的注释说明为何必须使用直接 eval ,
不能使用其它动态执行代码的方式,同时需要其他资深工程师进行 Code Review。
[建议] 尽量避免使用 eval 函数。
3.10.2. 动态执行代码
[建议] 使用 new Function 执行动态代码。
解释
通过 new Function 生成的函数作用域是全局使用域,不会影响当当前的本地作用域。如果有动态代码执行的需求,建议使用 new Function 。
示例
|
|
3.10.3. with
[建议] 尽量不要使用 with。
解释
使用 with 可能会增加代码的复杂度,不利于阅读和管理;也会对性能有影响。大多数使用 with 的场景都能使用其他方式较好的替代。所以,尽量不要使用 with。
3.10.4. delete
[建议] 减少 delete 的使用。
解释
如果没有特别的需求,减少或避免使用 delete 。delete 的使用会破坏部分 JavaScript 引擎的性能优化。
[建议] 处理 delete 可能产生的异常。
解释
对于有被遍历需求,且值 null 被认为具有业务逻辑意义的值的对象,移除某个属性必须使用 delete 操作。
在严格模式或IE下使用 delete 时,不能被删除的属性会抛出异常,因此在不确定属性是否可以删除的情况下,建议添加 try-catch 块。
示例
|
|
3.10.5. 对象属性
[建议] 避免修改外部传入的对象。
解释
JavaScript 因其脚本语言的动态特性,当一个对象未被 seal 或 freeze 时,可以任意添加、删除、修改属性值。
但是随意地对 非自身控制的对象 进行修改,很容易造成代码在不可预知的情况下出现问题。因此,设计良好的组件、函数应该避免对外部传入的对象的修改。
下面代码的 selectNode 方法修改了由外部传入的 datasource 对象。如果 datasource 用在其它场合(如另一个 Tree 实例)下,会造成状态的混乱。
|
|
对于此类场景,需要使用额外的对象来维护,使用由自身控制,不与外部产生任何交互的 selectedNodeIndex 对象来维护节点的选中状态,不对 datasource 作任何修改。
|
|
除此之外,也可以通过 deepClone 等手段将自身维护的对象与外部传入的分离,保证不会相互影响。
[建议] 具备强类型的设计。
解释
如果一个属性被设计为 boolean 类型,则不要使用 1 / 0 作为其值。对于标识性的属性,如对代码体积有严格要求,可以从一开始就设计为 number 类型且将 0 作为否定值。
从 DOM 中取出的值通常为 string 类型,如果有对象或函数的接收类型为 number 类型,提前作好转换,而不是期望对象、函数可以处理多类型的值。
4. 浏览器环境
4.1. 模块化
4.1.1. AMD
[强制] 使用 AMD 作为模块定义。
解释
AMD 作为由社区认可的模块定义形式,提供多种重载提供灵活的使用方式,并且绝大多数优秀的 Library 都支持 AMD,适合作为规范。
目前,比较成熟的 AMD Loader 有:
[强制] 模块 id 必须符合标准。
解释
模块 id 必须符合以下约束条件:
类型为 string,并且是由 / 分割的一系列 terms 来组成。例如:this/is/a/module。
term 应该符合 [a-zA-Z0-9_-]+ 规则。
不应该有 .js 后缀。
跟文件的路径保持一致。
4.1.2. define
[建议] 定义模块时不要指明 id 和 dependencies 。
解释
在 AMD 的设计思想里,模块名称是和所在路径相关的,匿名的模块更利于封包和迁移。模块依赖应在模块定义内部通过 local require 引用。
所以,推荐使用 define(factory) 的形式进行模块定义。
示例
|
|
[建议] 使用 return 来返回模块定义。
解释
使用 return 可以减少 factory 接收的参数(不需要接收 exports 和 module),在没有 AMD Loader 的场景下也更容易进行简单的处理来伪造一个 Loader。
示例
|
|
4.1.3. require
[强制] 全局运行环境中,require 必须以 async require 形式调用。
解释
模块的加载过程是异步的,同步调用并无法保证得到正确的结果。
示例
|
|
[强制] 模块定义中只允许使用 local require ,不允许使用 global require 。
解释
在模块定义中使用 global require,对封装性是一种破坏。
在 AMD 里,global require 是可以被重命名的。并且 Loader 甚至没有全局的 require 变量,而是用 Loader 名称做为 global require。模块定义不应该依赖使用的 Loader。
[强制] Package在实现时,内部模块的 require 必须使用 relative id 。
解释
对于任何可能通过 发布-引入 的形式复用的第三方库、框架、包,开发者所定义的名称不代表使用者使用的名称。因此不要基于任何名称的假设。在实现源码中,require 自身的其它模块时使用 relative id。
示例
|
|
[建议] 不会被调用的依赖模块,在 factory 开始处统一 require。
解释
有些模块是依赖的模块,但不会在模块实现中被直接调用,最为典型的是 css / js / tpl 等 Plugin 所引入的外部内容。此类内容建议放在模块定义最开始处统一引用。
示例
|
|
4.2. DOM
4.2.1. 元素获取
[建议] 对于单个元素,尽可能使用 document.getElementById 获取,避免使用 document.all 。
[建议] 对于多个元素的集合,尽可能使用 context.getElementsByTagName 获取。其中 context 可以为 document 或其他元素。指定 tagName 参数为 * 可以获得所有子元素。
[建议] 遍历元素集合时,尽量缓存集合长度。如需多次操作同一集合,则应将集合转为数组。
解释
原生获取元素集合的结果并不直接引用 DOM 元素,而是对索引进行读取,所以 DOM 结构的改变会实时反映到结果中。
示例
|
|
[建议] 获取元素的直接子元素时使用 children。避免使用 childNodes ,除非预期是需要包含文本、注释和属性类型的节点。
4.2.2. 样式获取
·[建议] 获取元素实际样式信息时,应使用 getComputedStyle 或 currentStyle 。
解释
通过 style 只能获得内联定义或通过 JavaScript 直接设置的样式。通过 CSS class 设置的元素样式无法直接通过 style 获取。
4.2.3. 样式设置
·[建议] 尽可能通过为元素添加预定义的 className 来改变元素样式,避免直接操作 style 设置。
·[强制] 通过 style 对象设置元素样式时,对于带单位非 0 值的属性,不允许省略单位。
解释
除了 IE,标准浏览器会忽略不规范的属性值,导致兼容性问题。
4.2.4. DOM 操作
[建议] 操作 DOM 时,尽量减少页面 reflow。
解释
页面 reflow 是非常耗时的行为,非常容易导致性能瓶颈。下面一些场景会触发浏览器的reflow:
DOM元素的添加、修改(内容)、删除。
应用新的样式或者修改任何影响元素布局的属性。
Resize浏览器窗口、滚动页面。
读取元素的某些属性(offsetLeft、offsetTop、offsetHeight、offsetWidth、scrollTop/Left/Width/Height、clientTop/Left/Width/Height、getComputedStyle()、currentStyle(in IE)) `。
[建议] 尽量减少 DOM 操作。
解释
DOM 操作也是非常耗时的一种操作,减少 DOM 操作有助于提高性能。举一个简单的例子,构建一个列表。我们可以用两种方式:
在循环体中 createElement 并 append 到父元素中。
在循环体中拼接 HTML 字符串,循环结束后写父元素的 innerHTML。
第一种方法看起来比较标准,但是每次循环都会对 DOM 进行操作,性能极低。在这里推荐使用第二种方法。
4.2.5. DOM 事件
[建议] 优先使用 addEventListener / attachEvent 绑定事件,避免直接在 HTML 属性中或 DOM 的 expando 属性绑定事件处理。
解释
expando 属性绑定事件容易导致互相覆盖。
[建议] 使用 addEventListener 时第三个参数使用 false。
解释
标准浏览器中的 addEventListener 可以通过第三个参数指定两种时间触发模型:冒泡和捕获。而 IE 的 attachEvent 仅支持冒泡的事件触发。所以为了保持一致性,通常 addEventListener 的第三个参数都为 false。
[建议] 在没有事件自动管理的框架支持下,应持有监听器函数的引用,在适当时候(元素释放、页面卸载等)移除添加的监听器。