Back-end11 minute read

As a JS Developer, ES6 Classes Are What Keep Me Up at Night

JavaScript is an oddball of a language with numerous approaches to almost any problem. When ES6 added the “class” keyword, did it save the day or just muddy the waters? In this article, Toptal Freelance JavaScript Developer Justen Robertson explores OOP in modern JS.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

JavaScript is an oddball of a language with numerous approaches to almost any problem. When ES6 added the “class” keyword, did it save the day or just muddy the waters? In this article, Toptal Freelance JavaScript Developer Justen Robertson explores OOP in modern JS.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Justen Robertson
Verified Expert in Engineering
16 Years of Experience

Justen has a decade of full-stack JavaScript experience working with the likes of Taylor Swift and the Red Hot Chili Peppers.

Share

JavaScript is an oddball of a language. Though inspired by Smalltalk, it uses a C-like syntax. It combines aspects of procedural, functional, and object-oriented programming (OOP) paradigms. It has numerous, often redundant, approaches to solving almost any conceivable programming problem and is not strongly opinionated about which are preferred. It’s weakly and dynamically typed, with a mazelike approach to type coercion that trips up even experienced developers.

JavaScript also has its warts, traps, and questionable features. New programmers struggle with some of its more difficult concepts—think asynchronicity, closures, and hoisting. Programmers with experience in other languages reasonably assume things with similar names and appearances will work the same way in JavaScript and are often wrong. Arrays aren’t really arrays; what’s the deal with this, what’s a prototype, and what does new actually do?

The Trouble with ES6 Classes

The worst offender by far is new to JavaScript’s latest release version, ECMAScript 6 (ES6): classes. Some of the talk around classes is frankly alarming and reveals a deep-rooted misunderstanding of how the language actually works:

“JavaScript is finally a real object-oriented language now that it has classes!”

Or:

“Classes free us up from thinking about JavaScript’s broken inheritance model.”

Or even:

“Classes are a safer, easier approach to creating types in JavaScript.”

These statements don’t bother me because they imply there’s something wrong with prototypical inheritance; let’s set aside those arguments. These statements bother me because none of them are true, and they demonstrate the consequences of JavaScript’s “everything for everyone” approach to language design: It cripples a programmer’s understanding of the language more often than it enables. Before I go any further, let’s illustrate.

JavaScript Pop Quiz #1: What’s the Essential Difference Between These Code Blocks?

function PrototypicalGreeting(greeting = "Hello", name = "World") {
  this.greeting = greeting
  this.name = name
}

PrototypicalGreeting.prototype.greet = function() {
  return `${this.greeting}, ${this.name}!`
}

const greetProto = new PrototypicalGreeting("Hey", "folks")
console.log(greetProto.greet())
class ClassicalGreeting {
  constructor(greeting = "Hello", name = "World") {
    this.greeting = greeting
    this.name = name
  }

  greet() {
    return `${this.greeting}, ${this.name}!`
  }
}

const classyGreeting = new ClassicalGreeting("Hey", "folks")

console.log(classyGreeting.greet())

The answer here is there isn’t one. These do effectively the same thing, it’s only a question of whether ES6 class syntax was used.

True, the second example is more expressive. For that reason alone, you might argue that class is a nice addition to the language. Unfortunately, the problem is a little more subtle.

JavaScript Pop Quiz #2: What Does the Following Code Do?

function Proto() {
  this.name = 'Proto'
  return this;
}

Proto.prototype.getName = function() {
  return this.name
}

class MyClass extends Proto {
  constructor() {
    super()
    this.name = 'MyClass'
  }
}

const instance = new MyClass()

console.log(instance.getName())

Proto.prototype.getName = function() { return 'Overridden in Proto' }

console.log(instance.getName())

MyClass.prototype.getName = function() { return 'Overridden in MyClass' }

console.log(instance.getName())

instance.getName = function() { return 'Overridden in instance' }


console.log(instance.getName())

The correct answer is that it prints to console:

> MyClass
> Overridden in Proto
> Overridden in MyClass
> Overridden in instance

If you answered incorrectly, you don’t understand what class actually is. This isn’t your fault. Much like Array, class is not a language feature, it’s syntactic obscurantism. It tries to hide the prototypical inheritance model and the clumsy idioms that come with it, and it implies that JavaScript is doing something that it is not.

You might have been told that class was introduced to JavaScript to make classical OOP developers coming from languages like Java more comfortable with the JavaScript/ES6 class inheritance model. If you are one of those developers, that example probably horrified you. It should. It shows that JavaScript’s class keyword doesn’t come with any of the guarantees that a class is meant to provide. It also demonstrates one of the key differences in the prototype inheritance model: Prototypes are object instances, not types.

JavaScript Prototypes vs. Classes

