The Troubles with JavaScript Classes

Tuesday, September 13, 2016

Over the summer I gave a talk titled “The New Dragons of JavaScript”. The idea was to provide, like the cartographers of the Old World, a map of where the dragons and sea serpents live in the new JavaScript feature landscape. These mythological beasts have a tendency to introduce confusion or pain in software development.  

One area I covered were the quirks you might run into with JavaScript classes. Some introductions explain how classes work by describing the de-sugaring a transpiler applies to transform a class into the classical constructor function and prototype manipulation we’ve used in JavaScript for many years.

class Employee {
    constructor(name) {
        this._name = name;
    }

    doWork() {
        return `${this._name} is working`;
    }
}

// above code becomes ...

let Employee = function(name) {
    this._name = name;
};

Employee.prototype = {
    doWork: function() {
        return `${this._name} is working`;
    }
}

Constructor functions and prototypes are a useful mental model to have at times, but also leads to trouble because classes aren’t exactly like using constructor functions. For example, functions in JavaScript will hoist, but classes do not. If you ever want to push the definition of a small utility class to the bottom of a file and try to use the class in the code at the top of the file, you’ll be setting yourself up for an error.

// this code works
const e = new Employee();

function Employee() {

}

// this code produces a ReferenceError
const e = new Employee();

class Employee {

}

Technically, classes (and variables declared with let and const) do hoist themselves, but they hoist themselves into an area the early specs referred to as the “temporal dead zone”. Accessing a symbol in its TDZ creates a RefernceError.  As an aside, “Temporal dead zone” is, I think, one of the greatest computer science terms ever conceived and should also be the title of a Hollywood film starring Mark Wahlberg.

Another difference between creating an object using a class and creating an object with a constructor function is in reflective code. It’s easy to discover the methods of an object instantiated with a constructor function using a for in loop.

const Human = function () { }
Human.prototype.doWork = function () { };

let names = [];
for (const p in new Human()) {
    names.push(p);
}
// ["doWork"]

The same code won’t work when using a class definition.

class Horse {
    constructor() {}            
    doWork() { }
}

names = [];
for (const p in new Horse()) {
    names.push(p);
}
// []

However, it is possible to get to the methods of a class using some Object APIs.

names = [];
const prototype = Object.getPrototypeOf(new Horse());
for(const name of Object.getOwnPropertyNames(prototype)) {
    names.push(name);
}
// ["constructor", "doWork"]

Coming soon – The Troubles with Modules.

Previously in this series – The Trouble with JavaScript Arrow Functions


Comments
gravatar Chris Woodward Tuesday, September 13, 2016
Matt Damon surely !? :-)
gravatar Dale Tuesday, September 13, 2016
There's a higher level issue here you hinted at with your statement "classes aren’t exactly like using constructor functions". People expect Javascript inheritance and objects to work exactly like they do in languages like C# and Java - this causes people to make really fundamental mistakes (like using prototypal inheritance at all haha). Real, practical, intuitive uses for classes over modules are very few and far between - extensibility and empty object identity are the only two I've found. The confusion that the "this" keyword is enormous - just don't use it - people should do themselves a favor and learn how to structure large systems with modules.
gravatar Ethan Cane Sunday, September 25, 2016
Temporal Dead Zone could only ever star Christopher Walken.
Comments are closed.

My Pluralsight Courses

K.Scott Allen OdeToCode by K. Scott Allen
What JavaScript Developers Should Know About ECMAScript 2015
The Podcast!