September 15, 2018
Share this:
JavaScript Beginner

Introduction to Readable Javascript

Every so often an article crops up with some big list of rules to follow to keep your code readable - It's something everyone seems to want, but any time it is mentioned, a bloodbath accompanies it in the comments from people who disagree on how to do it.

As a coding beginner, you eventually realise that you do need to make your code readable – people who write the clear, readable code are the ones that get hired, and the ones other developers like working alongside.

Rather than pass down a big list of rules that you must follow or else, I have attempted to distil advice about readable code into a series of guidelines and rules-of-thumb for you to follow, and steadily build on as you grow.

Why should we make code readable?

An important reason we try to make code readable is to reduce cognitive load - or how much focus we need to maintain to work with it.

Contrary to what you might think when you're learning to write code, most of the time working in the industry is spent reading code – It is often much harder than merely writing it yourself.

For developers, focus is one of the base currencies we deal with aside from time, caffeine, and money. Being able to keep your code elegant and readable is paramount to a code base you enjoy working on.

Keep it simple

A core tenet of readable code is to keep your code simple – Fair warning though, this is far easier said than done. It requires understanding the nuances of the problem and being able to craft a concise solution that deals with all the edge cases.

This kind of solution usually comes with experience with the problem – The best thing to do is to write a shitty solution first, and while it's still fresh in your mind, simplify it.

Tell people what you're doing

Making code readable means we want to let people know what's going on. The simplest way of doing this to tell them directly. In code, this comes in two forms: Comments, and the code itself.

