《重构2》一书中对重构进行了定义:

所谓重构(refactoring)是这样一个过程:在不扭转代码外在行为的前提下,对代码做出批改,以改良程序的内部结构。重构是一种经千锤百炼造成的井井有条的程序整顿办法,能够最大限度地减小整顿过程中引入谬误的概率。实质上说,重构就是在代码写好之后改良它的设计。

重构和性能优化有相同点,也有不同点。

雷同的中央是它们都在不改变程序性能的状况下批改代码;不同的中央是重构为了让代码变得更加容易了解、易于批改,性能优化则是为了让程序运行得更快。这里还得重点提一句,因为侧重点不同,重构可能使程序运行得更快,也可能使程序运行得更慢

重构能够一边写代码一边重构,也能够在程序写完后,拿出一段时间专门去做重构。没有说哪个形式更好,视集体状况而定。如果你专门拿一段时间来做重构,则倡议在重构一段代码后,立刻进行测试。这样能够防止批改代码太多,在出错时找不到谬误点。

重构的准则

  1. 事不过三,三则重构。即不能反复写同样的代码,在这种状况下要去重构。
  2. 如果一段代码让人很难看懂,那就该思考重构了。
  3. 如果曾经了解了代码,然而十分繁琐或者不够好,也能够重构。
  4. 过长的函数,须要重构。
  5. 一个函数最好对应一个性能,如果一个函数被塞入多个性能,那就要对它进行重构了。(4 和 5 不抵触)
  6. 重构的关键在于使用大量渺小且放弃软件行为的步骤,一步步达成大规模的批改。每个独自的重构要么很小,要么由若干小步骤组合而成。

重构的手法

在《重构2》这本书中,介绍了多达上百种重构手法。但我感觉以下八种是比拟罕用的:

  1. 提取反复代码,封装成函数
  2. 拆分性能太多的函数
  3. 变量/函数改名
  4. 替换算法
  5. 以函数调用取代内联代码
  6. 挪动语句
  7. 折分嵌套条件表达式
  8. 将查问函数和批改函数拆散

提取反复代码,封装成函数

假如有一个查问数据的接口 /getUserData?age=17&city=beijing。当初须要做的是把用户数据:{ age: 17, city: 'beijing' } 转成 URL 参数的模式:

let result = ''const keys = Object.keys(data)  // { age: 17, city: 'beijing' }keys.forEach(key => {    result += '&' + key + '=' + data[key]})result.substr(1) // age=17&city=beijing

如果只有这一个接口须要转换,不封装成函数是没问题的。但如果有多个接口都有这种需要,那就得把它封装成函数了:

function JSON2Params(data) {    let result = ''    const keys = Object.keys(data)    keys.forEach(key => {        result += '&' + key + '=' + data[key]    })    return result.substr(1)}

拆分性能太多的函数

上面是一个打印账单的程序:

