When first learning programming, I was very pleased by what seemed to be the inherent objectivity of code. Everything appeared to be very black and white, cut and dry, yes or no. I totally understood that there were many different ways to approach any given problem, but ultimately either your unique approach worked or it didn’t — there was no in-between.
My first encounter with how subjective programming can be came when I started to truly learn about inheritance. I say ‘truly’ because I first learned about class constructors when I taught myself Python and my genuine impression at that point was “okay I got it, this is what all classes look like and this is how all inheritance works.” Class inheritance was inheritance to me. I had no idea that there were different types of inheritance or even different implementations of those types.
We’re going to examine five ways you can construct new object instances and discuss each approach’s advantages and disadvantages with regard to inheritance by considering the following questions:
- How is an instance created?
- Which properties and methods does each instance have access to?
- How does an instance gain access to new methods and properties?
- How is this approach similar to or different from other approaches to object creation?
- What are the general pros, cons, and use cases for this approach?
“Favor object composition over class inheritance.”
~ The Gang of Four, “Design Patterns: Elements of Reusable Object Oriented Software”
How is an Object literal instance created?
Setting a variable to be an empty object is the same thing as calling new Object().
Whenever we simply declare a new Object with open-and-closed curly braces, this is known as an Object literal. Object literals implicitly call on the constructor property within the Object prototype to create instances of Object literals.
Which properties and methods does each Object literal instance have access to?
While the athlete Object here might not have any immediately visible properties or methods, it does come with one built in.
This __proto__ property, short for prototype, is attached to any instance of an Object and points to the prototype of the object constructor that was used to create that instance.
Since Object literals are essentially the same thing as calling the new Object() constructor function, Object literals inherit all methods and properties associated with the Object prototype such as toString and hasOwnProperty.
How does an Object literal instance gain access to new methods and properties?
Instances of Object literals are easy to extend. You can give them new properties and methods by simply attaching them to the new instance.
Whenever we extend instances, those new properties and methods belong to that individual instance — meaning other instances do not have access to these new properties and methods.
Since Object literals inherit from the Object prototype, we would need to extend the Object prototype to allow all instances of Objects to share custom properties.
While this technically works, it’s very uncommon and leads to some weird behavior.
How is using Object literals similar to or different from other approaches to object creation?
Since factory functions are basically just functions that return Object literals, you can think of a factory function as a machine that uses arguments to more easily generate customized Object literals.
Any instances created from the factory function will behave similarly to Object literals and instances of new Object();
Standalone Object literals can be more individual and specific than when Objects literals are used as prototypes. In our above examples, we gave most of the Object literals a name property of “Mookie.” Since not all athletes are named Mookie, those Object literals would not be good candidates to serve as a prototype. Objects used as prototypes need to be able to describe all instances of that object.
The way we approach inheritance with Object literals is most dissimilar to object constructor functions and class constructors. Instances of Object literals are only one step down the prototype chain from the Object prototype, but the other two are at least two steps down. Object literals are better for object composition whereas the other two more closely resemble classical inheritance patterns.
What are the general pros, cons, and use cases for Object literals?
Pros: Very simple structure with only one level of inheritance that is easy to understand syntacticly and conceptually. Great for object composition.
Cons: The only way to provide methods and properties to all instances of Object literals is to monkey patch the Object prototype, which gets messy in a hurry. Since we have to individually define the properties for every new Object literal, declaring a bunch of them all over the place would take forever.
Use cases: Object literals are best used as prototypes for other objects, so they are better for when you are intentionally trying to favor object composition (with Object.assign and Object.create) over class inheritance.
How is a factory function instance created?
Factory functions can be thought of as machines that generate Object literals. The main takeaway from this is that instances generated by factory functions will behave identically to the Object literals we discussed in the previous section.
Both objects have three attributes (name, age, team), one method (run), and a __proto__ property which points to the Object.prototype. The Object.prototype contains a constructor which is used to construct new Object instances.
Which properties and methods does each factory function instance have access to?
Objects produced by factory functions have access to the arguments passed into the function parameters as properties.
Note the differences between the mookie instance created by the factory function below and the empty bryce instance.
How does a factory function instance gain access to new methods and properties?
If each instance created by a factory function is basically an Object literal, and Object literals are created with the constructor belonging to the Object.prototype, this means that factory function instances inherit from the Object.prototype.
We would need to again monkey patch the Object.prototype to create any new methods or properties that could be shared by all factory function instances.
Since these factory function instances behave identically to Object literals, property and method extension is easy.
How is using factory functions similar to or different from other approaches to object creation?
Although factory functions look different from Object literals, their instances behave identically since they were both built by the Object.prototype constructor. Instances of both are also great candidates to use when intentionally trying to favor object composition over inheritance since the need to manipulate the Object.prototype makes prototypal inheritance tricky.
What are the general pros, cons, and use cases for factory functions?
Pros: It’s basically a machine that churns out Object literals with customized properties, which is much faster than individually declaring a ton of Object literals.
Cons: Since instances generated by factory functions come packaged with specific, customized properties (such as “name”, “age”, and “team”), they would not be good candidates for use as prototypes.
Use cases: Great for favoring object composition over inheritance with Object.assign. Take the example below, where we use the assign method to easily compose a new object by combining two different objects.
Object Literals as Prototypes
How is an instance created by using an Object literal as a prototype?
The X-factor of this approach to object creation is the use of the Object.create method.
Before we move on, it is crucial to understand the difference between the way we’ve created objects so far and the way Object.create creates objects.
For the mookie instance, Object.create takes in the athlete Object literal as an argument. Object arguments passed into Object.create are used as the prototype for instances instantiated with Object.create.
Alternatively, the bryce instance above calls on the Object function constructor to create a new Object instance with the properties described in athlete copied over as attributes.
Let’s see what these results look like under the hood.
Which properties and methods does each instance have access to?
Hopefully you noticed that in the example above that the bryce instance now has the properties and methods described in the athlete object but the mookie object does not. What gives?
Even though the mookie instance only has a __proto__ property, it can still access the athlete properties and methods just like bryce can. Let’s add a “name” property to the mookie object so the run method doesn’t log undefined.
This is a great opportunity to reinforce our understanding of the prototype chain. Remember that when you call a method on any object, such as mookie.run(), that object looks at all of its properties and methods and tries to find a method by the same name.
If the object can’t find the method within itself, it then looks to its prototype. What is in mookie’s __proto__ property?
The mookie instance’s __proto__ property has all of the properties and methods that belonged to our original athlete Object because mookie’s prototype is the athlete Object. This is why the mookie instance can call the .run() method even though the method is a property of the athlete object, which is assigned to be the mookie’s prototype by Object.create.
Why is mookie’s prototype the athlete Object and not the Object.prototype? Because that’s how Object.create works.
Up until this point, the prototype for all of our Object literals has been the Object.prototype. With Object.create, we can reference customized objects to be used as prototypes for future instances.
How does an instance gain access to new methods and properties?
Just like in the previous examples where we used different approaches to create objects, individual instances of objects are very easy to extend in a way that doesn’t affect other instances.
But what if we want to give new properties and instances to all athlete instances generated with Object.create? Since the athlete object itself serves as the prototype for new instances, giving all athlete instances new methods and properties is as simple as extending the athlete object itself.
Continue to be aware of where instances access their properties and methods from. The bryce and mookie instances don’t directly own the new .signAutograph method, but their prototype does.
How is using an Object literal as a prototype similar to or different from other approaches to object creation?
Prototypal inheritance with this approach is way cleaner than it was when we had to mess with the Object.prototype with Object literals and factory functions. We can easily extend the custom athlete object that we’re using as our prototype and it will only affect athlete instances that were instantiated with Object.create.
What are the general pros, cons, and use cases for Object literals as prototypes?
Pros: This approach works brilliantly for object composition with Object.assign. A standalone object that is combined with another object by using Object.assign is known as a mix-in.
Cons: The problem we encounter with the code above is that we can only extend the prototype (clearly defined as the athlete object in Object.create) and expect instances to inherit the extended behavior. Since the baseballPlayer object is a mix-in and not the prototype of the baseballMookie instance, changing the mix-in won’t result in accessible behavior by the prototype.
Use cases: This approach works very well with concatenative inheritance patterns such as Object.assign in addition to prototypal inheritance, meaning its very flexible and useful in a lot of circumstances.
Object Constructor Functions
How is an object constructor function instance created?
We can use the Athlete constructor function to bind the properties and methods defined within it to the object created by the new keyword before saving that new object in a variable.
Which properties and methods does each object constructor function instance have access to?
Notice how the properties passed in as arguments to the constructor function wind up belonging to the instance and not the Athlete constructor function prototype.
We can verify that these properties belong to the instance itself and not the prototype by using .hasOwnProperty method.
But where did the .hasOwnProperty method come from? The prototype chain, of course.
An Athlete instance inherits from the prototype of the Athlete function constructor…
…which inherits from the Object.prototype
How does an object constructor function instance gain access to new methods and properties?
If we want all Athlete instances to inherit new behavior, we will need to adjust the Athlete constructor function’s prototype so that instances can inherit that functionality through the prototype chain.
The mookie instance now has access the .jump method that was added to the Athlete constructor function’s prototype through the prototype chain.
How is using an object constructor function similar or different from other approaches to object creation?
This method most closely resembles a typical constructor that we would use for class inheritance. Instances can be used along with other objects for object composition, but the structure more closely resembles prototypal inheritance patterns.
Instances that use this pattern are two levels down from the Object.prototype in the prototype chain. This is different from Object literals and instances created by factory functions, which inherit from the Object.prototype as opposed to inheriting from prototypes that we defined ourselves.
If we compare the mookie Object literal instance and the tatis Athlete instance, we can see that the the tatis Athlete instance is one step further down the prototype chain than the mookie Object literal instance.
What are the general pros, cons, and use cases for an object constructor function?
Pros: Easily allows us to create many instances of a custom object “type” — in our example case, instances of Athletes. Both instances and prototypes are easily extendable, and having the Athlete.prototype as a bridge to the Object.prototype allows us to use prototypal inheritance without needing to mess with the Object.prototype.
Cons: IMPORTANT: You cannot afford to forget the new keyword when creating new instances. If you forget the new keyword, the this keyword will bind itself to the global ‘this’ (which in a browser would be the window object). That’s gonna get weird in a hurry.
How is a class instance created?
Classes use the new keyword alongside class constructors to create new instances. The syntax is identical to the approach when we use object constructor functions. Every class has a constructor that defines properties which will belong directly to new instances, but methods defined outside of the constructor scope will belong to the Class.prototype, not the instance itself.
Which properties and methods does each class instance have access to?
Instances of classes can access any method or property that belongs to itself or whichever prototype was referenced to construct the instance.
How does a class instance gain access to new methods and properties?
Class instances are easily extendable just like with other approaches to object creation.
Class prototypes are also easily extended, which allow instances to take advantage of any new properties and methods that are added later.
How are class constructors similar to or different from other approaches to object creation?
Class constructors work very similarly to object function constructors because the constructor function in a class is an object function constructor. The difference between classes and object function constructors is that object function constructors add to their prototype after definition…
…whereas classes can define methods that belong to the class prototype within the class definition itself.
Class constructors also allow us to create hierarchies and taxonomies with sub-classes or child classes. We can extend the Athlete class into a BaseballPlayer child class to give our tatis instance more baseball-specific properties and methods in addition to the properties and methods it inherits from the Athlete parent class.
Notice how the tatis instance, which was constructed by the BaseballPlayer child class, has access to all of the methods and properties defined in both BaseballPlayer and Athlete parent class. The mookie instance, constructed by the Athlete parent class, does not have access to the properties defined within the BaseballPlayer child class.
What are the general pros, cons, and use cases for this approach?