Consider the following code example (and I admit it's a little contrived):

function m(a) {
	return a >= 0 ? a << 2 : -(a << 2);
}

This function looks complex, so it's probably doing something important. Here is the full list:

  1. Return the absolute value of a * 4

I bet you didn't guess that.

Surprisingly, this function is very straightforward – But why does it look so complex? Because it doesn't give any hints about what it's trying to do.

  1. function s . We could use absMultiplyBy4 - You could strip it to multiplyBy4 but that may cause confusion when we start getting absolute values back
  2. a tells us nothing. Only by knowing what the function is doing can we tell that it's supposed to be a number. Since this is a mathematical equation, it's hard to provide something exceptionally meaningful, but we can at least hint at the type of variable we expect – num or val would be much better.
  3. Our intention is to multiply by 4. Using a * 4 over a << 2 is much clearer, since we are relying on elementary arithmetic instead of obscure binary arithmetic.
  4. We're returning the absolute value in a strange way. num >= 0 ? num : -num is a completely correct way to return an absolute number, but there is the standard library Math.abs to do it for us, and it's far more descriptive.
function absMultiplyBy4(num) {
	return Math.abs(num * 4);
} 

This is far more obvious.

Keep a consistent style

This is one piece of advice that you'll see everywhere, but I'm going to reiterate it here for completeness: Use a style guide. It doesn't really matter which one. If you don't know where to start, I would recommend giving the AirBnB Style Guide a read - it may not suit you perfectly, but it's a good starting point to work from.

The reason we use style guides is again to reduce the cognitive code of reading your code. It's hard enough to work out what code is doing without also having to work out what the hell is going on with the code style.

Even if you have no intention of using one, a style guide is well worth a read if you're starting out - it can introduce you to aspects of the language you had no idea existed.

In a professional setting, you'll often encounter linters like ESLint which check your code for style and consistency, and even clean it up for you. I'd definitely recommend you use these tools, but it's still a good idea to write style-conforming code instead of relying too heavily on them to clean up your mess.

The final point I want to make on style guides is what to do when you're working on a piece of code that doesn't conform to the style guide you're using – The correct thing to do in almost every circumstance is to maintain the style of the surrounding code. If you want to update the code to use the style guide, you should do it all at once.

Keep away from magic

An extension of "Tell people what you're doing" is to avoid magic in your codes - For those of you who aren't familiar with magic numbers and strings, they are values littered throughout your code, but it's unclear what they are for, or how similar values are related.

Think about this for a second:

$(elem).css({
	'padding': '15px',
	'margin-left': '-15px',
	'top': '15px',
});

Now I tell you that I want to change the padding from 15px to 20px. What other values do you need to change to account for that?

The answer is that you don't actually know. Any of those 15px values could be related to the padding – It's unclear (as you haven't told us what you're doing).

Now look at this:

const PADDING = 15; //px
const TOP = 15; //px

$(elem).css({
	'padding': PADDING + 'px',
	'margin-left': -PADDING + 'px',
	'top': TOP + 'px',
});

Keep your code focussed

Much like reading a book or an article, reading code is easier if there is a single, cohesive message throughout, and clear breaks when we change our message.

class ScrollAnimationController {
	constructor() {
		let self = this;
		this.elements = $('[data-animate]');
		$(window).on('scroll', function(event) {
			self.elements.forEach(function(elem) {
				let trig = elem.offset().x;
				($(window).scrollTop < trig ? () => {} : () => {
                    if (!elem.hasClass('animating') && !elem.hasClass('animate')) {
                    	setTimeout(() => { elem.toggleClass('animating', false).toggleClass('animate', true); }, self.animationTime);
                    	elem.toggleClass('animating', true);
                    }		
                })();
			});
		});
        this.animationTime = 300;
        this.elem.attr('data-loaded', true);
	}
}

This code snippet is a simple animate-on-scroll controller - It's terrible, and doesn't do the job well, but it's not unlike something you'd find in production code. There is a semblance of a story here, but we get sidetracked too often and lose track of the original thread.

Let's break down what's wrong with this little snippet.

  1. There is no code grouping in this. It's one big mass that becomes hard to read, and related code isn't kept together.
  2. this.animationTime is initialised lower down than it is called. It won't cause any issues at runtime, because the $(window).on('scroll', ...) listener isn't executed until later. It is confusing to the reader though.
  3. (scrollTop > trig ? () => {} : () => {...})() is a really strange way of doing an if {...} statement – It's not immediately obvious what is happening.
  4. setTimeout(()=>{...})  has too much happening on one line. There are 4 distinct points to take in, and it's way too hard to tell where one stops and another starts.

A first pass fix of this block of code

class ScrollAnimationController {
	constructor() {
		let self = this;

		this.elements = $('[data-animate]');
        this.animationTime = 300;
		
        this.elements.attr('data-loaded', true);

		$(window).on('scroll', function(event) {
			self.elements.forEach(function(elem) {
				let trig = node.offset().x;

				if ($(window).scrollTop > trig) {
                    if (!elem.hasClass('animating') && !elem.hasClass('animate')) {
                    	setTimeout(() => { 
							elem.toggleClass('animating', false)
								.toggleClass('animate', true); 
						}, self.animationTime);

						elem.toggleClass('animating', true);
                    }
                }
			});
		});
	}
}

It's already much better, but that entire $(window).on('scroll', ...} block is a mess. We're currently inside constructor so we should be focussing on setting up our object. The scroll function doesn't fit that story, so we should separate it out.

class ScrollAnimationController {
	constructor() {
		let self = this;

		this.elements = $('[data-animate]');
        this.animationTime = 300;
		
        this.elements.attr('data-loaded', true);

		$(window).on('scroll', function(event) {
			self.checkElements();
		});
	},

	checkElements() {
		let scrollTop = $(window).scrollTop;

		this.elements.forEach(function(elem) {
			let trig = elem.offset().x;

			if (scrollTop > trig) {
				if (!elem.hasClass('animating') && !elem.hasClass('animate')) {
                    setTimeout(() => { 
						elem.toggleClass('animating', false)
							.toggleClass('animate', true); 
					}, self.animationTime);

					elem.toggleClass('animating', true);
                }			
            }
		});
	}
}

This is about as good as we're going to get by rearranging the code. Although it's miles better, it's still not too easy to break down and read. So the final step of making this code readable is to add comments to sections that aren't as clear.

class ScrollAnimationController {
	constructor() {
		let self = this;

		this.elements = $('[data-animate]');
        this.animationTime = 300; //ms
		
		//Mark all elements as loaded so we know this function has run
        this.elements.attr('data-loaded', true);

		//Check elements on scroll
		$(window).on('scroll', function(event) {
			self.checkElements();
		});
	},

	/**
	* Triggers animation on all elements above the current scroll position
	*/
	checkElements() {
		const scrollTop = $(window).scrollTop;

		this.elements.forEach(function(elem) {
			let trig = elem.offset().x;

			if (scrollTop > trig) {
				//Set up animation timer the first time this
				//element is visible
				if (!elem.hasClass('animating') && !elem.hasClass('animate')) {
                    setTimeout(() => { 
						elem.toggleClass('animating', false)
							.toggleClass('animate', true); 
					}, self.animationTime);

					//Add animating class to track transition
					elem.toggleClass('animating', true);
                }			
            }
		});
	}
}

It can be tricky to know when to write comments, and what to write - in cases where you're not sure, it's best to err on the side of over-commenting (as I have here). I have a dedicated article expanding on how to write comments if you're struggling.

Afterword

In the end, code readability is highly subjective - What you find readable someone else might not. You can't please everyone, but that doesn't mean you can't become your own best friend and write code that is easy for yourself.

You aren't going to get all of this first time. It can take a long time to learn to write clear, concise code – usually by learning from the mistakes of your old code which was "readable" cough.

My advice is to avoid getting caught up too much on whether your code is readable: write it badly the first time, then clean it up once you're done.

Check out my follow up article on how to structure your code.

Share this: