原文: https://github.com/ecomfe/spec/blob/master/javascript-style-guide.md
注:部分规范有删改
本文档的目标是使JavaScript代码风格保持一致,容易被理解和被维护。虽然本文档是针对JavaScript设计的,但是在使用各种JavaScript的预编译语言时(如TypeScript等)时,适用的部分也应尽量遵循本文档的约定。
JavaScript
文件使用无 BOM
的 UTF-8
编码。解释:
UTF-8 编码具有更广泛的适应性。BOM 在使用程序或工具处理文件时可能造成不必要的干扰。
4
个空格做为一个缩进层级,不允许使用 2
个空格 或 tab
字符。switch
下的 case
和 default
必须增加一个缩进层级。示例:
// good
switch (variable) {
case '1':
// do...
break;
case '2':
// do...
break;
default:
// do...
}
// bad
switch (variable) {
case '1':
// do...
break;
case '2':
// do...
break;
default:
// do...
}
示例:
var a = !arr.length;
a++;
a = b + c;
{
前必须有一个空格。示例:
// good
if (condition) {
}
while (condition) {
}
function funcName() {
}
// bad
if (condition){
}
while (condition){
}
function funcName(){
}
if / else / for / while / function / switch / do / try / catch / finally
关键字后,必须有一个空格。示例:
// good
if (condition) {
}
while (condition) {
}
(function () {
})();
// bad
if(condition) {
}
while(condition) {
}
(function() {
})();
:
之后必须有空格,:
之前不允许有空格。示例:
// good
var obj = {
a: 1,
b: 2,
c: 3
};
// bad
var obj = {
a : 1,
b:2,
c :3
};
(
之间不允许有空格。示例:
// good
function funcName() {
}
var funcName = function funcName() {
};
funcName();
// bad
function funcName () {
}
var funcName = function funcName () {
};
funcName ();
,
和 ;
前不允许有空格。示例:
// good
callFunc(a, b);
// bad
callFunc(a , b) ;
if / for / while / switch / catch
等语句中,()
和 []
内紧贴括号部分不允许有空格。示例:
// good
callFunc(param1, param2, param3);
save(this.list[this.indexes[i]]);
needIncream && (variable += increament);
if (num > list.length) {
}
while (len--) {
}
// bad
callFunc( param1, param2, param3 );
save( this.list[ this.indexes[ i ] ] );
needIncreament && ( variable += increament );
if ( num > list.length ) {
}
while ( len-- ) {
}
{}
和 []
内紧贴括号部分不允许包含空格。解释:
声明包含元素的数组与对象,只有当内部元素的形式较为简单时,才允许写在一行。元素复杂的情况,还是应该换行书写。
示例:
// good
var arr1 = [];
var arr2 = [1, 2, 3];
var obj1 = {};
var obj2 = {name: 'obj'};
var obj3 = {
name: 'obj',
age: 20,
sex: 1
};
// bad
var arr1 = [ ];
var arr2 = [ 1, 2, 3 ];
var obj1 = { };
var obj2 = { name: 'obj' };
var obj3 = {name: 'obj', age: 20, sex: 1};
120
个字符。解释:
超长的不可分割的代码允许例外,比如复杂的正则表达式。长字符串不在例外之列。
示例:
// good
if (user.isAuthenticated()
&& user.isInRole('admin')
&& user.hasAuthority('add-admin')
|| user.hasAuthority('delete-admin')
) {
// Code
}
var result = number1 + number2 + number3
+ number4 + number5;
// bad
if (user.isAuthenticated() &&
user.isInRole('admin') &&
user.hasAuthority('add-admin') ||
user.hasAuthority('delete-admin')) {
// Code
}
var result = number1 + number2 + number3 +
number4 + number5;
,
或 ;
前换行。示例:
// good
var obj = {
a: 1,
b: 2,
c: 3
};
foo(
aVeryVeryLongArgument,
anotherVeryLongArgument,
callback
);
// bad
var obj = {
a: 1
, b: 2
, c: 3
};
foo(
aVeryVeryLongArgument
, anotherVeryLongArgument
, callback
);
示例:
// 仅为按逻辑换行的示例,不代表setStyle的最优实现
function setStyle(element, property, value) {
if (element == null) {
return;
}
element.style[property] = value;
}
120
时,根据逻辑条件合理缩进。示例:
// 较复杂的逻辑条件组合,将每个条件独立一行,逻辑运算符放置在行首进行分隔,或将部分逻辑按逻辑组合进行分隔。
// 建议最终将右括号 ) 与左大括号 { 放在独立一行,保证与 if 内语句块能容易视觉辨识。
if (user.isAuthenticated()
&& user.isInRole('admin')
&& user.hasAuthority('add-admin')
|| user.hasAuthority('delete-admin')
) {
// Code
}
// 按一定长度截断字符串,并使用 + 运算符进行连接。
// 分隔字符串尽量按语义进行,如不要在一个完整的名词中间断开。
// 特别的,对于HTML片段的拼接,通过缩进,保持和HTML相同的结构。
var html = '' // 此处用一个空字符串,以便整个HTML片段都在新行严格对齐
+ '<article>'
+ '<h1>Title here</h1>'
+ '<p>This is a paragraph</p>'
+ '<footer>Complete</footer>'
+ '</article>';
// 也可使用数组来进行拼接,相对 + 更容易调整缩进。
var html = [
'<article>',
'<h1>Title here</h1>',
'<p>This is a paragraph</p>',
'<footer>Complete</footer>',
'</article>'
];
html = html.join('');
// 当参数过多时,将每个参数独立写在一行上,并将结束的右括号 ) 独立一行。
// 所有参数必须增加一个缩进。
foo(
aVeryVeryLongArgument,
anotherVeryLongArgument,
callback
);
// 也可以按逻辑对参数进行组合。
// 最经典的是baidu.format函数,调用时将参数分为“模板”和“数据”两块
baidu.format(
dateFormatTemplate,
year, month, date, hour, minute, second
);
// 当函数调用时,如果有一个或以上参数跨越多行,应当每一个参数独立一行。
// 这通常出现在匿名函数或者对象初始化等作为参数时,如setTimeout函数等。
setTimeout(
function () {
alert('hello');
},
200
);
order.data.read(
'id=' + me.model.id,
function (data) {
me.attchToModel(data.result);
callback();
},
300
);
// 链式调用较长时采用缩进进行调整。
$('#items')
.find('.selected')
.highlight()
.end();
// 三元运算符由3部分组成,因此其换行应当根据每个部分的长度不同,形成不同的情况。
var result = thisIsAVeryVeryLongCondition
? resultA : resultB;
var result = condition
? thisIsAVeryVeryLongResult
: resultB;
// 数组和对象初始化的混用,严格按照每个对象的 { 和结束 } 在独立一行的风格书写。
var array = [
{
// ...
},
{
// ...
}
];
if...else...
、try...catch...finally
等语句,推荐使用在 }
号后添加一个换行 的风格,使代码层次结构更清晰,阅读性更好。示例:
if (condition) {
// some statements;
}
else {
// some statements;
}
try {
// some statements;
}
catch (ex) {
// some statements;
}
if / else / for / do / while
语句中,即使只有一行,也不得省略块 {...}
。示例:
// good
if (condition) {
callFunc();
}
// bad
if (condition) callFunc();
if (condition)
callFunc();
示例:
// good
function funcName() {
}
// bad
function funcName() {
};
// 如果是函数表达式,分号是不允许省略的。
var funcName = function () {
};
IIFE
必须在函数表达式外添加 (
,非 IIFE
不得在函数表达式外添加 (
。解释:
IIFE = Immediately-Invoked Function Expression.
额外的 ( 能够让代码在阅读的一开始就能判断函数是否立即被调用,进而明白接下来代码的用途。而不是一直拖到底部才恍然大悟。
示例:
// good
var task = (function () {
// Code
return result;
})();
var func = function () {
};
// bad
var task = function () {
// Code
return result;
}();
var func = (function () {
});
变量
使用 Camel命名法
。示例:
var loadingModules = {};
常量
使用 全部字母大写,单词间下划线分隔
的命名方式。示例:
var HTML_ENTITY = {};
函数
使用 Camel命名法
。示例:
function stringFormat(source) {
}
参数
使用 Camel命名法
。示例:
function hear(theBells) {
}
类
使用 Pascal命名法
。示例:
function TextNode(options) {
}
方法 / 属性
使用 Camel命名法
。示例:
function TextNode(value, engine) {
this.value = value;
this.engine = engine;
}
TextNode.prototype.clone = function () {
return this;
};
枚举变量
使用 Pascal命名法
,枚举的属性
使用 全部字母大写,单词间下划线分隔
的命名方式。示例:
var TargetState = {
READING: 1,
READED: 2,
APPLIED: 3,
READY: 4
};
命名空间
使用 Camel命名法
。示例:
equipments.heavyWeapons = {};
示例:
function XMLParser() {
}
function insertHTML(element, html) {
}
var httpRequest = new HTTPRequest();
类名
使用 名词
。示例:
function Engine(options) {
}
函数名
使用 动宾短语
。示例:
function getStyle(element) {
}
boolean
类型的变量使用 is
或 has
开头。示例:
var isReady = false;
var hasMoreCommands = false;
Promise对象
用 动宾短语的进行时
表达。示例:
var loadingData = ajax.get('url');
loadingData.then(callback);
//
后跟一个空格,缩进与下一行被注释说明的代码一致。/*...*/
这样的多行注释。有多行注释内容时,使用多个单行注释。/**...*/
形式的块注释中。解释:
{
开始, 以}
结束。解释:
常用类型如:{string}, {number}, {boolean}, {Object}, {Function}, {RegExp}, {Array}, {Date}。
类型不仅局限于内置的类型,也可以是自定义的类型。比如定义了一个类 Developer,就可以使用它来定义一个参数和返回值的类型。
类型定义 | 语法示例 | 解释 |
---|---|---|
String | {string} | -- |
Number | {number} | -- |
Boolean | {boolean} | -- |
Object | {Object} | -- |
Function | {Function} | -- |
RegExp | {RegExp} | -- |
Array | {Array} | -- |
Date | {Date} | -- |
单一类型集合 | {Array.<string>} | string 类型的数组 |
多类型 | {(number|boolean)} | 可能是 number 类型, 也可能是 boolean 类型 |
允许为null | {?number} | 可能是 number, 也可能是 null |
不允许为null | {!Object} | Object 类型, 但不是 null |
Function类型 | {function(number, boolean)} | 函数, 形参类型 |
Function带返回值 | {function(number, boolean):string} | 函数, 形参, 返回值类型 |
参数可选 | @param {string=} name | 可选参数, =为类型后缀 |
可变参数 | @param {...number} args | 变长参数, ...为类型前缀 |
任意类型 | {*} | 任意类型 |
可选任意类型 | @param {*=} name | 可选参数,类型不限 |
可变任意类型 | @param {...*} args | 变长参数,类型不限 |
@file
标识文件说明。示例:
/**
* @file Describe the file
*/
@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)
*/
@namespace
标识。示例:
/**
* @namespace
*/
var util = {};
@class
标记类或构造函数。解释:
对于使用对象 constructor
属性来定义的构造函数,可以使用 @constructor
来标记。
示例:
/**
* 描述
*
* @class
*/
function Developer() {
// constructor body
}
@extends
标记类的继承信息。示例:
/**
* 描述
*
* @class
* @extends Developer
*/
function Fronteer() {
Developer.call(this);
// constructor body
}
util.inherits(Fronteer, Developer);
@lends
进行重新指向。解释:
没有 @lends
标记将无法为该类生成包含扩展类成员的文档。
示例:
/**
* 类描述
*
* @class
* @extends Developer
*/
function Fronteer() {
Developer.call(this);
// constructor body
}
util.extend(
Fronteer.prototype,
/** @lends Fronteer.prototype */{
_getLevel: function () {
// TODO
}
}
);
@public
/ @protected
/ @private
中的任意一个,指明可访问性。解释:
生成的文档中将有可访问性的标记,避免用户直接使用非 public
的属性或方法。
示例:
/**
* 类描述
*
* @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 () {
};
@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
};
}
@param
标识。示例:
/**
* 函数描述
*
* @param {Object} option 参数描述
* @param {string} option.url option项描述
* @param {string=} option.method option项描述,可选参数
*/
function foo(option) {
// TODO
}
@override
标识。如果重写的形参个数、类型、顺序和返回值类型均未发生变化,可省略 @param
、@return
,仅用 @override
标识,否则仍应作完整注释。解释:
简而言之,当子类重写的方法能直接套用父类的方法注释时可省略对参数与返回值的注释。
@event
标识事件,事件参数的标识与方法描述的参数标识相同。示例:
/**
* 值变更时触发
*
* @event
* @param {Object} e e描述
* @param {string} e.before before描述
* @param {string} e.after after描述
*/
onchange: function (e) {
}
@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'
}
);
};
@const
标记,并包含说明和类型信息。示例:
/**
* 常量说明
*
* @const
* @type {string}
*/
var REQUEST_URL = 'myurl.do';
@typedef
标识来定义。示例:
// `namespaceA~` 可以换成其它 namepaths 前缀,目的是为了生成文档中能显示 `@typedef` 定义的类型和链接。
/**
* 服务器
*
* @typedef {Object} namespaceA~Server
* @property {string} host 主机
* @property {number} port 端口
*/
/**
* 服务器列表
*
* @type {Array.<namespaceA~Server>}
*/
var servers = [
{
host: '1.2.3.4',
port: 8080
},
{
host: '1.2.3.5',
port: 8081
}
];
对于内部实现、不容易理解的逻辑说明、摘要信息等,我们可能需要编写细节注释。
示例:
function foo(p1, p2) {
// 这里对具体内部逻辑进行说明
// 说明太长需要换行
for (...) {
....
}
}
解释:
var
定义。解释:
不通过 var 定义变量将导致变量污染全局环境。
示例:
// good
var name = 'MyName';
// bad
name = 'MyName';
var
只能声明一个变量。解释:
一个 var 声明多个变量,容易导致较长的行长度,并且在修改时容易造成逗号和分号的混淆。
示例:
// good
var hangModules = [];
var missModules = [];
var visited = {};
// bad
var hangModules = [],
missModules = [],
visited = {};
即用即声明
,不得在函数或其它形式的代码块起始位置统一声明所有变量。解释:
变量声明与使用的距离越远,出现的跨度越大,代码的阅读与维护成本越高。虽然JavaScript的变量是函数作用域,还是应该根据编程中的意图,缩小变量出现的距离空间。
示例:
// 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;
}
===
。仅当判断 null 或 undefined 时,允许使用 == null
。解释:
使用 === 可以避免等于判断中隐式的类型转换。
示例:
// 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') {
// ......
}
解释:
按执行频率排列分支的顺序好处是:
switch
代替 if
。示例:
// 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') {
// ......
}
else
块后没有任何语句,可以删除 else
。示例:
// 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';
// ......
}
length
。解释:
虽然现代浏览器都对数组长度进行了缓存,但对于一些宿主对象和老旧浏览器的数组对象,在每次 length 访问时会动态计算元素个数,此时缓存 length 能有效提高程序性能。
示例:
for (var i = 0, len = elements.length; i < len; i++) {
var element = elements[i];
// ......
}
解释:
逆序遍历可以节省变量,代码比较优化。
示例:
var len = elements.length;
while (len--) {
var element = elements[len];
// ......
}
typeof
。对象类型检测使用 instanceof
。null
或 undefined
的检测使用 == null
。示例:
// 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'
string
时,使用 + ''
。示例:
// good
num + '';
// bad
new String(num);
num.toString();
String(num);
number
时,通常使用 +
。示例:
// good
+str;
// bad
Number(str);
string
转换成 number
,要转换的字符串结尾包含非数字并期望忽略时,使用 parseInt
。示例:
var width = '200px';
parseInt(width, 10);
parseInt
时,必须指定进制。示例:
// good
parseInt(str, 10);
// bad
parseInt(str);
boolean
时,使用 !!
。示例:
var num = 3.14;
!!num;
number
去除小数点,使用 Math.floor / Math.round / Math.ceil
,不使用 parseInt
。示例:
// good
var num = 3.14;
Math.ceil(num);
// bad
var num = 3.14;
parseInt(num, 10);
'
。解释:
示例:
var str = '我是一个字符串';
var html = '<div class="cls">拼接HTML可以省去双引号转义</div>';
数组
或 +
拼接字符串。解释:
示例:
// 使用数组拼接字符串
var str = [
// 推荐换行开始并缩进开始第一个字符串, 对齐代码, 方便阅读.
'<ul>',
'<li>第一项</li>',
'<li>第二项</li>',
'</ul>'
].join('');
// 使用 + 拼接字符串
var str2 = '' // 建议第一个为空字符串, 第二个换行开始并缩进开始, 对齐代码, 方便阅读
+ '<ul>',
+ '<li>第一项</li>',
+ '<li>第二项</li>',
+ '</ul>';
解释:
使用模板引擎有如下好处:
{}
创建新 Object
。示例:
// good
var obj = {};
// bad
var obj = new Object();
属性
均可以不添加引号,则所有 属性
不得添加引号。示例:
var info = {
name: 'someone',
age: 28
};
属性
需要添加引号,则所有 属性
必须添加 '
。解释:
如果属性不符合 Identifier 和 NumberLiteral 的形式,就需要以 StringLiteral 的形式提供。
示例:
// good
var info = {
'name': 'someone',
'age': 28,
'more-info': '...'
};
// bad
var info = {
name: 'someone',
age: 28,
'more-info': '...'
};
示例:
// 以下行为绝对禁止
String.prototype.trim = function () {
};
.
。解释:
属性名符合 Identifier 的要求,就可以通过 .
来访问,否则就只能通过 [expr]
方式访问。
通常在 JavaScript 中声明的对象,属性命名是使用 Camel 命名法,用 .
来访问更清晰简洁。部分特殊的属性(比如来自后端的JSON),可能采用不寻常的命名方式,可以通过 [expr]
方式访问。
示例:
info.age;
info['more-info'];
for in
遍历对象时, 使用 hasOwnProperty
过滤掉原型中的属性。示例:
var newInfo = {};
for (var key in info) {
if (info.hasOwnProperty(key)) {
newInfo[key] = info[key];
}
}
[]
创建新数组,除非想要创建的是指定长度的数组。示例:
// good
var arr = [];
// bad
var arr = new Array();
for in
。解释:
数组对象可能存在数字以外的属性, 这种情况下 for in 不会得到正确结果.
示例:
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);
}
sort
方法。解释:
自己实现的常规排序算法,在性能上并不优于数组默认的 sort 方法。以下两种场景可以自己实现排序:
.length = 0
。50
行以内。解释:
将过多的逻辑单元混在一个大函数中,易导致难以维护。一个清晰易懂的函数应该完成单一的逻辑单元。复杂的操作应进一步抽取,通过函数的调用来体现流程。
特定算法等不可分割的逻辑允许例外。
示例:
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();
}
}
6
个以内。解释:
除去不定长参数以外,函数具备不同逻辑意义的参数建议控制在 6 个以内,过多参数会导致维护难度增大。
options
参数传递非数据输入型参数。解释:
有些函数的参数并不是作为算法的输入,而是对算法的某些分支条件判断之用,此类参数建议通过一个 options 参数传递。
如下函数:
/**
* 移除某个元素
*
* @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();
}
}
这种模式有几个显著的优势:
null
。解释:
在 JavaScript 中,无需特别的关键词就可以使用闭包,一个函数可以任意访问在其定义的作用域外的变量。需要注意的是,函数的作用域是静态的,即在定义时决定,与调用的时机和方式没有任何关系。
闭包会阻止一些变量的垃圾回收,对于较老旧的JavaScript引擎,可能导致外部所有变量均无法回收。
首先一个较为明确的结论是,以下内容会影响到闭包内变量的回收:
Chakra、V8 和 SpiderMonkey 将受以上因素的影响,表现出不尽相同又较为相似的回收策略,而JScript.dll和Carakan则完全没有这方面的优化,会完整保留整个 LexicalEnvironment 中的所有变量绑定,造成一定的内存消耗。
由于对闭包内变量有回收优化策略的 Chakra、V8 和 SpiderMonkey 引擎的行为较为相似,因此可以总结如下,当返回一个函数 fn 时:
对于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--) {
tasks[len]();
}
以上代码对 tasks 中的函数的执行均会输出 Current cursor is at 5
,往往不符合预期。
此现象称为 Lift 效应 。解决的方式是通过额外加上一层闭包函数,将需要的外部变量作为参数传递来解除变量的绑定关系:
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--) {
tasks[len]();
}
new Function()
的形式。示例:
var emptyFunction = function () {};
示例:
var EMPTY_FUNCTION = function () {};
function MyClass() {
}
MyClass.prototype.abstractMethod = EMPTY_FUNCTION;
MyClass.prototype.hooks.before = EMPTY_FUNCTION;
MyClass.prototype.hooks.after = EMPTY_FUNCTION;
constructor
。解释:
通常使用其他 library 的类继承方案都会进行 constructor 修正。如果是自己实现的类继承方案,需要进行 constructor 修正。
示例:
/**
* 构建类之间的继承关系
*
* @param {Function} subClass 子类函数
* @param {Function} superClass 父类函数
*/
function inherits(subClass, superClass) {
var F = new Function();
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
}
constructor
的正确性。示例:
function Animal(name) {
this.name = name;
}
// 直接prototype等于对象时,需要修正constructor
Animal.prototype = {
constructor: Animal,
jump: function () {
alert('animal ' + this.name + ' jump');
}
};
// 这种方式扩展prototype则无需理会constructor
Animal.prototype.jump = function () {
alert('animal ' + this.name + ' jump');
};
解释:
原型对象的成员被所有实例共享,能节约内存占用。所以编码时我们应该遵守这样的原则:原型对象包含程序不会修改的成员,如方法函数或配置项。
function TextNode(value, engine) {
this.value = value;
this.engine = engine;
}
TextNode.prototype.clone = function () {
return this;
};
事件名
必须全小写。解释:
在 JavaScript 广泛应用的浏览器环境,绝大多数 DOM 事件名称都是全小写的。为了遵循大多数 JavaScript 开发者的习惯,在设计自定义事件时,事件名也应该全小写。
event
参数。如果事件需要传递较多信息,应仔细设计事件对象。解释:
一个事件对象的好处有:
解释:
常见禁止默认行为的方式有两种:
eval
函数。解释:
直接 eval,指的是以函数方式调用 eval 的调用方法。直接 eval 调用执行代码的作用域为本地作用域,应当避免。
如果有特殊情况需要使用直接 eval,需在代码中用详细的注释说明为何必须使用直接 eval,不能使用其它动态执行代码的方式,同时需要其他资深工程师进行 Code Review。
eval
函数。new Function
执行动态代码。解释:
通过 new Function 生成的函数作用域是全局使用域,不会影响当当前的本地作用域。如果有动态代码执行的需求,建议使用 new Function。
示例:
var handler = new Function('x', 'y', 'return x + y;');
var result = handler($('#x').val(), $('#y').val());
with
。解释:
使用 with 可能会增加代码的复杂度,不利于阅读和管理;也会对性能有影响。大多数使用 with 的场景都能使用其他方式较好的替代。所以,尽量不要使用 with。
delete
的使用。解释:
如果没有特别的需求,减少或避免使用delete
。delete
的使用会破坏部分 JavaScript 引擎的性能优化。
delete
可能产生的异常。解释:
对于有被遍历需求,且值 null 被认为具有业务逻辑意义的值的对象,移除某个属性必须使用 delete 操作。
在严格模式或IE下使用 delete 时,不能被删除的属性会抛出异常,因此在不确定属性是否可以删除的情况下,建议添加 try-catch 块。
示例:
try {
delete o.x;
}
catch (deleteError) {
o.x = null;
}
解释:
JavaScript 因其脚本语言的动态特性,当一个对象未被 seal 或 freeze 时,可以任意添加、删除、修改属性值。
但是随意地对 非自身控制的对象 进行修改,很容易造成代码在不可预知的情况下出现问题。因此,设计良好的组件、函数应该避免对外部传入的对象的修改。
下面代码的 selectNode 方法修改了由外部传入的 datasource 对象。如果 datasource 用在其它场合(如另一个 Tree 实例)下,会造成状态的混乱。
function Tree(datasource) {
this.datasource = datasource;
}
Tree.prototype.selectNode = function (id) {
// 从datasource中找出节点对象
var node = this.findNode(id);
if (node) {
node.selected = true;
this.flushView();
}
};
对于此类场景,需要使用额外的对象来维护,使用由自身控制,不与外部产生任何交互的 selectedNodeIndex 对象来维护节点的选中状态,不对 datasource 作任何修改。
function Tree(datasource) {
this.datasource = datasource;
this.selectedNodeIndex = {};
}
Tree.prototype.selectNode = function (id) {
// 从datasource中找出节点对象
var node = this.findNode(id);
if (node) {
this.selectedNodeIndex[id] = true;
this.flushView();
}
};
除此之外,也可以通过 deepClone 等手段将自身维护的对象与外部传入的分离,保证不会相互影响。
解释:
document.getElementById
获取,避免使用document.all
。context.getElementsByTagName
获取。其中 context
可以为 document
或其他元素。指定 tagName
参数为 *
可以获得所有子元素。解释:
原生获取元素集合的结果并不直接引用 DOM 元素,而是对索引进行读取,所以 DOM 结构的改变会实时反映到结果中。
示例:
<div></div>
<span></span>
<script>
var elements = document.getElementsByTagName('*');
// 显示为 DIV
alert(elements[0].tagName);
var div = elements[0];
var p = document.createElement('p');
docpment.body.insertBefore(p, div);
// 显示为 P
alert(elements[0].tagName);
</script>
children
。避免使用childNodes
,除非预期是需要包含文本、注释和属性类型的节点。getComputedStyle
或 currentStyle
。解释:
通过 style 只能获得内联定义或通过 JavaScript 直接设置的样式。通过 CSS class 设置的元素样式无法直接通过 style 获取。
解释:
除了 IE,标准浏览器会忽略不规范的属性值,导致兼容性问题。
DOM
时,尽量减少页面 reflow
。解释:
页面 reflow 是非常耗时的行为,非常容易导致性能瓶颈。下面一些场景会触发浏览器的reflow:
DOM
操作。解释:
DOM 操作也是非常耗时的一种操作,减少 DOM 操作有助于提高性能。举一个简单的例子,构建一个列表。我们可以用两种方式:
第一种方法看起来比较标准,但是每次循环都会对 DOM 进行操作,性能极低。在这里推荐使用第二种方法。
addEventListener / attachEvent
绑定事件,避免直接在 HTML 属性中或 DOM 的 expando
属性绑定事件处理。解释:
expando 属性绑定事件容易导致互相覆盖。
addEventListener
时第三个参数使用 false
。解释:
标准浏览器中的 addEventListener 可以通过第三个参数指定两种时间触发模型:冒泡和捕获。而 IE 的 attachEvent 仅支持冒泡的事件触发。所以为了保持一致性,通常 addEventListener 的第三个参数都为 false。