JavaScript代码重构

背景

到目前,JavaScript常见的设计模式系列我已经写得差不多了。在设计模式的一系列文章中,总是先写一段反例代码,然后再通过设计模式重构之前的代码,这种强烈的对比会加深我们对该设计模式的理解。

设计模式和重构之间有着与生俱来的关系,从某种角度来看,设计模式的目的就是为了重构代码。在日常开发中,除了使用设计模式进行重构之外,还有一些常见的容易忽略的细节,这些细节可以帮助我们写出更好的、容易维护的代码。下面我们一一介绍这些细节。

提炼函数

在日常开发中,我们大部分时间都是跟函数打交道,所以我们希望这些函数有着良好的命名,函数的功能单一,函数的逻辑清晰明了。如果一个函数过长,而且需要加注释才能让这个函数易读,那这个函数就很有必要进行重构了。把代码独立出来,封装成函数,有如下的优点:

  • 避免出现超大函数。
  • 独立出来的函数有助于代码复用。
  • 独立出来的函数更容易被修改覆写。
  • 独立出来的函数如果拥有良好的命名,它本身就起到了注释的作用。

看下面负责获取用户信息的函数,获取用户信息后还需要打印用户信息有关的log,那么打印log相关的代码就可以封装在一个函数里面:

function getUserInfo () {
  ajax('http://xxx.com/getUserInfo', function (data) {
    console.log('userId' + data.userId)
    console.log('userName' + data.name)
    console.log('nickName' + data.nickName)
  })
}
复制代码

改成:

function getUserInfo () {
 ajax('http://xxx.com/getUserInfo', function (data) {
   printUserInfo(data)
 })
}

function printUserInfo (data) {
  console.log('userId' + data.userId)
  console.log('userName' + data.name)
  console.log('nickName' + data.nickName)
}
复制代码

合并重复的条件片段

如果一个函数中有一些条件分支语句,而这些条件语句内部散布了一些重复代码,那么就有必要去合并重复的代码。如下面的分页函数paging:

function paging (currentPage) {
  if (currentPage <= 0) {
    currentPage = 0
    jump(currentPage)
  } else if (currentPage >= totalPage) {
    currentPage = totalPage
    jump(currentPage)
  } else {
    jump(currentPage)
  }
}
复制代码

jump(currentPage)在每个分支中都出现了,可以分离出来,优化后:

function paging (currentPage) {
  if (currentPage <= 0) {
    currentPage = 0
  } else if (currentPage >= totalPage) {
    currentPage = totalPage
  }
  jump(currentPage)
}
复制代码

将条件分支语句提炼成函数

在程序设计中,复杂的条件分支语句导致程序难以阅读和理解,而且容易形成一个庞大的函数。假设有一个需求是编写一个计算商品价格的函数getPrice,商品的计算有一个规则:当商品在夏季的时候,商品八折出售,代码如下:

function getPrice (price) {
  var date = new Date()
  if (date.getMonth() >= 6 && date.getMonth() <= 9) {
    return price * 0.8
  }
  return price
}
复制代码

代码date.getMonth() >= 6 && date.getMonth() <= 9表达的意思很简单,就是判断当前日期是否处于夏天,但是阅读代码的人想要马上理解代码,还需要多花一点精力。优化之后:

function getPrice (price) {
  if (isSummer()) {
    return price * 0.8
  }
  return price
}

function isSummer () {
  var date = new Date()
  return date.getMonth() >= 6 && date.getMonth() <= 9)
}
复制代码

isSummer函数本身就起到了注释的作用,这样阅读代码的人一看就能理解。

合理使用循环

在函数体内,如果有些代码实际上负责一些重复性的工作,那么合理利用循环不仅可以完成同样的功能,还可以减少代码量。下面看一个创建XHR对象的代码:

function createXHR () {
  var xhr;
  try {
    xhr = new ActiveObject('MSXML2.XMLHttp.6.0')
  } catch (e) {
    try {
      xhr = new ActiveObject('MSXML2.XMLHttp.3.0')
    } catch (e) {
      xhr = new ActiveObject('MSXML2.XMLHttp')
    }
  }
  return xhr
}
复制代码

利用循环优化:

function createXHR () {
  var versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp']
  for (var i = 0, version; version = versions[i++]; ) {
    try {
      return new ActiveObject(version)
    } catch (e) {
      
    }
  }
}
复制代码

提前让函数退出代码嵌套条件分支

很多开发都有一种观点:每个函数只能有一个入口和一个出口,现代编程语言一般都限制函数只有一个入口,但是对于一个函数出口,则可以根据实际情况不同对待,下面是一个遵循函数只有一个出口地典型代码:

function del (obj) {
  var ret
  if (!obj.isReadOnly) {
    if (obj.isFolder) {
      ret = delFolder(obj)
    } else if (obj.isFile) {
      ret = deleteFile(obj)
    }
  }
  return ret
}
复制代码

嵌套的条件语句对于代码维护者来说绝对是噩梦,对于阅读代码的人来说,嵌套的if、else语句相比平铺的if、else,在阅读和理解上更加困难,有时候一个外层if分支的左括号和右括号相距一屏才能看完的代码。用《重构》里的话说,嵌套的条件语句往往是由一些深信“每个函数只能有一个出口”程序员写出的。下面优化代码:

function del (obj) {
  if (!obj.isReadOnly) {
    return 
  }
  if (obj.isFolder) {
    return delFolder(obj)
  }
  if (obj.isFile) {
    return deleteFile(obj)
  }
}
复制代码

传递对象参数代替过长的参数列表

