Ir al contenido
wannacode

Encapsulamiento

En resumen: El encapsulamiento significa que los datos de un objeto se cambian solo a través de sus métodos: en JavaScript, con campos privados # y getters y setters con get y set.

Problema

Menos 500 €

En la lección anterior pusimos la comprobación del retiro en un método. Pero el campo está abierto, y cualquiera puede escribir en él directamente:

const account = new Account("María", 100);
account.withdraw(500);
account.balance = -500;
console.log(account.balance);
Salida
-500

El método se negó con razón a retirar 500 €. La línea siguiente simplemente fijó el saldo a mano. La comprobación existe, pero se puede saltar. Necesitamos una forma de decir «solo la cuenta toca el saldo».

La idea

#balance: solo la clase lo toca

Pon # delante del nombre del campo: #balance. El campo tiene que declararse en la clase, y dentro escribes this.#balance:

class Account {
#owner;
#balance;
constructor(owner, balance) {
this.#owner = owner;
this.#balance = balance;
}
// ...
}

Un campo con # es privado: solo el código de dentro de la clase Account puede llegar a él. Si fuera hay una línea como

account.#balance = -500;

el programa ni siquiera arranca:

Error
SyntaxError: Private field '#balance' must be declared in an enclosing class

La clase oculta sus datos y los muestra solo a través de sus propios métodos. Esto se llama encapsulamiento. Java y C# tienen palabras clave para ello, los modificadores de acceso (private, public). En JavaScript, lo privado es #, y todo lo que no lleva # es público.

Más precisamente, para curiosos
  • Los métodos también pueden ser privados: #check() { … } se llama solo desde la clase, con this.#check().
  • Cuidado con el nombre. account.balance = -500 sin # no toca el campo privado: crea en el objeto un campo balance nuevo y público, sin ningún error.
  • Antes de que existiera # (hasta 2022), JavaScript usaba la convención _balance, como Python. La verás en código antiguo.

La idea

Leer sin tocar

Desde fuera, ahora no podemos ni siquiera leer el saldo. Así que la clase ofrece un método que solo lo devuelve:

bank.js
class Account {
#owner;
#balance;
constructor(owner, balance) {
this.#owner = owner;
this.#balance = balance;
}
get owner() {
return this.#owner;
}
get balance() {
return this.#balance;
}
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
}
}
withdraw(amount) {
if (amount > 0 && amount <= this.#balance) {
this.#balance -= amount;
return true;
}
return false;
}
}
const account = new Account("María", 100);
account.withdraw(30);
account.deposit(50);
console.log(`${account.owner}: ${account.balance} €`);
Salida
María: 120 €

get balance() es un getter: un método que desde fuera lees como un campo, account.balance, sin paréntesis. Devuelve el valor del privado #balance. El saldo cambia solo a través de deposit y withdraw, y ellos hacen las comprobaciones.

No hay setter, así que la línea account.balance = -500; no cambia nada: JavaScript ignora la escritura, sin ningún error. En modo estricto (la línea "use strict"; al principio del archivo), la misma línea detiene el programa:

Error
TypeError: Cannot set property balance of #<Account> which has only a getter

Ahora withdraw devuelve true o false: si el retiro salió bien. Así quien retira sabe si funcionó.

const account = new Account("María", 100);
account.withdraw(500);
account.deposit(50);
console.log(account.balance);
Paso a paso, en palabras
  1. #balance es privado: el cristal significa que desde fuera no hay camino hacia el campo.
  2. El retiro pasa por la comprobación del método: 500 es más que 100. Rechazado, y el saldo se queda como está.
  3. El depósito también se comprueba: 50 es un importe positivo. El método lo suma.
  4. El getter balance entrega una copia del saldo. El campo #balance en sí sigue oculto.

Pruébalo

Rechazado

¿Qué imprimirá el programa?

const account = new Account("María", 100);
console.log(account.withdraw(150));
account.deposit(-20);
console.log(account.balance);
Respuesta

La idea

Setter: un cambio con comprobación