The most important difference between JavaScript class- and prototype-based inheritance is that a class defines a type which can be instantiated at runtime, whereas a prototype is itself an object instance.

A child of an ES6 class is another type definition which extends the parent with new properties and methods, which in turn can be instantiated at runtime. A child of a prototype is another object instance which delegates to the parent any properties that aren’t implemented on the child.

Side note: You might be wondering why I mentioned class methods, but not prototype methods. That’s because JavaScript doesn’t have a concept of methods. Functions are first-class in JavaScript, and they can have properties or be properties of other objects.

A class constructor creates an instance of the class. A constructor in JavaScript is just a plain old function that returns an object. The only thing special about a JavaScript constructor is that, when invoked with the new keyword, it assigns its prototype as the prototype of the returned object. If that sounds a little confusing to you, you’re not alone—it is, and it’s a big part of why prototypes are poorly understood.

To put a really fine point on that, a child of a prototype isn’t a copy of its prototype, nor is it an object with the same shape as its prototype. The child has a living reference to the prototype, and any prototype property that doesn’t exist on the child is a one-way reference to a property of the same name on the prototype.

Consider the following:

let parent = { foo: 'foo' }
let child = { }
Object.setPrototypeOf(child, parent)

console.log(child.foo) // 'foo'

child.foo = 'bar'

console.log(child.foo) // 'bar'

console.log(parent.foo) // 'foo'

delete child.foo

console.log(child.foo) // 'foo'

parent.foo = 'baz'

console.log(child.foo) // 'baz'
Note: You'd almost never write code like this in real life—it's terrible practice—but it demonstrates the principle succinctly.

In the previous example, while child.foo was undefined, it referenced parent.foo. As soon as we defined foo on child, child.foo had the value 'bar', but parent.foo retained its original value. Once we delete child.foo it again refers to parent.foo, which means that when we change the parent’s value, child.foo refers to the new value.

Let’s look at what just happened (for the purpose of clearer illustration, we’re going to pretend these are Strings and not string literals, the difference doesn’t matter here):

Walking through the prototype chain to show how missing references are dealt with in JavaScript.

The way this works under the hood, and especially the peculiarities of new and this, are a topic for another day, but Mozilla has a thorough article about JavaScript’s prototype inheritance chain if you’d like to read more.

The key takeaway is that prototypes don’t define a type; they are themselves instances and they’re mutable at runtime, with all that implies and entails.

Still with me? Let’s get back to dissecting JavaScript classes.

JavaScript Pop Quiz #3: How Do You Implement Privacy in Classes?

Our prototype and class properties above aren’t so much “encapsulated” as “hanging precariously out the window.” We should fix that, but how?

No code examples here. The answer is that you can’t.

JavaScript doesn’t have any concept of privacy, but it does have closures:

function SecretiveProto() {
  const secret = "The Class is a lie!"
  this.spillTheBeans = function() {
    console.log(secret)
  }
}

const blabbermouth = new SecretiveProto()
try {
  console.log(blabbermouth.secret)
}
catch(e) {
  // TypeError: SecretiveClass.secret is not defined
}

blabbermouth.spillTheBeans() // "The Class is a lie!"

Do you understand what just happened? If not, you don’t understand closures. That’s okay, really—they’re not as intimidating as they’re made out to be, they’re super useful, and you should take some time to learn about them.

Let’s look at the other half of the JavaScript class versus function question.

JavaScript Pop Quiz #4: What’s the Equivalent to the Above Using the class Keyword?

Sorry, this is another trick question. You can do basically the same thing, but it looks like this:

class SecretiveClass {
  constructor() {
    const secret = "I am a lie!"
    this.spillTheBeans = function() {
      console.log(secret)
    }
  }

  looseLips() {
    console.log(secret)
  }
}

const liar = new SecretiveClass()
try {
  console.log(liar.secret)
}
catch(e) {
  console.log(e) // TypeError: SecretiveClass.secret is not defined
}
liar.spillTheBeans() // "I am a lie!"

Let me know if that looks any easier or clearer than in SecretiveProto. In my personal view, it’s somewhat worse—it breaks idiomatic use of class declarations in JavaScript and it doesn’t work much like you’d expect coming from, say, Java. This will be made clear by the following:

JavaScript Pop Quiz #5: What Does SecretiveClass::looseLips() Do?

Let’s find out:

try {
  liar.looseLips()
}
catch(e) {
  // ReferenceError: secret is not defined
}

Well… that was awkward.

JavaScript Pop Quiz #6: Which Do Experienced JavaScript Developers Prefer—Prototypes or Classes?

You guessed it, that’s another trick question—experienced JavaScript developers tend to avoid both when they can. Here’s a nice way to do the above with idiomatic JavaScript:

