可维护性
什么是可维护的代码
- 容易理解: 无需借助原始开发者, 任何人一看代码就知道他是干什么的,以及他是怎么实现的
- 符合常识:代码中的一切都显得顺利成章,无论操作有多么复杂
- 容易适配:即使数据发生变化也不用完全重写
- 容易扩展:代码架构经过认真设计,支持未来扩展核心功能。
- 容易调试:出问题时,代码可以给出明确的信息,通过他能直接定位问题
编码规范
可读性
- 代码缩进: 通常使用空格而不是 tab, 因为 tab 在不同编辑器中显示不同。
- 代码注释: 函数和方法, 大型代码块, 复杂的算法, 黑科技. 这些都需要加上注释可以让代码更容易理解和维护.
变量和函数命名
- 变量名应该是名词且具有语义, 例如 car, person.
- 函数名应该以动词开始, 例如 getName(). 返回布尔值的函数通常以 is 开头,比如 isEnabled().
- 对变量和函数都使用符合逻辑的名称,不用担心长度. 尽量用描述性和直观的词汇,但不要过于冗余.
- 变量,函数和方法应该以小写字母开头,使用驼峰大小写.如 getName, isPerson.
- 类名应该首字母大写,如 Person.
- 常量值应该是全部大写并以下划线相接,如 REQUEST_TIMEOUT.
松散耦合
解耦 HTML/JavaScript
- 不要把 JS 直接嵌入到 HTML 中.
<script>
document.write('hello js')
</script>
- 不要使用 HTML 属性添加事件处理程序
<input type = 'button' value = 'Click' onclick = 'doSomething()'>
- 避免在 js 中创建大量的 HTML.
解耦 CSS/JavaScript
- 避免使用 js 修改 css 样式
element.style.color = 'red'
保证层与层之间的适当分离至关重要, 显示出问题应该只到 css 文件中找问题. 行为出问题到 js 文件中找问题. 这些层之间的松散耦合可以提升应用程序的可维护性.
解耦应用程序逻辑和事件处理程序
// bad
function handleKeyPress(event) {
if(event.keyCode === 13) {
const target = event.target
const value = 5 * parseInt(target.value)
if(value > 10) {
document.getElementById('errorMsg').style.display = 'block'
}
}
}
// good
function validateValue(value) {
value = 5 * parseInt(target.value)
if(value > 10) {
document.getElementById('errorMsg').style.display = 'block'
}
}
function handleKeyPress(event) {
if(event.keyCode === 13) {
const target = event.target
validateValue(target.value)
}
}
解耦应用程序逻辑和事件处理逻辑应注意以下几点
- 不要把 event 对象传给其他方法, 而是只传入需要的数据, 例如 event.target.value
- 应用程序中每个可能的操作都应该无须事件处理程序就可以执行
- 事件处理程序应该处理事件, 而把后续处理交给应用程序
编码惯例
尊重对象所有权
- 创建包含想要功能的新对象,通过他与别人的对象交互. 不要给实例和原型添加属性
- 创建新自定义类型继承本来想修改的类型,可以给自定义类型添加新功能. 不要给实例或原型添加方法.
- 不要重定义已有的方法
不使用全局对象,而使用命名空间
// bad
const name = 'ming'
function getName() {
console.log(name)
}
// good
const myApp = {
name: 'ming',
getName() {
console.log(this.name)
}
}
使用常量
依赖常量的目标是从应用程序逻辑中分离数据,以便修改数据时不会引发错误.
- 重复出现的值
- 用户界面字符串
- URL
- 任何可能变化的值. 代码中出现的字面量值是否可能会变,会, 就应该把他提取到常量中
性能
作用域意识
避免全局查找
// bad 这个例子中 3 个地方引用到全局 document 对象,每次都要遍历一次作用域链.
function updateUI() {
const imgs = document.getElementsByTagName('img')
for(let i = 0, len = imgs.length; i < len; i++) {
imgs[i].title = `${document.title} image ${i}`
}
const msg = document.getElementById('msg')
msg.innerHTML = 'Update Complete.'
}
// good 通过在局部作用域中保存 document 对象的引用,能够明显提高这个函数的性能,因为只需要局部作用域链中查找
function updateUI() {
const docObj = document
const imgs = docObj.getElementsByTagName('img')
for(let i = 0, len = imgs.length; i < len; i++) {
imgs[i].title = `${docObj.title} image ${i}`
}
const msg = docObj.getElementById('msg')
msg.innerHTML = 'Update Complete.'
}
不使用 with
with 会创建自己的作用域
// bad
function updateUI () {
with(document.body) {
console.log(tagName) // document.body.tagName
innerHTML = 'HELLO BODY' // document.body.innerHTML
}
}
// good
// 语句更容易理解, 因为 tagName 和 innerHTML 属于谁更明确
function updateUI () {
const body = document.body
console.log(body.tagName)
body.innerHTML = 'HELLO BODY'
}
选择正确的方法
避免不必要的属性查找
// bad 6 次属性查找
const query = window.location.href.substring(window.location.href.indexOf('?'))
// good
// 4 次属性查找,比之前节省了 33%
const url = window.location.href
const query = url.substring(url.indexOf('?'))
通常,只要能够降低算法复杂度,就应该尽量通过在局部变量中保存值来替代属性查找. 另外, 如果实现某个需求既可以使用数组的数值索引, 又可以使用命名属性, 那就应该使用数值索引
-
优化循环
-
简化终止条件
-
简化循环体
-
使用后测试循环. for 和 while 属于先测试循环, do-while 是后测试循环
使用后测试循环一定是至少有一个值需要处理一次.如果这里的数组是空的,那么就会浪费一次测试
// bad
// 1 . 每次循环都要计算一次终止条件 values.length
for(let i = 0; i < values.length; i++) {
process(values[i])
}
// good
for(let i = 0, len = values.length; i < len; i++) {
process(values[i])
}
展开循环
如果循环是有限次的, 那么通常抛弃循环而直接多次调用函数会更快.
// 如果 values.length 是 3
process(values[0])
process(values[1])
process(values[2])
如果次数不能提前预知的,可以使用达夫设备(Duff's Device)的技术. 达夫设备的思路是以8的倍数作为迭代次数从而将循环展开为一系列语句.
- 达夫设备
let interations = Math.ceil(values.length / 8)
let startAt = values.length % 8
let i = 0
do {
switch(startAt) {
case 0: process(values[i++])
case 7: process(values[i++])
case 6: process(values[i++])
case 5: process(values[i++])
case 4: process(values[i++])
case 3: process(values[i++])
case 2: process(values[i++])
case 1: process(values[i++])
}
startAt = 0
}while(--iterations > 0)
例如数组长度为 10 , startAt 等于2, 命中case: 2, 且每一个 case 后没有 break, 因此第一循环只会调用 2 次 process.value([i++]),
- 更快的达夫设备实现(比原始的实现快 40% )
let iterations = Math.floor(values.length / 8)
let leftover = values.length % 8
let i = 0
if(leftover > 0) {
do {
process(values[i++])
}while(--leftover > 0)
}
do {
process(values[i++])
process(values[i++])
process(values[i++])
process(values[i++])
process(values[i++])
process(values[i++])
process(values[i++])
process(values[i++])
}while(iterationss-- > 0)
避免重复解释
- 避免使用 eval
- 避免使用 new Function 创建函数
- 避免使用字符串设置超时函数
// bad
eval('console.log("hello eval")')
const sayHi = new Function('console.log("hello new function")')
setTimeout('console.log("hello setTimeout")', 100)
其他性能优化注意事项
- 原生方法很快, 原生方法是使用 C 或 C++ 等编译型语言写的, 因此比 JavaScript 写的方法快的多. Math 对象上那些执行复杂数学运算的方法, 比执行相同的 js 函数快的多.
- switch 语句很快. 如果代码中有大量的 if-else 语句可以转换成 switch-case 语句, 把最可能的 case 放前面可以进一步提升性能
- 位操作很快. 像求模, 逻辑与,逻辑或 都很适合替换成位操作
语句最少化
多个变量声明
// bad
let count = 0;
let name = 'ming'
let now = Date.now();
// good
let count = 0, name = 'ming', now = Date.now();
插入迭代性值
// bad
let name = values[i]
i++
// good
let name = values[i++]
减少代码中的语句量不是绝对的法则,一味追求语句最少化,可能导致一条语句容纳过多的逻辑,最终难以理解
优化 DOM 交互
- 实时更新最小化 通过代码片段, 修改完后一次更新.
- 使用 innerHTML, 在给 innerHTML 赋值时,后台会创建 HTML 解析器, 然后使用原生 DOM 调用而不是 js 的 DOM 方法来创建 DOM 结构, 原生 DOM 方法更快, 因为该方法是执行编译代码而非解释代码.
- 使用事件委托