Marie Kondo Your Javascript Code with Functions

Devon Campbell - Aug 5 '19 - - Dev Community

Cover image by RISE Conf

In the previous article in this series, we explored callback functions. If you’ve read the entire series, you have a pretty good grasp of functions in Javascript including what they’re good for, how to declare them, and how to pass them around.

I mentioned in a previous article that you should strive for functions that do one thing. In this article, I’m going to look at some of my old code on Github and see if we can refactor it so that the functions follow this principle. First, let’s look at some cases where you might want to refactor code to use what you’ve learned about functions.

When to Refactor to Functions

To Stay D.R.Y.

D.R.Y. is an important software principle. It stands for “don’t repeat yourself.” If you find yourself repeating a value over and over across your code, that’s a good time to employ a variable. If you find yourself repeating a few lines of code in different places, that’s when you break out a function.

Instead of repeating your lines of code, write a function that contains those same lines and call it each time you need it. This makes your code easier to read because your function name should reflect what the lines of code are doing collectively. It also makes your code easier to refactor. If you find a bug in the lines of code, you can change them in the function and every call to the function is now fixed.

For Readability

Think about using a “for” loop to process every item in an array. A “for” loop for an array called movies would start like this:

for (var i = 0; i < movies.length; i++) {
Enter fullscreen mode Exit fullscreen mode

This has always been inscrutable to me. It doesn’t really convey any meaning. It’s just something you memorize as a programmer, but I hate the idea of my program being “readable” only because I’ve memorized some convention. Besides that, i is a terrible variable name, and we’re taught to avoid it… except in this circumstance where it’s customary. That doesn’t sit well with me.

I much prefer calling the array’s forEach method and passing in a function.

movies.forEach(function(movie) {
Enter fullscreen mode Exit fullscreen mode

You still have to memorize things to write this code, but it’s much easier to read and reason about what you’re doing than the for loop. As an added bonus, you can now refer to each array item as movie as you iterate (since that’s what we named the callback function’s parameter) instead of movies[i] which is meaningless.

When Your Functions Do Too Much

This is the one we’re looking at today, so let’s jump straight into the example and start splitting this Voltron apart.

Kids splitting Voltron back into his component robot tigers

Refactoring Huckle Buckle Beanstalk

I wrote a number guessing game (repo link) as a project for a bootcamp I did when I decided to change careers a few years back. Most of the logic is locked up in a single function called compareGuess (see line 20), which is what I want to focus on. Let’s break that apart into a few different functions, each with a single responsibility.

// Generate random number for guessing
var number = Math.floor(Math.random()*101);

// Global for previous guess
var previousGuess;

// Global for number of guesses
var numGuesses = 1;

function isNormalInteger(str) {
    return (/^[1-9]\d*$/).test(str);
}

// Checks to see if the guess is within the parameters given
function validGuess(guess) {
    return isNormalInteger(guess) && +guess <= 100 && +guess >= 1;
}

// Compare the guess to the number and previous guess. Place feedback on the page for the player.
function compareGuess(event) {
    event.preventDefault();
    // Grab the guess from the text input field
    var guess = $('#guess').val();


    if (validGuess(guess)) {
        // Turn off any error messages
        $('.error').addClass('off').removeClass('on');

        // Convert guess value to an integer for comparison
        guess = parseInt(guess, 10);
        // Feedback for a correct guess. Show the reset button to start a new game.
        if (guess === number) {
            $('#guess-vs-number').text('You got it! The number was ' + number + '.');
            $('#guess-vs-guess').hide();
            $('#num-guesses').text('You made ' + numGuesses + ' guesses.');
            $('#reset').removeClass('off');
        // Feedback for a low guess
        } else if (number > guess) {
            $('#guess-vs-number').text('Higher than ' + guess);
        // Feedback for a high guess
        } else {
            $('#guess-vs-number').text('Lower than ' + guess);
        }

        // Blank out the guess input field and return focus to it
        $('#guess').val('').focus();
        // Increment number of guesses
        numGuesses++;

        if (previousGuess) {
            // Find distances of the current and previous guesses from the actual number
            var previousDistance = Math.abs(number - previousGuess);
            var currentDistance = Math.abs(number - guess);

            // Feedback for guess versus previous guess comparison
            if (guess === previousGuess) {
                $('#guess-vs-guess').text("Same guess!");
            } else if (currentDistance < previousDistance){
                $('#guess-vs-guess').text("Getting warmer...");
            } else if (currentDistance > previousDistance) {
                $('#guess-vs-guess').text("Getting colder...");
            } else {
                $('#guess-vs-guess').text("Same distance...");
            }
        }
        // Set new previous guess
        previousGuess = guess;

        // Display the response
        $('.response').removeClass('off');
    } else {
        // Give error for invalid guess. Blank out the guess field and return focus.
        $('.error').removeClass('off').addClass('on');
        $('#guess').val('').focus();
    }
}

// Bind a click of the reset button to browser reload
$('#guess-form').on('click', '#reset', function(event) {
    event.preventDefault();
    location.reload();
});

// Bind form submission to the compareGuess function
$('#guess-form').submit(compareGuess);

// Bind enter key to the compareGuess function for browsers that don't always interpret an enter press as a form submission.
$('#guess').keypress(function(e) {
    if (e.which == 13) {
    compareGuess();
    }
});
Enter fullscreen mode Exit fullscreen mode

The first few lines of compareGuess are actually part of comparing the guess, but, after I check if the guess is right on line 32, I give the correct answer feedback which could be a separate function. That function might look like this:

function showCorrectFeedback() {
  $('#guess-vs-number').text('You got it! The number was ' + number + '.');
  $('#guess-vs-guess').hide();
  $('#num-guesses').text('You made ' + numGuesses + ' guesses.');
  $('#reset').removeClass('off');
}
Enter fullscreen mode Exit fullscreen mode

There are plenty of refactors I could do here like swapping the correct answer string to a template string to make it look nicer, but I’m not doing that since this code is run directly in the browser and older browsers don’t support ES6. Instead, I’ll focus mostly on breaking apart large functions.

Now, I need to go back to where this code was originally and call the new function instead.

if (guess === number) {
  showCorrectFeedback();
// Feedback for a low guess
} else if (number > guess) {
  
Enter fullscreen mode Exit fullscreen mode

If you’ve looked ahead in the code, you might be able to predict the next refactors I’m planning to do. I almost didn’t move the code for showing feedback on low or high guesses into their own functions just because each one is a single line, but I decided to do it for consistency.

function showLowGuessFeedback(guess) {
  $('#guess-vs-number').text('Higher than ' + guess);
}
function showHighGuessFeedback(guess) {
  $('#guess-vs-number').text('Lower than ' + guess);
}
Enter fullscreen mode Exit fullscreen mode

I had to change one thing with these two: I had to add a parameter which I call guess. The single line of code I brought into each of these already references guess, but that guess will not be in scope for these new functions. Instead, we’ll have to pass the guess into the feedback functions. We didn’t have to do that for the first function since it just shows number, which is a global variable.

Now, I’ll replace the old code with the new function calls.


} else if (number > guess) {
  showLowGuessFeedback(guess);
// Feedback for a high guess
} else {
  showHighGuessFeedback(guess);
}

Enter fullscreen mode Exit fullscreen mode

The problem for me with these two new functions is that they’re a bit too similar. In fact, they’re exactly the same save a single word. I think we could get by here with a single function instead.

I need to pass in the word I want to use (either “higher” or “lower”). Maybe there’s a name for these kinds of words, but I’m not aware of it. I’ll just call them “comparators.”

function showGuessFeedback(comparator, guess) {
  $('#guess-vs-number').text(comparator + ' than ' + guess);
}
Enter fullscreen mode Exit fullscreen mode

That means, I need to change the calls as well.


} else if (number > guess) {
  showGuessFeedback('Higher', guess);
// Feedback for a high guess
} else {
  showGuessFeedback('Lower', guess);
}

Enter fullscreen mode Exit fullscreen mode

The next chunk I want to refactor is down on line 50.


if (previousGuess) {
  // Find distances of the current and previous guesses from the actual number
  var previousDistance = Math.abs(number - previousGuess);
  var currentDistance = Math.abs(number - guess);

  // Feedback for guess versus previous guess comparison
  if (guess === previousGuess) {
    $('#guess-vs-guess').text("Same guess!");
  } else if (currentDistance < previousDistance){
    $('#guess-vs-guess').text("Getting warmer...");
  } else if (currentDistance > previousDistance) {
    $('#guess-vs-guess').text("Getting colder...");
  } else {
    $('#guess-vs-guess').text("Same distance...");
  }
}

Enter fullscreen mode Exit fullscreen mode

This code is no longer about checking whether the guess is right; it’s about telling the user if they’re getting warmer (their guess was closer than the previous one) or colder (their guess was further away than the previous one). Let’s pull that out into a separate function.

function showDistanceFeedback(guess) {
  if (previousGuess) {
    // Find distances of the current and previous guesses from the actual number
    var previousDistance = Math.abs(number - previousGuess);
    var currentDistance = Math.abs(number - guess);

    // Feedback for guess versus previous guess comparison
    if (guess === previousGuess) {
      $('#guess-vs-guess').text("Same guess!");
    } else if (currentDistance < previousDistance){ $('#guess-vs-guess').text("Getting warmer..."); } else if (currentDistance > previousDistance) {
      $('#guess-vs-guess').text("Getting colder...");
    } else {
      $('#guess-vs-guess').text("Same distance...");
  }
}
Enter fullscreen mode Exit fullscreen mode

We might be able to break this one apart even further, but this is already a big improvement. Now we call it.


// Blank out the guess input field and return focus to it
$('#guess').val('').focus();
// Increment number of guesses
numGuesses++;

showDistanceFeedback(guess);

// Set new previous guess
previousGuess = guess;

Enter fullscreen mode Exit fullscreen mode

This is still not amazing code, but the functions are, for the most part, each doing a single job now. The names we gave those functions will also make it easier to read the function if we have to return to this code months from now. Here’s all the refactored Javascript for the app:

// Generate random number for guessing
var number = Math.floor(Math.random()*101);

// Global for previous guess
var previousGuess;

// Global for number of guesses
var numGuesses = 1;

function isNormalInteger(str) {
    return (/^[1-9]\d*$/).test(str);
}

// Checks to see if the guess is within the parameters given
function validGuess(guess) {
    return isNormalInteger(guess) && +guess <= 100 && +guess >= 1;
}

function showCorrectFeedback() {
  $('#guess-vs-number').text('You got it! The number was ' + number + '.');
  $('#guess-vs-guess').hide();
  $('#num-guesses').text('You made ' + numGuesses + ' guesses.');
  $('#reset').removeClass('off');
}

function showGuessFeedback(comparator, guess) {
  $('#guess-vs-number').text(comparator + ' than ' + guess);
}

function showDistanceFeedback(guess) {
  if (previousGuess) {
    // Find distances of the current and previous guesses from the actual number
    var previousDistance = Math.abs(number - previousGuess);
    var currentDistance = Math.abs(number - guess);

    // Feedback for guess versus previous guess comparison
    if (guess === previousGuess) {
      $('#guess-vs-guess').text("Same guess!");
    } else if (currentDistance < previousDistance){ $('#guess-vs-guess').text("Getting warmer..."); } else if (currentDistance > previousDistance) {
      $('#guess-vs-guess').text("Getting colder...");
    } else {
      $('#guess-vs-guess').text("Same distance...");
  }
}

// Compare the guess to the number and previous guess. Place feedback on the page for the player.
function compareGuess(event) {
    event.preventDefault();
    // Grab the guess from the text input field
    var guess = $('#guess').val();

    if (validGuess(guess)) {
        // Turn off any error messages
        $('.error').addClass('off').removeClass('on');

        // Convert guess value to an integer for comparison
        guess = parseInt(guess, 10);
        // Feedback for a correct guess. Show the reset button to start a new game.
        if (guess === number) {
            showCorrectFeedback();
        // Feedback for a low guess
        } else if (number > guess) {
            showGuessFeedback('Higher', guess);
        // Feedback for a high guess
        } else {
            showGuessFeedback('Lower', guess);
        }

        // Blank out the guess input field and return focus to it
        $('#guess').val('').focus();
        // Increment number of guesses
        numGuesses++;

        showDistanceFeedback(guess);

        // Set new previous guess
        previousGuess = guess;

        // Display the response
        $('.response').removeClass('off');
    } else {
        // Give error for invalid guess. Blank out the guess field and return focus.
        $('.error').removeClass('off').addClass('on');
        $('#guess').val('').focus();
    }
}

// Bind a click of the reset button to browser reload
$('#guess-form').on('click', '#reset', function(event) {
    event.preventDefault();
    location.reload();
});

// Bind form submission to the compareGuess function
$('#guess-form').submit(compareGuess);

// Bind enter key to the compareGuess function for browsers that don't always interpret an enter press as a form submission.
$('#guess').keypress(function(e) {
    if (e.which == 13) {
    compareGuess();
    }
});
Enter fullscreen mode Exit fullscreen mode

Refactor Your Own Code

If you’ve read this Javascript function series, you should know enough about functions to start looking for opportunities to improve your own code by using them. If you have some old code you haven’t looked at in a while, practice what you’ve learned by pulling it up and using functions where they will make your code better.

Learning to write code is really fun… but code alone won’t get you work. 😢 Sign up for a free mentoring session 🎓 at Rad Devon if you want help turning what you know about code into an actual career. 💰

Articles in This Series

  1. Your App’s Playbook: Why Use Functions in Javascript
  2. Declaring Javascript Functions
  3. Understanding Javascript Callback Functions
  4. Marie Kondo Your Javascript Code with Functions
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .