The Guide I Wish I Had for JavaScript Object Creation Patterns
Everything you need to know about object creation in JavaScript
If you’re learning about object-oriented programming in JavaScript, you’ve probably already encountered these object creation patterns. This guide will be useful to the student who is trying to lock in a mental model of each pattern, their pros + cons, and how to model inheritance or property delegation. If you’re following along from Launch School, don’t read this until after you’ve finished lessons 1–4.
Object Factories
Object factories are functions that return objects that can be used to automate the process of creating objects. All objects will have the same “type” in the sense that they’ll have the same properties for state and the same methods, but using Object.getPrototypeOf
would return Object.prototype/{}
, so you can’t really figure out what function you used to construct instance objects.
This pattern has some drawbacks, including memory inefficiency due to each new instance having a complete copy of all methods and the inability to check out the matching prototype object used to create the object (the prototype is a generic object).
> let cello = createInstrument("cello","squeak",500)
> Object.getPrototypeOf(cello) // {}
To write an object factory function, you declare a function that takes arguments that will make up the object’s state. In the body of the function, return
an object literal. The properties should be set by using the arguments as you see below for instrument
, noise
, and value
, and methods can be added. Overall, you're creating a skeleton version of the objects you'll create using this function, and all of the properties will get added to each new object you make using this function, hence the memory inefficiency.
You might also see this type of pattern written with an arrow function expression. If you opt for this syntax, remember to wrap the object in parentheses. By default, JavaScript assumes you want to create a function body when you use braces {}
.
Despite the pitfalls with relationship modeling and code redundancies, some developers think it’s ideal to code using factory functions and mix-ins because inheritance cannot model all scenarios.
Object Factories With Mix-Ins
Using object factories with mix-ins is good for modeling objects that don’t have a clear “is-a” relationship. In the example below, we can create three distinct and unrelated types of objects — platypuses (aka platypi or platypodes), penguins, and humans — that share some behavior but are not in any way subclassable. Rather than duplicating code in methods for each object factory, the behavior is mixed-in using Object.assign
, which copies all enumerable properties from a source object into a target object.
In the code, details
, swim
, and layEggs
are objects with properties that all have function values. There are three functions for creating three different types of objects: createPlatypus
, createPenguin
, and createHuman
. These three functions can be used to create new instances of these types of objects.
Taking a look at createPlatypus
, we see the function takes a name parameter. In the function body, Object.assign
is used to assign the enumerable properties from the details
, swim
, and layEggs
objects into an empty object. After the properties from those three objects are copied into the new object, it uses the addDetails
method, which was copied into createPlatypus
from the details
object when we used the Object.assign
method, which copies in enumerable properties. Thus, it creates a new platypus object with a name property and the methods from the details
, swim
, and layEggs
objects.
If you check the properties of objects instantiated from these functions, what do you think you’ll see?
> let platypus = createPlatypus("platypus");
> let penguin = createPenguin("penguin");
> let human = createHuman("human");
> human
{ displayDetails: [Function: displayDetails], swim: [Function: swim], name: 'human' }
> penguin
{ displayDetails: [Function: displayDetails], swim: [Function: swim], layEggs: [Function: layEggs], name: 'penguin' }
> platypus
{ displayDetails: [Function: displayDetails], swim: [Function: swim], layEggs: [Function: layEggs], name: 'platypus'}
If the use of the addDetails
method was confusing there, here is another version where we set the details in the function itself. Note that within these functions, the execution context is going to be the global object, but we can use an empty object assigned to a variable to add some properties locally as shown on lines 22–25.
Looking below at lines 30–32, what is happening here is that we use the Object.assign
method to copy the properties from the details
, swim
, and layEggs
objects into the target, which is an empty object {}
. Then we chain another method call and use addDetails
, which the object now has, and add in the name
argument that we passed in the function call.
Constructor Pattern
The constructor pattern makes use of the new
operator and a function to create new objects. When a function is invoked with the new
operator/keyword, it becomes a constructor function and a few things happen behind the scenes. These steps are described using the code snippet below:
- The
clarinet
variable is assigned to a new object created by invoking theInstrument
constructor function with thenew
operator.
- The
[[Prototype]]
/__proto__
property of theclarinet
object is set to the prototype of the constructor function. - The execution context for the function execution
(this)
points to the new object. - The constructor function executes.
- The new object is returned.
Constructors With Prototypes (Pseudo-Classical Pattern)
In JavaScript, functions have a prototype
property, which has a constructor
property that points back to the function. For object instances, the actual prototype (__proto__
property) for an object instantiated by new functionName()
points to the prototype
property of the function used to construct the object, functionName.prototype
.
To avoid defining the functions on each newly constructed object, the functions can be added to Instrument.prototype
instead so methods called by instances can be delegated to Instrument
's prototype
property.
This diagram + video overview helps to demonstrate what that all means:
The main thing to differentiate is the difference between the prototype
property/object on our Instrument
function vs. the prototype of an instance object, which is linked via the internal [[Prototype]]
/__proto__
property.
There are a few key advantages in using the pseudo-classical pattern instead of object factory functions.
- You can now model relationships between objects.
- Shared behavior lives in the prototype and is delegated instead of being copied into each new object.
- If you need to modify the behavior/methods in the prototype, it’s reflected innately and you don’t need to edit your objects.
Inheritance With the Pseudo-Classical Pattern
Pseudo-classical refers to how constructor inheritance mimics classes from other OOP languages.
In pseudo-classical inheritance, a constructor’s prototype inherits from another constructor’s prototype, i.e., a sub-type inherits from a super-type.
In the code, all objects created by the StringInstrument
constructor inherit from StringInstrument.prototype
, which inherits from Instrument.prototype
. Due to this, all string instrument objects can access the methods from Instrument.prototype
.
If a method from Instrument.prototype
needs to be modified to work with the StringInstrument.prototype
, we can define that on the StringInstrument.prototype
object with the same method property name so it shares an interface, i.e., the method can be called on either object and return the expected output.
In the code below, we create two functions, Instrument
and StringInstrument
. To set the prototype
property, we use Object.create()
on line 16. Why are we creating a new object? If we assign the existing prototype of the Instrument
function to the StringInstrument
function's prototype
property, we'll no longer have an inheritance structure because the prototype objects will be the same thing in memory.
What not to do:
. // code omitted for brevity
.
.
StringInstrument.prototype = Instrument.prototype; // DON'T DO THISStringInstrument.prototype.tuneStrings = function() {
console.log("I have strings.")
}let oboe = new Instrument("oboe","squeaky squeak");// Oboe will think it is both a string instrument + an instrumentoboe.tuneStrings(); // I have strings. => should be a TypeError
console.log(oboe instanceof StringInstrument); // true => should be false
Another Way to Set the Inheritance Chain
If you look at the properties you are setting in the Instrument
and StringInstrument
functions, you'll see that they're similar. That suggests you can use the Instrument
constructor in StringInstrument
. To do that, invoke Instrument
with its execution context explicitly set to the execution context of StringInstrument
as seen with the function method call
below. The first argument is this
, then you pass any number of arguments.
Here’s the same thing but with ES6 class syntax:
The MDN docs has a great overview, so here it is adapted to the code above:
- The
constructor()
method defines the constructor function that represents ourInstrument
class. play()
is a class method. Any methods you want associated with the class are defined inside it, after the constructor.- For subclasses, the
this
initialization to a newly allocated object is always dependent on the parent class constructor, i.e., the constructor function of the class from which you're extending. - Here we are extending the
Instrument
class — theStringInstrument
subclass is an extension of theInstrument
class. So forStringInstrument
, thethis
initialization is done by theInstrument
constructor. - To call the parent constructor, we have to use the
super()
operator.
Objects Linking to Other Objects (OLOO)
OLOO embraces only caring about objects linked to other objects. OLOO is a delegation pattern rather than an inheritance pattern. We look at what objects want to delegate to other objects. OLOO objects don’t make use of constructors or .prototype
.
If you instantiate a new object with Object.create()
with the variable name cello
assigned as such: let cello = Object.create(instrumentPrototype)
, this object currently has no properties.
> cello // {}
To finish the initialization of your object using the OLOO pattern, use an initialization method from the prototype object. It’s common to use init
as the name of the initialization method but not required. When you use object.create
to make a new object, the object you pass as an argument is the intended prototype of the new object, so now all of the methods from instrumentPrototype
object are available to the cello
object through property delegation.
You can use Object.getOwnPropertyNames(obj)
to verify that the methods were not copied into the new object.
As this pattern most closely resembles an object factory, we can compare it to object factories. The key advantage of OLOO vs. an object factory is memory efficiency. All objects created with the OLOO pattern inherit methods from a prototype object, whereas factory functions copy the methods into each new object made with the factory function.
Let me know if you have any further questions and I can write back or add more to the article!