You are here:

Home / For students / JavaScript / Clean code /

Clean code

Some advice for keeping your code clean and manageable

Long functions ​

When a function becomes too long, extract parts of it into separate functions.

Example ​

js
function DrawEverything(canvas) {
	// lots of code
	// ...
	// ...
	// ...
}

Becomes:

js
function DrawEverything(canvas) {
	drawBackground(canvas);
	drawEnemies(canvas);
	drawPlayer(canvas);
	drawProjectiles(canvas);
}

Long classes ​

Same as long functions, try to break them up.

Too many arguments ​

If you have a function with many arguments, create a class to group the related values into a single package.

Example ​

js
function printDocument(
	paperSize,
	marginLeft,
	marginRight,
	marginTop,
	marginBotton,
	numCopies,
	resolution,
	text,
	fontColor,
	fontSize,
	fontFamily,
	lineSpacing,
	paragraphIndentation
) {
	// do the printing
}

Becomes:

js
class Margin {
	constructor (left, right, top bottom) {
		this.left = left;
		this.right = right;
		this.top = top;
		this.bottom = bottom;
	}
}

class PrintParameters {
	constructor (paperSize, margin, numCopies, resolution) {
		this.paperSize = paperSize;
		this.margin = margin;
		this.numCopies = numCopies;
		this.resolution = resolution;
	}
}

class Font {
	constructor (color, size, family) {
		this.color = color;
		this.size = size;
		this.family = family;
	}
}

class DocumentFormat {
	constructor (font, lineSpacing, paragraphIndentation) {
		this.font = font;
		this.lineSpacing = lineSpacing;
		this.paragraphIndentation = paragraphIndentation;
	}
}

class TextDocument {
	constructor (text, format) {
		this.text = text;
		this.format = format;
	}
}

function printDocument(printParameters, textDocument) {
	// do the printing
}

Too many attributes ​

Similar to the previous issue, but instead of having too many function arguments, you have too many properties in a class.

When a class has too many properties, group them into new classes.

Data duplication ​

Avoid having multiple variables holding copies of the same value. If certain value can be trivially computed from existing variables, it's usually better to provide a function for computing it instead of storing it in a variable.

Example ​

js
class Player {
	constructor(health) {
		this.health = health;
		this.isDead = false;
	}
	
	receiveDamage(amount) {
		this.health -= amount;
		if (this.health < 0) {
			this.health = 0;
		}
		
		if (this.health == 0) {
			this.isDead = true;
		}
	}
	
	heal(amount) {
		this.health += amount;
		
		if (this.health > 0) {
			this.isDead = false;
		}
	}
}

Becomes:

js
class Player {
	constructor (health) {
		this.health = health;
	}
	
	receiveDamage(amount) {
		this.health -= amount;
		
		if (this.health < 0) {
			this.health = 0;
		}
	}
	
	heal(amount) {
		this.health += amount;
	}
	
	getIsDead ( ) {
		return this.health <= 0;
	}
}

Note

  • Second variant is shorter, because there is no need to update this.isDead every time health changes.
  • In the second variant, it is impossible for the value to be incorrect as long as the function isDead( ) is correct. In the first variant, forgetting to update this.isDead (or updating it incorrectly) will introduce a bug which can be difficult to find.

Code duplication in classes ​

If you have multiple classes with similar methods and/or attributes, consider extracting the common part into a base class and using inheritance.

Example ​

js
class Vector2 {
	constructor(x, y) {
		this.x = x;
		this.y = y;
	}
}

class Player {
	constructor(name){
		this.position = new Vector2(0,0);
		this.name = name;
	}

	move(offset){
		this.position.x += offset.x;
		this.position.y += offset.y;
	}

	// Player-related methods
	// ...
	// ...
}

class Skeleton {
	constructor(){
		this.position = new Vector2(0,0);
		this.name = "Skeleton";
	}

	move(offset){
		this.position.x += offset.x;
		this.position.y += offset.y;
		console.log("rattling of bones echoes through the dungeon");
	}

	// Skeleton-related methods
	// ...
	// ...
}

class Slime {
	constructor(){
		this.position = new Vector2(0,0);
		this.name = "Slime";
	}

	move(offset){
		this.position.x += offset.x;
		this.position.y += offset.y;
	}

	// Slime-related methods
	// ...
	// ...
}

Becomes:

js
class Vector2 {
	constructor(x, y) {
		this.x = x;
		this.y = y;
	}
}

class Character {
	constructor(name){
		this.position = new Vector2(0,0);
		this.name = name;
	}

	move(offset){
		this.position.x += offset.x;
		this.position.y += offset.y;
	}
}

class Player extends Character {
	constructor(name){
		super(name);
	}

	// Player-related methods
	// ...
	// ...
}

class Skeleton extends Character {
	constructor(){
		super("Skeleton");
	}

	move(offset){
		super.move(offset);
		console.log("rattling of bones echoes through the dungeon");
	}

	// Skeleton-related methods
	// ...
	// ...
}

class Slime extends Character {
	constructor(){
		super("Slime");
	}

	// Slime-related methods
	// ...
	// ...
}

Code duplication in functions ​

If you have multiple functions with similar code, consider extracting the common part into a separate function.

Example ​

js
function castFireball() {
	console.log("preparing to cast..."); // accidental inconsistency
	console.log("reading the spell scroll: Fireball...");
	console.log("a massive sphere of flame flies towards the enemy!");
}

function castHeal() {
	console.log("preparing to cast a spell..."); // accidental inconsistency
	console.log("reading the spell scroll: Heal...");
	console.log("you summon healing energies, which make you feel better");
}

Becomes:

js
function readSpell(name) {
	console.log("preparing to cast a spell...");
	console.log(`reading the spell scroll: ${name}...`);
}

function castFireball() {
	readSpell("Fireball");
	console.log("a massive sphere of flame flies towards the enemy!");
}

function castHeal() {
	readSpell("Heal");
	console.log("you summon healing energies, which make you feel better");
}

Note

In the first version you could modify one of the functions and forget to modify the other, introducing an inconsistency.

The second version protects you from this.

Panic early ​

If you have a feeling that something might go wrong in your code, implement a sanity check to receive an immediate warning. Javascript is particularly notorious for pretending like everything is fine and continuing the execution with corrupt data (undefined, etc).

MDN link: throw statement.

Example ​

js
class Player {
	constructor (health) {
		this.health = health;
	}
	
	receiveDamage(amount) {
		this.health -= amount;
		
		if (this.health < 0) {
			this.health = 0;
		}
	}
	
	heal(amount) {
		this.health += amount;
	}
	
	getIsDead ( ) {
		if (this.health < 0) {
		   throw "health should never be negative!";
		   // something clearly went wrong, no reason to continue execution!
		}
		return this.health == 0;
	}
}

Magic numbers ​

When using numeric constants (for example, π\pi), create named constants instead of writing the values directly in your code.

Example ​

js
class Circle {
	constructor(radius){
		this.radius = radius;
	}

	getCircumference(){
		return this.radius * 2.0 * 3.1415;
	}

	getArea(){
		return 3.14 * (this.radius ** 2);
	}
}

Becomes:

js
const PI = 3.1415;

class Circle {
	constructor(radius){
		this.radius = radius;
	}

	getCircumference(){
		return this.radius * 2.0 * PI;
	}

	getArea(){
		return PI * (this.radius ** 2);
	}
}

Note

In the first variant different precision was used for π\pi in different places. This can create confusion.

Creating a constant ensures the value is always the same throughout the code.

upward arrowBack to top