Aprende leyendo en orden

Getters y setters — ejecutar código al leer o asignar una propiedad

Los getters y setters ejecutan código al leer o asignar una propiedad. Propiedades calculadas, validación de valores asignados, pasar por el setter desde el constructor y propiedades de solo lectura.

Supón que la página de inscripción a un seminario muestra los lugares disponibles como «capacidad − inscripciones». Si calculas los lugares disponibles al crear la instancia y guardas el resultado, lo que se muestra no cambia cuando se inscribe más gente. Y si alguien asigna una capacidad menor que el número de inscripciones, nada lo impide.

Este artículo trata los getters y setters, que ejecutan código cuando se lee o se asigna una propiedad.

Recalcular en cada lectura — get y propiedades calculadas

Si calculas los lugares disponibles como «capacidad − inscripciones» en el constructor y guardas el resultado, se quedan con el valor del momento de la creación aunque después cambies el número de inscripciones. Si lo conviertes en un método, puedes recalcularlo, pero entonces quien lo usa tiene que acordarse de no poner paréntesis para la capacidad y sí ponerlos para los lugares disponibles.

Un getter (un método de una clase con get delante, que se ejecuta cuando se lee la propiedad) se escribe sin parámetros, como get remainingSeats() { ... }, y al leer la propiedad obtienes lo que devuelve con return.

Una propiedad como remainingSeats, que se calcula a partir de otras propiedades, se llama propiedad calculada.

class Seminar {
  reserved = 0;                  // Número de inscripciones

  constructor(capacity) {
    this.capacity = capacity;    // Capacidad
  }

  // Se calcula con la capacidad y las inscripciones del momento, cada vez que se lee
  get remainingSeats() {
    return this.capacity - this.reserved;
  }
}

const seminar = new Seminar(30);
console.log(seminar.remainingSeats);   // 30 (se lee sin paréntesis)

// Leerlo otra vez después de que se inscriben 12 personas
seminar.reserved = 12;
console.log(seminar.remainingSeats);   // 18
¿Guardar los lugares disponibles o calcularlos en cada lectura?
Guardar lugares enel constructorGuarda 30 - 0al crearseAsignar 12a reservedAl leerlosigue en 30Definir lugarescon getNo guarda nadaal crearseAsignar 12a reservedAl leer, 30 - 12da 18
Las dos filas hacen la misma asignación antes de leer. Un getter calcula con el valor que tiene reserved en el momento de leer.

El 30 de la fila de arriba se calculó una sola vez, en el new, y se guardó. El getter de la fila de abajo no guarda nada y ejecuta la expresión del return cada vez que lees seminar.remainingSeats, así que, por muchas veces que cambies reserved, no necesitas agregar una línea que recalcule los lugares disponibles.

Llamar a un getter con paréntesis lanza un TypeError

Un getter se escribe como un método, pero al usarlo no se ponen paréntesis. Si escribes seminar.remainingSeats(), estás llamando como función al 18 que se devolvió, y eso lanza TypeError: seminar.remainingSeats is not a function.

Un menú sigue mostrando los precios con impuestos anteriores después de una subida de precios. Corrígelo sin cambiar el código que lee los precios. MenuItem y menu ya están declarados.

① Reemplaza priceWithTax, que se calcula en el constructor, por una propiedad que se calcule cada vez que se lee.

② Sube 40 yenes el precio sin impuestos de todos los productos.

③ Muestra «Nombre: X yenes con impuestos» para cada producto.

④ Muestra «Impuesto: X yenes» para el primer producto.

(Si se ejecuta correctamente, aparecerá una explicación.)

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

Comprobar los valores asignados — set y validación

Cuando se acerca la fecha del seminario, puede que la sede te permita subir la capacidad de 30 a 40. Podrías escribir una comprobación con if y throw en el constructor, pero solo se ejecuta en el new. Si después asignas seminar.capacity = 5, un valor menor que las 12 inscripciones, nada lo impide y los lugares disponibles aparecen como -7.

Un setter (un método de una clase con set delante, que se ejecuta cuando se asigna la propiedad) recibe el valor asignado en su parámetro value. El valor en sí lo guardas con otro nombre, como _capacity, y lo devuelves desde un getter con el nombre de la propiedad, get capacity.