A veces el código de fuera necesita cambiar un campo, pero no a cualquier valor. La edad de un animal puede cambiar, pero no puede ser negativa:

class Animal {
#age = 0;
get age() {
return this.#age;
}
set age(value) {
if (value >= 0) {
this.#age = value;
}
}
}
const cat = new Animal();
cat.age = 4;
cat.age = -3;
console.log(cat.age);
Salida
4

set age(value) es un setter: el método que se llama con cat.age = -3. value es el valor a la derecha de =, aquí -3. La comprobación falla y la edad se queda en 4. Desde fuera, la escritura parece la de un campo normal, pero pasa por la comprobación.

La idea

¿Qué campo lleva setter?

No todos los campos necesitan un setter, y algunos ni siquiera un getter. Piensa en lo que el código de fuera debería poder hacer con el producto:

  • el nombre y el precio se fijan al crear el producto y después solo se leen: solo getter;
  • la cantidad en el carrito cambia, pero es como mínimo 1: un getter y un setter con comprobación.
class Product {
#name;
#price;
#quantity = 1;
constructor(name, price) {
this.#name = name;
this.#price = price;
}
get name() {
return this.#name;
}
get price() {
return this.#price;
}
get quantity() {
return this.#quantity;
}
set quantity(value) {
if (value >= 1) {
this.#quantity = value;
}
}
}
const coffee = new Product("Café", 7.50);
coffee.quantity = 3;
coffee.quantity = 0;
console.log(`${coffee.name} x${coffee.quantity}`);
Salida
Café x3

La regla es: empieza con # para todo y abre solo lo que necesitas. Un setter que guarda cualquier cosa sin comprobar trae de vuelta el problema de la primera tarjeta.

Pruébalo

¿Qué falta?

El saldo solo debe verse dentro de la clase. ¿Cómo se llama el campo?
class Account {
   = 0;
}

Pruébalo

Encuentra el error

El campo es #balance, y aun así el saldo pasa a ser -500:

const account = new Account();
account.balance = 100;
account.withdraw(30);
account.balance = -500;
console.log(account.balance);
Salida
-500

¿Qué línea de la clase rompe la protección?

Elige la línea con el error.

Ejercicio

Datos ocultos

Resuelve las tareas en tu editor. Todos los campos son privados, con #.

1. Persona. Person con un nombre y una edad, un constructor, los getters name y age y introduce().

Salida
María tiene 34 años
Hola, soy María y tengo 34 años

2. Una cuenta sin números rojos. Account con un titular y un saldo. Un depósito acepta solo un importe positivo. Un retiro devuelve si salió bien y nunca deja que el saldo baje de 0. El código de fuera puede leer el saldo, pero no escribirlo.

El programa
const account = new Account("Pablo", 50);
account.deposit(25);
account.deposit(-10);
console.log(account.withdraw(100));
console.log(account.withdraw(40));
console.log(`Saldo: ${account.balance} €`);
Salida
false
true
Saldo: 35 €

3. Animal. Animal con una especie y una edad. La especie se fija en el constructor y después solo se lee. La edad tiene un setter (set age) que rechaza un número negativo e imprime un mensaje. El constructor también pasa por la misma comprobación: en el constructor escribe this.age = age;, sin #.

El programa
const cat = new Animal("gato", 3);
cat.age = 4;
cat.age = -1;
console.log(`${cat.species}, ${cat.age} años`);
Salida
Edad no válida: -1
gato, 4 años

Más tareas

  • Nota. Una clase Grade con el nombre de una asignatura y una nota de 2 a 6. El setter rechaza todo lo que está fuera de esos límites. Pruébalo con 5, 7 y 1.
  • Producto con descuento. Añade al Product de la lección un método que aplica un descuento en porcentaje y acepta solo del 1 al 90 %. El precio no tiene setter.

Test

Ponte a prueba

1. El campo es #balance. ¿Desde dónde se puede cambiar?
2. ¿Por qué la cuenta no tiene un setter para el saldo?
3. En set age(value), ¿qué es value?
4. ¿Qué significa encapsulamiento?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·