Understanding Encapsulation in JavaScript
What Is Encapsulation?
Encapsulation is an object-oriented programming principle that involves bundling data (properties) and behaviors (methods) together and restricting direct access to some of these components. The idea is to hide internal implementation details and expose a clear and stable interface for interacting with the object.
In classical OOP languages like Java or C++, encapsulation is often achieved through access modifiers like public, private, and protected. JavaScript has historically lacked built-in privacy for object fields, but with newer language features, we now have various ways to achieve encapsulation.
Encapsulation Before Modern Class Fields
Before ES6 and the introduction of private fields, JavaScript developers used closures and naming conventions to simulate encapsulation:
Example using closures:
function createCounter() {
let count = 0; // private variable (only accessible inside this function)
return {
increment() {
count++;
},
getValue() {
return count;
}
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getValue()); // 1
// We cannot directly access 'count' from outside.
ES6 Classes and Encapsulation
ES6 class syntax introduced a more familiar object-oriented style, but still didn’t provide true privacy for instance fields. Class methods and properties defined without special syntax are public by default.
class Person {
constructor(name) {
this.name = name; // public property
}
greet() {
console.log(`Hello, my name is ${this.name}`);
}
}
In this example, name is public and can be accessed and changed from outside:
const p = new Person("Alice");
p.name = "Bob"; // allowed
p.greet(); // "Hello, my name is Bob"
Private Class Fields (ES2022+)
Modern JavaScript introduced private class fields and methods using a # prefix. Private fields can only be accessed within the class body. They are truly private at runtime and cannot be accessed or modified outside the class.
Example:
class BankAccount {
#balance; // private field
constructor(initialBalance) {
this.#balance = initialBalance;
}
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
}
}
withdraw(amount) {
if (amount > 0 && amount <= this.#balance) {
this.#balance -= amount;
}
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount(100);
account.deposit(50);
console.log(account.getBalance()); // 150
// console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class
Key Points About Private Fields:
Public vs Private Fields
Static Members
Static members are properties and methods that belong to the class itself, rather than instances of the class. You define them with the static keyword. They are often used as utility functions or constants relevant to the class but not tied to individual instances.
Example:
class MathUtils {
static PI = 3.14159;
static add(a, b) {
return a + b;
}
}
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.add(2, 3)); // 5
// Cannot do: const m = new MathUtils(); m.PI is not instance-bound
Static fields and methods cannot access instance properties directly since they don’t operate on instances, and this inside a static method refers to the class, not an instance.
Combining Access Modifiers
While JavaScript does not have traditional public/private keywords (other than the # for private fields), you can achieve a mixture of public and private members using:
This combination allows you to define a clear API (public interface) and hide internal details (private fields).
Example:
class Employee {
#salary; // private
static company = "ABC Corp"; // static public field
constructor(name, salary) {
this.name = name; // public
this.#salary = salary; // private
}
// public method
getInfo() {
console.log(`Name: ${this.name}, Salary: ${this.#salary}`);
}
// private method (ES2022+)
#calculateBonus() {
return this.#salary * 0.1;
}
giveBonus() {
const bonus = this.#calculateBonus();
this.#salary += bonus;
}
}
const emp = new Employee("John", 1000);
emp.getInfo(); // "Name: John, Salary: 1000"
emp.giveBonus();
emp.getInfo(); // "Name: John, Salary: 1100"
// emp.#salary = 2000; // Error: Cannot access private field
// emp.#calculateBonus(); // Error: Cannot access private method
console.log(Employee.company); // "ABC Corp"
Multiple Choice Questions
10 Coding Exercises with Full Solutions and Explanations
Exercise 1: Task: Create a class Counter with a private field #count. Add methods increment() and getValue() to manipulate and retrieve the count.
class Counter {
#count = 0;
increment() {
this.#count++;
}
getValue() {
return this.#count;
}
}
const c = new Counter();
c.increment();
console.log(c.getValue()); // 1
// console.log(c.#count); // Error: private field
Explanation: #count is private. You can only interact through increment() and getValue().
Exercise 2: Task: Create a Person class with a public name field and a private #age field. Add a getAge() method to return the age.
class Person {
#age;
constructor(name, age) {
Recommended by LinkedIn
this.name = name;
this.#age = age;
}
getAge() {
return this.#age;
}
}
const p = new Person("Alice", 30);
console.log(p.name); // "Alice"
console.log(p.getAge()); // 30
// console.log(p.#age); // Error
Explanation: Age is private, accessible only via getAge().
Exercise 3: Task: Add a private method #validateAmount(amount) inside a BankAccount class and use it in deposit() and withdraw() methods.
class BankAccount {
#balance = 0;
#validateAmount(amount) {
return typeof amount === 'number' && amount > 0;
}
deposit(amount) {
if (this.#validateAmount(amount)) {
this.#balance += amount;
}
}
withdraw(amount) {
if (this.#validateAmount(amount) && amount <= this.#balance) {
this.#balance -= amount;
}
}
getBalance() {
return this.#balance;
}
}
const acct = new BankAccount();
acct.deposit(100);
acct.withdraw(50);
console.log(acct.getBalance()); // 50
Explanation: The #validateAmount method is private and can only be used internally.
Exercise 4: Task: Create a class MathUtils with a static method square(n) that returns n*n. Test it without creating an instance.
class MathUtils {
static square(n) {
return n * n;
}
}
console.log(MathUtils.square(5)); // 25
Explanation: Static method called on the class directly.
Exercise 5: Task: Add a static field PI = 3.14 to MathUtils and log it.
class MathUtils {
static PI = 3.14;
}
console.log(MathUtils.PI); // 3.14
Explanation: Static field PI is accessed from MathUtils itself.
Exercise 6: Task: Create a User class with a static private field #count to track the number of users created. Increment it in the constructor and create a static method to retrieve the count.
class User {
static #count = 0;
constructor() {
User.#count++;
}
static getUserCount() {
return User.#count;
}
}
new User();
new User();
console.log(User.getUserCount()); // 2
Explanation: #count is a static private field, updated whenever a new user is instantiated.
Exercise 7: Task: Create a class Rectangle with public width, height and private method #calculateArea(). Add a public area() method that uses #calculateArea() to return area.
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
#calculateArea() {
return this.width * this.height;
}
area() {
return this.#calculateArea();
}
}
const rect = new Rectangle(5, 10);
console.log(rect.area()); // 50
// console.log(rect.#calculateArea()); // Error: private method
Explanation: #calculateArea() is private and can only be called by area() method internally.
Exercise 8: Task: Define a class SecureStore that holds a private #data object. Add methods setItem(key, value) and getItem(key) to manipulate data.
class SecureStore {
#data = {};
setItem(key, value) {
this.#data[key] = value;
}
getItem(key) {
return this.#data[key];
}
}
const store = new SecureStore();
store.setItem('token', 'abc123');
console.log(store.getItem('token')); // 'abc123'
// console.log(store.#data); // Error
Explanation: #data can’t be accessed outside.
Exercise 9: Task: Create a class Counter with a private #count and a static method resetCounter(instance) that sets the instance’s count to 0 by calling a public method on the instance (like instance.reset()).
class Counter {
#count = 0;
increment() {
this.#count++;
}
reset() {
this.#count = 0;
}
getValue() {
return this.#count;
}
static resetCounter(instance) {
instance.reset();
}
}
const cnt = new Counter();
cnt.increment();
cnt.increment();
console.log(cnt.getValue()); // 2
Counter.resetCounter(cnt);
console.log(cnt.getValue()); // 0
Explanation: The static method manipulates an instance by calling its public method, not by accessing private fields directly.
Exercise 10: Task: Combine static, private, and public features. Create a class IDGenerator with a static private field #currentID = 0. Add a static method generate() that increments #currentID and returns a new ID each time.
class IDGenerator {
static #currentID = 0;
static generate() {
return ++this.#currentID;
}
}
console.log(IDGenerator.generate()); // 1
console.log(IDGenerator.generate()); // 2
// console.log(IDGenerator.#currentID); // Error: private
Explanation: #currentID is private and static, incremented each time generate() is called.
Summary
Encapsulation in JavaScript can be achieved using private fields (#), closure-based patterns, or simply by careful design. With the modern class syntax, you can define private fields and methods to truly hide implementation details. Static members let you define class-level utilities and constants. Together, these features support robust, safe, and maintainable code.