JavaScript Refactoring Techniques: Specific to Generic Code

Share this article

Hand drawing arrow sign in opposite direction from others
This article was peer reviewed by Dan Prince. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

In a recent thread on SitePoint’s forums, some code was given to let one dropdown box control when another dropdown box is visible. Even though the code worked just fine, I realized that it left much to be desired. It was brittle and incapable of withstanding even small changes to the accompanying HTML.

Here is the original CSS code:

#second { display: none; }
#second.show { display: block; }

and the original JavaScript code:

document.getElementById("location").onchange = function () {
  if (this[this.selectedIndex].value === "loc5") {
    document.getElementById("second").className = "show";
  } else {
    document.getElementById("second").className = "";
  }
};

In this article, I’ll demonstrate some simple JavaScript refactoring techniques that can be applied to the above code in order to make it easier to reuse and more accommodating to future change.

Knowing Which Path to Take

JavaScript has many ways to achieve the same task, and some of them work better than others. Are there ways to improve the code right now so that we don’t have to come back to it later on? Sure! But when there are several possible methods of doing something, how can we determine which one is likely to work best?

One common technique for improving code is to remove duplication (using the don’t repeat yourself principle). From there though, it can be more useful to go from specific to more generic code which allows us to handle a wider range of situations.

Specific code tends to be brittle when it comes to handling future changes. Code doesn’t exist in a vacuum, and will need to change in response to other actions around it and in the HTML code. With the benefit of past experience though, we can look at common changes that occur and improvements that reduce number of times we need to revisit the code. Invariably you will find that this means making the code more generic.

But beware! It can be easy to make our code too generic to the point that it becomes difficult to understand. Striking a good balance between generic and readable is where we find improved code.

JavaScript Refactoring Techniques: Specific to Generic

During the course of test driven development (TDD) you can’t help but to come across this principle as a part of the process:

As the tests get more specific, the code gets more generic.

The Cycles of TDD by Robert C. Martin covers this idea well. The main benefit here is that generic code ends up being able to handle a wider range of situations and scenarios.

Note, if you’d like to learn more about TDD, check out our short mini course Test-Driven Development with Node.js.

Looking at the above code, some obvious specific to generic improvements are immediately available.

  • Storing the strings in variables would help us to manage them from the one place.
  • The onchange event handler is problematic in so far as it can be overwritten. We should consider using addEventListener instead.
  • The className property will overwrite existing class names. We should consider using classList instead.

After making all of these improvements, we’ll end up with code that is more resilient to future changes, and is easier to update. So let’s get started …

Use Variables to Prevent Duplication

The ID of the dropdown box (“location”) and its trigger value (“loc5”) are useful references to keep together. The second <select> element is also being referred to twice, which we can pull out to a separate variable to prevent clutter and provide easier maintenence.

For example, instead of having two references to the same element that would need to be changed if the element’s ID changed:

// bad code
if (...) {
  document.getElementById("second").className = "show";
} else {
  document.getElementById("second").className = "";
}

We can store a reference to this element in a variable, limiting future change to only the place where the variable is assigned:

// good code
var target = document.getElementById("second");
if (...) {
  target.className = "show";
} else {
  target.className = "";
}

By pulling these strings out together to the top of the code, and separating out the parts of the if condition, the specific to generic technique results in code that is easier to maintain, both now and in the future. If any of the identifiers or option values are changed, they can all be easily found in the one place, instead of hunting through the code for all of their occurences.

// improved code
var source = document.getElementById("location");
var target = document.getElementById("second");
var triggerValue = "loc5";

source.onchange = function () {
  var selectedValue = this[this.selectedIndex].value;
  if (selectedValue === triggerValue) {
    target.className = "show";
  } else {
    target.className = "";
  }
};

Improve Event Handling

Traditional event handlers are still quite popular (and have been used correctly in this case), but they have some issues. Chief among them is that when setting an event handler for an element in this way, you will overwrite any previous handler for the same event.

// bad code
source.onchange = function () {
  // ...
};

Currently the above code works. We can demonstrate this using a test.


A quick note about testing

