Fundamentals of Web Application Development · DRAFTFreeman

Prototypal Patterns

JavaScript is, truly, an object-oriented language. Hearing the term “object-oriented” may cause one’s mind to automatically jump to the idea of classes, which, in some other languages, form the foundational building blocks of code architecture. Classes are constructed as abstract “blueprints” from which concrete instances of objects are created at run-time, each object having its own form and function (that is, instance variables and methods) as prescribed by the ancestry of classes from which it inherits.

For those with a background in other OO languages, the instinct at the very beginning of a project may be to tackle every kind of real-world object in the problem domain and immediately map them to their own classes in code (or at least in large networks of tangled UML diagrams). Indeed, in a language such as Java, this is the only way in which one may proceed. Classes form the very foundation of the language. Everything is derived from a class. You can’t compile without them. We never ask the question – nor are we allowed to ask – do I need to use classes… at all?

Outside of strictly classical languages, the above is a question well worth asking ourselves, both at the beginning of a project and on a regular basis throughout development. In the multi-paradigm world of JavaScript, the practice of classifying objects is but one tool in the toolbox, among a plethora of design patterns that may – or may not – be appropriate for a given use, in a given codebase, for any given project.

I’ll let you in on a secret… for the vast majority of your time spent working in code as a JavaScript developer, you will not be worried about trying to re-create the notion of classes. In fact, approaching a project by starting to create a classical taxonomy is widely considered to be an anti-pattern in more functional practices; a premature optimization, at best, which is all too likely to lock in a rigid design that is difficult to grow and maintain along with the needs of the project.

In a “normal” JavaScript project, if you find that inheritance hierarchies are becoming very deeply nested and intertwined, or you’re having to jump through hoops to reason about the binding of this, then you would very likely benefit from taking a step back and asking – do I need all of these type abstractions and couplings? It is likely that the majority of data your program receives, such as from calls to an external API, will already be highly structured. We are not likely to need the rigid constraints and tight coupling imposed by a classical taxonomy.

JavaScript has no concept of classes in a… well, classical sense. For those who wish, JavaScript is flexible enough to implement in a way that approximately mimics classes. It does have a few class-like syntactic keywords. But these similarities, as we will see, are merely superficial.

Because the drive to apply classes on top of JavaScript is so strong, there is a great deal of code and convention running in production on the web that attempts to accomplish this, so it is important to understand how we may go about doing so. We will explore one example of such mimicry in this section, focusing on the underlying mechanisms of JavaScript which actually make it all possible. Along the way, we will point out the similarities and pitfalls one can encounter when these mechanisms do not behave how we might expect when viewed through a classical mindset.

The Prototype Chain

JavaScript employs what is known as prototypal inheritance, though we will soon see that “inheritance” may not be the most intuitive name for this phenomenon. Each object in JavaScript has an internal property, denoted here as [[Prototype]], which is simply a reference to another object in memory. By default, new objects are created with a [[Prototype]] which points to the built-in object Object.prototype.

We can explicitly set this [[Prototype]] pointer for each object:

let foo = { a: 42 }; // -> foo[[Prototype]] === Object.prototype
let bar = { b: 59 };

Object.setPrototypeOf(bar, foo); // -> bar[[Prototype]] === foo
let baz = { c: 99 };

Object.setPrototypeOf(baz, bar); // -> baz[[Prototype]] === bar

Thus, a virtual chain is created amongst the objects:

baz -> bar -> foo -> Object.prototype

The direction of the arrows here represents the direction of delegation, not the direction of “inheritance”.

Whenever we attempt to access a property of an object, we’re instructing the engine to perform a lookup for a property with a given name. The engine will start looking for that property on the immediate object. If it doesn’t find the property, it will traverse up the [[Prototype]] chain of objects and inspect each one, in turn, until it finds a property with the given name, at which point it will resolve the lookup with that value. If it reaches the top of the chain (usually Object.prototype) and still doesn’t find the property, it then resolves the lookup as undefined. Continuing from the previous example,

