JavaScript language stands on two pillars: functional programming and prototypal delegation. The addition of classes
in JavaScript is mere a syntactic sugar to give it Object-oriented programming feel: unwrap them and you will find functions inside.
Prototypal Delegation
Prototypal delegation is all about delegating the responsibility to the prototype higher up in the [[Prototype]] chain.
function foo(){}
Object.getPrototypeOf(foo) === Function.prototype; // true
Object.getPrototypeOf(Function.prototype) === Object.prototype; // true
Object.getPrototypeOf(Object.prototype); // null
The prototype chain will look like this:
foo --> Function.prototype --> Object.prototype --> null
In simple words, if you try to look for a property that is not owned by the object, the JavaScript engine will traverse up its prototype chain until it finds it. Let's see an example to understand it.
const recipe = { name: "Garlic Naan" };
const recipeBook = {
getRecipeName() {
console.log(this.name);
},
};
// Set 'recipeBook' as the prototype of 'recipe'
Object.setPrototypeOf(recipe, recipeBook);
// Prototypal delegation in action
recipe.getRecipeName(); // prints 'Garlic Naan'
The prototype chain will look like this:
recipe --> recipeBook -> Object.prototype --> null
The object recipe
doesn't own the getRecipeName
property. But by setting recipeBook
as its prototype, we have delegated the job of printing the name
to the recipeBook
instance. This is called the prototypal delegation.
Now, let say you also want recipe
to delegate the task of orderRecipe()
to another instance order
. You can do this by extending the prototype chain like:
const order = {
orderRecipe() {
console.log(`${this.name} ordered!`);
},
};
// Extending the prototype chain
Object.setPrototypeOf(recipeBook, order);
recipe.orderRecipe(); // prints 'Garlic Naan ordered!'
The prototype chain will extend to this:
recipe --> recipeBook --> order --> Object.prototype --> null
Now, I think it should be easy to relate why you are able to invoke recipe.hasOwnProperty()
even though none of the object literals we declared owned hasOwnProperty
. This is because all object literals implicitly inherit from Object.prototype, which means the hasOwnProptery()
task has been delegated to Object.protoype
.
Here's the complete code example:
const recipe = { name: "Garlic Naan" };
const recipeBook = {
getRecipeName() {
console.log(this.name);
},
};
// Set 'recipeBook' as the prototype of 'recipe'
Object.setPrototypeOf(recipe, recipeBook);
const order = {
orderRecipe() {
console.log(`${this.name} ordered!`);
},
};
// Extending the prototype chain
Object.setPrototypeOf(recipeBook, order);
// Prototypal delegation in action
recipe.getRecipeName(); // prints 'Garlic Naan'
recipe.orderRecipe(); // prints 'Garlic Naan ordered!'
recipe.hasOwnProperty("name"); //true
Constructor function and the new keyword
Before I leave you with this delegation concept, I also want to talk about constructor functions and why do you need to use the new operator when creating instances. I hope with the prototype concept aside it should be easy to demystify their existence.
Every function (except fat arrow) in JavaScript has a property called prototype
which is just a plain object with constructor
property. This is different from the internal [[Prototype]] relationship.
Let's revisit the previous recipe
example and see how can you establish the same prototypal relationship using the constructor function.
// Constructor function 'Recipe'
function Recipe(name) {
this.name;
}
Recipe.hasOwnProperty("prototype"); // true
Recipe.prototype.constructor === Recipe; // true
Visually it will look similar to the diagram below:
The property (prototype) is special because when you invoke Recipe()
using the new operator, the new operator uses Recipe.prototype
as a prototype for instances it creates. Once the instance is created, the new operator passes that instance as this
internally as one of the parameters to Recipe()
.
const recipe = new Recipe('Garlic Naan');
Now, it should be clear that why do we need to add properties to Recipe.prototype
: they become available on all the Recipe
instances via prototypal delegation.
__proto__ is a pseudo property for accessing the prototype of an object
// Adding properties to 'Recipe.prototype' will make them
// available on all `Recipe` instances.
Recipe.prototype.getRecipeName = function () {
console.log(this.name);
};
Similarly, we can extend the chain and delegate the task of ordering the recipe to another object by setting it as a Recipe.prototype
prototype.
// Order constructor
function Order() {}
Order.prototype.recipeOrder = {
recipeOrder() {
console.log(`${this.name} ordered!`);
},
};
// Setting up the delegation aka Prototypal inheritance
Object.setPrototypeOf(Recipe.prototype, Order.prototype);
recipe.orderRecipe(); // prints 'Garlic Naan ordered!'
The complete code example using Function constructor looks like:
// Constructor function 'Recipe'
function Recipe(name){this.name}
Recipe.hasOwnProperty('prototype'); // true
Recipe.prototype.constructor === Recipe; // true
const recipe = new Recipe('Garlic Naan');
Recipe.prototype.getName = function () {
console.log(this.name);
};
// Order constructor
function Order() {}
Order.prototype.recipeOrder = {
recipeOrder() {
console.log(`${this.name} ordered!`);
},
};
// Setting up the delegation aka Prototypal inheritance
Object.setPrototypeOf(Recipe.prototype, Order.prototype);
// Prototypal delegation in action
recipe.getRecipeName(); // prints 'Garlic Naan'
recipe.orderRecipe(); // prints 'Garlic Naan ordered!'
recipe.hasOwnProperty("name"); //true
Here's how the final prototype chain looks like:
Closing thoughts
Prototypes in JavaScript may seem daunting to begin with, but I hope this article has eased your learning path. Understanding the foundation of JavaScript is the key to become a good developer. If you want to explore more about the prototype chain, then I highly recommend reading this chapter by Dr. Axel. Thank you for reading 😍.
Top comments (1)
My 0.50c about the constructor section: arrow functions don't have
prototype
. Neither bound functions, but the later can be constructables, unlike arrow functions. It's because of the[[Constructor]]
internal method.For instance:
var fn = (function () {}).bind(Array); new fn()
works although the result of thebind
call is an "exotic object" without prototype. Now, the following example throws an error, because arrow functions lacks said internal method:var fn = (() => {}).bind(Array); new fn()