有时候一个函数可能接收多个参数,而参数的数量越多,函数就难以理解、使用和测试。下面看一个函数:

function setUserInfo (id, name, address, sex, mobile) {
  console.log('id' + id)
  console.log('name' + name)
  console.log('address' + address)
  console.log('sex' + sex)
  console.log('mobile' + mobile)
}

setUserInfo(12, 'sven', 'guangzhou', 'mail', '137****')
复制代码

使用这个函数的时候得小心翼翼,如果搞反了某两个参数的位置,那么将得到不同的结果。这个时候可以把参数放在一个对象里传递:

function setUserInfo (userInfo) {
  console.log('id' + userInfo.id)
  console.log('name' + userInfo.name)
  console.log('address' + userInfo.address)
  console.log('sex' + userInfo.sex)
  console.log('mobile' + userInfo.mobile)
}
setUserInfo({
  id: '12',
  name: 'sven',
  address: 'guangzhou',
  sex: 'mail',
  mobile: '137***'
})
复制代码

少用三元运算符

一些程序员喜欢大规模使用三元运算符代替传统的if-else语句,理由是三元运算性能高、代码量少,其实这些理由很难站住脚。
即使三元运算符真的比if-else效率高,这一点差距也是可以忽略的,在实际的开发中,把一段代码循环一百万次,使用三元运算符和if-else的时间开销在同一个级别里。同样损失了代码的可读性和可维护性,三元运算符节省的代码量也可以忽略不计。如果条件逻辑简单且清晰,我们可以使用三元运算符:

var global = typeof window !== 'undefined' ? window : this
复制代码

如果逻辑分支非常复杂,我们还是使用if-else,如下例子:

if (!aup || !bup) {
  return a === doc ? -1 :
    b === doc ? 1 :
    aup ? -1 : 
    bup ? 1 : 
    sortInput ? (indexOf.call(sortInput, a) - ndexOf.call(sortInput, b) ) : 0
}
复制代码

合理使用链式调用

经常使用jQuery的程序员很喜欢使用链式调用,在JavaScript中,很容易实现链式调用,即让方法调用结束后返回对象自身,看下面代码:

function User () {
  this.id = null
  this.name = null
}

User.prototype.setId = function (id) {
  this.id = id
  return this
}

User.prototype.setName = function (name) {
  this.name = name
  return this
}

console.log(new User().setId(12).setName('seven'))
复制代码

使用链式调用不会造成太多阅读上的困难,也能节省一些字符和中间变量,但是链式调用带来的坏处就是在调试的时候非常不方便,如果我们发现其中一条链有错误,必须得先把链拆开才能加上一些调试log或者增加断点,这样才能定位错误出现的地方。如果该链的结构相对稳定,后期不容易修改,可以考虑使用链式调用。

分解大型类

在一个H5版本的“街头霸王”游戏中,其中有一个负责创建游戏人物的Spirit类,这个类非常庞大,不仅要负责创建人物精灵,还包括了人物的攻击、防御等动作方法,代码如下:

function Spirit (name) {
  this.name = name
}

Spirit.prototype.attack = function (type) {
  if (type === 'waveBoxing') {
    console.log(this.name + ': 使用波动拳')
  } else if (type === 'whirlKick') {
    console.log(this.name + ': 使用旋风腿')
  }
}

var spirit = new Spirit('RYU')
spirit.attack('waveBoxing')
spirit.attack('whirlKick')
复制代码

后来发现attack方法越来越庞大,所以,它可以作为一个单独的类存在。面向对象设计鼓励将行为分布在合理数量的更小对象之中:

function Attack (spirit) {
  this.spirit = spirit
}

Attack.prototype.start = function (type) {
  return this.list[type].call(this)
}

Attack.prototype.list = {
  waveBoxing: function () {
    console.log(this.spirit.name + ': 使用波动拳')
  },
  whirlKick: function () {
    console.log(this.spirit.name + ': 使用旋风腿')
  }
}
复制代码

将Attack封装成单独的类,现在Spirit类变得精简了许多,只需要把攻击方法委托给Attack类,这也是策略模式的运用之一:

function Spirit (name) {
  this.name = name
  this.attackObj = new Attack(this)
}

Spirit.prototype.attack = function (type) {
  this.attackObj.start(type)
}

var spirit = new Spirit('RYU')
spirit.attack('waveBoxing')
spirit.attack('whirlKick')
复制代码

用return 退出多重循环

在一个函数体内,如果有两重循环语句,当到达某个临界条件时退出外层的循环。我们可以引入一个控制标记变量:

function func () {
  var flag  = false
  for (var i = 0; i < 10; i++) {
    for (var j = 0; j < 10; j++) {
      if (i * j > 30) {
        flag = true
        break
      }
    }
    if (flag === true) {
      break
    }
  }
}
复制代码

第二种方式,设置循环标记:

function func () {
  outerloop:
  for (var i = 0; i < 10; i++) {
    innerloop:
    for (var j = 0; j < 10; j++) {
      if (i * j > 30) {
        break outerloop
      }
    }
  }
}
复制代码

更简单的做法是直接使用return退出整个方法:

function func () {
  for (var i = 0; i < 10; i++) {
    for (var j = 0; j < 10; j++) {
      if (i * j > 30) {
        return
      }
    }
  }
}
复制代码

如果在循环结束之后还有代码还要执行,可以这样写:

function func () {
  for (var i = 0; i < 10; i++) {
    for (var j = 0; j < 10; j++) {
      if (i * j > 30) {
        return doSomething()
      }
    }
  }
}

function doSomething () {
  console.log('do something')
}
复制代码

转载于:https://juejin.im/post/5d051b24518825095b418f7f

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值