Categories
Refactoring

JavaScript Refactoring Tips — Making Functions Clearer and Cleaner

JavaScript is an easy to learn programming language. It’s easy to write programs that run and does something. However, it’s hard to write a piece of clean JavaScript code.

In this article, we’ll look at how to make our functions clearer.

Using Destructuring for Object Parameters

If we want our function to take lots of arguments, then we should take one big object argument.

To do that, we should use the destructuring syntax to destructure the properties of the object we pass in as the argument as variables.

For instance, instead of writing:

const greet = (obj) => {
  return `${obj.greeting}, ${obj.firstName}${obj.lastName}`;
}

We should write:

const greet = ({
  greeting,
  firstName,
  lastName
}) => {
  return `${greeting}, ${firstName}${lastName}`;
}

This way, we have a lot less repetition, and it makes everyone clear what properties the object has in the arguments.

It’s like having multiple arguments without actually having to pass in multiple arguments.

It’s a lot more flexible as the order of properties in an object doesn’t matter as with multiple parameters, but we still can access each property individually as variables.

Naming Our Callback Functions

Naming things makes reading code easier. This is the same with callback functions. For instance, instead of writing:

const arr = [1, 2, 3].map(a => a * 2);

We should instead write:

const double = a => a * 2;
const arr = [1, 2, 3].map(double);

Now we know that our callback function actually is used for doubling each entry of the original array.

Make Our Conditionals Descriptive

Conditionals can be made more descriptive by writing the conditional expressions in a conditional statement in their own function.

For instance, instead of writing:

if (score === 100 ||
  remainingPlayers === 1 ||
  remainingPlayers === 0) {
  quitGame();
}

We should write:

const winnerExists = () => {
  return score === 100 ||
    remainingPlayers === 1 ||
    remainingPlayers === 0
}

if (winnerExists()) {
  quitGame();
}

This way, we know that those conditions are conditions for checking if a winner exists in our game code.

In the first example, we have a long-expression inside the parentheses of the if that most people probably don’t know that it’s checking.

But in the 2nd example, once we put it in a named function, we know that the conditions are actually checking for.

Having a named function in our conditionals is much clearer than just having a bunch of boolean expressions together.

Replacing Switch Statements with Maps or Objects

switch statements are long and error-prone because of its length. Therefore, we should replace them with shorter pieces of code if we can.

Many switch statements can be replaced with maps or objects. For instance, if we have the following switch statement:

const getValue = (prop) => {
  switch (prop) {
    case 'a': {
      return 1;
    }
    case 'b': {
      return 2;
    }
    case 'c': {
      return 3;
    }
  }
}
const val = getValue('a');

We can replace that with an object or map as follows:

const obj = {
  a: 1,
  b: 2,
  c: 3
}
const val = obj['a'];

As we can see, the switch was long. We need to nest multiple blocks with multiple return statements just to get a value returned given a prop value.

With an object, we have an object obj :

const obj = {
  a: 1,
  b: 2,
  c: 3
}

which has 3 properties with string keys that we can return a value from if we access it with the bracket notation as we did above. With the bracket notation, the keys don’t have to be valid identifiers. Instead, they can be any string.

We can also replace the object with a map as follows:

const map = new Map([['a', 1], ['b', 2], ['c', 3]])
const val = map.get('a')

As we can see, with maps, the code is also a lot shorter. We defined the Map with one line by passing in an array with entries that consists of arrays of key and value in that order.

Then we just use the Map instance’s get method to get the value from the key.

One benefit that maps have over objects is that we can have other values like numbers, booleans, or objects as keys. Whereas objects can only have strings or symbols as keys.

Also, we don’t have to worry about inherited properties with maps.

Conclusion

Object parameters can be made clearer and shorter with the destructuring syntax. This way, the properties can be selectively accessed as variables.

Conditionals can be made more descriptive by putting the conditional expressions in its own named function. Likewise, we should name our callback functions to make reading the code easier.

