Module: OOP and Prototypes

Prototypes

JavaScript Essentials: OOP and Prototypes - Prototypes

Prototypes are a fundamental concept in JavaScript, forming the basis of its inheritance mechanism. Understanding prototypes is crucial for grasping how JavaScript achieves object-oriented programming (OOP) without traditional classes (until ES6 introduced class syntax, which is largely syntactic sugar over prototypes).

What are Prototypes?

  • Every JavaScript function automatically has a prototype property. This property is an object.
  • When a function is used as a constructor (using the new keyword), the newly created object inherits properties and methods from the function's prototype object. This is the core of prototypal inheritance.
  • Each object has an internal link to another object called its [[Prototype]]. This link allows the object to inherit properties and methods from its prototype. In most JavaScript environments, you can access this link using Object.getPrototypeOf(object).
  • The prototype chain: If an object doesn't have a property or method directly, JavaScript will look up the prototype chain to find it. This continues until it reaches the end of the chain (which is null).

In simpler terms: Think of a prototype as a blueprint for creating objects. Objects created from that blueprint inherit the characteristics defined in the blueprint.

How Prototypes Work: An Example

// Constructor function
function Animal(name) {
  this.name = name;
}

// Add a method to the prototype
Animal.prototype.sayHello = function() {
  console.log("Hello, my name is " + this.name);
};

// Create objects using the constructor
const dog = new Animal("Buddy");
const cat = new Animal("Whiskers");

// Call the method on the objects
dog.sayHello(); // Output: Hello, my name is Buddy
cat.sayHello(); // Output: Hello, my name is Whiskers

// Check the prototype
console.log(Object.getPrototypeOf(dog) === Animal.prototype); // Output: true
console.log(dog.__proto__ === Animal.prototype); // Output: true (deprecated, but commonly seen)

Explanation:

  1. Animal(name): This is a constructor function. It's used to create Animal objects.
  2. Animal.prototype.sayHello = ...: This adds a method sayHello to the Animal's prototype object. This method is not a property of individual Animal objects (like dog or cat) directly.
  3. new Animal("Buddy"): When new Animal("Buddy") is called:
    • A new object is created.
    • The this keyword inside the Animal function is bound to this new object.
    • The name property of the new object is set to "Buddy".
    • The new object's [[Prototype]] (accessible via Object.getPrototypeOf()) is set to Animal.prototype.
  4. dog.sayHello(): When dog.sayHello() is called, JavaScript first checks if dog has a sayHello property. It doesn't. Then, it looks at dog's prototype (Animal.prototype). It finds sayHello there and executes it, with this bound to dog.

Key takeaway: Methods added to the prototype are shared by all objects created from that constructor. This saves memory because each object doesn't need its own copy of the method.

Prototype Chain

The prototype chain is what allows JavaScript to resolve property and method lookups even when they aren't directly defined on an object.

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

Animal.prototype.sayHello = function() {
  console.log("Hello, my name is " + this.name);
};

function Dog(name, breed) {
  Animal.call(this, name); // Call the Animal constructor to set the name
  this.breed = breed;
}

// Set up the prototype chain: Dog inherits from Animal
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // Important: Reset the constructor property

Dog.prototype.bark = function() {
  console.log("Woof!");
};

const myDog = new Dog("Fido", "Golden Retriever");

myDog.sayHello(); // Output: Hello, my name is Fido (inherited from Animal)
myDog.bark();     // Output: Woof! (defined on Dog)
console.log(myDog.name); // Output: Fido (inherited from Animal)
console.log(myDog.breed); // Output: Golden Retriever (defined on Dog)

Explanation:

  1. Dog.prototype = Object.create(Animal.prototype);: This is the crucial step. It sets the prototype of Dog to a new object whose prototype is Animal.prototype. This establishes the inheritance relationship. Object.create() is the preferred way to create a new object with a specific prototype.
  2. Dog.prototype.constructor = Dog;: After setting the prototype, the constructor property of Dog.prototype is overwritten. It's important to reset it to Dog so that instanceof and constructor calls work correctly.
  3. Animal.call(this, name);: Inside the Dog constructor, Animal.call(this, name) calls the Animal constructor with the Dog object as this. This ensures that the name property is initialized correctly.

Prototype Chain Lookup:

When you access myDog.sayHello(), JavaScript does the following:

  1. Checks if myDog has a sayHello property. It doesn't.
  2. Checks myDog's prototype (Dog.prototype). It doesn't have sayHello.
  3. Checks Dog.prototype's prototype (Animal.prototype). It finds sayHello there and executes it.

hasOwnProperty()

The hasOwnProperty() method is useful for checking if an object has a property directly, without looking up the prototype chain.

const dog = new Animal("Buddy");
dog.breed = "Labrador";

console.log(dog.hasOwnProperty("name"));   // Output: false (inherited)
console.log(dog.hasOwnProperty("breed"));  // Output: true (own property)
console.log(dog.hasOwnProperty("sayHello")); // Output: false (inherited)

Object.create()

Object.create() is a powerful method for creating new objects with a specified prototype. It's a cleaner and more flexible alternative to manipulating prototype directly.

const animalPrototype = {
  sayHello: function() {
    console.log("Hello!");
  }
};

const cat = Object.create(animalPrototype);
cat.name = "Mittens";

cat.sayHello(); // Output: Hello!
console.log(cat.name); // Output: Mittens

ES6 Classes (Syntactic Sugar)

ES6 introduced the class keyword, which provides a more familiar syntax for creating objects and dealing with inheritance. However, under the hood, it still uses prototypes.

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

  sayHello() {
    console.log("Hello, my name is " + this.name);
  }
}

const dog = new Animal("Buddy");
dog.sayHello(); // Output: Hello, my name is Buddy

This class syntax is essentially a more readable way to define constructor functions and manipulate prototypes. It doesn't change the underlying prototypal inheritance mechanism.

Important Considerations

  • Modifying Prototypes: Be careful when modifying prototypes of built-in objects (like Array or Object). It can have unintended consequences for other code that relies on those prototypes.
  • Performance: While prototypal inheritance is efficient, excessive prototype chain lookups can impact performance. Keep the prototype chain relatively short.
  • instanceof operator: The instanceof operator checks if an object is an instance of a particular constructor function (or any function in its prototype chain).

Understanding prototypes is essential for writing robust and efficient JavaScript code. While ES6 classes provide a more convenient syntax, knowing how prototypes work under the hood will give you a deeper understanding of the language and allow you to solve more complex problems.