Fundamental Object Design Patterns in JavaScript
June 17, 2018
As a JavaScript developer, much of the code you will be writing will deal with objects. We create objects to help organize code, reduce redundancy, and reason about problems using object-oriented techniques. The benefits of object-oriented design are readily apparent, but recognizing the usefulness of objects is just the first step. Once you have decided to use an object-oriented structure in your code, the next step is to decide how to do it. In JavaScript, this is not so simple as designing a class and instantiating objects from it (as JavaScript has no true classes, but that’s a question for another blog post.) There are many different design patterns for creating like objects, and today we are going to explore some of the most common ones. Each pattern has its own pros and cons, and hopefully by the end of this blog post you’ll be ready to decide which of these options are right for you.
Every developer has their own preferences, but I would offer up the following criteria to consider when deciding on an appropriate object design pattern for your code.
- Readability: Like all good code, object-oriented code should be readable not only to you, but also to other developers. Some design patterns are easier to interpret than others and you should always keep readability in mind. If you are having a hard time understanding what your code is doing, then other developers will almost certainly have no clue.
- Repetition: One of the major benefits of object-oriented code is that it reduces redundancy. If your code is likely to have many objects of the same type, then an object-oriented design is almost certainly appropriate. However, some patterns reduce redundancy more than others. Keep this in mind, while simultaneously considering that greater redundancy reduction may result in loss (or at least more difficult implementation) of certain customization options.
- Hierarchical Structure: As we mentioned before, JavaScript has no true classes, and it’s best not to think of your objects in this fashion; however, there are options for delegating like behavior to various sets and subsets of objects. This is done using prototype delegation, wherein an object will search its entire prototype chain for a given property. In this fashion, it is possible to create a hierarchical structure of objects where an object of a lower type in the structure can delegate behavior up its prototype chain (for example, a
Chicken
object delegating alayEgg
behavior to a higher-up prototypeBird
object.) Prior to selecting a design pattern, take a moment to consider whether you expect a hierarchical structure to be necessary, and if so, which behaviors should be placed on which object types.
And with those few short recommendations complete, let’s get to our review of the most common design patterns you are likely to encounter.
Factory Object Creation Pattern
The Factory Object Creation Pattern, or simply the Factory Pattern, uses so called “factory functions” to create objects of a similar type. Each object created by such a function has the same properties, to include both state and behavior. Take for example the following:
function makeRobot(name, job) {
return {
name: name,
job: job,
introduce: function() {
console.log("Hi! I'm " + this.name + ". My job is " + this.job + ".");
},
};
}
var bender = makeRobot("Bender", "bending");
bender.introduce(); // Hi! I'm Bender. My job is bending.
var wallE = makeRobot("Wall-E", "trash collection");
wallE.introduce(); // Hi! I'm Wall-E. My job is trash collection.
Here, we have a function, makeRobot()
, which takes two parameters (name
and job
) and uses them to assign state to an object literal inside the function, which it then returns. In addition, the function defines a method, introduce()
, on the same object. In this example we instantiate two robot objects, both of which have the same properties (albeit with different values.) If we wanted to, we could create thousands more robots in exactly the same way and reliably predict what their properties would be each time.
Although the factory pattern is useful for creating like objects, it has two major drawbacks. First, there is no way to check whether a given object was created by a certain factory. We cannot, for example, say something like bender instanceof makeRobot
to find out how bender
was created. Second, the factory pattern does not share behaviors, rather, it simply creates new versions of a behavior every time it is called and adds them to the object being created. As a result, methods are repeated anew on every object created by the factory function, taking up valuable space. In a large program, this could prove extremely slow and wasteful.
Constructor Pattern
One way to address some of the weaknesses of the factory pattern is to use the so-called Constructor Pattern. In this pattern, we use a “constructor function,” which is really just a regular function that is called using the new
keyword. By using thenew
keyword we are telling JavaScript to execute the function in a special fashion, and four key things will happen:
- The function will immediately create a new object.
- The function execution context (this) will be set as the new object.
- The function code will execute within the new object’s execution context.
- The function will implicitly return the new object, absent some other explicit return.
Let’s alter our previous example and try making some robots using the constructor pattern.
function Robot(name, job) {
this.name = name;
this.job = job;
this.introduce = function() {
console.log("Hi! I'm " + this.name + ". My job is " + this.job + ".");
};
}
var bender = new Robot("Bender", "bending");
bender.introduce(); // Hi! I'm Bender. My job is bending.
console.log(bender instanceof Robot); // true
var wallE = new Robot("Wall-E", "trash collection");
wallE.introduce(); // Hi! I'm Wall-E. My job is trash collection.
console.log(wallE instanceof Robot); // true
This snippet looks a lot like the previous one, except this time we use the this
keyword inside the function to reference a new object, set some state and properties on it, and then implicitly return when the function finishes executing. For the sake of convention (not any actual syntactical reason), we have called our function simply Robot
with a capital “R”. And, unlike with the factory pattern, we can even check to see whether a given object was constructed by the Robot
function with instanceof
.
You might be tempted to think of this as though we had created a Robot
“class”, but it is important to remember that we are not creating copies of Robot
as we might be in a true class language. Rather, we are exploiting a link that is created between the newly instantiated object’s prototype and its corresponding constructor function’s prototype, which facilitates prototypal delegation. We haven’t really taken advantage of that functionality in the above snippet though as we are still creating a new introduce()
method on every single new robot. Let’s see if we can fix that.
Pseudo-classical Pattern
Thus far we haven’t really explored prototypal delegation other than to mention briefly that it exists. Now it’s time to see it in action and eliminate some code redundancy at the same time. Object prototypes and their delegation behavior are worthy of an entire blog post, but we can get at least a basic picture here. In essence, when a certain property is called on a certain object, for example someRobot.introduce()
, it goes to look for that property first on itself. If no such property exists, it then looks at the properties available to its prototype object, which in turn looks at its prototype object if necessary, and so on all the way up to the top-level Object.prototype
. The prototype chain allows delegation of behavior, wherein we don’t have to define some shared method on lower-level objects of the same type. Instead, we can define the behavior on whichever prototype they all share and thus eliminate redundancy by only defining the code once. Here it is in action with our robots.
function Robot(name, job) {
this.name = name;
this.job = job;
}
Robot.prototype.introduce = function() {
console.log("Hi! I'm " + this.name + ". My job is " + this.job + ".");
};
var bender = new Robot("Bender", "bending");
bender.introduce(); // Hi! I'm Bender. My job is bending.
console.log(Object.getPrototypeOf(bender) === Robot.prototype); // true
var wallE = new Robot("Wall-E", "trash collection");
wallE.introduce(); // Hi! I'm Wall-E. My job is trash collection.
console.log(Object.getPrototypeOf(wallE) === Robot.prototype); // true
As in the constructor pattern we are using the new
keyword to create a new object, assign some state, and then implicitly return that object. However, in this case we do not define the introduce()
method on each of our robots. Rather, we define it on the Robot.prototype
object, which as we have seen, acts as the prototype of each new object created by the Robot
constructor function. When we attempt to call, for example, wallE.introduce()
, the wallE
object sees that it has no such method and goes searching up its prototype chain, quickly finding a method by that name on Robot.prototype
. Indeed, if we check wallE
’s prototype by using bject.getPrototypeOf()
, we can see that it is indeed Robot.prototype
.
This design pattern, known as the pseudo-classical pattern, solves both of the problems we initially saw in the factory pattern; however, it still presents us with the somewhat uncomfortable illusion of a class-based system. This can lead to some unfortunate detours in our mental model of how JavaScript really operates, and some unexpected gotchas in actual program execution. One solution to this problem, popularized by Kyle Simpson, author of You Don’t Know JS, is the Object Linked to Other Object (OLOO) Pattern, which we shall explore next.
Object Linked to Other Object Pattern
If the pseudo-classical pattern is a tentative combination of the constructor pattern and prototypal delegation, then OLOO might be thought of as a full-on embrace of JavaScript’s prototype system. In this pattern, we don’t use a function to create objects at all. Instead, we define a blueprint object of sorts, which we then explicitly use as the prototype for any individual objects we need. We can see this in action with one last set of robots.
var Robot = {
init: function(name, job) {
this.name = name;
this.job = job;
},
introduce: function() {
console.log("Hi! I'm " + this.name + ". My job is " + this.job + ".");
},
};
var bender = Object.create(Robot);
bender.init("Bender", "bending");
bender.introduce(); // Hi! I'm Bender. My job is bending.
console.log(Object.getPrototypeOf(bender) === Robot); // true
var wallE = Object.create(Robot);
wallE.init("Wall-E", "trash collection");
wallE.introduce(); // Hi! I'm Wall-E. My job is trash collection.
console.log(Object.getPrototypeOf(wallE) === Robot); // true
In this snippet, we first define a Robot
object, which will serve as the prototype for all future robots. The Robot
object contains all the behaviors we expect of our robots; however, it does not set any state. Rather, we define an init()
method on Robot
, which we will use to set state on any future robots. Speaking of future robots, instead of creating them with a function, we do so by using the Object.create()
method, which accepts a prototype as an argument. By passing Robot
to the Object.create()
method, we ensure that the resulting object has Robot
for its prototype. We then call the init()
method on our individual robots to set the necessary state. We can even check to see whether a given object is of a certain type by using the handy Object.getPrototypeOf()
method, as we did in previous snippets.
OLOO allows us to share like behaviors and check the type of individual objects, all while sidestepping the class illusions inherent in the constructor and pseudo-classical patterns. For many developers, this method is preferred because it provides for easy-to-understand code that is also efficient and clean.
Which object creation patterns you ultimately choose is up to you, but hopefully this has been a good introduction to some of the available options. Objects in JavaScript are incredibly powerful, especially when combined with effective use of object prototypes—and we haven’t even begun to explore the options made available to us by fully exploiting multiple steps on the prototype chain. That, however, is a topic for another day. Until then, happy coding!
Note: This article was originally published on Medium.