变量

[强制] 变量、函数在使用前必须先定义。
// 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;
}

条件

[强制] 在 Equality Expression 中使用类型严格的 ===。仅当判断 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);

字符串

[强制] 字符串开头和结束使用单引号 '。

解释:

输入单引号不需要按住 shift,方便输入。

实际使用中,字符串经常用来拼接 HTML。为方便 HTML 中包含双引号而不需要转义写法。

var str = '我是一个字符串';
var html = '<div class="cls">拼接HTML可以省去双引号转义</div>';
[建议] 使用 数组 或 + 拼接字符串。

解释:

使用 + 拼接字符串,如果拼接的全部是 StringLiteral,压缩工具可以对其进行自动合并的优化。所以,静态字符串建议使用 + 拼接。

在现代浏览器下,使用 + 拼接字符串,性能较数组的方式要高。

如需要兼顾老旧浏览器,应尽量使用数组拼接字符串。

// 使用数组拼接字符串
var str = [
// 推荐换行开始并缩进开始第一个字符串, 对齐代码, 方便阅读.
'<ul>',
'<li>第一项</li>',
'<li>第二项</li>',
'</ul>'
].join(''); // 使用 `+` 拼接字符串
var str2 = '' // 建议第一个为空字符串, 第二个换行开始并缩进开始, 对齐代码, 方便阅读
+ '<ul>',
+ '<li>第一项</li>',
+ '<li>第二项</li>',
+ '</ul>';
[建议] 使用字符串拼接的方式生成HTML,需要根据语境进行合理的转义。

解释:

在 JavaScript 中拼接,并且最终将输出到页面中的字符串,需要进行合理转义,以防止安全漏洞。下面的示例代码为场景说明,不能直接运行。

// HTML 转义
var str = '<p>' + htmlEncode(content) + '</p>'; // HTML 转义
var str = '<input type="text" value="' + htmlEncode(value) + '">'; // URL 转义
var str = '<a href="/?key=' + htmlEncode(urlEncode(value)) + '">link</a>'; // JavaScript字符串 转义 + HTML 转义
var str = '<button onclick="check(\'' + htmlEncode(strLiteral(name)) + '\')">提交</button>';

对象

[强制] 使用对象字面量 {} 创建新 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': '...'
};
[建议] 属性访问时,尽量使用 .。

解释:

属性名符合 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'];

// 这里仅作演示, 实际中应使用 Object 类型
arr.other = 'other things'; // 正确的遍历方式
for (var i = 0, len = arr.length; i < len; i++) {
console.log(i);
} // 错误的遍历方式
for (var 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 = '';
submitButton.disabled = false;
}
else {
warning.innerText = 'Please enter it';
submitButton.disabled = true;
}
} // 直接阅读该函数会难以明确其主线逻辑,因此下方是一种更合理的表达方式: function syncViewStateOnUserAction() {
syncXStateToView();
checkAAvailability();
} function syncXStateToView() {
y.checked = x.checked; if (x.checked) {
z.value = '';
}
} function checkAAvailability() {
if (a.value) {
clearWarnignForA();
}
else {
displayWarningForAMissing();
}
}
[建议] 一个函数的参数控制在 6 个以内。

解释:

除去不定长参数以外,函数具备不同逻辑意义的参数建议控制在 6 个以内,过多参数会导致维护难度增大。

某些情况下,如使用 AMD Loader 的 require 加载多个模块时,其 callback 可能会存在较多参数,因此对函数参数的个数不做强制限制。

[建议] 通过 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();
}
}
这种模式有几个显著的优势: boolean 型的配置项具备名称,从调用的代码上更易理解其表达的逻辑意义。
当配置项有增长时,无需无休止地增加参数个数,不会出现 removeElement(element, true, false, false, 3) 这样难以理解的调用代码。
当部分配置参数可选时,多个参数的形式非常难处理重载逻辑,而使用一个 options 对象只需判断属性是否存在,实现得以简化。

闭包

[建议] 在适当的时候将闭包内大对象置为 null。

解释:

在 JavaScript 中,无需特别的关键词就可以使用闭包,一个函数可以任意访问在其定义的作用域外的变量。需要注意的是,函数的作用域是静态的,即在定义时决定,与调用的时机和方式没有任何关系。

闭包会阻止一些变量的垃圾回收,对于较老旧的 JavaScript 引擎,可能导致外部所有变量均无法回收。

首先一个较为明确的结论是,以下内容会影响到闭包内变量的回收:

嵌套的函数中是否有使用该变量。

嵌套的函数中是否有 直接调用eval。

是否使用了 with 表达式。

Chakra、V8 和 SpiderMonkey 将受以上因素的影响,表现出不尽相同又较为相似的回收策略,而 JScript.dll 和 Carakan 则完全没有这方面的优化,会完整保留整个 LexicalEnvironment 中的所有变量绑定,造成一定的内存消耗。

