语言特性
更新时间:
变量
推荐级别:[建议] 变量、函数在使用前必须先定义。
解释:
不通过 var 定义变量将导致变量污染全局环境。
反例
name = 'MyName'
正例
var name = 'MyName'
原则上不建议使用全局变量,对于已有的全局变量或第三方框架引入的全局变量,需要根据检查工具的语法标识。
示例:
/* globals jQuery */
var element = jQuery('#element-id')
推荐级别:[强烈] 每个 var 只能声明一个变量。
解释:
一个 var 声明多个变量,容易导致较长的行长度,并且在修改时容易造成逗号和分号的混淆。
反例
var hangModules = [],
missModules = [],
visited = {}
正例
var hangModules = []
var missModules = []
var visited = {}
推荐级别:[强烈] 变量必须 即用即声明,不得在函数或其它形式的代码块起始位置统一声明所有变量。
解释:
变量声明与使用的距离越远,出现的跨度越大,代码的阅读与维护成本越高。虽然 JavaScript 的变量是函数作用域,还是应该根据编程中的意
图,缩小变量出现的距离空间。
反例
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
}
正例
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
}
条件
推荐级别:[强烈] 在 Equality Expression 中使用类型严格的 ===。仅当判断 null 或 undefined 时,允许使用 == null。
解释:
使用 === 可以避免等于判断中隐式的类型转换。
反例
if (age == 30) {
// ......
}
正例
if (age === 30) {
// ......
}
推荐级别:[建议] 尽可能使用简洁的表达式。
反例
// 字符串为空
if (name === '') {
// ......
}
正例
// 字符串为空
if (!name) {
// ......
}
反例
// 字符串非空
if (name !== '') {
// ......
}
正例
// 字符串非空
if (name) {
// ......
}
反例
// 数组非空
if (collection.length > 0) {
// ......
}
正例
// 数组非空
if (collection.length) {
// ......
}
反例
// 布尔不成立
if (notTrue === false) {
// ......
}
正例
// 布尔不成立
if (!notTrue) {
// ......
}
反例
// null 或 undefined
if (noValue === null || typeof noValue === 'undefined') {
// ......
}
正例
// 布尔不成立
if (noValue == null) {
// ......
}
推荐级别:[建议] 按执行频率排列分支的顺序。
解释:
按执行频率排列分支的顺序好处是:
1.阅读的人容易找到最常见的情况,增加可读性。
2.提高执行效率。
推荐级别:[建议] 对于相同变量或表达式的多值条件,用 switch 代替 if。
反例
var type = typeof variable
if (type === 'object') {
// ......
} else if (type === 'number' || type === 'boolean' || type === 'string') {
// ......
}
正例
switch (typeof variable) {
case 'object':
// ......
break
case 'number':
case 'boolean':
case 'string':
// ......
break
}
推荐级别:[建议] 如果函数或全局中的 else 块后没有任何语句,可以删除 else。
反例
function getName() {
if (name) {
return name
} else {
return 'unnamed'
}
}
正例
function getName() {
if (name) {
return name
}
return 'unnamed'
}
循环
推荐级别:[建议] 不要在循环体中包含函数表达式,事先将函数提取到循环体外。
解释:
循环体中的函数表达式,运行过程中会生成循环次数个函数对象。
反例
for (var i = 0, len = elements.length; i < len; i++) {
var element = elements[i]
addListener(element, 'click', function () {})
}
正例
function clicker() {
// ......
}
for (var i = 0, len = elements.length; i < len; i++) {
var element = elements[i]
addListener(element, 'click', clicker)
}
推荐级别:[建议] 对循环内多次使用的不变值,在循环外用变量缓存。
反例
for (var i = 0, len = elements.length; i < len; i++) {
var element = elements[i]
element.style.width = wrap.offsetWidth + 'px'
// ......
}
正例
var width = wrap.offsetWidth + 'px'
for (var i = 0, len = elements.length; i < len; i++) {
var element = elements[i]
element.style.width = width
// ......
}
推荐级别:[建议] 对有序集合进行遍历时,缓存 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 时,使用 + ''。
反例
new String(num)
num.toString()
String(num)
正例
num + ''
推荐级别:[建议] 转换成 number 时,通常使用 +。
反例
Number(str)
正例
;+str
推荐级别:[建议] string 转换成 number,要转换的字符串结尾包含非数字并期望忽略时,使用 parseInt。
示例
var width = '200px'
parseInt(width, 10)
推荐级别:[强烈] 使用 parseInt 时,必须指定进制。
反例
parseInt(str)
正例
parseInt(str, 10)
推荐级别:[建议] 转换成 boolean 时,使用 !!。
示例:
var num = 3.14
!!num
推荐级别:[建议] number 去除小数点,使用 Math.floor / Math.round / Math.ceil,不使用 parseInt。
反例
var num = 3.14
parseInt(num, 10)
正例
var num = 3.14
Math.ceil(num)
对象
推荐级别:[强烈] 使用对象字面量 {} 创建新 Object。
反例
var obj = new Object()
正例
var obj = {}
推荐级别:[建议] 对象创建时,如果一个对象的所有 属性 均可以不添加引号,建议所有 属性 不添加引号。
示例
var info = {
name: 'someone',
age: 28,
}
推荐级别:[建议] 对象创建时,如果任何一个 属性 需要添加引号,则所有 属性 建议添加 '。
解释:
如果属性不符合 Identifier 和 NumberLiteral 的形式,就需要以 StringLiteral 的形式提供。
反例
var info = {
name: 'someone',
age: 28,
'more-info': '...',
}
正例
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]
}
}
数组
推荐级别:[强烈] 使用数组字面量 [] 创建新数组,除非想要创建的是指定长度的数组。
反例
var arr = new Array()
正例
var arr = []
推荐级别:[强烈] 遍历数组不使用 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 方法。以下两种场景可以自己实现排序:
1.需要稳定的排序算法,达到严格一致的排序结果。
2.数据特点鲜明,适合使用桶排。
推荐级别:[建议] 清空数组使用 .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 参数传递非数据输入型参数。
反例
/**
* 移除某个元素
*
* @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.before = EMPTY_FUNCTION
MyClass.prototype.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。
动态特性
eval
推荐级别:[强烈] 避免使用直接 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 的场景都能使用其他方式较好的替代。所以,尽量不要使用 with。
delete
推荐级别:[建议] 减少 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 等手段将自身维护的对象与外部传入的分离,保证不会相互影响。
推荐级别:[建议] 具备强类型的设计。
解释
- 如果一个属性被设计为 boolean 类型,则不要使用 1 或 0 作为其值。对于标识性的属性,如对代码体积有严格要求,可以从一开始就设计为 number 类型且将 0 作为否定值。
- 从 DOM 中取出的值通常为 string 类型,如果有对象或函数的接收类型为 number 类型,提前作好转换,而不是期望对象、函数可以处理多类型的值。