console.log(foo.a); // 42
console.log(foo.b); // undefined
console.log(foo.c); // undefined
console.log(bar.a); // 42 <-- woah!
console.log(bar.b); // 59
console.log(bar.c); // undefined
console.log(baz.a); // 42 <-- !
console.log(baz.b); // 59 <-- !
console.log(baz.c); // 99

Here we see the effects of the prototype chain in action. When we attempt to lookup bar.a, the engine doesn’t find a property a on bar itself, so it inspects bar’s [[Prototype]], which happens to be foo. On foo, it finds the property a: 42, and so resolves the bar.a lookup with the value 42.

Et voilà, the illusion of inheritance.

Notice that the direction of the prototype chain is one-way; that is, there is no way for the engine to look “down” the chain to resolve property lookups. For example, the lookup on bar.c only looks on bar -> foo -> Object.prototype. None of these objects have a property named c, so the lookup resolves with the value undefined.

It is extremely important to realize that no values are being copied between objects. This is a crucial difference between prototypal delegation and classical inheritance. Each of these lookups – and the engine’s traversal of the prototype chain – happens individually at run-time. If we mutate a property on an object, the objects “down” the prototype chain will appear to have undergone the same mutation.

console.log(foo.a); // 42
console.log(bar.a); // 42
foo.a = 43;
console.log(bar.a); // 43

If we set a property on an object, the actual assignment does not perform a lookup on the property. The object will be assigned its own property by that name which shadows any properties on its prototype chain that have the same name. Said another way, value assignments do not “travel up” the prototype chain.

console.log(foo.a); // 43 (resolved from foo.a)
console.log(bar.a); // 43 (resolved from foo.a)
console.log(baz.a); // 43 (resolved from foo.a)
bar.a = 45; // (adds a on bar which shadows foo.a)
console.log(foo.a); // 43 (resolved from foo.a)
console.log(bar.a); // 45 (resolved from bar.a)
console.log(baz.a); // 45 (resolved from bar.a)

In this paradigm, we might say that baz “inherits from” bar, which inherits from foo, which inherits from Object.prototype. But, as you just saw, the behavior is not directed “downwards” towards objects which inherit; the behavior works from the current object “upwards” through the prototype chain. For this reason, I often prefer to use the term prototypal delegation (instead of “inheritance”) to describe this behavior. With the prototype chain, the responsibility of defining properties (including methods) can be delegated to other objects.

You will often see the use of the built-in method Object.create().

let myObj = Object.create(someObj);

This is essentially identical to

let myObj = {};
Object.setPrototypeOf(myObj, someObj);

Implementing Classes with Prototypes

We do not have static classes in JavaScript. However, as we just say, we have objects and a mechanism that can mimic inheritance. So, how might we use these pieces to go about implementing a class-like pattern?

We need a type of template, something roughly analogous to the “blueprint” we so often think of in a classical sense. Well, we have objects – so let’s use one! We can create an object which contains properties and methods to which we would like “instance” objects to delegate by default.

let Person = {
  greet: function() {
   console.log(`Hi, I'm ${this.name}.`);
  },
};

let alice = Object.create(Person);
alice.name = 'Alice';
alice.greet(); // "Hi, I'm Alice."
let bob = Object.create(Person);
bob.name = 'Bob';
bob.greet(); // "Hi, I'm Bob."
bob.greet === alice.greet; // true

Here, through use of a prototype chain, we have allowed bob and alice to delegate some of their behavior to a common object, Person. On the last line of the example above, we see that the greet methods on both objects point to the exact same function in memory. Inside of greet, we use the implicit binding of this to the current context object in order to access specific properties of alice and bob.

Person is not a static class, nor any other kind of special, abstract mechanism – it’s just an object. It exists, in memory, bound by the same lexical rules as any other variable we may have declared in this scope, and might later be manipulated, or passed around, or automatically garbage-collected like any other object.

Constructor Functions