El _ al principio es una convención que significa «no lo toques directamente desde afuera».

class Seminar {
  constructor(capacity, reserved) {
    this.reserved = reserved;     // Número de inscripciones
    this._capacity = capacity;    // El valor de la capacidad en sí va en _capacity
  }

  get capacity() { return this._capacity; }

  set capacity(value) {           // El valor asignado llega en value
    if (value < this.reserved) {
      throw new Error(`La capacidad no puede ser menor que las ${this.reserved} inscripciones: ${value}`);
    }
    this._capacity = value;
  }
}

const seminar = new Seminar(30, 12);
seminar.capacity = 40;             // Se ejecuta set capacity y guarda 40
console.log(seminar.capacity);     // 40 (se ejecuta get capacity)
seminar.capacity = 5;              // Error: La capacidad no puede ser menor que las 12 inscripciones: 5
Qué método se ejecuta al asignar y al leer
get capacityset capacity(value)① seminar.capacity= 40② Leerseminar.capacity③ seminar.capacity= 5Se ejecuta set:40 es 12 o másSe ejecuta get ydevuelve _capacitySe ejecuta set:5 es menor que 12_capacitypasa a 40Devuelve 40Lanza el error;sigue en 40
Cuando la propiedad está a la izquierda de =, se ejecuta set; cuando se lee en cualquier otro lugar, se ejecuta get. Un valor que no pasa la comprobación nunca llega a _capacity.

La línea ③ lanza un error, así que, si no la envuelves en try, las líneas siguientes no se ejecutan. Mientras asignes a capacity, el valor pasa por el setter, pero si escribes directamente seminar._capacity = 5, se salta la comprobación. En el siguiente artículo verás cómo evitarlo.

Asignar al mismo nombre dentro de un setter lanza un RangeError

Si escribes this.capacity = value; dentro de set capacity(value), esa asignación vuelve a llamar al setter, que vuelve a asignar, y así sucesivamente hasta lanzar RangeError: Maximum call stack size exceeded. Guarda el valor con otro nombre, como _capacity.

Vas a aplicar a la temperatura configurada lo que se ingresa en el panel de control de un aire acondicionado. AirConditioner y temperatureInputs ya están declarados.

① Agrega una propiedad temperature que lea y escriba la temperatura configurada y que lance la excepción «La temperatura debe estar entre 16 y 30 grados: X» si el valor está fuera de rango.

② Asigna las entradas en orden y muestra «Temperatura: X grados» si se aplica, o el mensaje de la excepción si no.

③ Muestra «Nombre del ambiente: funcionando a X grados» con la temperatura final.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

Pasar por el setter también al crear — asignar en el constructor

En el Seminar de arriba, el constructor guarda el valor directamente en _capacity, así que new Seminar(5, 12) crea una instancia sin ninguna comprobación. Si escribes el mismo if también en el constructor, tienes que corregir dos lugares cada vez que cambia la condición, y si olvidas uno, new y la asignación dan resultados distintos.

Si dentro del constructor asignas al nombre sin _, como en this.capacity = capacity, el setter también se ejecuta en esa línea. La comprobación queda en un solo lugar, el setter, y tanto los valores que pasas a new como los que asignas después se comprueban con la misma condición.

class Seminar {
  constructor(capacity, reserved) {
    this.reserved = reserved;     // Primero guarda las inscripciones que usa la comprobación
    this.capacity = capacity;     // El nombre no tiene _, así que se ejecuta set capacity
  }

  get capacity() { return this._capacity; }
  set capacity(value) {
    if (value < this.reserved) {
      throw new Error(`La capacidad no puede ser menor que las ${this.reserved} inscripciones: ${value}`);
    }
    this._capacity = value;
  }
}

try {
  new Seminar(5, 12);             // new también pasa por la comprobación del setter
} catch (error) {
  console.log(error.message);     // La capacidad no puede ser menor que las 12 inscripciones: 5
}
Asignar en el constructor llama al setter
Cuerpo del try (aquí se llama a new)
  • new Seminar(5, 12) — llama al constructor
  • La excepción del setter sube hasta aquí y no se devuelve ninguna instancia