Philosophy: tests are a great way to ensure that the code you write behaves in the way you expect. They reduce the likelihood that changes you make to your code cause something else elsewhere in the code to break. An introduction to testing is unfortunately outside the scope of this article (although SitePoint has a lot of great content on this topic). You will still be able to follow along without having written a test in your life.

Syntax: The following tests use the Jasmine testing framework. Jasmine tests (aka specs) are defined by calling the global Jasmine it function, which takes a string and a further function as arguments. The string is the title of the spec and the function is the spec itself. You can read more about Jasmine on the project’s homepage.

Note that this article will focus on testing front-end code. If you’re looking for something focused on the backend, be sure to check out our course: Test-Driven Development in Node.js


Running the tests

Given the previous state of our code, the following two tests will pass:

it("should add the 'show' class name when the 'loc5' option is selected", function() {
  changeSelectTo(source, "loc5");
  expect(target.classList.contains("show")).toBe(true);
});

it("should remove the 'show' class name when an option value different from 'loc5' is selected", function() {
  changeSelectTo(source, "loc2");
  expect(target.classList.contains("show")).toBe(false);
});

The changeSelectTo function alters the value of the <select> element and the expectation (built using Jasmine’s expect function) ascertains that the element has the correct class name.

But as soon as the onchange handler is altered — which is something any other code is capable of doing — the function that changed the class name is lost and things start to go wrong. We can demonstrate this with a further test:

it("should toggle the class name even when the onchange event is replaced", function () {
  changeSelectTo(source, "loc2");
  expect(target.classList.contains("show")).toBe(false);

  // Overwrite the onchange handler
  source.onchange = function doNothing() { return; };

  changeSelectTo(source, "loc5");
  expect(target.classList.contains("show")).toBe(true); // fails
});

This test fails, as can be seen in this CodePen. Please note that the Jasmine specific code is in a separate Pen, found here.

Refactoring our code to make the test pass

We can easily make this test pass by using addEventListener, which allows any number of functions to be assigned to one event. The false parameter states whether event capture (when true) or event bubbling (when false) is used for the order of events. Quirksmode gives a good overview of the event order for events.

// good code
source.addEventListener("change", function (evt) {
  // ...
}, false);

Here’s how the code is affected by this change:

// improved code
var source = document.getElementById("location");
var target = document.getElementById("second");
var triggerValue = "loc5";

source.addEventListener("change", function () {
  var selectedValue = this[this.selectedIndex].value;
  if (selectedValue === triggerValue) {
    target.className = "show";
  } else {
    target.className = "";
  }
}, false);

With the addEventListener line active all of the tests now pass.

See the Pen mAbzLL by SitePoint (@SitePoint) on CodePen.

Note: in the test code, I’ve named the function toggleShowOnSelectedValue, to make it easier for you to swap between approaches when testing the different onchange techniques:

//source.onchange = toggleShowOnSelectedValue;
source.addEventListener("change", toggleShowOnSelectedValue, false);

Give it a try in the CodePen above. Try toggling the commented out lines and watch what happens.

Improve Class Handling

Another issue with the code is that the second <select> element will lose any previous classes that it might have had, due to className replacing anything that was there before.

// bad code
target.className = "show";

We can see the problem happening in the following failing, which expects a class of indent to still be on the select element after it has been displayed:

it("should retain any existing class names that were on the target element", function () {
  changeSelectTo(source, "loc2");
  target.classList.add("indent");
  expect(target.classList.contains("indent")).toBe(true);

  changeSelectTo(source, "loc5");
  expect(target.classList.contains("indent")).toBe(true); // fails
});

Due to className replacing the entire class name, any other classes that used to be there are removed too.

You can see the failing test in the following CodePen. Please note that the Jasmine specific code is in a separate Pen, found here.

Instead of having these potential problems, we can use classList to add and remove the class names.

// good code
target.classList.add("show");
// ...
target.classList.remove("show");

This now results in the test passing, as can be shown below.:

See the Pen JRPxjG by SitePoint (@SitePoint) on CodePen.

The resulting code after these improvements is now:

// improved code
var source = document.getElementById("location");
var target = document.getElementById("second");
var triggerValue = "loc5";

