A Comparative Guide to JavaScript Object Creation Patterns
Table of Contents
If you started learning programming with a traditional class-based language, JavaScript’s Object Oriented Programming might look confusing at first. You will find a lot of articles on different object creation patterns which might be a bit overwhelming. In this article, we will analyze 4 common object creation patterns. I will start with a brief review of some of the underlying concepts/features. Then, I will examine some code samples and discuss how each pattern works using some visual aid.
The aim of this article is to build up how each object creation pattern implements prototypal inheritance in steps and solidify your mental model. Let’s start with some background.
Inheritance in JavaScript #
The term inheritance in OOP commonly refers to class-based languages where there is a sub-class/super-class relationship, being sub-classes inherit behaviour and data from super-classes.
JavaScript does not have true-inheritance as we know it from class-based languages. Instead, it uses something called prototypal inheritance. When we create an object, say child
from another object called parent
, child
references it’s properties and methods to parent
object’s prototype. This way it can delegate an access request up in the prototype chain which is why we call this behavior delegation.
Comparison of prototype
and Hidden [[Prototype]]
Properties #
- In JavaScript,
prototype
property only exists in Functions. It is basically an object that Function returns when invoked as a constructor with thenew
keyword. [[Prototype]]
is a property that all objects contain. This property points to prototype object that the initial object created from.
Let’s look at the above diagram to see what happens when we create an empty function vs an empty object.
On the left, we have the Sample
function. When we create a function, it initially inherits from Function.prototype
which is referenced to it’s [[Prototype]]
property. Functions also contain a prototype
property that contains a prototype object. This object contains a property called constructor
that refers back to the function. We will make use of constructor
later.
On the right, we have our object Sample
with [[Prototype]]
property. The value that [[Prototype]]
points to depends on how the object is created. If it is created using the object literal syntax it will reference to Object.prototype
. If the object is created by a constructor function, it will be referencing to prototype
property of that function.
new
Keyword in JavaScript #
Lastly, let’s remember what happens when we prepend a constructor function call with new
keyword. Let’s say we have a construction function ConstFunc
and we have a line of code like this:
let newObj = new ConstFunc(arg1, arg2);
- When invoked, constructor function
ConstFunc
creates a new object this
(execution context) assigned to this new object as part of the function call.ConstFunc
is executed in this context with passed in arguments.- If there is no explicit
return
statement, the value ofthis
will be returned.
The created object will have the below relationship with the constructor function
newObj.constructor === [ Function: ConstFunc ]
ConstFunc.prototype === ConstFunc {}
newObj.constructor.prototype === ConstFunc {}
Now we covered all the basics, the rest will be quite straightforward.
Constructor Functions #
function Album(name = 'N/A', artist = 'N/A', year = 'N/A') {
this.name = name;
this.artist = artist;
this.year = year;
this.readTag = function() {
console.log(this.name + ' by ' + this.artist);
console.log('Released in ' + this.year);
};
}
let inAbsentia = new Album('In Absentia', 'Porcupine Tree', '2002');
inAbsentia.constructor; // [Function: Album]
Album.prototype; // Album {...}
inAbsentia.constructor === Album; // true
inAbsentia instanceof Album; // true
Album.prototype.isPrototypeOf(inAbsentia); // true
Object.getPrototypeOf(inAbsentia); // Album {...}
Album.isPrototypeOf(inAbsentia); // false
Album.prototype.constructor; // [Function: Album]
Object.getPrototypeOf(inAbsentia) === Album.prototype //true
Previously, we’ve discussed how constructor functions work while we explained how the new
keyword works. The key takeaway from this pattern is Album.prototype
. It is the prototype object for constructor function’s return value.
When we created the new objects using the constructor function Album
, all shared methods are copied into instances of Album.prototype
. Therefore, although [[Prototype]]
property of the new objects point to Album.prototype
, they will still use their own copy of the methods.
Factory Pattern #
function makeAlbum(name = 'N/A', artist = 'N/A', year = 'N/A') {
return {
name: name,
artist: artist,
year: year,
readTag: function() {
console.log(this.name + ' by ' + this.artist);
console.log('Released in ' + this.year);
},
};
}
var inAbsentia = makeAlbum('In Absentia', 'Porcupine Tree', '2002');
inAbsentia.readTag(); // In Absentia by Porcupine Tree
// Released in 2002
var deadwing = makeAlbum('Deadwing', 'Porcupine Tree', '2005');
deadwing.readTag(); // Deadwing by Porcupine Tree
// Released in 2005
Factory pattern makes use of a function that returns an object when invoked. Above example makes use of the object literal syntax for object creation, but other ways such as new Object()
can be also used.
Factory pattern is computationally inefficient since all the methods will be copied to every single object that is returned. Unlike constructor functions it is not possible to track down how these objects are created, or how they are related to makeAlbum
function. Also, it is not possible to update the “shared” behavior once the objects are returned by the factory function.
Pseudo-Classical Pattern #
let Album = function(name = 'N/A', artist = 'N/A', year = 'N/A') {
this.name = name;
this.artist = artist;
this.year = year;
};
Album.prototype.readTag = function() {
console.log(this.name + ' by ' + this.artist);
console.log('Released in ' + this.year);
};
Album.prototype.type = 'Prog-rock';
let inAbsentia = new Album('In Absentia', 'Porcupine Tree', '2002');
let deadwing = new Album('Deadwing', 'Porcupine Tree', '2005');
inAbsentia.readTag(); // In Absentia by Porcupine Tree
// Released in 2002
deadwing.readTag(); // Deadwing by Porcupine Tree
// Released in 2005
console.log(inAbsentia.type); // Prog-rock
console.log(deadwing.type); // Prog-rock
Pseudo-classical pattern combines constructor function and prototype pattern. This popular pattern resolves the problem of inefficiencies that have been discussed on the previous two patterns and introduces prototypal inheritance. Pseudo-classical pattern achieves this by creating a distinction between private properties and shared properties/methods. These are separated into a constructor function and the constructor function’s prototype.
JavaScript class
is introduced with ES6. Essentially, this does the same thing as the pseudo-classical model, it is just a syntactic sugar. It improves the organization of the code and provides a constructor method.
class Album {
constructor(name = 'N/A', artist = 'N/A', year = 'N/A') {
this.name = name;
this.artist = artist;
this.year = year;
this.type = 'Prog-rock';
}
readTag() {
console.log(this.name + ' by ' + this.artist);
console.log('Released in ' + this.year);
}
};
Lastly, The Pseudo classical pattern can be combined in the constructor function:
let Album = function(name = 'N/A', artist = 'N/A', year = 'N/A') {
this.name = name;
this.artist = artist;
this.year = year;
if (typeof this.readTag !== 'function') {
Object.getPrototypeOf(this).readTag = function() {
console.log(this.name + ' by ' + this.artist);
console.log('Released in ' + this.year);
};
}
if (!this.type) {
Object.getPrototypeOf(this).type = 'Prog-rock';
}
};
let inAbsentia = new Album('In Absentia', 'Porcupine Tree', '2002');
let deadwing = new Album('Deadwing', 'Porcupine Tree', '2005');
inAbsentia.readTag(); // In Absentia by Porcupine Tree
// Released in 2002
deadwing.readTag(); // Deadwing by Porcupine Tree
// Released in 2005
console.log(inAbsentia.type); // Prog-rock
console.log(deadwing.type); // Prog-rock
The if
statement checks if the “to be created” object has already a property called readTag
or type
. Since inAbsentia
is the first object that has been created by Album
constructor, this will evaluate true
and the shared behavior will be defined on the object’s prototype. When we create other objects with the same constructor function, this block of code will be skipped by the if
statement.
OLOO (Object Linking to Other Object) #
OLOO, is an object creation pattern based on creating new objects with Object.create
method using prototype objects. It is a relatively simpler pattern since it is not dealing with constructors and prototype properties.
let Album = {
name: 'N/A',
artist: 'N/A',
year: 'N/A',
readTag() {
console.log(this.name + ' by ' + this.artist);
console.log('Released in ' + this.year);
},
init(name, artist, year) {
this.name = name;
this.artist = artist;
this.year = year;
return this;
},
};
let inAbsentia = Object.create(Album).init('In Absentia', 'Porcupine Tree', '2002');
let unknownAlbum = Object.create(Album);
inAbsentia.readTag(); //In Absentia by Porcupine Tree
// Released in 2002
console.log(Album.isPrototypeOf(inAbsentia)); // true
console.log(Album.isPrototypeOf(unknownAlbum)); // true
console.log(inAbsentia.constructor.prototype); // Object.Prototype
console.log(Object.getPrototypeOf(inAbsentia)); // Album
Album.check = function() {
console.log('I inherit');
};
inAbsentia.check(); // "I inherit!"
With OLOO, object creation and initialization occur at different times. The latter can be done by using an optional init
method as shown above.
Because OLOO Pattern does not use constructors, examining inheritance with methods like Object.prototype.constructor
will not work as expected and simply return Object.prototype
(the top element in the prototypal inheritance hierarchy). Instead, isPrototypeOf
and Object.getPrototypeOf
could be used for such purpose. Also, we can not use instanceof
because the right-hand operand has to be a function.
Above inAbsentia
and unknowAlbum
have different properties, but they share the same methods as Album
.
This approach provides behavior delegation rather than copying of all methods at object creation time. Therefore, when we add a new method check
into prototype object and call this method on the already created instance inAbsentia
, this method call will be delegated to its prototype.
TL;DR #
There are a lot of good article’s on object creation patterns in JavaScript. In this article, I’ve tried to focus on the underlying principles first.
[[Prototype]]
is a hidden property contained in all objects. It is referenced to initial object’s prototype object.prototype
is a functions prototype object that it returns when invoked withnew
keyword.- When an object is created invoking a constructor function the returned object has a
constructor
property that points to constructor function.
At the second part I’ve discussed and compared 4 common object creation patterns with code samples and diagrams. Here are some key takeaways:
- Factory Pattern gathers object creation functionality in a single function, prevents repetition (more on DRY). Each function invocation creates a new object. It is inefficient, hard to trace the sources of the objects. Objects have a copy of their own methods.
- Constructor Function, takes one step further from factory pattern by introducing a fake prototypal inheritance. Created object will still have their own copy of methods, but their source can be traced by using
constructor
property on the instances. - Pseudo-Classical Pattern is a combination of constructor function and prototype pattern. It resolves the above inefficiencies by introducing prototypal inheritance. It achieves this by separating private properties and shared properties into constructor function and it’s prototype.
- OLOO(Object Linking to Other Object), is a simpler solution that uses a prototype object instead of a constructor function. It relies of creating new object using
Object.create
with prototype object passed in as argument.
We now know all these patterns but which is “the best”? My answer would be It depends… The latter two are better practices when it comes to building your code organization. However, the former two are also important to understand the principals behind prototypal inheritance and can save you some time when dealing with smaller problems.