Finally, switch statements should be replaced with maps and objects as much as possible.

Categories
Refactoring

JavaScript Refactoring Tips — Reducing Function Complexity

JavaScript is an easy to learn programming language. It’s easy to write programs that run. However, it’s hard to write a piece of clean JavaScript code.

In this article, we’ll look at ways to reduce the complexity of our JavaScript functions.

Move Repeated Code into a Single Location

We should move repeated code into a single location so that we can change them easily if needed as we only need to change one instead of multiple pieces of code.

For instance, instead of writing:

const button = document.querySelector('button');
let toggled = true;
button.addEventListener('click', () => {
  toggled = !toggled;
  if (toggled) {
    document.querySelector("p").style.color = 'red';
  } else {
    document.querySelector("p").style.color = 'green';
  }
})

where we have 2 instances of document.querySelector(“p”) , we should write:

const button = document.querySelector('button');
const p = document.querySelector("p");
let toggled = true;
button.addEventListener('click', () => {
  toggled = !toggled;
  if (toggled) {
    p.style.color = 'red';
  } else {
    p.style.color = 'green';
  }
})

As we can see, it’s shorter and we only have to change the selector once if we decide that we don’t want to select the p element anymore.

Another common instance of code is magic numbers, where we have code with numbers that we don’t know the meaning of from the code itself.

For instance, we have code that looks like:

let x = 1;
let y = 1;
let z = 1;

Where we have 3 1’s that we don’t know the meaning of. We can remove the repetition and make the numbers self-documenting by writing:

const numPeople = 1;
let x = numPeople;
let y = numPeople;
let z = numPeople;

Then we know that 1 is the value of numPeople . Now we know that 1 means the number of people.

Simplify Functions

Functions should be as simple as possible. Therefore, they should only do one thing and not have too many lines of code in them. They shouldn’t have more than 30 lines of code.

The old cases that we use JavaScript functions shouldn’t be used. For instance, we shouldn’t use IIFEs for modules or blocks. Also, we shouldn’t define constructor functions anymore.

Instead, we should use the class syntax, where we can include multiple instance methods of the class inside the class.

This reduces the need for long functions significantly.

Also, functions should be pure whenever we can define them. This means they shouldn’t commit side effects.

For instance, the best simple functions are functions like:

const add = (a, b) => a + b;

The function above doesn’t have any side effects, as it doesn’t modify any variables outside the function. Also, it’s pure because it always returns the same outputs for the same inputs.

It also only does one thing, which is adding numbers. Therefore, the cognitive load on the reader of the code isn’t going to be too taxed since it only does one thing and nothing else.

Use Guard Clauses Instead of Nested If Statements

Guard clauses are where we end the function when the function encounters some condition.

It replaces nested if blocks that are long and hard to read.

For instance, instead of writing the following function:

const greet = (firstName, lastName, greeting) => {
  if (typeof firstName === 'string') {
    if (typeof lastName === 'string') {
      if (typeof greeting === 'string') {
        return `${greeting}, ${firstName}${lastName}`;
      }
    }
  }
}

We write:

const greet = (firstName, lastName, greeting) => {
  if (typeof firstName !== 'string') {
    throw new Error('first name is required');
  }
  if (typeof lastName !== 'string') {
    throw new Error('last name is required');
  }
  if (typeof greeting !== 'string') {
    throw new Error('greeting is required');
  }
  return `${greeting}, ${firstName}${lastName}`;
}

In the 2nd example, we eliminated the nested if statements by throwing errors if each argument isn’t a string.

This reduced the nested if statements to no nested if statements while doing the same thing.

Nesting is hard to read and understand, and we should get rid of them everywhere.

Conclusion

Repeated code is always bad. We should always remember the Do not Repeat Yourself (DRY) principle.

Also, a lot of things that previously used traditional functions for shouldn’t be used anymore. Many functions can be replaced with modules, blocks, classes, and arrow functions.