function printBill(data = []) {    // 汇总数据    const total = {}    data.forEach(item => {        if (total[item.department] === undefined) {            total[item.department] = 0        }        total[item.department] += item.value    })    // 打印汇总后的数据    const keys = Object.keys(total)    keys.forEach(key => {        console.log(`${key} 部门:${total[key]}`)    })}printBill([    {        department: '销售部',        value: 89,    },    {        department: '后勤部',        value: 132,    },    {        department: '财务部',        value: 78,    },    {        department: '总经办',        value: 90,    },    {        department: '后勤部',        value: 56,    },    {        department: '总经办',        value: 120,    },])

能够看到这个 printBill() 函数实际上蕴含有两个性能:汇总和打印。咱们能够把汇总数据的代码提取进去,封装成一个函数。这样 printBill() 函数就只须要关注打印性能了。

function printBill(data = []) {    const total = calculateBillData(data)    const keys = Object.keys(total)    keys.forEach(key => {        console.log(`${key} 部门:${total[key]}`)    })}function calculateBillData(data) {    const total = {}    data.forEach(item => {        if (total[item.department] === undefined) {            total[item.department] = 0        }        total[item.department] += item.value    })    return total}

变量/函数改名

无论是变量命名,还是函数命名,都要尽量让他人明确你这个变量/函数是干什么的。变量命名的规定着重于形容“是什么”,函数命名的规定着重于形容“做什么”。

变量

const a = width * height

下面这个变量就不太好,a 很难让人看进去它是什么。

const area = width * height

改成这样就很好了解了,原来这个变量是示意面积。

函数

function cache(data) {    const result = []    data.forEach(item => {        if (item.isCache) {            result.push(item)        }    })    return result}

这个函数名称会让人很纳闷,cache 代表什么?是设置缓存还是删除缓存?再一细看代码,噢,原来是获取缓存数据。所以这个函数名称改成 getCache() 更加适合。

替换算法

function foundPersonData(person) {    if (person == 'Tom') {        return {            name: 'Tom',            age: 18,            id: 21,        }    }    if (person == 'Jim') {        return {            name: 'Jim',            age: 20,            id: 111,        }    }    if (person == 'Lin') {        return {            name: 'Lin',            age: 19,            id: 10,        }    }    return null}

下面这个函数的性能是依据用户姓名查找用户的详细信息,能够看到这个函数做了三次 if 判断,如果没找到数据就返回 null。这个函数不利于扩大,每多一个用户就得多写一个 if 语句,咱们能够用更不便的“查找表”来替换它。

function foundPersonData(person) {    const data = {        'Tom': {            name: 'Tom',            age: 18,            id: 21,        },        'Jim': {            name: 'Jim',            age: 20,            id: 111,        },        'Lin': {            name: 'Lin',            age: 19,            id: 10,        },    }    return data[person] || null}

批改后代码构造看起来更加清晰,也不便将来做扩大。

以函数调用取代内联代码

如果一些代码所做的事件和已有函数的性能反复,那就最好用函数调用来取代这些代码。

let hasApple = falsefor (const fruit of fruits) {    if (fruit == 'apple') {        hasApple = true        break    }}

例如下面的代码,能够用数组的 includes() 办法代替:

const hasApple = fruits.includes('apple')

批改后代码更加简洁。

挪动语句

让存在关联的货色一起呈现,能够使代码更容易了解。如果有一些代码都是作用在一个中央,那么最好是把它们放在一起,而不是夹杂在其余的代码两头。最简略的状况下,只需应用挪动语句就能够让它们汇集起来。就像上面的示例一样:

const name = getName()const age = getAge()let revenueconst address = getAddress()// ...
const name = getName()const age = getAge()const address = getAddress()let revenue// ...

因为两块数据区域的性能是不同的,所以除了挪动语句外,我还在它们之间空了一行,这样让人更容易辨别它们之间的不同。

折分嵌套条件表达式

当很多的条件表达式嵌套在一起时,会让代码变得很难浏览:

function getPayAmount() {    if (isDead) {        return deadAmount()    } else {        if (isSeparated) {            return separatedAmount()        } else if (isRetired) {            return retireAmount()        } else {            return normalAmount()        }    }}
function getPayAmount() {    if (isDead) return deadAmount()    if (isSeparated) return separatedAmount()    if (isRetired) return retireAmount()    return normalAmount()}

将条件表达式拆分后,代码的可浏览性大大加强了。

将查问函数和批改函数拆散

个别的查问函数都是用于取值的,例如 getUserData()getAget()getName() 等等。有时候,咱们可能为了不便,在查问函数上附加其余性能。例如上面的函数:

function getValue() {    let result = 0    this.data.forEach(val => result += val)    // 这里插入了一个奇怪的操作    sendBill()    return result}

千万不要这样做,函数很重要的性能是职责拆散。所以咱们要将它们离开:

function getValue() {    let result = 0    this.data.forEach(val => result += val)    return result}function sendBill() {    // ...}

这样函数的性能就很清晰了。

小结

古人云:尽信书,不如无书。《重构2》也不例外,在看这本书的时候肯定要带着批判性的眼光去浏览它。

外面介绍的重构手法有很多,多达上百种,但这些手法不肯定实用所有人。所以肯定要有取舍,将外面有用的手法摘抄下来,时不时的看几遍。这样在写代码时,重构能力像呼吸一样天然,即应用了你也不晓得。

参考资料

  • 《重构2》

带你入门前端工程 全文目录:

  1. 技术选型:如何进行技术选型?
  2. 对立标准:如何制订标准并利用工具保障标准被严格执行?
  3. 前端组件化:什么是模块化、组件化?
  4. 测试:如何写单元测试和 E2E(端到端) 测试?
  5. 构建工具:构建工具有哪些?都有哪些性能和劣势?
  6. 自动化部署:如何利用 Jenkins、Github Actions 自动化部署我的项目?
  7. 前端监控:解说前端监控原理及如何利用 sentry 对我的项目履行监控。
  8. 性能优化(一):如何检测网站性能?有哪些实用的性能优化规定?
  9. 性能优化(二):如何检测网站性能?有哪些实用的性能优化规定?
  10. 重构:为什么做重构?重构有哪些手法?
  11. 微服务:微服务是什么?如何搭建微服务项目?
  12. Severless:Severless 是什么?如何应用 Severless?