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')
}
複製代碼
相關文章
相關標籤/搜索