Cuerpo del constructor llamado
  • this.reserved = 12 — guarda las inscripciones antes de la comprobación
  • this.capacity = 5 — el nombre no tiene _, así que llama a set capacity
Cuerpo del set capacity(value) llamado
  • value es 5 y this.reserved es 12
  • 5 < 12, así que hace throw — no entra nada en _capacity
La línea this.capacity = 5 llama a set capacity. La excepción sale del constructor y llega al try.

Si mueves this.reserved = reserved debajo de this.capacity = capacity, this.reserved es undefined cuando se ejecuta el setter. En una comparación numérica, undefined se convierte en NaN, y 5 < NaN es false, así que la instancia se crea con una capacidad de 5.

Vas a registrar el nombre completo de los empleados, separado en nombre y apellido. staffNames ya está declarado.

① Define Employee, que recibe un nombre completo y guarda el nombre y el apellido por separado.

② Agrega fullName, que lee y escribe «Nombre Apellido» y lanza la excepción «Separa el nombre y el apellido con un espacio: X» si no hay espacio.

③ Crea los empleados uno por uno y muestra el mensaje de la excepción de los que no se puedan crear.

④ Asigna «Ana López» al fullName del primer empleado y luego muestra el nombre, el apellido y el nombre completo de todos.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

Impedir la asignación — propiedades con solo un getter

Los lugares disponibles dependen de la capacidad y de las inscripciones. Si el código de la página puede escribir seminar.remainingSeats = 100, la página muestra 100 lugares disponibles con una capacidad de 30 y 12 inscripciones, y los tres valores dejan de cuadrar.

Una propiedad que tiene solo un getter, sin un setter con el mismo nombre, es de solo lectura (se puede leer, pero no asignar). En el cuerpo de una clase y en modo estricto (un modo que convierte algunos errores en excepciones), que es el que usa este entorno, la línea que le asigna un valor lanza un TypeError. Fuera de esos casos, la asignación se ignora sin avisar.

class Seminar {
  constructor(capacity, reserved) {
    this.capacity = capacity;
    this.reserved = reserved;
  }

  get remainingSeats() { return this.capacity - this.reserved; }   // Sin setter
}

const seminar = new Seminar(30, 12);
try {
  seminar.remainingSeats = 100;          // Asignar a un nombre que no tiene set
} catch (error) {
  console.log(error.name);               // TypeError
}
console.log(seminar.remainingSeats);     // 18 (igual que antes de la asignación)

// Para cambiar los lugares disponibles, cambia las inscripciones de las que dependen
seminar.reserved = 20;
console.log(seminar.remainingSeats);     // 10
¿Se acepta o se rechaza una asignación a los lugares disponibles?
Guardar lugarescomo un valorremainingSeats= 100Entra 100tal cualNo cuadra con30 lugares y 12Definir lugaressolo con getremainingSeats= 100No hay set:TypeErrorSigue en30 - 12 = 18
En la fila de arriba, la asignación se acepta y queda un valor que no cuadra con la capacidad ni con las inscripciones. Con solo un getter, la asignación se rechaza en el acto.

La fila de abajo lanza un error porque un nombre con solo un getter no tiene dónde guardar el valor asignado, y tampoco se crea una clave nueva en la instancia. La tabla de abajo resume las formas que usamos en el Seminar de este artículo, junto con un método para comparar.

FormaCómo se usaSirve para
Propiedad normal (reserved)seminar.reserved = 20 la guarda tal cualValores sin comprobar
Solo getter (remainingSeats)Solo lectura: seminar.remainingSeatsValores calculados de otros
Getter y setter (capacity)seminar.capacity = 40 se compruebaValores a comprobar al asignar
Método (p. ej., reserve(count))Con paréntesis y argumentosResultados según los argumentos
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1Con un getter que devuelve 30 - this.reserved, ¿qué obtienes si cambias reserved a 20 y luego lees el getter?

Pregunta 2Si set capacity contiene this.capacity = value; y asignas 40, ¿qué pasa?

Pregunta 3¿Qué pasa si asignas 100 a seminar.remainingSeats, que solo tiene un getter?