source.addEventListener("change", function () {
  var selectedValue = this[this.selectedIndex].value;
  if (selectedValue === triggerValue) {
    target.classList.add("show");
  } else {
    target.classList.remove("show");
  }
}, false);

If you are worried about using the classList API as you want to support IE9 and older browsers, you can instead use a separate addClass and removeClass functions to achieve similar results.

Conclusion

Improving your code doesn’t have to be a hard or difficult task.

The specific to generic principle is a beneficial side-effect that comes from test-driven development. Regardless of whether you test code or not though, you too can benefit from these generic code techniques that make your code more flexible. This frees you from returning so often to fix up your code.

Try working some of these improvements in to your own code, and let us know how they improved things for you – or hit us up in the forums for further discussion and assistance.

Frequently Asked Questions (FAQs) on JavaScript Refactoring Techniques

What are the key benefits of refactoring JavaScript code?

Refactoring JavaScript code has several benefits. Firstly, it improves the readability and maintainability of the code, making it easier for other developers to understand and work on. Secondly, it can enhance the performance of the code by eliminating unnecessary lines and optimizing functions. Thirdly, it can help in identifying and fixing bugs or errors in the code. Lastly, refactoring can make the code more reusable, reducing the time and effort required for future development.

How can I identify code that needs refactoring?

Identifying code that needs refactoring can be a bit tricky, especially for beginners. However, there are some signs that can indicate the need for refactoring. These include code duplication, long methods or functions, large classes, high complexity, and poor performance. Using code analysis tools can also help in identifying problematic areas in the code.

What are some common JavaScript refactoring techniques?

There are several refactoring techniques that can be used in JavaScript. Some of the most common ones include extracting methods, renaming variables or functions, removing dead code, simplifying conditional expressions, and replacing temp with query. Each of these techniques has its own benefits and should be used based on the specific needs of the code.

How can I ensure that refactoring does not introduce new bugs in the code?

To ensure that refactoring does not introduce new bugs in the code, it is important to have a good set of unit tests in place. These tests should cover all the major functionalities of the code. Before starting the refactoring process, run the tests to ensure that everything is working correctly. After refactoring, run the tests again to check if any functionality has been affected.

Can refactoring improve the performance of JavaScript code?

Yes, refactoring can significantly improve the performance of JavaScript code. By eliminating unnecessary lines of code, optimizing functions, and simplifying complex expressions, refactoring can make the code run faster and more efficiently. However, it’s important to note that performance should not be the only reason for refactoring. The main goal of refactoring should be to improve the readability and maintainability of the code.

Is it necessary to refactor the entire code at once?

No, it’s not necessary to refactor the entire code at once. In fact, it’s often recommended to refactor the code incrementally, focusing on one area or function at a time. This approach makes the refactoring process more manageable and reduces the risk of introducing new bugs in the code.

How can I learn more about JavaScript refactoring techniques?

There are several resources available online to learn about JavaScript refactoring techniques. These include online tutorials, blogs, books, and video courses. Participating in coding challenges and open-source projects can also provide practical experience in refactoring.

What tools can I use for refactoring JavaScript code?

There are several tools available for refactoring JavaScript code. These include IDEs like WebStorm and Visual Studio Code, which provide built-in refactoring features. There are also code analysis tools like ESLint and JSHint, which can help in identifying problematic areas in the code.

Can refactoring help in reducing the size of JavaScript code?

Yes, refactoring can help in reducing the size of JavaScript code. By eliminating unnecessary lines of code and making the code more concise, refactoring can significantly reduce the size of the code. This can be particularly beneficial for web development, as it can reduce the load time of web pages.

Is refactoring a one-time process?

No, refactoring is not a one-time process. It’s a continuous process that should be part of the regular development cycle. As the code evolves and new features are added, it’s important to keep refactoring the code to maintain its quality and efficiency.

Paul WilkinsPaul Wilkins
View Author

I'm a web developer living in Christchurch (thanks for all the quakes) where JavaScript is my forte. When off the computer I volunteer down at the local community centre, or enjoy playing tabletop games such as Carcassonne or Stone Age with friends.

css classesjameshRefactoringtddTesting
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week