Aprende leyendo en orden

Fundamentos de class — definir una clase y crear instancias

Cómo usar class para crear muchos objetos con la misma forma: instancias con constructor y new, métodos que leen this y cuándo basta un objeto literal.

Socios, productos, cupones: los datos que tienen los mismos campos y se usan en los mismos cálculos aparecen una y otra vez en una aplicación. Si escribes cada uno como un objeto literal, terminas copiando los campos y la fórmula en cada elemento.

En este artículo verás class, que reúne en un solo lugar la forma de los datos y el código que trabaja con ellos, y cómo crear objetos a partir de ella, uno por uno, con new.

Definir la forma de los datos en un solo lugar — class y new

Imagina que manejas dos tipos de cupones de descuento. Si escribes { code: "WELCOME", discount: 500 } una vez por cupón y en uno solo te equivocas y pones discont, no se lanza ningún error: simplemente el descuento de ese cupón queda en undefined. Como los nombres de las claves se repiten en cada cupón, te toca revisarlos todos para encontrar la errata.

Una clase (la sintaxis class, que define un tipo de objeto que se crea con new) se escribe como class Coupon { ... }, y dentro va un constructor (la parte a la que llama new), sin la palabra clave function.

Dentro del constructor, this es la nueva instancia (un objeto creado a partir de una clase), y new la devuelve aunque no haya return.

// Define en un solo lugar la forma de un cupón con una clase
class Coupon {
  constructor(code, discount) {
    this.code = code;   // Izquierda: el code de la instancia. Derecha: el argumento recibido
    this.discount = discount;
  }
}

// Crea los cupones uno por uno con new
const welcome = new Coupon("WELCOME", 500);
const summer = new Coupon("SUMMER", 1000);
console.log(welcome.code);                // WELCOME
console.log(summer.discount);             // 1000
console.log(welcome instanceof Coupon);   // true

// Si la llamas sin new, se lanza un error
// Coupon("SPRING", 300);                 // TypeError
Qué pasa con new y sin new
Definición declass Couponnew Coupon("WELCOME", 500)new Coupon("SUMMER", 1000)Coupon("SPRING", 300)Guarda "WELCOME"en this.codeGuarda "SUMMER"en this.codeSin new: no entraal constructorwelcome recibela instanciasummer recibeotra instanciaLanza unTypeErrorpasapasa
Los argumentos se escriben en this dentro del constructor. Sin new, la llamada lanza un error antes de entrar al constructor.

Los nombres de las claves aparecen en un solo lugar, dentro del constructor, así que una errata como discont solo puede estar ahí y solo hay un lugar que corregir. En el caso de la columna derecha del diagrama, la consola muestra TypeError: Class constructor Coupon cannot be invoked without 'new'.

Crea datos de productos con una clase. productName y productPrice ya están declaradas.

① Define Product para que cada instancia guarde un nombre de producto y un precio.

② Pasa las dos variables para crear mouse.

③ Muestra mouse con el formato «Nombre: precio yenes».

④ Llama a Product sin new dentro de un bloque try y muestra el name del error.

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

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

Calcular con los valores de una instancia — métodos y this

Si calculas el importe con descuento con total - welcome.discount tanto en la pantalla del carrito como en la de confirmación del pedido, cada vez que cambie la regla de precios tendrás dos lugares que corregir. Aunque lo muevas a una función, los campos siguen en la clase y el cálculo queda fuera de ella.

Un método (una función que pertenece a un objeto y se llama con un punto, como welcome.apply(3000)) se escribe dentro de las llaves de la clase como apply(total) { ... }, también sin function. Dentro de un método, this es la instancia escrita a la izquierda del punto, y this.discount lee su valor en el momento de la llamada.

class Coupon {
  constructor(code, discount) {
    this.code = code;
    this.discount = discount;
  }

  // Resta del total el descuento de este cupón y devuelve el resultado
  apply(total) {
    return total - this.discount;   // this es la instancia a la izquierda del punto
  }
}

// Crea dos tipos de cupones
const welcome = new Coupon("WELCOME", 500);
const summer = new Coupon("SUMMER", 1000);

// El mismo apply da otro resultado según la instancia a la izquierda del punto
console.log(welcome.apply(3000));   // 2500
console.log(summer.apply(3000));    // 2000
Lo que va a la izquierda del punto se convierte en this
welcome.apply(3000)this eswelcomethis.discountvale 500Devuelve 2500summer.apply(3000)this essummerthis.discountvale 1000Devuelve 2000
this es la variable escrita a la izquierda de apply. Con el mismo argumento, un this distinto da un resultado distinto.
Dónde existen los argumentos del constructor
Dentro de class Coupon
Dentro de constructor(code, discount)
  • Argumentos code / discount: solo se usan dentro de estas llaves
  • this.discount = discount: guarda el valor en la instancia
Dentro de apply(total)
  • this.discount: lee el valor guardado en la instancia
  • total - discount: el nombre no es visible, ReferenceError
El nombre discount no es visible fuera de las llaves del constructor. apply lee, en cambio, el valor guardado en this.

Si olvidas this. dentro de apply y escribes total - discount, JavaScript busca el nombre discount empezando por las llaves más internas y avanzando hacia afuera. Por eso el resultado depende de si existe fuera de la clase una variable con el mismo nombre.

Olvidar this. no siempre lanza un error

