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:
- MDN Web Docs: Private fields
The official documentation on private class fields syntax and behavior. - MDN Web Docs: Closures
Learn how closures work and how they are used for data privacy. - MDN Web Docs: Getters and Setters
Understand how to use getters and setters in JavaScript classes and objects. - JavaScript Revealing Module Pattern
A detailed explanation of the revealing module pattern by Addy Osmani.