Lesson 22-In-Depth Analysis of Prototypes, Inheritance, and Prototype Chains

Fundamentals of JavaScript’s Prototype System

Overview of JavaScript’s Object Model

JavaScript is a prototype-based object-oriented programming language, fundamentally different from traditional class-based languages like Java or C++. In JavaScript, objects are the primary building blocks, and prototypes serve as the core mechanism for connecting these objects.

Every JavaScript object has an internal link to another object, known as its prototype. When accessing a property of an object, if the property is not found on the object itself, the JavaScript engine traverses the prototype chain upward until it finds the property or reaches the end of the chain (null).

// Creating a simple object
const person = {
  name: 'John',
  age: 30
};

// Accessing object properties
console.log(person.name); // Output: John

In this example, the person object directly contains the name and age properties. However, JavaScript’s power lies in its ability to inherit properties and methods from other objects via the prototype chain.

Constructors and Prototype Objects

In JavaScript, constructors are functions used to create objects of a specific type. Each constructor has a prototype property, which points to an object containing properties and methods shared by all instances created by that constructor.

// Defining a constructor
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// Adding a method to the constructor’s prototype
Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};

// Creating instances using the constructor
const john = new Person('John', 30);
const jane = new Person('Jane', 25);

// Calling methods
john.greet(); // Output: Hello, my name is John and I am 30 years old.
jane.greet(); // Output: Hello, my name is Jane and I am 25 years old.

In this example, the Person constructor has a prototype property, to which we add the greet method. Instances created with new Person() inherit the methods from Person.prototype.

Core Concept of the Prototype Chain

The prototype chain is JavaScript’s mechanism for implementing inheritance. Every object has an internal [[Prototype]] property (accessible via __proto__ or Object.getPrototypeOf()), which points to its prototype object. If the prototype object has its own prototype, a chain-like structure is formed, known as the prototype chain.

// Creating an object
const animal = {
  eat() {
    console.log('Eating...');
  }
};

// Creating another object with animal as its prototype
const dog = Object.create(animal);
dog.bark = function() {
  console.log('Barking...');
};

// Calling methods
dog.bark(); // Output: Barking...
dog.eat();  // Output: Eating... (found via the prototype chain)

In this example, the dog object has a bark method but no eat method. When dog.eat() is called, the JavaScript engine first looks for the eat method on the dog object, and upon not finding it, it traverses the prototype chain to the animal object, where it finds and executes the method.

Deep Dive into Prototypes and the Prototype Chain

Complete Structure of the Prototype Chain

The prototype chain in JavaScript is often more complex than simple examples suggest. Let’s examine a more comprehensive prototype chain structure:

// Creating a constructor
function Animal(name) {
  this.name = name;
}

// Adding a method to the constructor’s prototype
Animal.prototype.eat = function() {
  console.log(`${this.name} is eating.`);
};

// Creating another constructor
function Dog(name, breed) {
  Animal.call(this, name); // Call the parent constructor
  this.breed = breed;
}

// Setting Dog’s prototype to an instance of Animal
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // Fix the constructor reference

// Adding a method to Dog’s prototype
Dog.prototype.bark = function() {
  console.log(`${this.name} is barking.`);
};

// Creating an instance
const myDog = new Dog('Rex', 'German Shepherd');

// Calling methods
myDog.bark(); // Output: Rex is barking.
myDog.eat();  // Output: Rex is eating.

In this example, the prototype chain structure is as follows:

  1. myDog object
  2. Dog.prototype object
  3. Animal.prototype object
  4. Object.prototype object
  5. null (end of the prototype chain)

Prototype Chain Lookup Mechanism

When accessing a property on an object, the JavaScript engine follows these steps:

  1. Check the object itself for the property.
  2. If not found, check the object’s [[Prototype]] (accessed via __proto__ or Object.getPrototypeOf()).
  3. If still not found, continue checking the prototype’s [[Prototype]].
  4. Repeat until the property is found or the chain ends at null.
// Creating a prototype chain
const grandparent = { a: 1 };
const parent = Object.create(grandparent);
parent.b = 2;
const child = Object.create(parent);
child.c = 3;

// Accessing properties
console.log(child.c); // 3 (found on child)
console.log(child.b); // 2 (found on parent)
console.log(child.a); // 1 (found on grandparent)
console.log(child.d); // undefined (not found)

Prototypes and the instanceof Operator

The instanceof operator checks whether a constructor’s prototype property appears in an object’s prototype chain.

function Person(name) {
  this.name = name;
}

const john = new Person('John');

