Encapsulation

What is encapsulation?

Objects provide a public interface to other code that wants to use an object. Generally from outside an object its properties and methods can be retrieved and/or altered by using property accessors; in JavaScript the dot notation or the bracket notation.

An object's internal state, that what is going on inside the object, should be kept private. Code outside an object should not have to care about or should not be able to interfere with what is going on inside the object. An object's internal state should only be accessed by the object's own methods, not from other objects. Many OOP languages provide ways to prevent outer code from accessing an object's internal state. Maintaining a clear division between an object's public interface and its private internal state, is called encapsulation. Encapsulation is an important principle in OOP.

Consider, for instance, an object representing a bank account. That object would probably have a property "account balance", representing the amount of money in that bank account. You sure do not want code outside that object to have direct access to that property, being able to alter that amount in whatever way, circumventing object's methods to safely put money in or withdraw money from that account. And there is probably a method to just show the account balance to the account holder. Those methods are part of the public interface: they are public. But the account balance property itself should be part of the object's private internal state.

Private fields and methods

In JavaScript, fields and methods in a class are public by default. They can be made private by prefixing their name with a hash #; so called "hash names". The hash is an integral part of the name: myProperty and #myProperty are two different properties. The privacy encapsulation is enforced by JavaScript itself; accessing private fields or methods outside the class throws a SyntaxError.

Private fields and methods must be declared directly in the class body before they can be referred to anywhere in the class. They cannot be declared implicitly like by using this in a constructor.


class MyClass {
  myProperty;
  #myProperty;
  
  constructor(value) {
    this.myProperty = "myProperty: " + value;
    this.#myProperty = "#myProperty: " + value;
    // this.#myOtherProperty = value; //  SyntaxError: reference to undeclared private field or method #myOtherProperty 	
  }

  getMyPrivateProperty() {
    return this.#myProperty
  }  
}

const myInstance = new MyClass(123);

console.log(myInstance.myProperty); // logs: "myProperty: 123"
// console.log(myInstance.#myProperty); // SyntaxError: reference to undeclared private field or method #myProperty
console.log(myInstance.getMyPrivateProperty()); // logs: "#myProperty: 123"

console.log('myProperty' in myInstance); // logs: true
console.log('#myProperty' in myInstance); // logs: false

const randomObject = { __proto__: myInstance };
console.log(randomObject.myProperty); // logs: "myProperty: 123"
// console.log(randomObject.#myProperty); // SyntaxError: reference to undeclared private field or method #myProperty

Unlike public methods, private methods are not set on the MyClass.prototype, since they only live within the current class's body.

Getters and setters can also be private:


class MyClass {
  #message;
  
  constructor(msg) {
    this.#decoratedMessage = msg; // invokes private setter
  }  

  get #decoratedMessage() {
    return this.#message;
  }
  
  set #decoratedMessage(msg) {
    this.#message = `๐ŸŽ‰${msg}๐Ÿ˜˜`;
  }
  
  getDecoratedMessage() {
    return this.#decoratedMessage;  // invokes private getter
  }
}

console.log(new MyClass("Hello world").getDecoratedMessage()); // logs: "๐ŸŽ‰Hello world๐Ÿ˜˜"

BTW. Note that the setter in the example above creates a private "pseudo-property". Additionally declaring #decoratedMessage as a private field would have been a redeclaration of a private field, which throws a SyntaxError.

In JavaScript, the constructor is public and cannot be made private. To prevent classes from being constructed from outside the class, you can use a trick that will be explained later.

An example

In the next example some user defined value is stored in an instance of ValueContainer. The initial value is set when an instance is created. The stored value can be changed and retrieved via the public currentValue property. The currentValue property holds the current stored value. With a public instance method reset(), the current value can be reset to the initial value and with the read-only initialValue property the initial value can be retrieved.

