""
Index

Object-Oriented Programming (OOP) in JavaScript

JavaScript for Beginners

15.1 Constructor Functions

In JavaScript, constructor functions are a traditional way to create multiple objects with similar properties and methods. They provide a blueprint to produce many instances, each with their own unique data.

What Is a Constructor Function?

A constructor function is simply a regular function that is used with the new keyword to create new objects. By convention, constructor function names start with a capital letter to distinguish them from normal functions.

How Does this Work Inside a Constructor?

Inside a constructor function, this refers to the new object being created. When you call a constructor with new, JavaScript automatically:

  1. Creates a new empty object.
  2. Sets this to point to that new object.
  3. Runs the constructor function code.
  4. Returns the new object (unless the constructor explicitly returns something else).

Defining a Constructor Function

Here’s a simple example defining a Person constructor:

function Person(name, age) {
  this.name = name;  // Assign name property to the new object
  this.age = age;    // Assign age property to the new object
  
  this.greet = function() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  };
}

Creating Multiple Instances

Using the new keyword, you can create as many Person objects as you want, each with its own name and age:

const alice = new Person("Alice", 30);
const bob = new Person("Bob", 25);

alice.greet();  // Hello, my name is Alice and I am 30 years old.
bob.greet();    // Hello, my name is Bob and I am 25 years old.

Each instance has its own copy of the name, age, and the greet method.

Example: Car Constructor

Let’s look at another example—a Car constructor function:

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;

  this.getInfo = function() {
    return `${this.year} ${this.make} ${this.model}`;
  };
}

const car1 = new Car("Toyota", "Corolla", 2020);
const car2 = new Car("Tesla", "Model 3", 2023);

console.log(car1.getInfo());  // Output: 2020 Toyota Corolla
console.log(car2.getInfo());  // Output: 2023 Tesla Model 3

Summary

Constructor functions form the foundation of object creation in JavaScript and are essential to understand before moving on to more advanced OOP concepts like prototypes and ES6 classes.

Index

15.2 Prototypes and Inheritance

JavaScript uses a prototype-based inheritance model, which is quite different from the classical inheritance found in many other programming languages. Understanding prototypes is key to writing efficient, reusable object-oriented code in JavaScript.

What Is a Prototype?

Every JavaScript object has an internal link to another object called its prototype. This prototype object can have properties and methods that the original object can access indirectly. This chain of links is called the prototype chain.

Why Use Prototypes?

When you create multiple objects using a constructor function, you usually want to share common methods among them. If you define methods inside the constructor, each instance gets its own copy — which wastes memory.

Instead, JavaScript allows you to add shared methods to the constructor’s prototype. All instances then refer to the same method, saving memory and improving performance.

Example: Prototype Sharing

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

