Prototypes and Prototypal Inheritance in JavaScript
Prototypes are a core feature of JavaScript's object system. Unlike classical inheritance found in languages like Java or C++, JavaScript uses prototypal inheritance, where objects inherit from other objects directly. This lesson provides an in-depth exploration of prototypes, the prototype chain, and various inheritance patterns, enabling you to harness the power of JavaScript's flexible object model.
Understanding Prototypes
What is a Prototype?
- Definition: A prototype is an object from which other objects inherit properties and methods.
- Role in JavaScript: Every JavaScript object has an internal property called
[[Prototype]]
(commonly accessed via__proto__
), which points to its prototype object.
Prototype vs. prototype
Property
[[Prototype]]
(or__proto__
): The internal reference from an object to its prototype.prototype
Property: A property available on functions (specifically constructor functions) that is used when creating new objects with thenew
keyword.
Example:
function Person(name) {
this.name = name
}
Person.prototype.sayHello = function () {
console.log(`Hello, my name is ${this.name}.`)
}
const alice = new Person('Alice')
alice.sayHello() // Outputs: Hello, my name is Alice.
Explanation:
Person
is a constructor function.Person.prototype
is the prototype object for all instances created bynew Person()
.alice.__proto__
points toPerson.prototype
.
The Prototype Chain
How Does the Prototype Chain Work?
- Definition: The prototype chain is a series of linked objects that JavaScript uses to find properties and methods.
- Mechanism:
- When accessing a property on an object, JavaScript first looks for the property on the object itself.
- If not found, it looks up the
[[Prototype]]
chain until it finds the property or reachesnull
.
Visualization:
alice ---> Person.prototype ---> Object.prototype ---> null
alice
inherits fromPerson.prototype
.Person.prototype
inherits fromObject.prototype
.Object.prototype
is the top-level prototype, with its[[Prototype]]
set tonull
.
Accessing Properties Through the Prototype Chain
Example:
console.log(alice.toString()) // Outputs: [object Object]
Explanation:
toString
is not defined onalice
orPerson.prototype
.- JavaScript looks up the prototype chain and finds
toString
onObject.prototype
.
Inheritance Patterns in JavaScript
Constructor Functions and Prototypes
- Definition: Using functions as constructors to create objects with shared properties and methods via the
prototype
property.
Example:
function Animal(name) {
this.name = name
}
Animal.prototype.eat = function () {
console.log(`${this.name} is eating.`)
}
const dog = new Animal('Dog')
dog.eat() // Outputs: Dog is eating.
Explanation:
- Instances of
Animal
share methods defined onAnimal.prototype
.
Prototypal Inheritance with Object.create()
- Definition: Creating a new object that directly inherits from an existing object.
Example:
const animal = {
eat: function () {
console.log(`${this.name} is eating.`)
},
}
const cat = Object.create(animal)
cat.name = 'Cat'
cat.eat() // Outputs: Cat is eating.
Explanation:
cat
's[[Prototype]]
points toanimal
.- Methods and properties are inherited directly from
animal
.
ES6 Classes and extends
- Definition: Syntactic sugar over prototypal inheritance introduced in ES6 for easier and cleaner inheritance.
Example:
class Vehicle {
constructor(make) {
this.make = make
}
drive() {
console.log(`${this.make} is driving.`)
}
}
class Car extends Vehicle {
constructor(make, model) {
super(make)
this.model = model
}
honk() {
console.log(`${this.make} ${this.model} says beep beep!`)
}
}
const myCar = new Car('Toyota', 'Corolla')
myCar.drive() // Outputs: Toyota is driving.
myCar.honk() // Outputs: Toyota Corolla says beep beep!
Explanation:
Car
extendsVehicle
, inheriting its properties and methods.super(make)
calls the constructor of the parent class.
Mixins
- Definition: Copying properties from one object to another to achieve inheritance.
Example:
const canFly = {
fly: function () {
console.log(`${this.name} is flying.`)
},
}
function Bird(name) {
this.name = name
}
Object.assign(Bird.prototype, canFly)
const eagle = new Bird('Eagle')
eagle.fly() // Outputs: Eagle is flying.
Explanation:
Object.assign
copies properties fromcanFly
toBird.prototype
.
Prototype Chain in Detail
Object Creation and Prototypes
- Object Literals: Objects created using
{}
have[[Prototype]]
pointing toObject.prototype
.
Example:
const obj = {}
console.log(obj.__proto__ === Object.prototype) // Outputs: true
Function Prototypes
- Function Objects: Functions in JavaScript are also objects.
Example:
function foo() {}
console.log(foo.__proto__ === Function.prototype) // Outputs: true
Custom Prototypes
- Changing an Object's Prototype:
Example:
const parent = { greeting: 'Hello' }
const child = Object.create(parent)
console.log(child.greeting) // Outputs: Hello
- Setting Prototype Manually:
const parent = { greeting: 'Hello' }
const child = {}
Object.setPrototypeOf(child, parent)
console.log(child.greeting) // Outputs: Hello
Inheritance Patterns Explained
Classical Inheritance Emulation
- Constructor Inheritance:
Example:
function Parent(name) {
this.name = name
}
Parent.prototype.sayName = function () {
console.log(`Name: ${this.name}`)
}
function Child(name, age) {
Parent.call(this, name) // Inherit properties
this.age = age
}
Child.prototype = Object.create(Parent.prototype) // Inherit methods
Child.prototype.constructor = Child
Child.prototype.sayAge = function () {
console.log(`Age: ${this.age}`)
}
const child = new Child('Frank', 10)
child.sayName() // Outputs: Name: Frank
child.sayAge() // Outputs: Age: 10
Explanation:
- Inheritance of Properties: Using
Parent.call(this, name)
to inherit properties. - Inheritance of Methods: Setting
Child.prototype
to an object created fromParent.prototype
. - Resetting Constructor: Assigning
Child.prototype.constructor
toChild
to maintain the correct constructor reference.
Parasitic Inheritance
- Definition: Creating a new object by augmenting an existing object.
Example:
function createAugmentedObject(original) {
const clone = Object.create(original)
clone.sayHello = function () {
console.log('Hello!')
}
return clone
}
const original = { name: 'Grace' }
const augmented = createAugmentedObject(original)
augmented.sayHello() // Outputs: Hello!
console.log(augmented.name) // Outputs: Grace
Combination Inheritance
- Definition: Combining constructor stealing and prototype chaining.
Example:
function SuperType(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType(name, age) {
SuperType.call(this, name) // Inherit properties
this.age = age
}
SubType.prototype = new SuperType() // Inherit methods
SubType.prototype.sayAge = function () {
console.log(this.age)
}
const instance1 = new SubType('Heidi', 25)
instance1.colors.push('black')
console.log(instance1.colors) // Outputs: ['red', 'blue', 'green', 'black']
instance1.sayName() // Outputs: Heidi
instance1.sayAge() // Outputs: 25
const instance2 = new SubType('Ivan', 30)
console.log(instance2.colors) // Outputs: ['red', 'blue', 'green']
Explanation:
- Each instance has its own copy of properties, preventing shared state issues.
- Methods are shared via the prototype chain.
Best Practices
Prefer ES6 Classes for Inheritance
- Benefits:
- Cleaner syntax.
- Easier to read and maintain.
- Consistent with other object-oriented languages.
Example:
class Employee {
constructor(name) {
this.name = name
}
work() {
console.log(`${this.name} is working.`)
}
}
class Manager extends Employee {
manage() {
console.log(`${this.name} is managing.`)
}
}
const manager = new Manager('Jack')
manager.work() // Outputs: Jack is working.
manager.manage() // Outputs: Jack is managing.
Use Object.create()
for Simple Inheritance
- When to Use:
- When you need a simple prototype-based inheritance without the need for constructors.
Example:
const animal = {
init: function (name) {
this.name = name
},
makeSound: function () {
console.log(`${this.name} makes a sound.`)
},
}
const dog = Object.create(animal)
dog.init('Dog')
dog.makeSound() // Outputs: Dog makes a sound.
Avoid Modifying the __proto__
Property Directly
- Reason:
- Modifying
__proto__
is discouraged due to performance and readability concerns. - Use
Object.setPrototypeOf()
orObject.create()
instead.
- Modifying
Be Careful with Shared Properties
- Issue:
- Objects sharing the same prototype can inadvertently share mutable properties (like arrays or objects).
Example:
function Person() {}
Person.prototype.hobbies = []
const person1 = new Person()
const person2 = new Person()
person1.hobbies.push('Reading')
console.log(person2.hobbies) // Outputs: ['Reading']
Solution:
- Define properties inside the constructor function to ensure each instance has its own copy.
Exercises
Exercise 1: Understanding the Prototype Chain
Question:
Given the following code, what will be the output of console.log(baz);
?
function Foo() {}
Foo.prototype.bar = 'bar'
const foo = new Foo()
const baz = foo.bar
console.log(baz) // Output?
Answer:
bar
Explanation:
foo
does not have its ownbar
property.- JavaScript looks up the prototype chain and finds
bar
onFoo.prototype
. baz
is assigned the value'bar'
.
Exercise 2: Using Object.create()
Question:
Create an object car
that inherits from vehicle
and has its own brand
property. Use Object.create()
for inheritance.
const vehicle = {
type: 'Vehicle',
drive: function () {
console.log(`${this.brand} is driving.`)
},
}
// Your code here
car.drive() // Outputs: Toyota is driving.
Answer:
const car = Object.create(vehicle)
car.brand = 'Toyota'
car.drive() // Outputs: Toyota is driving.
Explanation:
car
's prototype is set tovehicle
usingObject.create(vehicle)
.car
gains access todrive
method fromvehicle
.
Exercise 3: Fixing Constructor Reference
Question:
In the following code, the constructor
property of Child.prototype
is incorrect. Fix it.
function Parent() {}
function Child() {}
Child.prototype = Object.create(Parent.prototype)
// Fix here
const child = new Child()
console.log(child.constructor === Child) // Should be true
Answer:
Child.prototype.constructor = Child
const child = new Child()
console.log(child.constructor === Child) // Outputs: true
Explanation:
- After setting
Child.prototype
to a new object, theconstructor
property points toParent
. - Resetting
Child.prototype.constructor
toChild
corrects this.
Exercise 4: ES6 Class Inheritance
Question:
Convert the following constructor function inheritance to use ES6 classes.
function Animal(name) {
this.name = name
}
Animal.prototype.eat = function () {
console.log(`${this.name} eats.`)
}
function Dog(name, breed) {
Animal.call(this, name)
this.breed = breed
}
Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.constructor = Dog
Dog.prototype.bark = function () {
console.log(`${this.name} barks.`)
}
const myDog = new Dog('Buddy', 'Golden Retriever')
myDog.eat() // Outputs: Buddy eats.
myDog.bark() // Outputs: Buddy barks.
Answer:
class Animal {
constructor(name) {
this.name = name
}
eat() {
console.log(`${this.name} eats.`)
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name)
this.breed = breed
}
bark() {
console.log(`${this.name} barks.`)
}
}
const myDog = new Dog('Buddy', 'Golden Retriever')
myDog.eat() // Outputs: Buddy eats.
myDog.bark() // Outputs: Buddy barks.
Explanation:
- The
class
syntax simplifies inheritance. extends
is used for inheritance, andsuper()
calls the parent constructor.
Exercise 5: Shared Properties Issue
Question:
Identify the issue with the following code and provide a solution.
function Student() {}
Student.prototype.grades = []
const student1 = new Student()
const student2 = new Student()
student1.grades.push(90)
console.log(student2.grades) // Outputs: [90]
Answer:
Issue:
- The
grades
array is shared across all instances because it's defined on the prototype. - Mutating
grades
on one instance affects all instances.
Solution:
- Define
grades
inside the constructor to ensure each instance has its own copy.
function Student() {
this.grades = []
}
Student.prototype.addGrade = function (grade) {
this.grades.push(grade)
}
const student1 = new Student()
const student2 = new Student()
student1.addGrade(90)
console.log(student1.grades) // Outputs: [90]
console.log(student2.grades) // Outputs: []
Prototypes and prototypal inheritance are central to understanding JavaScript's object model. By mastering the prototype chain and various inheritance patterns, you can write more efficient and maintainable code. This knowledge also prepares you for technical interviews, where a deep understanding of JavaScript's inheritance mechanisms is essential.
Practice Problems
What is the purpose of the `constructor` property in an object's prototype?
Loading...
How does the prototype chain work when accessing properties on an object?
Loading...
Explain the difference between the `__proto__` property and the `prototype` property.
Loading...
How can you implement inheritance in JavaScript using ES6 classes?
Loading...
What is prototypal inheritance in JavaScript?
Loading...
Let's continue exploring the next page. Take your time, and proceed when you're ready.