function secretFactory() {
  const secret = "Favor composition over inheritance, `new` is considered harmful, and the end is near!"
  const spillTheBeans = () => console.log(secret)

  return {
    spillTheBeans
  }
}

const leaker = secretFactory()
leaker.spillTheBeans()

This isn’t just about avoiding the inherent ugliness of inheritance, or enforcing encapsulation. Think about what else you might do with secretFactory and leaker that you couldn’t easily do with a prototype or a class.

For one thing, you can destructure it because you don’t have to worry about the context of this:

const { spillTheBeans } = secretFactory()

spillTheBeans() // Favor composition over inheritance, (...)

That’s pretty nice. Besides avoiding new and this tomfoolery, it allows us to use our objects interchangeably with CommonJS and ES6 modules. It also makes composition a little easier:

function spyFactory(infiltrationTarget) {
  return {
    exfiltrate: infiltrationTarget.spillTheBeans
  }
}

const blackHat = spyFactory(leaker)

blackHat.exfiltrate() // Favor composition over inheritance, (...)

console.log(blackHat.infiltrationTarget) // undefined (looks like we got away with it)

Clients of blackHat don’t have to worry about where exfiltrate came from, and spyFactory doesn’t have to mess around with Function::bind context juggling or deeply nested properties. Mind you, we don’t have to worry much about this in simple synchronous procedural code, but it causes all kinds of problems in asynchronous code that are better off avoided.

With a little thought, spyFactory could be developed into a highly sophisticated espionage tool that could handle all kinds of infiltration targets—or in other words, a façade.

Of course you could do that with a class too, or rather, an assortment of classes, all of which inherit from an abstract class or interface…except that JavaScript doesn’t have any concept of abstracts or interfaces.

Let’s return to the greeter example to see how we’d implement it with a factory:

function greeterFactory(greeting = "Hello", name = "World") {
  return {
    greet: () => `${greeting}, ${name}!`
  }
}

console.log(greeterFactory("Hey", "folks").greet()) // Hey, folks!

You might have noticed these factories are getting more terse as we go along, but don’t worry—they do they same thing. The training wheels are coming off, folks!

That’s already less boilerplate than either the prototype or the class version of the same code. Secondly, it achieves encapsulation of its properties more effectively. Also, it has a lower memory and performance footprint in some cases (it may not seem like it at first glance, but the JIT compiler is quietly working behind the scenes to pare down duplication and infer types).

So it’s safer, it’s often faster, and it’s easier to write code like this. Why do we need classes again? Oh, of course, reusability. What happens if we want unhappy and enthusiastic greeter variants? Well, if we’re using the ClassicalGreeting class, we probably jump directly into dreaming up a class hierarchy. We know we’ll need to parameterize the punctuation, so we’ll do a little refactoring and add some children:

// Greeting class
class ClassicalGreeting {
  constructor(greeting = "Hello", name = "World", punctuation = "!") {
    this.greeting = greeting
    this.name = name
    this.punctuation = punctuation
  }

  greet() {
    return `${this.greeting}, ${this.name}${this.punctuation}`
  }
}

// An unhappy greeting
class UnhappyGreeting extends ClassicalGreeting {
  constructor(greeting, name) {
    super(greeting, name, " :(")
  }
}

const classyUnhappyGreeting = new UnhappyGreeting("Hello", "everyone")

console.log(classyUnhappyGreeting.greet()) // Hello, everyone :(

// An enthusiastic greeting
class EnthusiasticGreeting extends ClassicalGreeting {
  constructor(greeting, name) {
	super(greeting, name, "!!")
  }

  greet() {
	return super.greet().toUpperCase()
  }
}

const greetingWithEnthusiasm = new EnthusiasticGreeting()

console.log(greetingWithEnthusiasm.greet()) // HELLO, WORLD!!

It’s a fine approach, until someone comes along and asks for a feature that doesn’t fit cleanly into the hierarchy and the whole thing stops making any sense. Put a pin in that thought while we try to write the same functionality with factories:

const greeterFactory = (greeting = "Hello", name = "World", punctuation = "!") => ({
  greet: () => `${greeting}, ${name}${punctuation}`
})

// Makes a greeter unhappy
const unhappy = (greeter) => (greeting, name) => greeter(greeting, name, ":(")

console.log(unhappy(greeterFactory)("Hello", "everyone").greet()) // Hello, everyone :(

// Makes a greeter enthusiastic
const enthusiastic = (greeter) => (greeting, name) => ({
  greet: () => greeter(greeting, name, "!!").greet().toUpperCase()
})

console.log(enthusiastic(greeterFactory)().greet()) // HELLO, WORLD!!

It’s not obvious that this code is better, even though it’s a bit shorter. In fact, you could argue that it’s harder to read, and maybe this is an obtuse approach. Couldn’t we just have an unhappyGreeterFactory and an enthusiasticGreeterFactory?

