The Prototypal Nature of JavaScript : A Deep Dive into OOP

The Prototypal Nature of JavaScript : A Deep Dive into OOP

JavaScript, the lingua franca of the web, has a unique and powerful object-oriented programming (OOP) paradigm rooted in its prototypal nature. Unlike traditional class-based OOP languages like Java or C++, JavaScript allows objects to inherit directly from other objects, making it dynamic and versatile. In this deep dive, we’ll explore everything from the fundamentals of prototype and __proto__ to how constructs like Object.create, the new keyword, and modern ES6 class syntax work under the hood.

If you’ve ever wondered how JavaScript achieves inheritance, or you’ve struggled to explain the difference between prototype and __proto__, this guide will clear things up. Let’s start from the basics and progressively get into the nitty-gritty details, complete with diagrams and code examples.


Understanding the Foundations: prototype and __proto__

In JavaScript, every object has an internal link called [[Prototype]]. This link connects the object to another object—its prototype. This prototype chain enables inheritance, allowing properties and methods to be shared across objects.

What is prototype?

The prototype property is a special property of functions. Every function in JavaScript is also an object, and as such, it has a prototype property. This property is an object itself and acts as a blueprint for objects created using that function as a constructor.

  • Functions have a prototype.

  • Objects do not have a prototype property, but they have __proto__.

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

console.log(User.prototype); // {} (an empty object by default)

Functions as Objects: The Dual Nature of JavaScript Functions

In JavaScript, functions are both callable and objects. As objects, they have properties like prototype.

function multiplyBy2(num) {
  return num * 2;
}

multiplyBy2.stored = 5;
console.log(multiplyBy2.stored); // 5

console.log(typeof multiplyBy2.prototype); // object

This duality allows functions to serve as both constructors and blueprints for objects.

What is __proto__?

The __proto__ property is present in all objects. It is a reference to the object’s prototype, which links it to the next object in the prototype chain.

const obj = {};
console.log(obj.__proto__ === Object.prototype); // true

The Relationship Between prototype and __proto__

When a function is used as a constructor (via the new keyword), the prototype property of the constructor becomes the __proto__ of the created object. This is the core mechanism for JavaScript’s inheritance.

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

User.prototype.sayHello = function () {
  console.log(`Hello, ${this.name}!`);
};

const user1 = new User('Alice');

// Prototype Chain
console.log(user1.__proto__ === User.prototype); // true
console.log(User.prototype.__proto__ === Object.prototype); // true

Diagram:

user1 -> __proto__ -> User.prototype -> __proto__ -> Object.prototype -> null

Creating Objects: Object.create and Manual Prototypes

The Object.create method provides a direct way to create objects and link them to a specific prototype. Unlike the new keyword, Object.create allows explicit control over the prototype chain.

Example: Using Object.create

const userPrototype = {
  greet() {
    console.log('Hello from the prototype!');
  },
};

const user = Object.create(userPrototype);
user.name = 'Bob';

user.greet(); // Hello from the prototype!

console.log(user.__proto__ === userPrototype); // true

How Object.create Works

  1. Creates a new object.

  2. Sets the new object’s __proto__ to the specified prototype.

  3. Returns the new object.

Advantages:

  • Fine-grained control over prototypes.

  • Avoids unnecessary setup required by constructor functions.


Automating Object Creation: The new Keyword

The new keyword streamlines object creation by automating some of the manual steps involved with Object.create. Here’s what happens when you use new:

What Happens Behind the Scenes?

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

const user1 = new User('Charlie');
  1. Create a new object.

    • A new object is created.
  2. Link the new object’s __proto__ to the constructor’s prototype.

  3. Set this inside the constructor to the new object.

  4. Execute the constructor function.

  5. Return the new object.

console.log(user1.__proto__ === User.prototype); // true

Adding Methods to the Prototype

By attaching methods to the prototype, all instances of a constructor function can share them.

User.prototype.sayHello = function () {
  console.log(`Hello, ${this.name}!`);
};

user1.sayHello(); // Hello, Charlie!

Classes: Syntactic Sugar Over Prototypes

Introduced in ES6, classes provide a cleaner syntax for creating objects and managing inheritance. However, under the hood, they still use prototypes.

class User {
  constructor(name) {
    this.name = name;
  }

  sayHello() {
    console.log(`Hello, ${this.name}!`);
  }
}

const user1 = new User('Diana');
user1.sayHello(); // Hello, Diana!

What Happens Behind the Scenes?

  1. A function User is created.

  2. The User function gets a prototype property.

  3. Methods inside the class are added to User.prototype.

console.log(User.prototype.constructor === User); // true

Inheritance: Extending Prototypes

JavaScript allows inheritance via prototypes. The extends keyword simplifies subclassing.

Example: Subclassing with extends

class Admin extends User {
  constructor(name, accessLevel) {
    super(name);
    this.accessLevel = accessLevel;
  }

  showAccessLevel() {
    console.log(`${this.name} has ${this.accessLevel} access.`);
  }
}

const admin = new Admin('Eve', 'high');
admin.sayHello(); // Hello, Eve!
admin.showAccessLevel(); // Eve has high access.

Prototype Chain with extends

admin -> __proto__ -> Admin.prototype -> __proto__ -> User.prototype -> __proto__ -> Object.prototype -> null

How super Works

The super keyword calls the parent class’s constructor, ensuring this is correctly initialized.


Execution Context Diagrams

To truly understand how JavaScript works, let’s visualize the execution context for some key operations.

Example: new Keyword

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

User.prototype.sayHello = function () {
  console.log(`Hello, ${this.name}!`);
};

const user1 = new User('Alice');
user1.sayHello();
  1. Global Context:

    • User function is stored in memory.

    • user1 is declared but uninitialized.

  2. Function Context (User):

    • this is bound to the new object.

    • Properties (name) are assigned to this.

  3. Prototype Lookup:

    • user1.sayHello() triggers a lookup.

    • sayHello is found on User.prototype.


Conclusion

Understanding JavaScript’s prototypal nature is crucial for mastering OOP in the language. From the intricacies of __proto__ and prototype to modern classes and inheritance, JavaScript provides powerful tools for object-oriented design. Armed with this knowledge, you’re ready to tackle complex problems.

Happy coding!