background

So far, I’ve written a series of common design patterns for JavaScript. In a series of articles on design patterns, the contrast between writing a counter example code and refactoring the previous code through the design pattern will deepen our understanding of the design pattern. There is an inherent relationship between design patterns and refactoring, and in some ways, the purpose of design patterns is to refactor code. In addition to refactoring with design patterns in everyday development, there are some common, overlooked details that can help you write better, maintainable code. Here are the details.

Refining function

In daily development, we spend most of our time working with functions, so we want them to be well named, simple, and logical. If a function is too long and needs to be commented to make it readable, it may be worth refactoring. Separating code into functions has the following advantages:

  • Avoid super-large functions.
  • Separate functions facilitate code reuse.
  • Independent functions are easier to modify and override.
  • A well-named stand-alone function acts as an annotation in itself.

The following function is responsible for obtaining user information. It also needs to print the log related to user information, so the code to print the log related can be wrapped in a function:

function getUserInfo () {
  ajax('http://xxx.com/getUserInfo'.function (data) {
    console.log('userId' + data.userId)
    console.log('userName' + data.name)
    console.log('nickName' + data.nickName)
  })
}
Copy the code

To:

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)
}
Copy the code

Merge repeated conditional fragments

If a function has conditional branch statements that are interspersed with duplicate code, then it is necessary to merge duplicate code. The following paging function is called:

function paging (currentPage) {
  if (currentPage <= 0) {
    currentPage = 0
    jump(currentPage)
  } else if (currentPage >= totalPage) {
    currentPage = totalPage
    jump(currentPage)
  } else {
    jump(currentPage)
  }
}
Copy the code

Jump (currentPage) appears in each branch and can be isolated and optimized:

function paging (currentPage) {
  if (currentPage <= 0) {
    currentPage = 0
  } else if (currentPage >= totalPage) {
    currentPage = totalPage
  }
  jump(currentPage)
}
Copy the code

Distill conditional branch statements into functions

In programming, complex conditional branching statements make programs difficult to read and understand, and easy to form a large function. Let’s say we have a requirement to write a function getPrice that calculates the price of an item with one rule: when the item is in the summer, the item is sold at 20% off, as follows:

function getPrice (price) {
  var date = new Date(a)if (date.getMonth() >= 6 && date.getMonth() <= 9) {
    return price * 0.8
  }
  return price
}
Copy the code

The code date.getMonth() >= 6 && date.getMonth() <= 9 tells you if the current date is summer or not, but readers need to work a little harder to understand the code immediately. After optimization:

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

function isSummer () {
  var date = new Date(a)return date.getMonth() >= 6 && date.getMonth() <= 9)}Copy the code

The isSummer function itself acts as a comment so that people reading the code can understand it at first glance.

Fair use cycle

If some of the code in the function body is actually doing repetitive work, the proper use of loops can not only accomplish the same function, but also reduce the amount of code. Here is a code to create an XHR object:

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
}
Copy the code

Using cycle optimization:

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) {
      
    }
  }
}
Copy the code

Let the function exit code nested conditional branches ahead of time

Modern programming languages generally limit functions to one entry and one exit. However, for a function exit, it can be treated differently depending on the situation. Here is a typical code that follows a function that has only one exit:

function del (obj) {
  var ret
  if(! obj.isReadOnly) {if (obj.isFolder) {
      ret = delFolder(obj)
    } else if (obj.isFile) {
      ret = deleteFile(obj)
    }
  }
  return ret
}
Copy the code

Nested conditional statements are an absolute nightmare for code maintainers. Nested if and else statements are more difficult to read and understand than tiled if and else statements. Sometimes an outer if branch’s open parentheses and close parentheses are one screen away from each other. In the language of Refactoring, nested conditional statements are often written by programmers who believe that there is only one exit per function. Optimize the code below:

function del (obj) {
  if(! obj.isReadOnly) {return 
  }
  if (obj.isFolder) {
    return delFolder(obj)
  }
  if (obj.isFile) {
    return deleteFile(obj)
  }
}
Copy the code

Pass object arguments instead of long argument lists

Sometimes a function may take more than one argument, and the larger the number of arguments, the harder the function is to understand, use, and test. Let’s look at a function:

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 * * * *')
Copy the code

You have to be careful with this function, because if you reverse the position of one of the two arguments, you will get different results. The argument can be passed in an object:

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 * * *'
})
Copy the code

Use less ternary operators

Some programmers prefer to use ternary operators instead of traditional if-else statements on a large scale because of the high performance and low code requirements of ternary operations, but these arguments are hard to justify. Even if ternary operators are really more efficient than if-else, the difference is negligible. In real development, loiterating a piece of code a million times would cost the same amount of time using ternary operators as if-else. The readability and maintainability of the code are also lost, and the amount of code saved by the ternary operation is negligible. If the conditional logic is simple and clear, we can use the ternary operator:

var global = typeof window! = ='undefined' ? window : this
Copy the code

If the logic branch is very complex, we still use if-else, as in the following example:

if(! aup || ! bup) {return a === doc ? - 1 :
    b === doc ? 1 :
    aup ? - 1 : 
    bup ? 1 : 
    sortInput ? (indexOf.call(sortInput, a) - ndexOf.call(sortInput, b) ) : 0
}
Copy the code

Use chain calls wisely

Chained calls are popular with regular jQuery programmers. In JavaScript, it’s easy to implement chained calls, which return the object itself after a method call. Look at this code:

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'))
Copy the code

Use the chain call won’t cause too much reading difficulties, also can save some characters and the intermediate variable, but the disadvantage brought by chain call is very inconvenient when debugging, if we find that there is an error in one of the chain, must first unwrapped chain can add some debug log or add breakpoints, so as to locate errors occur. If the chain structure is relatively stable and not easy to modify later, consider using chain calls.

Decompose large classes

In a H5 version of “Street Fighter” game written by Tencent, there is a Spirit class responsible for creating game characters. In the first version, this class is very large, not only responsible for creating character Sprite, but also including character attack, defense and other action methods, the code is as follows:

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

Spirit.prototype.attack = function (type) {
  if (type === 'waveBoxing') {
    console.log(this.name + ': Use undulating fist ')}else if (type === 'whirlKick') {
    console.log(this.name + ': Use whirlwind legs')}}var spirit = new Spirit('RYU')
spirit.attack('waveBoxing')
spirit.attack('whirlKick')
Copy the code

It turned out that the Attack method was getting bigger and bigger, so it could exist as a separate class. Object-oriented design encourages the distribution of behavior among a reasonable number of smaller objects:

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 + ': Use undulating fist ')},whirlKick: function () {
    console.log(this.spirit.name + ': Use whirlwind legs')}}Copy the code

By encapsulating Attack as a separate class, the Spirit class is now much more streamlined, delegating only Attack methods to the Attack class, which is one of the uses of the strategy pattern:

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')
Copy the code

Exit multiple loops with return

In a function body, if there is a double loop, the outer loop exits when a critical condition is reached. We can introduce a control tag variable:

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}}}Copy the code

The second way is to set the loop flag:

function func () {
  outerloop:
  for (var i = 0; i < 10; i++) {
    innerloop:
    for (var j = 0; j < 10; j++) {
      if (i * j > 30) {
        break outerloop
      }
    }
  }
}
Copy the code

It’s easier to exit the entire method with return:

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

If you have code to execute after the loop ends, you can write:

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')}Copy the code