We want the initial value (#init) to be private. We do not want anything outside an instance to be able to change this value. But we do want to be able to read it from outside an instance. We used a getter (and not a setter) to make initialValue read-only.

We used a getter and setter for currentValue (in conjunction with private field #currentValue). We could have made the currentValue field a public field and simply change or retrieve it outside an instance via the regular property accessors (dot notation or the bracket notation). We used accessors instead in order to be able to later easily change the class and add extra functionality to the setter, for instance to insure that only numbers are accepted as new values.


class ValueContainer {
  #init;
  #currentValue;
   
  constructor(value) {
    this.#init = value;
    this.#currentValue = value;	
  }  

  get currentValue() {
    return this.#currentValue;
  }
  
  set currentValue(value) {
	this.#currentValue = value;	
  }
  
  // read-only:
  get initialValue() {
    return this.#init;
  }  
  
  reset() {
	this.#currentValue = this.#init;	
  }  
}

const myValue = new ValueContainer("My little secret.");

console.log(myValue.currentValue); // logs: "My little secret."
myValue.currentValue = "123";
console.log(myValue.currentValue); // logs: 123
myValue.initialValue = null; // Ignored. In strict mode: TypeError: setting getter-only property "initialValue"
console.log(myValue.initialValue); // logs: "My little secret."
myValue.reset();
console.log(myValue.currentValue); // logs: "My little secret."

BTW. Note that the #currentValue; statement cannot be omitted: a private field needs to be declared directly in the class body before it can be used with this.

Abstraction vs. encapsulation

In the context of object-oriented programming, encapsulation is often confused with abstraction. There are many types of abstraction in computer programming, but in relation to OOP it generally refers to displaying only the essential data to the user of the code or application. In other words, hiding the technical complexity of systems behind simpler APIs or interfaces.

Encapsulation "hides" data by preventing access to this data by code from outside the class. Abstraction "hides" data by exposing the interface to the user and hiding the technical implementation details from the user. Abstraction itself does not prevent access to any code. The interface and its methods and fields are typically public and not private.

There are no special constructs for creating interfaces in JavaScript, as there are in C++ or Java. An interface in Java is a kind of special "class" that you cannot use to create instances but that groups together all fields and methods essential to the user. Classes that contain the technical implementation details extend, and therefore inherit, from this interface (or interfaces).

To demonstrate abstraction in JavaScript we could do something like in the next example. We used encapsulation in this example, but this is not essential to the demonstration of abstraction. If all methods would have been public, it still would have been an example of abstraction. The last two examples below are better examples of abstraction in JavaScript.


class Car {
  // Technical implementation methods 'hidden' from the user, i.e., the driver:
  #steering() { console.log("Execute the technical implementation of steering") }
  #throttling() { console.log("Execute the technical implementation of throttling") }
  #braking() { console.log("Execute the technical implementation of braking") }
  #shiftingGear() { console.log("Execute the technical implementation of shifting gear") }

  // Car interface methods available to the driver:
  steer() { this.#steering() }
  throttle() { this.#throttling() }
  brake() { this.#braking() }
  shiftGear() { this.#shiftingGear() }  
}

const drivingMyCar = new Car();
drivingMyCar.brake(); // logs: "Execute the technical implementation of braking"

The next example of abstraction in JavaScript uses mixins, which will be explained in a later chapter.


const CarFunctions = {
  steer() { console.log("Execute the technical implementation of steering") },
  throttle() { console.log("Execute the technical implementation of throttling") },
  brake() { console.log("Execute the technical implementation of braking") },
  shiftGear() { console.log("Execute the technical implementation of shifting gear") },
}

class CarInterface {
  steer() { this.steer() }
  throttle() { this.throttle() }
  brake() { this.brake() }
  shiftGear() { this.shiftGear() } 
}

Object.assign(CarInterface.prototype, CarFunctions);

const drivingMyCar = new CarInterface();
drivingMyCar.brake(); // logs: "Execute the technical implementation of braking"

We can also implement this using composition, which will also be explained in a later chapter:


function steer() { console.log("Execute the technical implementation of steering") };
function throttle() { console.log("Execute the technical implementation of throttling") };
function brake() { console.log("Execute the technical implementation of braking") };
function shiftGear() { console.log("Execute the technical implementation of shifting gear") };

class CarInterface {}
CarInterface.prototype.steer = steer;
CarInterface.prototype.throttle = throttle;
CarInterface.prototype.brake = brake;
CarInterface.prototype.shiftGear = shiftGear;

const drivingMyCar = new CarInterface();
drivingMyCar.brake(); // logs: "Execute the technical implementation of braking"