JavaScript Classes For Beginners

JavaScript Classes For Beginners

On Wednesday, a guy I’m working with said “Simon, what’s all this .prototype crap in the the code?” An hour later, he understood. He’s new to JavaScript, but not to development - he’s a good PHP developer, but got confused no-end about classes in JavaScript.

ES6, the latest-and-greatest specification of JavaScript, now comes with it’s own class keyword. But you still need to know how it works underneath as there may be reasons why you need to access the .prototype.

Let’s start at the start

If you’ve done any object-oriented programming for longer than 5 minutes, you’ll know about classes…

class Person {
    constructor (firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    getName () {
        return this.firstName + " " + this.lastName;
    }
}

var me = new Person("Simon", "Emms");
me.getName(); // Returns "Simon Emms"

This is a super simple example of a class that is basically the across pretty much every language out there. This example is in the ES6 syntax, but it wouldn’t be difficult to port to PHP, Java, C++ etc.

How JavaScript used to do it

This is actually how ES6 actually works. The class keyword is just some sugar that transpiles to the old way of doing this. Babel demonstrates this.

Everything in JavaScript is an object - strings, number, arrays, objects. And so are functions. Don’t believe me?

function hello () {}
typeof hello // "function"
hello instanceof Object // true

Inside the Function object is a hidden object called prototype. There are lots of explanations out there on how this works. All the beginner needs to know is prototype is what is used when calling a function with new.

Let’s look at the Person class again, but this time as written in ES5.

function Person (firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
}

Person.prototype.getName = function () {
    return this.firstName + " " + this.lastName;
};

We now have two functions - we have a function called Person and an anonymous function assigned to getName, but the syntax inside is the same as the Person class. The Person function has now become the constructor method. And because we have called it with new, the scope of this is that of the instance of the ‘class’.

You’ll need to understand the scope when you add a static method in.

Person.sayHello = function (firstName) {
    return "Hello " + firstName;
}

Person.sayHello("Simon"); // Hello Simon

The value of this in the getName and sayHello is different. In getName, it’s the instance of the class. In sayHello, it’s the same as Person (although you shouldn’t use this in static method under normal circumstances). This means that you can’t access each other by using this. The static method can be accessed from the method by using Person.sayHello(), but the static method is entirely ignorant of the instance unless you pass it in to the method.

There shouldn’t be any surprises here, as this is the same in all OO languages.

We’re using ES6/CoffeeScript/TypeScript - why do I need to know this?

Most of the time, you won’t. You should be using modern tooling (TypeScript FTW!) to build modern applications. However, you should still know how this works because you ought to know how a language works. And sometimes, you need to stub things out.

Stubbing what?

Let’s imagine you’ve got a nicely decoupled application. Your database drivers get setup with configuration at runtime and you pass around an instance of things. Your bootstrap makes a single call to the database to verify it’s connected ok. Life is good.

Then you need to unit test and you want to simulate things. As the bootstrap is calling the new for you, you don’t have an instance you can pass in, so you have to go into the prototype.

// db.js
class Db {
    constructor (connection) {
        this.connection = connection;
    }

    find (obj) {
        // ... stuff to find the object
    }
}

// app.js
let db = new Db(connection);
db.find(...);

Your test might look something like this (this example is using proxyquire and sinon in a mocha/chai test)…

let Db = sinon.stub();
Db.prototype.find = sinon.stub();

let App = proxyquire("./app", {
    "./db": Db
});

let inst = new App();

expect(Db.prototype.find).to.be.calledOnce
    .calledWithNew;

This is very much a hypothetical example. In most circumstances, you won’t need to break into the prototype. In fact, if you have to break into the prototype, it might be a sign that you could do something a bit better (certainly, if I saw something like this, I’d refactor it out). But you will one day need to know this.