console.log(john instanceof Person); // true
console.log(john instanceof Object); // true

function Animal() {}
console.log(john instanceof Animal); // false

The instanceof operator works by traversing the object’s prototype chain to check if the constructor’s prototype property is present.

Detailed Analysis of Inheritance Patterns

Prototype Chain Inheritance

Prototype chain inheritance is the simplest inheritance method, achieved by setting a subclass’s prototype to an instance of the parent class.

function Parent() {
  this.name = 'Parent';
}

Parent.prototype.sayName = function() {
  console.log(this.name);
};

function Child() {
  this.childName = 'Child';
}

// Key step: Set Child’s prototype to a Parent instance
Child.prototype = new Parent();

const child = new Child();
child.sayName(); // Output: Parent

Advantages:

  • Simple to implement.
  • Allows reuse of parent class methods.

Disadvantages:

  • All subclass instances share the parent instance’s properties (problematic for reference types).
  • Cannot pass arguments to the parent constructor.

Constructor Inheritance (Classical Inheritance)

Constructor inheritance uses the parent constructor within the child constructor to inherit properties.

function Parent(name) {
  this.name = name;
}

function Child(name, age) {
  Parent.call(this, name); // Key step: Call parent constructor
  this.age = age;
}

const child = new Child('Child', 5);
console.log(child.name); // Output: Child
console.log(child.age);  // Output: 5

Advantages:

  • Avoids sharing issues with reference-type properties.
  • Allows passing arguments to the parent constructor.

Disadvantages:

  • Cannot inherit methods from the parent’s prototype.

Combination Inheritance (Most Common)

Combination inheritance merges the benefits of prototype chain and constructor inheritance, making it the most commonly used approach.

function Parent(name) {
  this.name = name;
}

Parent.prototype.sayName = function() {
  console.log(this.name);
};

function Child(name, age) {
  Parent.call(this, name); // Inherit instance properties
  this.age = age;
}

// Inherit prototype methods
Child.prototype = new Parent();
Child.prototype.constructor = Child; // Fix constructor reference

const child = new Child('Child', 5);
child.sayName(); // Output: Child

Advantages:

  • Combines benefits of prototype chain and constructor inheritance.
  • Widely used.

Disadvantages:

  • Calls the parent constructor twice (once in Child.prototype = new Parent(), once in Parent.call(this, name)).

Prototypal Inheritance

Prototypal inheritance, similar to ES5’s Object.create(), creates a new object based on an existing one.

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

const parent = {
  name: 'Parent',
  sayName: function() {
    console.log(this.name);
  }
};

const child = object(parent);
child.name = 'Child';
child.sayName(); // Output: Child

Advantages:

  • Similar to ES5’s Object.create().
  • Suitable for scenarios not requiring a separate constructor.

Disadvantages:

  • Reference-type properties are shared across instances.

Parasitic Inheritance

Parasitic inheritance enhances an object based on prototypal inheritance.

function createAnother(original) {
  const clone = object(original); // Create object using prototypal inheritance
  clone.sayHi = function() { // Enhance the object
    console.log('hi');
  };
  return clone;
}

const parent = {
  name: 'Parent',
  sayName: function() {
    console.log(this.name);
  }
};

const child = createAnother(parent);
child.sayHi(); // Output: hi
child.sayName(); // Output: Parent

Advantages:

  • Enhances objects based on prototypal inheritance.
  • Suitable for scenarios focusing on objects rather than custom types or constructors.

Disadvantages:

  • Reference-type properties are shared.

Parasitic Combination Inheritance (Most Ideal)

Parasitic combination inheritance is the most ideal inheritance method for reference types, avoiding the issue of calling the parent constructor twice.

function inheritPrototype(child, parent) {
  const prototype = Object.create(parent.prototype); // Create a copy of the parent’s prototype
  prototype.constructor = child; // Fix constructor reference
  child.prototype = prototype; // Assign the copy to the child’s prototype
}

function Parent(name) {
  this.name = name;
}

Parent.prototype.sayName = function() {
  console.log(this.name);
};

function Child(name, age) {
  Parent.call(this, name); // Call parent constructor once
  this.age = age;
}

// Key step: Parasitic combination inheritance
inheritPrototype(Child, Parent);

Child.prototype.sayAge = function() {
  console.log(this.age);
};

const child = new Child('Child', 5);
child.sayName(); // Output: Child
child.sayAge();  // Output: 5

Advantages:

  • Most ideal inheritance method.
  • Calls the parent constructor only once.
  • Maintains the prototype chain.
  • Avoids the drawbacks of combination inheritance.

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
Share your love