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:
- The
this
context is set to reference a brand new object (with precedence over all other previously-discussedthis
binding rules). - The new object’s
[[Prototype]]
is set to point tofoo.prototype
. - The main body of the function then otherwise executes as normal.
- 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.