Prototypal Inheritance with Object.create
This article is the second in a comprehensive five part series on Prototypal Inheritance.
Part 1 — Understanding the Prototype Chain
Part 2 — Prototypal Inheritance with Object.create
Part 3 — Prototypal Inheritance with Constructor Functions
Part 4 — Coming soon!
Part 5 — Coming soon!
If you’re not familiar with what the prototype chain is, or how JavaScript traverses the prototype chain, I highly recommend you read Part 1 of this series.
In part 1, we mentioned that there were three primary ways to build a prototype chain in JavaScript:
- Using
Object.create
- Using constructor functions
- Using classes
In this post, I will go through how to build prototype chains using Object.create
. I highly recommend attempting the exercises at the very bottom of this page before moving on to Part 3 of the series.
As always, I’d love your questions, feedback, and constructive criticism!
Part 2 — Prototypal Inheritance with Object.Create
We can use object.create
to build a prototype chain.
Syntax
Object.create(proto)
Object.create(proto, propertiesObject)
Rather than providing a complex definition of each parameter (you can always refer to the mdn docs if that is what you want), I’ll illustrate the functionality of Object.create
by walking through a few examples.
Example 1
const proto = {
sender: 'rana@gmail.com'
}
const child = Object.create(proto)
child.recipient = 'samir@gmail.com'
console.log(child.sender); // rana@gmail.com
console.log(child.recipient); // samir@gmail.com
console.log(Object.getPrototypeOf(child) === proto) // true
In this example, we create an object child
, and set that object’s prototype to another object called proto
. We essentially created this prototype chain:
Or more simply
null ← Object.prototype ← proto ← child
Example 2
const animal = {
walk: function walk() { console.log(`${this.name}: I am walking`) }
};
const bear = Object.create(animal, {
growl: {
value: function growl() { console.log(`${this.name}: rrrrrrrr`) }
}
});
const baloo = Object.create(bear, {
name: {
value: 'Baloo'
}
});
baloo.growl(); // Baloo: rrrrrrrr
baloo.walk(); // Baloo: I am walking
The above snippet creates the following prototype chain:
null ← Object.prototype ← animal ← bear ← baloo
Or, in more detail
When the method baloo.growl()
is invoked, the JavaScript runtime sees that the baloo
object itself does not possess a property called growl
. Thus, it checks if the prototype object of baloo
(bear
) has a growl
property, which it does. Now, notice that the growl
function includes a reference to this
. Typically, the this
keyword refers to the object on which the method was invoked. As growl was called on baloo
, and baloo
has a name
property with the value Baloo
, the this.name
property within the growl
method evaluates to Baloo
. Therefore, the console.log
statement receives the string ‘Baloo: rrrrrrrr
' as its argument.
Similarly, when baloo.walk()
is called, the JavaScript runtime performs the following steps:
- Checks if
baloo
has a walk property; it does not. - Checks if
baloo
’s prototype (bear
) has a walk property; it does not. - Checks if
bear
’s prototype (animal
) has a walk property; it does. - Executes the
walk
function settingthis
tobaloo
You might have noticed that in the above example, we passed a second argument into object.create
.
const bear = Object.create(animal, {
growl: {
value: function growl() { console.log(`${this.name}: rrrrrrrr`) }
}
});
This object defined the property growl
on bear
, and gave it a value (the growl
function).
Basically, the second argument of Object.create
is an object that specifies additional properties to be added to the newly created object. Each property is defined by a property descriptor, which can include the following attributes:
value
: The value associated with the property (default:undefined
).writable
: A boolean indicating if the value associated with the property can be changed using an assignment operator (default:false
).enumerable
: A boolean indicating if the property will be included during enumeration of the object's properties (default:false
).configurable
: A boolean indicating if the property descriptor can be modified and if the property can be deleted from the object (default:false
).
While the propertiesObject
is not directly related to prototypal inheritance, it is important to understand its role in order to effectively call Object.create
. Therefore, let’s go through a few more examples where we pass it in as a second argument.
Example 3
const transaction = {
sender: 'aseel@gmail.com',
reciever: 'mo@gmail.com'
}
const moneyTransaction = Object.create(transaction, {
funds: {
value: 0.0,
enumerable: false,
writable: true,
configurable: false
}
});
console.log(Object.keys(moneyTransaction)); // []
In the above example, we create a moneyTransaction
object whose prototype is transaction
. We give moneyTransaction
a single property, funds
. However, when we console.log(Object.keys(moneyTransaction))
, we see an empty array. This is because we set enumerable
to false.
Example 4
'use strict'
const transaction = {
sender: 'aseel@gmail.com',
reciever: 'mo@gmail.com'
}
const moneyTransaction = Object.create(transaction, {
funds: {
value: 0.0,
enumerable: true,
configurable: true
},
currency: {
value: 'CAD',
enumerable: true,
configurable: false
}
});
Object.defineProperty(moneyTransaction, 'funds', { enumerable: false })
// does not throw
delete Object.funds; // does not throw
console.log(Object.keys(moneyTransaction)); // ['currency']
Object.defineProperty(moneyTransaction, 'currency', {
enumerable: false
}) // TypeError: Cannot redefine property: currency
In the above example, we give moneyTransaction
two properties, funds
and currency
. We specify that we want funds
to be configurable, but currency
to not be configurable. Therefore, we are able to successfully modify the enumerable descriptor of the funds
property then delete it. However, when we try to modify the enumerable
descriptor of currency, we get a TypeError
.
Finally, note that when you create a property by using the dot notation directly on the object, as in moneyTransaction.funds = 20
, that act is equivalent to defining a property with a descriptor with all settings set to true.
Example 5
'use strict'
const transaction = {
sender: 'aseel@gmail.com',
reciever: 'mo@gmail.com'
}
const moneyTransaction = Object.create(transaction);
moneyTransaction.funds = 0.0;
console.log(Object.getOwnPropertyDescriptor(moneyTransaction, 'funds'));
// { value: 0, writable: true, enumerable: true, configurable: true
We saw how we can use Object.create
to build a prototype chain. However, you probably noticed that this method doesn’t scale well. Let’s circle back to Example 2. Say we wanted to create 10 different bears whose name property is enumerable but not writable, we’d have to do something like this:
const bear1 = Object.create(bear, {
name: {
value: 'bear1',
enumerable: true,
writable: false
}
});
// ...
const bear9 = Object.create(bear, {
name: {
value: 'bear9',
enumerable: true,
writable: false
}
const bear10 = Object.create(bear, {
name: {
value: 'bear10',
enumerable: true,
writable: false
}
});
Obviously, this is not ideal. Instead, we can create a function to generate instances of bear
.
const animal = {
walk: function walk() { console.log(`${this.name}: I am walking`) }
};
const bear = Object.create(animal, {
growl: {
value: function growl() { console.log(`${this.name}: rrrrrrrr`) }
}
});
function createBearInstance(name) {
return Object.create(bear, {
name: {
value: name,
enumerable: true,
writable: false
}
})
}
const bear1 = createBearInstance('bear1');
const bear2 = createBearInstance('bear2');
const bear3 = createBearInstance('bear3');
bear1.growl(); // bear1: rrrrrrrr
bear2.walk(); // bear2: I am walking
bear3.growl(); // bear3: rrrrrrrr
console.log(Object.getPrototypeOf(bear2) === bear); // true
This is clearly an improvement . You might be wondering though, can’t you write a function to build the animal prototype? to build the generic bear and set it’s prototype to animal? You definitely can. In the next section, we’ll see how we can build prototype chains using constructor functions.
Continue to Part 3 — Prototypal Inheritance with Constructor Functions (coming soon).
Exercises
The exercises below should provide you with opportunities to practice creating objects using Object.create
and exploring the concept of prototype chains in JavaScript. Feel free to expand upon these exercises to further improve your understanding.
Exercise 1
Create a prototype chain for different types of vehicles. Start by creating a base vehicle object with a method drive
. Then, create specific vehicle objects (e.g., car, motorcycle, truck) that have vehicle as their prototype. Each of those vehicle objects should have a numWheels
(number of wheels) property. Check that your code works by running the following
console.log(Object.getPrototypeOf(car) === vehicle); // true
console.log(Object.getPrototypeOf(motorcycle) === vehicle); // true
console.log(Object.getPrototypeOf(truck) === vehicle); // true
car.drive(); // Driving the vehicle with 4 wheels.
motorcycle.drive(); // Driving the vehicle with 2 wheels.
truck.drive(); // Driving the vehicle with 6 wheels.
Solution:
const vehicle = {
drive() {
console.log(`Driving the vehicle with ${this.numWheels} wheels.`);
}
}
const car = Object.create(vehicle);
car.numWheels = 4;
const motorcycle = Object.create(vehicle);
motorcycle.numWheels = 2;
const truck = Object.create(vehicle);
truck.numWheels = 6;
Exercise 2
Build a prototype chain to represent a food menu. Create a base foodItem
object. Then create appetizer
, mainCourse
and dessert
objects that have foodItem
as their prototype. The foodItem
object should have a single property, describe
, which is a function that when invoked prints the item’s name and description (for example ‘Appetizer — Very Sweet’). You should allow the description, but not the name, of a food item to be modified. Make sure you use strict mode.
Check that your code is valid by running the following
console.log(Object.getPrototypeOf(dessert) === foodItem); // true
console.log(Object.getPrototypeOf(mainCourse) === foodItem); // true
console.log(Object.getPrototypeOf(appetizer) === foodItem); // true
dessert.describe(); // Dessert - Very sweet
mainCourse.describe(); // Main Course - Savory
appetizer.describe(); // Appetizer - Just enough salt
dessert.description = 'Just sweet enough';
dessert.describe(); // Dessert - Just sweet enough
dessert.name = 'WoopsyDoopsy'; // throws TypeError in strict mode
Solution:
'use strict'
const foodItem = {
describe() {
console.log(`${this.name} - ${this.description}`);
}
}
const appetizer = Object.create(foodItem, {
name: {
value: 'Appetizer'
},
description: {
value: 'Just enough salt',
writable: true
}
});
const mainCourse = Object.create(foodItem, {
name: {
value: 'Main Course'
},
description: {
value: 'Savory',
writable: true
}
});
const dessert = Object.create(foodItem, {
name: {
value: 'Dessert'
},
description: {
value: 'Very sweet',
writable: true
}
});
Exercise 3
Build a prototype chain for different types of people:
- Create a base
Person
object with shared propertiesname
andage
, and functionintroduce
- Create a
Student
object whose prototype isPerson
. A student should be initialized with an additional propertygrade
, and should have astudy
function. - Create a
Teacher
object whose prototype isPerson
. A teacher should be initialized with an additional property,subject
, and should have ateach
function. - Create a
Parent
object whose prototype isPerson
. A parent should be initialized with an additional property,numChildren
, and should have acare
function. - Create a student named Faisal.
- Create a teacher named Izzy.
- Create a parent named Babak.
Test your code by running the following
console.log(Object.getPrototypeOf(Student) === Person); // true
console.log(Object.getPrototypeOf(Parent) === Person); // true
console.log(Object.getPrototypeOf(Teacher) === Person); // true
console.log(Object.getPrototypeOf(faisal) === Student); // true
faisal.init("Faisal", 18, 12);
faisal.introduce(); // Hi, my name is Faisal and I am 18 years old.
faisal.study(); // Faisal is studying.
console.log(Object.getPrototypeOf(izzy) === Teacher); // true
izzy.init("Izzy", 33, "Math");
izzy.introduce(); // Hi, my name is Izzy and I am 33 years old.
izzy.teach(); // Izzy is teaching Math.
console.log(Object.getPrototypeOf(babak) === Parent); // true
babak.init("Babak", 43, 2);
babak.introduce(); // Hi, my name is Babak and I am 43 years old.
babak.care(); // Babak is taking care of their children.
Solution:
const Person = {
init(name, age) {
this.name = name;
this.age = age;
},
introduce() {
console.log(`Hi, my name is ${this.name} and I am ${this.age} years old.`);
}
};
const Student = Object.create(Person);
Student.init = function init(name, age, grade) {
Person.init.call(this, name, age);
this.grade = grade;
}
Student.study = function study() {
console.log(`${this.name} is studying`);
}
const Teacher = Object.create(Person);
Teacher.init = function init(name, age, subject){
Person.init.call(this, name, age);
this.subject = subject;
}
Teacher.teach = function() {
console.log(`${this.name} is teaching ${this.subject}.`);
};
const Parent = Object.create(Person);
Parent.init = function init(name, age, numChildren) {
Person.init.call(this, name, age);
this.numChildren = numChildren;
};
Parent.care = function() {
console.log(`${this.name} is taking care of their children.`);
};
const faisal = Object.create(Student);
const izzy = Object.create(Teacher);
const babak = Object.create(Parent);