Consider the pattern above. Each time we want to create a new object “of a type Person” (that is, a new object which delegates to the Person object), we must take several steps: first create a new object, then set its [[Prototype]] reference, then add any unique initial state to it. After creating even a few objects, this can seem quite repetitive and tedious. What can we do with repetitive code? Move it to its own function!

function Person(name) {
  let obj = Object.create(Person.methods);

  obj.name = name;

  return obj;
}

Person.methods = {
  greet: function() {
   console.log(`Hi, I'm ${this.name}.`);
  },
};

let alice = Person('Alice');
alice.greet(); // "Hi, I'm Alice."
let bob = Person('Bob');
bob.greet(); // "Hi, I'm Bob."
bob.greet === alice.greet; // true

Inside of function Person, we create a new object whose [[Prototype]] is set to a given object, do any other initialization work we need, taking into account any parameters passed into the function, and finally return the new object. Why did we set the [[Prototype]] of the new objects to point to Person.methods? Remember – we want the prototype to be a single object in memory, thus we don’t want to create a new one on-the-fly every time Person is invoked. We also don’t want to introduce a new named variable into our current lexical scope that may later be mutated unintentionally. So, we create a new object as a named property on the Person function to serve as the prototype. This pattern is referred to as a factory function, since the function Person acts as a “factory” for producing Person-like objects.

This pattern of using a function in order to construct new objects of a certain “type” is so common that JavaScript provides some special syntax to make it easier to write: the new operator. When a function call is preceded by the new operator, the function is invoked in constructor mode.

When a function foo is invoked in constructor mode, a few special things take place behind the scenes:

  1. The this context is set to reference a brand new object (with precedence over all other previously-discussed this binding rules).
  2. The new object’s [[Prototype]] is set to point to foo.prototype.
  3. The main body of the function then otherwise executes as normal.
  4. If there is no return statement, the new object is implicitly returned.

So, our previous example can be re-written to use the new operator as

function Person(name) {
  this.name = name;
}

Person.prototype.greet = function() {
 console.log(`Hi, I'm ${this.name}.`);
};

let alice = new Person('Alice');
alice.greet(); // "Hi, I'm Alice."
let bob = new Person('Bob');
bob.greet(); // "Hi, I'm Bob."
bob.greet === alice.greet; // true
bob.greet === Person.prototype.greet; // true

Nice! Much more succinct. Notice that we didn’t have to create the object Person.prototype. By default, every function object has a property named prototype which is an empty object whose [[Prototype]] points to Object.prototype. Thus, in the code, we can start adding properties and methods onto the prototype right away, without having to waste an entire extra line to construct it. But remember (do you hear an echo?) – Person.prototype is just another object. It can be mutated, or replaced entirely, at your whim.

Whew. We made it! In the prior two sections, not only did we see a pattern for how behavior like classical inheritance is commonly implemented; more importantly, we explored the mechanisms of contextual this binding and prototypal delegation which make it possible.

The rest of our discussion in this part, I promise, will be much more fun.

You may have heard that ES2015 introduces a new class syntax, which includes such keywords as constructor, super, and extends. “JavaScript finally has classes!” report credulous bloggers across the web. Indeed, if you look at some of the example syntax for ES2015 “classes”, one might be tempted to draw even closer parallels between JavaScript and classical paradigms such as Java. This is not the case – the class syntax introduced in ES2015 is only syntactic shorthand for reproducing the behaviors we explored above. It does not – at all – change how object construction or prototypal delegation works behind the scenes.

As you may imagine, the introduction of this class syntax has been highly controversial in the JavaScript community. Proponents argue that the more Java-like syntax will help ease transitioning developers into the language and get them working quickly; opponents argue that the further syntactic abstraction will only lead to more confusion as the underlying mechanisms are further obscured – and, thus, not understood. For an exhausting (if not exhaustive, and, self-admittedly, rather opinionated) index of reference material covering this topic, see github.com/joshburgess/not-awesome-es6-classes.