Finally, nested if statements should be replaced with guard clauses since they can do the same checks as the nested if statements without having hard to read nested if statements.

Categories
Refactoring

JavaScript Refactoring Tips — Classes and Functions

JavaScript is an easy to learn programming language. It’s easy to write programs that run and does something. However, it’s hard to write a piece of clean JavaScript code.

In this article, we’ll look at how to refactor our JavaScript functions so that it’s clean and easy to read.

Refactor Constructors to Classes

In JavaScript, a class is just syntactic sugar for constructor functions, so they’re actually functions.

All classes do is provide a more intuitive syntax for constructor functions by avoiding dealing with prototypes and using call to call parent constructor functions when we extend another constructor.

For instance, instead of writing:

function Foo(name) {
  this.name = name;
}

Foo.prototype.getName = function() {
  return this.name;
}

We write:

class Foo {
  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

As we can see, with the class syntax, it’s clear where the constructor is and what instance methods our class contain with the class syntax.

Instead of attaching instance methods to the prototype of the constructor function, we add it to the class.

We also don’t have to write out the keyword function all the time, which makes typing shorter while keeping the code easy to understand. There’s no ambiguity with this syntax.

If we want to do inheritance, instead of writing:

function Animal(name) {
  this.name = name;
}
Animal.prototype.getName = function() {
  return this.name;
}

function Cat(name) {
  Animal.call(this, name);
}
Cat.prototype.constructor = Animal;

We write:

class Animal {
  constructor(name) {
    this.name = name;
  }
  getName() {
    return this.name;
  }
}

class Cat extends Animal {
  constructor(name) {
    super(name);
  }
}

This way, we get an error is we forgot to call the parent’s constructor, which we don’t get when we forgot to call the parent constructor in the constructor function example.

We also make it clear that we’re extending the Animal class with the extends keyword instead of having to set the parent constructor in the child constructor ourselves.

Avoiding Traditional Functions As Much As Possible

Traditional functions were used for everything before ES2015. It’s used for encapsulating code, used as blocks, used for constructors as we have above, used for callbacks, etc.

Most of these cases have been replaced by other constructors that were introduced with ES2015.

If we want to encapsulate code, we can use blocks. For instance, instead of writing an immediately invoked function expression (IIFE) as follows:

(function() {
  let x = 1;
  console.log(x);
})()

We can instead write:

{
  let x = 1;
  console.log(x);
}

The 2nd example is shorter and we only have to delimit the block with the curly braces.

The variable x isn’t available outside the block with the code above.

Another way to encapsulate code is to use modules. For instance, we can write the following code:

module.js

export const x = 1;
const y = 2;
const z = 3;
export default y;

index.js

import { x } from "./module";
import y from "./module";
console.log(x, y);

In the code above, we only expose what we have when exporting them with the export keyword. Therefore, x and y are available for import from another module.

As we can see, x and y were imported from module.js . But z couldn’t be because it wasn’t exported.

Therefore, we don’t need IIFEs like the following anymore:

const module = (function() {
  const x = 1;
  const y = 2;
  const z = 3;
  return {
    x,
    y
  }
})();

The code above is longer and it uses a function unnecessarily. Also, as the function has more members, it’ll get longer, and it isn’t a good idea to have long functions.

For callbacks, we can use arrow functions instead. For instance, instead of writing:

const arr = [1, 2, 3].map(function(a) {
  return a * 2;
})

We should write:

const arr = [1, 2, 3].map(a => a * 2)

It’s shorter, so we don’t have to type as much as we don’t have to type our the function keyword. Also, the return is implicit for one lined arrow functions. And we also don’t have to worry about the value of this and arguments since arrow functions don’t bind to any of these values.

Conclusion

To refactor functions, we should convert constructors to classes. Otherwise, we should convert them to blocks, modules, and arrow functions as we see fit.