// Adding a method to the prototype, shared by all instances
Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name}.`);
};

const alice = new Person("Alice", 30);
const bob = new Person("Bob", 25);

alice.greet();  // Hello, my name is Alice.
bob.greet();    // Hello, my name is Bob.

// Checking prototype relationship
console.log(alice.__proto__ === Person.prototype);  // true

Here, greet is defined only once on Person.prototype, but both alice and bob can use it.

The Prototype Chain

If you try to access a property or method on an object, JavaScript first looks at the object itself. If it doesn't find it there, it looks up the prototype chain until it finds it or reaches the end (null).

console.log(alice.hasOwnProperty("name"));       // true (own property)
console.log(alice.hasOwnProperty("greet"));      // false (in prototype)
console.log(typeof alice.greet);                  // "function" (found via prototype)

Inheritance via Prototypes

You can create inheritance relationships between constructors by linking their prototypes.

Suppose we want a Student to inherit from Person:

function Student(name, age, grade) {
  Person.call(this, name, age);  // Call Person constructor with Student's context
  this.grade = grade;
}

// Set Student's prototype to inherit from Person.prototype
Student.prototype = Object.create(Person.prototype);

// Fix constructor pointer because it points to Person now
Student.prototype.constructor = Student;

// Add a method specific to Student
Student.prototype.study = function() {
  console.log(`${this.name} is studying.`);
};

const student1 = new Student("Charlie", 20, "A");
student1.greet();   // Inherited method: Hello, my name is Charlie.
student1.study();   // Student method: Charlie is studying.

In this example:

Summary

Prototypes unlock the power of JavaScript inheritance, enabling code reuse and memory efficiency in your programs!

Index

15.3 ES6 Classes

With ES6, JavaScript introduced the class syntax — a cleaner and more intuitive way to write constructor functions and work with inheritance. Although classes are mostly syntactic sugar over the existing prototype system, they make object-oriented programming easier to read and write.

Class Declaration and Constructor

A class declaration defines a blueprint for creating objects. Inside the class, the constructor method sets up new instances.

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  // Method shared by all instances
  greet() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

const alice = new Person("Alice", 30);
alice.greet();  // Output: Hello, my name is Alice.

Inheritance with extends and super

Classes can extend other classes to inherit properties and methods.

class Student extends Person {
  constructor(name, age, grade) {
    super(name, age);  // Call Person's constructor
    this.grade = grade;
  }

  // Override greet method
  greet() {
    console.log(`Hi, I am ${this.name}, and my grade is ${this.grade}.`);
  }

  study() {
    console.log(`${this.name} is studying.`);
  }
}

const student1 = new Student("Bob", 22, "A");
student1.greet();   // Hi, I am Bob, and my grade is A.
student1.study();   // Bob is studying.

Summary

Feature Description Example
Class Declaration Defines blueprint with class keyword class Person { ... }
Constructor Initializes new instances constructor(name, age) { ... }
Methods Defined inside class, shared via prototype greet() { ... }
Inheritance Use extends to inherit a class class Student extends Person
Super Call Call parent constructor with super() super(name, age)
Method Overriding Redefine parent method in subclass greet() { ... }

Why Use ES6 Classes?

Final Example: Using Classes

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

  speak() {
    console.log(`${this.name} makes a sound.`);
  }
}

class Dog extends Animal {
  speak() {
    console.log(`${this.name} barks.`);
  }
}

const dog = new Dog("Rex");
dog.speak();  // Rex barks.

This simple example demonstrates how subclasses can extend and customize parent class behavior, all with a clean and concise syntax.

With ES6 classes, JavaScript’s object-oriented programming becomes more accessible and expressive — giving you a powerful tool for building structured and maintainable code!

Index

15.4 Encapsulation with Closures and Private Fields

Encapsulation is a fundamental concept in object-oriented programming that restricts direct access to some of an object's components, protecting the internal state and exposing only what’s necessary. In JavaScript, encapsulation can be achieved using closures and, more recently, private class fields.

Encapsulation with Closures

Before private fields were introduced, one common way to create private variables and methods was through closures in constructor or factory functions. Variables defined inside the constructor or factory function are not accessible outside, but can be used by privileged methods.

Example: Private Variables Using Closures in a Constructor Function

function BankAccount(initialBalance) {
  let balance = initialBalance;  // Private variable

  // Privileged method has access to private variable
  this.deposit = function(amount) {
    if (amount > 0) {
      balance += amount;
      console.log(`Deposited: $${amount}`);
    }
  };

  this.getBalance = function() {
    return balance;
  };
}

const account = new BankAccount(100);
account.deposit(50);
console.log(account.getBalance());  // Output: 150

// Trying to access balance directly will fail
console.log(account.balance);  // undefined

Encapsulation with Factory Functions and Closures

Similarly, you can use factory functions to create objects with private data:

function createCounter() {
  let count = 0;  // private variable

  return {
    increment() {
      count++;
    },
    getCount() {
      return count;
    }
  };
}

const counter = createCounter();
counter.increment();
console.log(counter.getCount());  // 1
console.log(counter.count);       // undefined

Private Class Fields (#fieldName)

Starting with ES2022, JavaScript introduced private class fields and methods using a # prefix. These fields are truly private and cannot be accessed or modified from outside the class.

Example: Using Private Fields in a Class

class User {
  #password;  // Private field

  constructor(username, password) {
    this.username = username;
    this.#password = password;
  }

  checkPassword(input) {
    return input === this.#password;
  }

  // Private method
  #encryptPassword() {
    // pretend encryption logic here
    return btoa(this.#password);
  }

  showEncryptedPassword() {
    return this.#encryptPassword();
  }
}

const user = new User("alice", "secret123");
console.log(user.username);               // alice
console.log(user.checkPassword("secret123"));  // true

// Trying to access private field or method from outside causes error
console.log(user.#password);  // SyntaxError
console.log(user.#encryptPassword());  // SyntaxError

Why Encapsulation Matters

Summary

Technique How It Works Accessibility
Closures Variables scoped inside functions Private to constructor/factory functions
Private Class Fields (#) Fields declared with # inside classes Truly private, inaccessible outside class

Encapsulation in JavaScript protects your data and helps build robust, secure, and maintainable applications. Whether using closures or modern private fields, it’s an essential part of writing good object-oriented code.

Index