Unlike Java and C#, which are primarily object-oriented programming (OOP) languages, JavaScript supports multiple programming paradigms. This means it is possible to write object-oriented JavaScript (OOJS), although it is not the only way to write JavaScript code. JavaScript is also a prototype-based language, while Java and C# are class-based languages. This meant that until the introduction of classes in ECMAScript 2015 (ES6), OOJS was quite difficult for developers who came from class-based languages. Furthermore, it was not possible to achieve true encapsulation—a fundamental principle of OOP—until the introduction of private class members in ECMAScript 2022 (ES13).

Encapsulation

Cay Horstmann explains the principle of encapsulation, and why it is so important in OOP, in Chapter 4 of his book Core Java:

Encapsulation (sometimes called information hiding) is a key concept in working with objects. Formally, encapsulation is simply combining data and behavior in one package and hiding the implementation details from the users of the object. The bits of data in an object are called its instance fields, and the procedures that operate on the data are called its methods. A specific object that is an instance of a class will have specific values of its instance fields. The set of those values is the current state of the object. Whenever you invoke a method on an object, its state may change.

The key to making encapsulation work is to have methods never directly access instance fields in a class other than their own. Programs should access object data only through the object’s methods. Encapsulation is the way to give an object its “black box” behavior, which is the key to reuse and reliability. This means a class may totally change how it stores its data, but as long as it continues to use the same methods to manipulate the data, no other object will know or care.

An Employee class

The following Employee class is from Core Java. I have rewritten it in JavaScript.

class Employee {
#name;
#salary;
#hireDay;

constructor(name, salary, year, month, day) {
this.#name = name;
this.#salary = salary;
this.#hireDay = new Date(year, month - 1, day);
}

getName() {
return this.#name;
}

getSalary() {
return this.#salary;
}

getHireDay() {
return new Date(this.#hireDay.getTime());
}

raiseSalary(byPercent) {
const raise = (this.#salary * byPercent) / 100;
this.#salary += raise;
return this.#salary;
}
}

Apart from its constructor, this class contains:

  • Three private instance fields: #name, #salary, and #hireDay.
  • Three public field accessor methods: getName(), getSalary(), and getHireDay().
  • One public field mutator method: raiseSalary().

Note that the instance fields are prefixed with a #. This is known as an access modifier and is used to make class members private in JavaScript. In this case, it ensures that the only methods that can access the instance fields are the methods of the Employee class itself.

The #name and #salary fields

The #name field is read-only because it is private and the Employee class has no corresponding field mutator method. The #salary field is writable, but it can only be written by the raiseSalary() method.

const employee = new Employee("Poppy Sweeting", 40_000, 2021, 8, 16);

// Name can be read but not written.
const name = employee.getName();
console.log(name); // "Poppy Sweeting"

// Salary can only be written by the `raiseSalary()` method.
const salary = employee.raiseSalary(10);
console.log(salary); // 44_000

The #hireDay field

The getHireDay() method is careful to return a new Date object. Unlike primitive values, objects are mutable by default. If the getHireDay() method returned the value of the #hireDay field directly, a user of the class would be able to change an employee’s hire day—even though the field is private!

const employee = new Employee("Poppy Sweeting", 40_000, 2021, 8, 16);

// Get the employee's hire day.
let hireDay = employee.getHireDay();
console.log(hireDay); // Mon, 16 Aug 2021

// Attempt to add 5 years' extra tenure.
hireDay.setFullYear(hireDay.getFullYear() - 5);
console.log(hireDay); // Tue, 16 Aug 2016

// The original hire day is unchanged.
hireDay = employee.getHireDay();
console.log(hireDay); // Mon, 16 Aug 2021

Benefits of encapsulation

It might seem easier to make the #name, #salary, and #hireDay fields public. It would certainly be less verbose. However, this would mean that any code outside the class could modify these fields, which would defeat the point of encapsulation!

Robustness

Because the #name field is read-only, we can rest assured that its value will never be corrupted. Although the #salary field is writable, it can only be written in a way that we expect. If the value ever did turn out wrong, we would know that the problem was isolated to the raiseSalary() method.

Error prevention

Using field mutator methods means that we can check for errors before setting the values of instance fields. For example, we could enforce that an employee’s salary be raised by a positive percentage:

class Employee {
// . . .

raiseSalary(byPercent) {
if (byPercent <= 0) {
throw new RangeError("Percentage must be positive.");
}

const raise = (this.#salary * byPercent) / 100;
this.#salary += raise;
return this.#salary;
}
}

Maintainability

Because the #name property is private, we could change its internal implementation without affecting any code outside the class. For example, if we wanted to store first and last names separately, we would only need to change the bodies of the constructor and the getName() method. Any code that read the name via the getName() method would be entirely unaffected, because the method’s return value would be the same.

Before

An employee’s name is represented by a single private property: #name. The return value of the getName() method is "Poppy Sweeting" in this instance.

class Employee {
#name;

constructor(name) {
this.#name = name;
}

getName() {
return this.#name;
}
}

const employee = new Employee("Poppy Sweeting");

const name = employee.getName();
console.log(name); // "Poppy Sweeting"

After

The Employee class is updated so that an employee’s name is represented by two private properties: #firstName and #lastName. The return value of the getName() method is still "Poppy Sweeting" in this instance.

class Employee {
#firstName;
#lastName;

constructor(name) {
// This is called "destructuring assignment".
[this.#firstName, this.#lastName] = name.split(" ");
}

getName() {
return this.#firstName + " " + this.#lastName;
}
}

const employee = new Employee("Poppy Sweeting");

const name = employee.getName();
console.log(name); // Still "Poppy Sweeting"

Summary

Encapsulation is a fundamental principle of object-oriented programming (OOP). It means that we combine an object’s data and behaviour in a single “package” and hide its implementation details. With the introduction of private class members in ECMAScript 2022 (ES13), it is now possible to achieve true encapsulation when writing object-oriented JavaScript (OOJS).