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 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.

Prototypes vs. Classes

The most important difference between 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.

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 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.

About the author

Justen Robertson, United States
member since December 7, 2017
Justen is a full-stack JavaScript developer with over a decade of experience. He has worked on projects for some of the biggest brands in the publishing and recording industries and has worked directly with small businesses and nonprofits in many fields. His broad skillset enables him to cover all the web technology needs of small businesses and startups or to fill the gaps in larger teams. [click to continue...]
Hiring? Meet the Top 10 Freelance JavaScript Developers for Hire in December 2018

Comments

Julian G.
Hi! I'm not a JS developer, not even close a "bad" JS developer, however I use it from time to time as I'm a Qt / QML developer. Regarding the statement: ``` 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()) ``` I do not feel horrified, in fact I found this very alike to C pointers to functions (which is the polymorphic mechanism C has), I have to say that I like the mech. I dislike the .prototype. syntax (it seems to me quite complex). Good article, thanks.
Justen Robertson
Sure, and that's a much closer analogy to what's happening in Javascript too. I agree that the whole .prototype. syntax is clumsy. Likewise, thanks for your thoughts :)
Arsenii Fomin
Thank you for the great article! Spent 10 minutes to understand how "enthusiastic(unhappy(greeterFactory))().greet()" works :) As for my experience, I came to web development from solid (or better say S.O.L.I.D.) C++ background 3 years ago with a lot of incapsulation in my head. I have heard a lot of bad things about javascript and decided to find a good book to understand what to avoid, read "Good parts of javascript", started to use javascript in the projectes and become a fan of the language. When you understand that to solve any problem you basically need only one tool - function (which is type/object/closure/"struct with functions"), it's really impressive. And in the most cases (in comparison with C++) you never need classess, new, this and all that stuff. Btw why haven't you mentioned possibility to have private fields when creating new obects via composition?
Justen Robertson
Thanks :) JavaScript is much less of a monster when you stick to a narrow subset of its features that compliment eachother. Yeah I would have liked to get into more depth about how to achieve the equivalent of private fields. There are some great approaches involving closures with Object.defineProperties to control object descriptors that I frequently employ, but I didn't want to meander too deep into the weeds on that. Maybe in a future article.
papoola
Thanks for the great article!
schollii
Very interesting article. For me the prototype and class examples were easy and no surprises (perhaps having programmed for 30 years in many languages helps). I also found the closures easy, but not so the composition of greeters, and based on comments by others, I'm not the only one. So I'm not tempted to use that kind of composition although I really like your example of how closures can increase encapsulation by not exposing attributes that ought to be private but in js couldn't if it weren't for that technique. The greeter example to me should never be used in practice but there is serious food for thought there, no doubt, so I'll certainly keep an eye open for variations on the same theme. Thanks for an interesting read!
Justen Robertson
Thanks :) It's true, in real life you probably would not do something so complicated to solve such a simple problem. Of course you also probably wouldn't create a class hierarchy to solve the problem! There are times when this kind of approach is useful though, in cases where something more straightforward isn't possible.
comments powered by Disqus
Subscribe
Free email updates
Get the latest content first.
No spam. Just great articles & insights.
Free email updates
Get the latest content first.
Thank you for subscribing!
Check your inbox to confirm subscription. You'll start receiving posts after you confirm.
Trending articles
Relevant Technologies
About the author
Justen Robertson
Javascript Developer
Justen is a full-stack JavaScript developer with over a decade of experience. He has worked on projects for some of the biggest brands in the publishing and recording industries and has worked directly with small businesses and nonprofits in many fields. His broad skillset enables him to cover all the web technology needs of small businesses and startups or to fill the gaps in larger teams.