Table of Contents
Move classes into separate files
Move utility functions to separate file
Move event listeners into separate file
Move game setup functionality into separate file
Abstract beefy groupings of code
Conclusion
Christopher Lis
Christopher Lis
Last updated

Senior Frontend Engineer

How to Refactor Messy JavaScript Projects

There'll be a time where you're developing a project and all you need is one JavaScript file. This works great, and in fact, it's the way I recommend starting off simple project, however, as time goes on, this singular file is likely to get bloated with classes, functions, variables, and more. As a result it's important to be on the lookout for when things feel unruly, then refactor and organize accordingly.

Below are the methods I use to make a messy project more bearable.

Move classes into separate files

Placing your classes in a singular file will boost your productivity at first, but put too many in there and it becomes hard to differentiate between what's part of your implementation and what's part of your class design:

What a messy project with classes looks like
The messy way...

A simple fix for this is to take each class and place them within their own file:

Project where classes are placed in their own files
The cleaner way

You can clear hundreds of lines of code and enable file-searching ability for classes within your text editor using this method. Due to it's ease of implementation, it comes in as the number one way you can refactor a messy project, just make sure that you import your new files or else they won't be read by your index.js file.

Move utility functions to separate file

Utility functions make our lives easier by abstracting algorithms into bite sized functions, but they'll easily muddy up our code if they're all placed in a singular file like index.js:

Messy utility functions...
The messy way...

Instead, it makes sense to place these in a separate utils.js to signify, "This is where all of our helper functions go":

Cleaner utility functions
The cleaner way

Since it's likely your project makes use of a handful of utility functions, this method also ranks near the top for best ways to refactor a messy project—it's super easy to implement as well, so we can immediately make impact to our project's organization.

Move event listeners into separate file

Similar to the issue had with utility functions, event listeners can clutter up an index.js file as well:

Messy event listeners
The messy way...

Since events are their own category, seperate from a project's main implementation, it makes sense to place these within their own file:

Cleaner event listeners
The cleaner way

Another easy change that immediately frees up space and makes things easier to find.

Move game setup functionality into separate file

Next, I'd recommend moving any game setup your have into a separate file. Game setup means anything like map generation, level variables, or global effects. Say all of my my map generation code is in index.js:

Messy index.js with map gen
The messy way...

As you can see, this is a beefy chunk of code, and what you don't see is that there are about 100 more lines of map generation code beneath it. Since this relates to initial setup rather than actual game functionality, it makes sense to place this in a separate file:

Cleaned up map gen
The cleaner way

We can run this code by pulling it in via a script tag into an index.html file, and as long as this file preceeds index.js in load order, everything should work as it did before. However, if you don't want to introduce variables like map into the global namespace, you can transform the generation code into a function:

./js/setup.js
import Boundary from './classes/Boundary.js'
import { createImage } from './utils.js'

function generateBoundaries() {
  const map = [
    ['1', '-', '-', '-', '-', '-', '-', '-', '-', '-', '2'],
    ['|', '.', '.', '.', '.', '.', '.', '.', '.', '.', '|'],
    ['|', '.', 'b', '.', '[', '7', ']', '.', 'b', '.', '|'],
    ['|', '.', '.', '.', '.', '_', '.', '.', '.', '.', '|'],
    ['|', '.', '[', ']', '.', '.', '.', '[', ']', '.', '|'],
    ['|', '.', '.', '.', '.', '^', '.', '.', '.', '.', '|'],
    ['|', '.', 'b', '.', '[', '+', ']', '.', 'b', '.', '|'],
    ['|', '.', '.', '.', '.', '_', '.', '.', '.', '.', '|'],
    ['|', '.', '[', ']', '.', '.', '.', '[', ']', '.', '|'],
    ['|', '.', '.', '.', '.', '^', '.', '.', '.', '.', '|'],
    ['|', '.', 'b', '.', '[', '5', ']', '.', 'b', '.', '|'],
    ['|', '.', '.', '.', '.', '.', '.', '.', '.', 'p', '|'],
    ['4', '-', '-', '-', '-', '-', '-', '-', '-', '-', '3']
  ]

  const boundaries = []

  map.forEach((row, i) => {
    row.forEach((symbol, j) => {
      switch (symbol) {
        case '-':
          boundaries.push(
            new Boundary({
              position: {
                x: Boundary.width * j,
                y: Boundary.height * i
              },
              image: createImage('./img/pipeHorizontal.png')
            })
          )
          break
      }
    })
  })

  return boundaries
}

This example requires a web server that can handle importing modules, yet it provides a namespace safe way to generate a number of Boundary objects that you can utilize on a level-by-level basis. For more information on this sort of usage, check out my Pacman game dev course.

Abstract beefy groupings of code

Like how we just handled map generation in the last section, another great way to refactor messy codebases is to abstract beefy groupings of code. If you see around 5-30 lines of code that you just don't understand at first glance, it might be time to refactor that grouping into something more readable. The key here is to start small, abstracting large statements almost always causes more trouble that abstracting small pieces, so find a grouping of ~5 lines, see if it makes sense to refactor it, if not, increase your grouping amount.

Take this player movement code for example:

index.js
if (keys.w.pressed && lastKey === 'w') {
  for (let i = 0; i < boundaries.length; i++) {
    const boundary = boundaries[i]
    if (
      circleCollidesWithRectangle({
        circle: {
          ...player,
          velocity: {
            x: 0,
            y: -5
          }
        },
        rectangle: boundary
      })
    ) {
      player.velocity.y = 0
      break
    } else {
      player.velocity.y = -5
    }
  }
}

This states that whenever a player presses w, move the player upwards. However, that's not very apparent based on the code we're looking at—it takes at least 10 seconds to a minute to understand what's really happening. As a result, the time it takes to comprehend makes this grouping a good candidate for abstracting.

We know the code makes our player go up, so when we press w, what should we really say for more readable code? Here's an idea:

if (keys.w.pressed && lastKey === 'w') {
  player.moveUp(boundaries)
}

These three lines of code are way more understandable than what we had earlier, but if we were to call things this way, we'd need to ensure we create a player method resembling the function we just called:

./js/classes/Player.js
import { circleCollidesWithRectangle } from '../utils.js'

class Player {
  moveUp(boundaries) {
    for (let i = 0; i < boundaries.length; i++) {
      const boundary = boundaries[i]
      if (
        circleCollidesWithRectangle({
          circle: {
            ...this,
            velocity: {
              x: 0,
              y: -5
            }
          },
          rectangle: boundary
        })
      ) {
        this.velocity.y = 0
        break
      } else {
        this.velocity.y = -5
      }
    }
  }
}

Note, I changed any instance of player to this since now we're referencing the player's properties directly within the class definition. I also had to import circleCollidesWithRectangle so we can use it within Player.js, then passed boundaries through as an argument so we can use our level's generated boundaries without having to reference them within a global namespace.

In the end, we may have written more code, but it is much more understandable and organized since we can easily locate everything related to moving our player up, compared to having to use some brain power to find it prior.

Conclusion

These are just some of the methods I use to refactor messy codebases, thankfully they're easy to implement and will provide you with valuable code-maintenance skills that'll serve you for years to come in your coding career. To learn more about this topic in depth, as I mentioned, I cover this exact topic in my Pacman course which you can take here.

Otherwise, you can follow me on Twitter for course updates and more. Thanks for reading and I'll see you in the next one ✌️

Comments

Want to participate?

Create a free Chris Courses account to begin

No comments yet, be the first to add one

Providing the lift to launch your development career

© 2024 Chris Courses. All rights reserved.