JAVASCRIPT ENCAPSULATION
One of the most important concepts of object-oriented programming is encapsulation, or the delimitation of internal interfaces from the external ones. This practice is a “must” in the development of any application that is more complex than “hello world”. To understand it, let’s step out of the development world and look to the real world. Usually, the devices we use are quite complex. Being able to delimit their internal interface from the external one allows us to use them without major problems.
AN EXAMPLE FROM THE REAL WORLD
Let’s take the example of a coffee machine. Simple on the outside: a button, a display, a couple of holes … And, of course, the result: an excellent coffee! :). There are many details. But we still manage to use it even without knowing them. Coffee machines are pretty reliable, right? They can last for years, and only in case something stops working, we take them for repair. The secret behind the reliability and simplicity of a coffee machine is that all the details are optimized and hidden. If we removed the cover of the coffee machine, then its use would be much more complex (where should we press?), And dangerous (we could get an electric shock). As we will see later, objects are like coffee machines in programming. But in order to hide their internal details, we will not use a security cover, but rather a special language syntax and some conventions.
INTERNAL AND EXTERNAL INTERFACE
In object-oriented programming, properties and methods are divided into two groups:
- Internal interface – methods and properties, accessible from the other methods of the class, but not from the outside.
- External interface – methods and properties, also accessible from outside the class.
Continuing with the analogy of the coffee machine, what is hidden internally (a pump, a heating mechanism and so on) is its internal interface. The internal interface is used to make the object work, its elements interact with each other. For example, the pump is connected to the heating mechanism. But seen from the outside, the coffee machine is protected by a cover, so that no one can access it. The details are hidden and inaccessible, but we can take advantage of its features through its external interface. So, all we need to use an object is its external interface. We may be completely unaware of its internal workings, and that would be fine.
This was a general introduction.
In JavaScript, there are two types of fields for an object (properties and methods):
Public: accessible everywhere. These define the external interface. Until now we have always used public properties and methods.
Private: accessible only from within the classroom. These define the internal interface.
In many other programming languages there is also the concept of a “protected” field: accessible only from within the class and from those that extend it (such as private fields, but in addition they are also accessible by inheriting classes). These are equally useful for defining the internal interface. They are generally more common than private fields, as our intention is usually to make them accessible in subclasses as well. Protected fields are not implemented in JavaScript at the language level, but in practice they are very useful, which is why they are often emulated. We will now build a coffee machine in JavaScript, with all kinds of properties described. A coffee machine is made up of many details; we will not model it in its entirety (although we could), in order to keep the example simple.
PROTECTED FIELDS
Let’s try to modify the waterAmount property making it protected, in order to have more control over it. For example, we don’t want anyone to be able to set it to a negative value. Protected properties are usually prefixed with an underscore _. This is not a language forcing, but rather a common convention among programmers, which specifies that these properties and methods should not be accessible from the outside. So our property becomes _waterAmount:
class CoffeeMachine {
_waterAmount = 0;
set waterAmount(value) {
if (value < 0) {
value = 0;
}
this._waterAmount = value;
}
get waterAmount() {
return this._waterAmount;
}
constructor(power) {
this._power = power;
}
}
// creiamo la macchina del caffè
let coffeeMachine = new CoffeeMachine(100);
// aggiungiamo acqua
coffeeMachine.waterAmount = -10; // _waterAmount diventerà 0, non -10
Access is now under control, so it is no longer possible to set the amount of water to a negative value.
PROPERTY READ-ONLY
Let’s try making the power property read-only. In some cases, we may need to define a property under construction, and not want to change it later. This is exactly the case for a coffee machine: the power cannot vary. To do this, we can simply define a getter, and no setter:
class CoffeeMachine {
// …
constructor(power) {
this._power = power;
}
get power() {
return this._power;
}
}
// creiamo la macchina del caffè
let coffeeMachine = new CoffeeMachine(100);
alert(`Power is: ${coffeeMachine.power}W`); // Power is: 100W
coffeeMachine.power = 25; // Errore (nessun setter)
PROTECTED FIELDS ARE INHERITED
If we inherit class MegaMachine extends CoffeeMachine, then nothing will prevent us from accessing this._waterAmount or this._power from the methods in the new class. Hence, the protected methods are inherited. Unlike the private ones, which we will see shortly.
PRIVATE FIELDS
Private fields should be preceded by #. These will only be accessible from within the classroom. For example, here we have a private property #waterLimit and a private method for controlling the water level #fixWaterAmount:
class CoffeeMachine {
#waterLimit = 200;
#fixWaterAmount(value) {
if (value < 0) return 0;
if (value > this.#waterLimit) return this.#waterLimit;
}
setWaterAmount(value) {
this.#waterLimit = this.#fixWaterAmount(value);
}
}
let coffeeMachine = new CoffeeMachine();
// non possiamo accedere ai metodi privati dall’esterno della classe
coffeeMachine.#fixWaterAmount(123); // Errore
coffeeMachine.#waterLimit = 1000; // Errore
Language-wise, # is a special character to indicate that that field is private. We cannot therefore access it from the outside or from a sub-class. Furthermore, private fields do not conflict with public ones. We can have both a private #waterAmount and a public waterAmount field. For example, let’s make waterAmount a property to access #waterAmount:
class CoffeeMachine {
#waterAmount = 0;
get waterAmount() {
return this.#waterAmount;
}
set waterAmount(value) {
if (value < 0) value = 0;
this.#waterAmount = value;
}
}
let machine = new CoffeeMachine();
machine.waterAmount = 100;
alert(machine.#waterAmount); // Errore
Unlike protected fields, private fields are provided by the language itself. And that’s a good thing. In case we were inheriting from CoffeeMachine then we would not have direct access to #waterAmount. We should rely on the waterAmount getter / setter. In many cases, such a limitation is too severe. If we extend a CoffeeMachine, we may rightfully want to access its internal fields. This is why protected fields are used more often, even if they are not really supported by the language syntax.
DEEPENING AI
Encapsulation in JavaScript is a fundamental concept in object-oriented programming (OOP) of restricting direct access to certain properties or methods of an object so that they can be modified only through a controlled interface. The main purpose is to ensure that an object’s internal data is protected from accidental or unintentional modification, thereby improving the security and maintainability of the code.
How to implement encapsulation in JavaScript.
1. Private variables and functions: Prior to ECMAScript 2015 (ES6), JavaScript did not have a native way to create private variables. However, encapsulation can be achieved using closures and constructor functions.
Example with closures:
function Persona(nome) {
let _nome = nome; // Variabile privata
this.getNome = function() {
return _nome;
};
this.setNome = function(nuovoNome) {
_nome = nuovoNome;
};
}
const persona = new Persona(“Mario“);
console.log(persona.getNome()); // Mario
persona.setNome(“Luigi“);
console.log(persona.getNome()); // Luigi
In this example, the variable _nome is not directly accessible from the outside, but it can be read or modified via the public methods getNome and setNome. This type of encapsulation is achieved by the fact that Persona’s internal functions have access to the _nome variable, but it is not directly exposed.
2. Classes with ECMAScript 6 (ES6): With the introduction of classes in ES6, we can simulate encapsulation by using weakly private variables (using a convention such as the _ prefix to indicate that a property is private).
Example with classes (simulation):
class Persona {
constructor(nome) {
this._nome = nome; // Convenzione per variabili “private”
}
getNome() {
return this._nome;
}
setNome(nuovoNome) {
this._nome = nuovoNome;
}
}
const persona = new Persona(“Mario“);
console.log(persona.getNome()); // Mario
persona.setNome(“Luigi“);
console.log(persona.getNome()); // Luigi
In this case, the variable _nome is treated as “private” by convention, even though it is technically accessible from the outside.
3. Private properties with ECMAScript 2019 (ES10): As of ECMAScript 2019, JavaScript has introduced native support for private properties using the # prefix.
Example with private properties:
class Persona {
#nome; // Variabile privata
constructor(nome) {
this.#nome = nome;
}
getNome() {
return this.#nome;
}
setNome(nuovoNome) {
this.#nome = nuovoNome;
}
}
const persona = new Persona(“Mario“);
console.log(persona.getNome()); // Mario
persona.setNome(“Luigi“);
console.log(persona.getNome()); // Luigi
// console.log(persona.#nome); // Errore: proprietà privata non accessibile
In this example, the #nome property is completely private and cannot be accessed or modified from the outside except through the defined public methods.
Advantages of encapsulation
1. Data protection: Reduces the risk of unintended or unwanted changes to the object’s internal data.
2. Modularity: Each object manages its own data and behaviors independently, making it easier to understand and maintain the code.
3. Flexibility: You can change the internal implementation of an object without changing the external code that uses it, as long as the public interface remains the same.
Examples of practical use
Encapsulation is often used to:
-Limit access to sensitive data.
-Prevent direct access to implementation details that should not be exposed.
-Create a stable interface for objects that can be used by developers without knowing the internal details.
Conclusion
Encapsulation in JavaScript is a fundamental concept for creating robust and modular code. With closure techniques, classes introduced in ES6, and native private properties as of ES10, encapsulation can be implemented in a variety of ways, depending on the needs of the project and compatibility with versions of JavaScript.
Leave A Comment