Then your client comes along and says, “I need a new greeter that is unhappy and wants the whole room to know about it!”

console.log(enthusiastic(unhappy(greeterFactory))().greet()) // HELLO, WORLD :(

If we needed to use this enthusiastically unhappy greeter more than once, we could make it easier on ourselves:

const aggressiveGreeterFactory = enthusiastic(unhappy(greeterFactory))

console.log(aggressiveGreeterFactory("You're late", "Jim").greet())

There are approaches to this style of composition that work with prototypes or classes. For example, you could rethink UnhappyGreeting and EnthusiasticGreeting as decorators. It would still take more boilerplate than the functional-style approach used above, but that’s the price you pay for the safety and encapsulation of real classes.

The thing is, in JavaScript, you’re not getting that automatic safety. JavaScript frameworks that emphasize class usage do a lot of “magic” to paper over these kinds of problems and force JS classes to behave themselves. Have a look at Polymer’s ElementMixin source code some time, I dare you. It’s arch-wizard levels of JavaScript arcana, and I mean that without irony or sarcasm.

Of course, we can fix some of the issues discussed above with Object.freeze or Object.defineProperties to greater or lesser effect. But why imitate the form without the function, while ignoring the tools JavaScript does natively provide us that we might not find in languages like Java? Would you use a hammer labeled “screwdriver” to drive a screw, when your toolbox had as actual screwdriver sitting right next to it?

Finding the Good Parts

JavaScript developers often emphasize the language’s good parts, both colloquially and in reference to the book of the same name. We try to avoid the traps set by its more questionable language design choices and stick to the parts that let us write clean, readable, error-minimizing, reusable code.

There are reasonable arguments about which parts of JavaScript qualify, but I hope I’ve convinced you that class is not one of them. Failing that, hopefully you understand that inheritance in JavaScript can be a confusing mess and that class neither fixes it nor spares you having to understanding prototypes. Extra credit if you picked up on the hints that object-oriented design patterns work fine without classes or ES6 inheritance.

I’m not telling you to avoid class entirely. Sometimes you need inheritance, and class provides cleaner syntax for doing that. In particular, class X extends Y is much nicer than the old prototype approach. Beside that, many popular front-end frameworks encourage its use and you should probably avoid writing weird non-standard code on principle alone. I just don’t like where this is going.

In my nightmares, a whole generation of JavaScript libraries are written using class, with the expectation that it will behave similarly to other popular languages. Whole new classes of bugs (pun intended) are discovered. Old ones are resurrected that could easily have been left in the Graveyard of Malformed JavaScript if we hadn’t carelessly fallen into the class trap. Experienced JavaScript developers are plagued by these monsters, because what is popular is not always what is good.

Eventually we all give up in frustration and start reinventing wheels in Rust, Go, Haskell, or who knows what else, and then compiling to Wasm for the web, and new web frameworks and libraries proliferate into multilingual infinity.

It really does keep me up at night.

Understanding the basics

  • What is ECMAScript 6?

    ES6 is the latest stable implementation of ECMAScript, the open standard on which JavaScript is based. It adds a number of new features to the language including an official module system, block-scoped variables and constants, arrow functions, and numerous other new keywords, syntaxes, and built-in objects.

  • Is ES6 the latest ECMAScript version?

    ES6 (ES2015) is the most recent standard that is stable and fully implemented (except for proper tail calls and some nuances) in the latest versions of major browsers and other JS environments. ES7 (ES2016) and ES8 (ES2017) are also stable specs, but implementation is quite mixed.

  • Is ES6 object-oriented?

    JavaScript has strong support for object-oriented programming, but it uses a different inheritance model (prototypical) compared to most popular OO languages (which use classical inheritance). It also supports procedural and functional programming styles.

  • What are ES6 classes?

    In ES6, the “class” keyword and associated features are a new approach to creating prototype constructors. They are not true classes in a way that would be familiar to users of most other object-oriented languages.

  • Which keywords can be used to implement inheritance in ES6?

    One can implement inheritance in JavaScript ES6 through the “class” and “extends” keywords. Another approach is via the “constructor” function idiom plus the assignment of functions and static properties to the constructor’s prototype.

  • What is prototype inheritance in JavaScript?

    In prototypical inheritance, prototypes are object instances to which child instances delegate undefined properties. In contrast, classes in classical inheritance are type definitions, from which child classes inherit methods and properties during instantiation.

Hire a Toptal expert on this topic.
Hire Now
Justen Robertson

Justen Robertson

Verified Expert in Engineering
16 Years of Experience

Portland, OR, United States

Member since April 23, 2018

About the author

Justen has a decade of full-stack JavaScript experience working with the likes of Taylor Swift and the Red Hot Chili Peppers.

authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.