Prototypal Inheritance with Object.create

Mom. Software Engineer.
9 min readJun 17, 2023

--

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:

  1. Using Object.create
  2. Using constructor functions
  3. 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 setting this to baloo

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 properties name and age, and function introduce
  • Create a Student object whose prototype is Person. A student should be initialized with an additional property grade, and should have a study function.
  • Create a Teacher object whose prototype is Person. A teacher should be initialized with an additional property, subject, and should have a teach function.
  • Create a Parent object whose prototype is Person. A parent should be initialized with an additional property, numChildren, and should have a care 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);

--

--

Mom. Software Engineer.
0 Followers

I am a mom to two beautiful daughters, and a Software Engineer. This blog is my very personal experience trying to navigate both identities.