JavaScript代码重构

背景

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

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

提炼函数

在平常开发中,咱们大部分时间都是跟函数打交道,因此咱们但愿这些函数有着良好的命名,函数的功能单一,函数的逻辑清晰明了。若是一个函数过长,并且须要加注释才能让这个函数易读,那这个函数就颇有必要进行重构了。把代码独立出来,封装成函数,有以下的优势:java

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

看下面负责获取用户信息的函数,获取用户信息后还须要打印用户信息有关的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)
  })
}
复制代码

改为:ajax

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函数自己就起到了注释的做用,这样阅读代码的人一看就能理解。oop

合理使用循环

在函数体内,若是有些代码实际上负责一些重复性的工做,那么合理利用循环不只能够完成一样的功能,还能够减小代码量。下面看一个建立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')
}
复制代码
相关文章
相关标签/搜索