由于对闭包内变量有回收优化策略的 Chakra、V8 和 SpiderMonkey 引擎的行为较为相似,因此可以总结如下,当返回一个函数 fn 时:

  1. 如果 fn 的 [[Scope]] 是 ObjectEnvironment(with 表达式生成 ObjectEnvironment,函数和 catch 表达式生成 DeclarativeEnvironment),则:

    如果是 V8 引擎,则退出全过程。

    如果是 SpiderMonkey,则处理该 ObjectEnvironment 的外层 LexicalEnvironment。
  2. 获取当前 LexicalEnvironment 下的所有类型为 Function 的对象,对于每一个 Function 对象,分析其 FunctionBody:

    如果 FunctionBody 中含有 直接调用 eval,则退出全过程。

    否则得到所有的 Identifier。

    对于每一个 Identifier,设其为 name,根据查找变量引用的规则,从 LexicalEnvironment 中找出名称为 name 的绑定 binding。

    对 binding 添加 notSwap 属性,其值为 true。
  3. 检查当前 LexicalEnvironment 中的每一个变量绑定,如果该绑定有 notSwap 属性且值为 true,则:

    如果是 V8 引擎,删除该绑定。

    如果是 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--) {
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 参数。如果事件需要传递较多信息,应仔细设计事件对象。

解释:

一个事件对象的好处有:

顺序无关,避免事件监听者需要记忆参数顺序。

每个事件信息都可以根据需要提供或者不提供,更自由。

扩展方便,未来添加事件信息时,无需考虑会破坏

[建议] 设计自定义事件时,应考虑禁止默认行为。

解释:

常见禁止默认行为的方式有两种:

事件监听函数中 return false。

事件对象中包含禁止默认行为的方法,如 preventDefault。

禁止使用evail推荐使用new Function()
[建议] 减少 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 等手段将自身维护的对象与外部传入的分离,保证不会相互影响。

DOM

[建议] 对于单个元素,尽可能使用 document.getElementById 获取,避免使用document.all。
[建议] 对于多个元素的集合,尽可能使用 context.getElementsByTagName 获取。其中 context 可以为 document 或其他元素。指定 tagName 参数为 * 可以获得所有子元素。
[建议] 遍历元素集合时,尽量缓存集合长度。如需多次操作同一集合,则应将集合转为数组。
[建议] 获取元素的直接子元素时使用 children。避免使用childNodes,除非预期是需要包含文本、注释和属性类型的节点。

解释:

原生获取元素集合的结果并不直接引用 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>
[建议] 获取元素实际样式信息时,应使用 getComputedStyle 或 currentStyle。

解释:

通过 style 只能获得内联定义或通过 JavaScript 直接设置的样式。通过 CSS class 设置的元素样式无法直接通过 style 获取。

[建议] 尽可能通过为元素添加预定义的 className 来改变元素样式,避免直接操作 style 设置
[强制] 通过 style 对象设置元素样式时,对于带单位非 0 值的属性,不允许省略单位。

解释:

除了 IE,标准浏览器会忽略不规范的属性值,导致兼容性问题。

[建议] 操作 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 进行操作,性能极低。在这里推荐使用第二种方法。

最新文章

  1. iOS三种正则表达式
  2. MongoDB碎碎念
  3. sqlserver如何创建镜像图文教程(转)
  4. vs2015 企业版 专业版 密钥
  5. Android之Activity的生命周期
  6. chrome升级后LODOP打印插件无法使用
  7. 学习SQLite之路(四)
  8. Python2.7的安装
  9. django重量级web框架
  10. 导航栏视图设置 tabbleView 是设置总背景图
  11. spring整合freemarker 自定义标签
  12. codeforces GYM 100114 J. Computer Network 无相图缩点+树的直径
  13. HDU 2196-Computer(树形dp)
  14. Chrome Browser
  15. Spark1.0.0 属性配置
  16. 设计模式-GoF23
  17. 2016NEFU集训第n+3场 E - New Reform
  18. Composer PHP依赖管理的新时代
  19. 【openstack N版】——手把手教你制作生产环境镜像
  20. Serverless无服务应用架构纵横谈

热门文章

  1. java2 - 语言基础
  2. 深入研究Sphinx的底层原理和高级使用
  3. JS中的Undefined和Null的区别
  4. 浏览器的 bfcache 特性
  5. UVA - 11270 轮廓线DP
  6. 【BZOJ3993】 星际战争
  7. Java并发编程笔记1-竞争条件&amp;初识原子类&amp;可重入锁
  8. 64位Kali无法顺利执行pwn1问题的解决方案
  9. 网络基础tcp/ip协议二
  10. 裁剪Ubuntu内核和模块管理2