Si fuera de la clase hay una variable con el mismo nombre, como const discount = 300;, el total - discount sin this. no lanza un ReferenceError: usa ese 300 sin avisar. Si todos los cupones devuelven el mismo importe sin importar con cuál llames al método, sospecha que falta un this..

Calcula el precio con impuestos de un teclado con un método de Product. Product y keyboard ya están declarados.

① Agrega a Product un método taxIncluded que devuelva el precio con impuestos (impuesto del 10 %, redondeado hacia abajo).

② Muestra el nombre de keyboard y su precio con impuestos con el formato «Nombre: N yenes con impuestos».

③ Cambia el precio de keyboard a 9800.

④ Vuelve a mostrarlo con el mismo formato que en ②.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

Cada instancia guarda sus propios valores — varias instancias

Cada vez que se usa un cupón, la cantidad de usos que le quedan baja en uno. Si guardas esas cantidades fuera de la clase, en un objeto como { WELCOME: 3, SUMMER: 3 }, cada pantalla que use cupones necesita su propio código para buscar la cantidad por code y volver a escribir el nuevo valor.

Dentro de un método también puedes asignar a una propiedad de this, como en this.remaining = this.remaining - 1. El lado derecho lee la cantidad actual y el resultado, uno menos, se escribe de nuevo en el remaining de la misma instancia. Así, la cantidad empieza en el 3 que pasaste a new y baja en uno con cada llamada.

class Coupon {
  constructor(code, remaining) {
    this.code = code;
    this.remaining = remaining;   // Cuántos usos quedan
  }

  // Resta uno a la cantidad de la instancia a la izquierda del punto
  use() {
    this.remaining = this.remaining - 1;
  }
}

// Crea dos cupones con la misma cantidad y úsalos un número distinto de veces
const welcome = new Coupon("WELCOME", 3);
const summer = new Coupon("SUMMER", 3);
welcome.use();
welcome.use();
summer.use();
console.log(welcome.remaining);   // 1
console.log(summer.remaining);    // 2
use() solo resta en la instancia a la izquierda del punto
welcome.use()dos vecesthis eswelcomeremaining pasade 3 → 2 → 1summer nocambiasummer.use()una vezthis essummerremaining pasade 3 → 2welcome nocambia
El use() de welcome y el de summer nunca tocan la cantidad del otro. Solo baja la cantidad de la instancia escrita a la izquierda de use().

Las instancias se pueden guardar tanto en variables como en los elementos de un array. Si llamas a un método sobre un elemento, como en coupons[0].use(), this es ese elemento. Tengas los cupones que tengas, el código que baja la cantidad es la única línea que hay dentro de use().

Administra el stock de tres productos de un almacén con una instancia por producto. Product, con un método ship que descuenta el stock, ya está declarado.

① Crea instancias para 12 brazos para monitor, 30 hubs USB y 8 soportes para laptop, y ponlas en un array.

② Despacha 5 unidades solo del primer producto.

③ Muestra «Nombre: N en stock» para cada producto, uno por línea.

④ Muestra el stock total con el formato «Total: N».

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

Elegir según cuántos necesitas — class frente a objeto literal

Para datos que usas una sola vez, como la configuración de una pantalla, basta con escribir { ... } en el momento. Pero con datos como los cupones, donde creas muchos elementos con la misma forma y el mismo cálculo, escribir literales te obliga a copiar hasta el cuerpo del cálculo en cada uno.

Un objeto literal también puede tener métodos: escribe apply(total) { ... } junto a los pares clave-valor, y el this de adentro es ese objeto. Lo que cambia respecto a una clase es dónde se escribe el cuerpo del método, y esa diferencia aparece en el resultado de Object.keys.

// class: el cuerpo se escribe una sola vez, dentro de la definición
class Coupon {
  constructor(code, discount) {
    this.code = code;
    this.discount = discount;
  }
  apply(total) { return total - this.discount; }
}
const welcome = new Coupon("WELCOME", 500);

// Literal: el cuerpo se escribe completo en cada elemento
const summer = {
  code: "SUMMER",
  discount: 1000,
  apply(total) { return total - this.discount; },
};

// Tienen claves distintas
console.log(Object.keys(welcome).join(", "));   // code, discount
console.log(Object.keys(summer).join(", "));    // code, discount, apply

El cuerpo está en un lugar distinto, pero welcome.apply(3000) se llama exactamente igual que summer.apply(3000), así que el código que llama no cambia. Cómo puede una instancia llamar a un método cuyo cuerpo está en la clase lo verás en el artículo sobre prototipos. La tabla de abajo compara las dos formas.

AspectoObjeto literalclass
Objetos creadosSolo el { } que escribesUno por cada new
Cuerpo del métodoCopiado en cada unoUna vez en la clase
Claves en Object.keyscode, discount, applycode, discount
instanceof Couponfalsetrue
Ideal paraAjustes o datos de un solo usoMuchos con la misma lógica

Sin comas después de los métodos de una clase

Al convertir un literal en una clase, si dejas la coma después de un método, como en apply(total) { ... },, obtienes SyntaxError: Unexpected token ',' y no se ejecuta ni una línea. Dentro de las llaves de una clase, los métodos van uno tras otro, sin separador.

QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1Después de class Coupon {}, ¿qué pasa si llamas a Coupon("SPRING") sin new?

Pregunta 2Fuera de la clase hay una variable discount que vale 300. Si falta this., ¿qué devuelve welcome.apply(3000)?

Pregunta 3¿Qué devuelve Object.keys para una instancia de una clase con constructor(code, discount) y apply?