JavaScript Object-Oriented Programming: Encapsulation

JavaScript Object-Oriented Programming: Encapsulation

Encapsulation is one of the core principles of Object-Oriented Programming (OOP). It means bundling the data (properties) and the methods (functions) that work on that data into a single unit, usually an object. But beyond just grouping, encapsulation controls how data inside an object is accessed or modified. It hides the internal details from the outside world, exposing only what is necessary. This helps in protecting the object’s state and prevents unintended interference, making programs easier to maintain and debug.

In JavaScript, encapsulation is particularly interesting because, unlike classical OOP languages such as Java or C++, it initially did not provide direct syntax for private properties. Instead, JavaScript developers used clever patterns and functions like closures to simulate encapsulation. More recently, newer versions of JavaScript have introduced features like private class fields that make true encapsulation simpler to achieve. In this article, we will explore how to create encapsulated objects in JavaScript using various approaches, each with fun, real code examples.

Creating Objects with Public Properties and Methods

Let’s start with the simplest form of objects in JavaScript. By default, all properties and methods inside an object are public, meaning they can be accessed and changed from outside. While this is straightforward, it doesn’t offer encapsulation.

Here is a basic example of a Player object with public properties and a method to increase the player’s score:

function Player(name) {

  this.name = name;  // public property
  this.score = 0;    // public property

  this.increaseScore = function(points) {
    this.score += points;
  };

}

const harry = new Player('Harry');
console.log(harry.name);  // Outputs: Harry
console.log(harry.score); // Outputs: 0

harry.increaseScore(10);
console.log(harry.score); // Outputs: 10

In this example, both name and score are public. You can access and change harry.score directly from outside, which may not always be ideal. Encapsulation helps protect such data by hiding it from outside access.

Using Closures for Private Variables

Before the introduction of private class fields, JavaScript developers used closures inside constructor functions or factory functions to achieve data hiding. The idea is to declare variables inside a function’s scope so they are not accessible from outside but can be used by inner functions.

Consider a BankAccount that keeps the balance private and provides methods to deposit and get the balance:

function BankAccount(owner) {

  let balance = 0;  // private variable

  this.owner = owner;  // public property

  this.deposit = function(amount) {
    if (amount > 0) {
      balance += amount;
    }
  };

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

}

const severus = new BankAccount('Severus Snape');
console.log(severus.owner);      // Outputs: Severus Snape
console.log(severus.getBalance()); // Outputs: 0

severus.deposit(100);
console.log(severus.getBalance()); // Outputs: 100

// Trying to access balance directly will fail
console.log(severus.balance); // Outputs: undefined

Here, the balance variable is truly private because it exists only inside the constructor function’s scope. The deposit and getBalance methods form a closure over balance, allowing controlled access. Outside code cannot directly read or modify balance.

Using ES6 Classes with Private Fields (using #)

In modern JavaScript (ES2020 and later), private class fields were introduced, marked by a # prefix. These fields cannot be accessed or modified outside the class body, making encapsulation cleaner and native.

Let’s look at a SecretAgent class where the agent’s codeName is private:

class SecretAgent {

  #codeName;  // private field

  constructor(name, codeName) {
    this.name = name;    // public property
    this.#codeName = codeName;  // private property
  }

  revealCodeName() {
    return `${this.name}'s code name is ${this.#codeName}`;
  }

}

const james = new SecretAgent('James Bond', '007');
console.log(james.name);           // Outputs: James Bond
console.log(james.revealCodeName()); // Outputs: James Bond's code name is 007

// Direct access to private field throws error
// console.log(james.#codeName);  // SyntaxError: Private field '#codeName' must be declared in an enclosing class

In this example, #codeName is fully private. Attempts to read or write to it outside the class will fail, thus preserving encapsulation securely and neatly.

Encapsulation with Getters and Setters

Getters and setters provide a way to control how properties are accessed and changed while keeping the underlying data hidden or protected. They allow for validation or transformation of data during access or assignment.

Consider a Person class that encapsulates an age property with validation via getters and setters:

class Person {

  #age;

  constructor(name, age) {
    this.name = name;
    this.age = age;  // setter will be called
  }

  get age() {
    return this.#age;
  }

  set age(value) {

    if (value < 0) {
      console.log('Age cannot be negative. Setting age to 0.');
      this.#age = 0;
    } else {
      this.#age = value;
    }

  }

}

const hermione = new Person('Hermione Granger', 19);
console.log(`${hermione.name} is ${hermione.age} years old.`); // Outputs: Hermione Granger is 19 years old.

hermione.age = -5;  // Invalid age, setter corrects it
console.log(`${hermione.name} is now ${hermione.age} years old.`); // Outputs: Age cannot be negative. Setting age to 0. Hermione Granger is now 0 years old.

Here, the private field #age is accessed via the getter and setter. This design keeps #age hidden but allows controlled access with validation logic in the setter.

Revealing Module Pattern for Encapsulation

Another popular technique for encapsulation uses the Revealing Module Pattern. This pattern leverages closures and returns an object exposing only selected functions, hiding internal data.

Here is a GameScore module that tracks a private score and exposes public functions:

const GameScore = (function() {

  let score = 0;  // private variable

  function increment(points) {
    score += points;
  }

  function getScore() {
    return score;
  }

  return {
    increment,
    getScore
  };

})();

console.log(GameScore.getScore()); // Outputs: 0

GameScore.increment(5);

console.log(GameScore.getScore()); // Outputs: 5

// Trying to access score directly is not possible
console.log(GameScore.score); // Outputs: undefined

In this example, the score variable is private within the immediately invoked function expression (IIFE). Only the methods increment and getScore are exposed, providing controlled access.

Fun Example: Encapsulated Robot with Private Energy Level

To bring it all together, let’s build a fun Robot class that encapsulates a private energy level. The robot can charge, work, and report its energy through a public method:

class Robot {

  #energy;

  constructor(name) {
    this.name = name;
    this.#energy = 100;  // private energy level
  }

  charge(amount) {
    if (amount > 0) {
      this.#energy += amount;
      console.log(`${this.name} charged by ${amount} units.`);
    }
  }

  work(hours) {

    const energyConsumed = hours * 10;

    if (this.#energy >= energyConsumed) {
      this.#energy -= energyConsumed;
      console.log(`${this.name} worked for ${hours} hours.`);
    } else {
      console.log(`${this.name} does not have enough energy to work for ${hours} hours.`);
    }

  }

  checkEnergy() {
    console.log(`${this.name} has ${this.#energy} energy units left.`);
  }

}

const artoo = new Robot('R2-D2');
artoo.checkEnergy();  // Outputs: R2-D2 has 100 energy units left.

artoo.work(5);        // Outputs: R2-D2 worked for 5 hours.
artoo.checkEnergy();  // Outputs: R2-D2 has 50 energy units left.

artoo.charge(30);     // Outputs: R2-D2 charged by 30 units.
artoo.checkEnergy();  // Outputs: R2-D2 has 80 energy units left.

This example uses private class fields to hide the energy level and expose only selected actions to interact with it. This way, the energy cannot be altered directly from outside, ensuring the robot’s state stays consistent.

Conclusion

Encapsulation in JavaScript is the practice of hiding an object’s internal state and requiring all interaction to be performed through methods. This article demonstrated several ways to achieve encapsulation, starting with public properties, then using closures for privacy, ES6 private fields for true encapsulation, getters and setters for controlled access, and the revealing module pattern for functional encapsulation. Each technique lets you protect your object’s data and control how it’s accessed or modified, improving code organization and security.

References

If you want to learn more about encapsulation and related JavaScript features, these resources are excellent starting points:

Scroll to Top