1 . 高程 4 - 最佳实践

329 阅读5分钟

可维护性

什么是可维护的代码

  • 容易理解: 无需借助原始开发者, 任何人一看代码就知道他是干什么的,以及他是怎么实现的
  • 符合常识:代码中的一切都显得顺利成章,无论操作有多么复杂
  • 容易适配:即使数据发生变化也不用完全重写
  • 容易扩展:代码架构经过认真设计,支持未来扩展核心功能。
  • 容易调试:出问题时,代码可以给出明确的信息,通过他能直接定位问题

编码规范

可读性

  • 代码缩进: 通常使用空格而不是 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 方法更快, 因为该方法是执行编译代码而非解释代码.
  • 使用事件委托