Marie Kondo Your Javascript Code with Functions
Picture 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++) {…
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) {…
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.
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();
}
});
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');
}
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) {
…
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);
}
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);
}
…
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);
}
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);
}
…
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...");
}
}
…
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...");
}
}
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;
…
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();
}
});
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.