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.prototypeProperty: A property available on functions (specifically constructor functions) that is used when creating new objects with thenewkeyword.
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:
Personis a constructor function.Person.prototypeis 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
aliceinherits fromPerson.prototype.Person.prototypeinherits fromObject.prototype.Object.prototypeis the top-level prototype, with its[[Prototype]]set tonull.
Accessing Properties Through the Prototype Chain
Example:
console.log(alice.toString()) // Outputs: [object Object]
Explanation:
toStringis not defined onaliceorPerson.prototype.- JavaScript looks up the prototype chain and finds
toStringonObject.prototype.
Inheritance Patterns in JavaScript
Constructor Functions and Prototypes
- Definition: Using functions as constructors to create objects with shared properties and methods via the
prototypeproperty.
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
Animalshare 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:
CarextendsVehicle, 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.assigncopies properties fromcanFlytoBird.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.prototypeto an object created fromParent.prototype. - Resetting Constructor: Assigning
Child.prototype.constructortoChildto 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:
foodoes not have its ownbarproperty.- JavaScript looks up the prototype chain and finds
baronFoo.prototype. bazis 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 tovehicleusingObject.create(vehicle).cargains access todrivemethod 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.prototypeto a new object, theconstructorproperty points toParent. - Resetting
Child.prototype.constructortoChildcorrects 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
classsyntax simplifies inheritance. extendsis 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
gradesarray is shared across all instances because it's defined on the prototype. - Mutating
gradeson one instance affects all instances.
Solution:
- Define
gradesinside 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?
The constructor property in an object's prototype refers back to the constructor function that created the object. It allows instances to identify their constructor and is useful for type checking and inheritance. When manually setting an object's prototype, it's common practice to reset the constructor property to maintain the correct reference.
Explain the difference between the `__proto__` property and the `prototype` property.
__proto__: An object's internal[[Prototype]]reference, pointing to its prototype object. It is used at runtime to resolve property lookups.prototype: A property of constructor functions (functions intended to be used withnew) that defines the prototype for instances created by that constructor. When an object is created usingnew Constructor(), its__proto__is set toConstructor.prototype.
How does the prototype chain work when accessing properties on an object?
When accessing a property on an object, JavaScript first looks for the property on the object itself. If the property is not found, it looks up the [[Prototype]] chain, checking each prototype object in turn until it finds the property or reaches the end of the chain (null). This process allows objects to inherit properties and methods from their prototypes.
How can you implement inheritance in JavaScript using ES6 classes?
In ES6, you can use the class syntax along with the extends keyword to implement inheritance.
Example:
class Parent {
constructor(name) {
this.name = name
}
greet() {
console.log(`Hello, ${this.name}`)
}
}
class Child extends Parent {
constructor(name, age) {
super(name)
this.age = age
}
displayAge() {
console.log(`I am ${this.age} years old.`)
}
}
const child = new Child('Alice', 10)
child.greet() // Outputs: Hello, Alice
child.displayAge() // Outputs: I am 10 years old.
What is prototypal inheritance in JavaScript?
Prototypal inheritance is a feature in JavaScript where objects inherit properties and methods from other objects through the prototype chain. Each object has a [[Prototype]] reference to another object, allowing it to access properties and methods defined on its prototype. This inheritance model is more flexible than classical inheritance found in other languages, enabling objects to inherit directly from other objects.
Let's continue exploring the next page. Take your time, and proceed when you're ready.