Lección 6 de 8. JavaScript tiene clases desde 2015, pero por debajo siguen funcionando los prototipos de siempre. Esta lección enseña las dos cosas, la herencia, los campos privados y this, que no funciona como en Java ni como en Python.
Antes de las clases: los prototipos
JavaScript no se diseñó con clases sino con prototipos: cada objeto tiene un enlace oculto a otro objeto, su prototipo, y cuando le pides una propiedad que no tiene, la busca allí. Las clases que se ven después son una forma más cómoda de escribir lo mismo, así que conviene ver primero lo que hay debajo:
const animal = {
respirar() {
return `${this.nombre} respira`;
},
};
const perro = Object.create(animal); // perro tiene animal como prototipo
perro.nombre = "Toby";
perro.ladrar = function () {
return "¡guau!";
};
console.log(perro.ladrar(), "|", perro.respirar());
console.log(Object.getPrototypeOf(perro) === animal);
console.log(Object.hasOwn(perro, "respirar"), "respirar" in perro);salida¡guau! | Toby respira true false true
perro no tiene respirar, pero lo encuentra en su prototipo. Object.hasOwn lo confirma: la propiedad no es suya, aunque in, que también mira en la cadena, diga que sí. Y fíjate en que dentro de respirar, this es perro, no animal: this es el objeto con el que se llamó al método, se encuentre donde se encuentre.
Clases
Una clase reúne en un solo bloque el constructor, que prepara cada objeto nuevo, y los métodos, que comparten todos:
class Producto {
#stock = 0; // campo privado
static iva = 0.16; // propiedad de la clase, no de cada objeto
constructor(nombre, precio) {
this.nombre = nombre;
this.precio = precio;
}
get precioConIva() {
return +(this.precio * (1 + Producto.iva)).toFixed(2);
}
reponer(n) {
this.#stock += n;
return this; // permite encadenar
}
describir() {
return `${this.nombre}: ${this.precio} USD, ${this.#stock} en stock`;
}
}const cafe = new Producto("café", 7.5).reponer(10).reponer(2);
const miel = new Producto("miel", 4.9);
console.log(cafe.describir());
console.log(miel.describir());
console.log(cafe.precioConIva, Producto.iva);
console.log(typeof Producto, cafe instanceof Producto);
console.log(cafe.describir === miel.describir);salidacafé: 7.5 USD, 12 en stock miel: 4.9 USD, 0 en stock 8.7 0.16 function true true
| En la clase | Qué es | Se usa como |
|---|---|---|
constructor(...) | se ejecuta con cada new | new Producto("café", 7.5) |
describir() { } | método, uno solo para todos los objetos | cafe.describir() |
get precioConIva() { } | se lee como una propiedad, pero se calcula | cafe.precioConIva, sin paréntesis |
static iva | pertenece a la clase, no a los objetos | Producto.iva |
#stock | campo privado | solo desde dentro de la clase |
La penúltima línea de la salida enseña lo que hay debajo: una clase es una función (typeof da "function"), y la última, que describir es la misma para los dos objetos. No se copia en cada uno: vive una sola vez en Producto.prototype, el prototipo de todos los productos.
Campos privados de verdad
Durante años, la convención para decir «esto es privado» era empezar el nombre por guion bajo, _stock, y confiar en que nadie lo tocara. Con # el lenguaje lo impide. Ni siquiera llega a ejecutarse:
const cafe = new Producto("café", 7.5);
console.log(cafe.#stock);salidaSyntaxError: Private field '#stock' must be declared in an enclosing class
Es un error de sintaxis, así que el archivo entero se rechaza al cargarlo. Y desde fuera, cafe["#stock"] no da error, pero devuelve undefined: es otra propiedad, una normal que se llama así.
Getters y setters que validan
class Temperatura {
#celsius = 0;
get celsius() {
return this.#celsius;
}
set celsius(valor) {
if (valor < -273.15) throw new RangeError(`${valor} °C está por debajo del cero absoluto`);
this.#celsius = valor;
}
get fahrenheit() {
return this.#celsius * 9 / 5 + 32;
}
}
const t = new Temperatura();
t.celsius = 25;
console.log(t.celsius, t.fahrenheit);
try {
t.celsius = -300;
} catch (e) {
console.log(e.name + ":", e.message);
}salida25 77 RangeError: -300 °C está por debajo del cero absoluto
Desde fuera, t.celsius = 25 parece una asignación normal, pero pasa por el set, que puede comprobar el valor. Las excepciones y el try...catch se ven a fondo en la lección 7.
Herencia: extends y super
class Libro extends Producto {
constructor(nombre, precio, autor) {
super(nombre, precio); // primero, el constructor del padre
this.autor = autor;
}
describir() {
return `${super.describir()} (de ${this.autor})`;
}
}
const libro = new Libro("Rayuela", 18, "Cortázar").reponer(4);
console.log(libro.describir());
console.log(libro.precioConIva); // heredado
console.log(libro instanceof Libro, libro instanceof Producto);
console.log(Object.getPrototypeOf(Libro.prototype) === Producto.prototype);salidaRayuela: 18 USD, 4 en stock (de Cortázar) 20.88 true true true
libro.reponer, JavaScript lo busca en el objeto, luego en Libro.prototype, luego en Producto.prototype, y así hasta null. extends no copia nada: encadena los prototipos. Esquema: decodigo.com.extends une las dos cadenas de prototipos, y por eso el libro tiene reponer y precioConIva sin que Libro los declare. super(...) llama al constructor del padre y super.describir(), a la versión del padre de un método que el hijo ha sustituido.
En un constructor hijo, super tiene que ir antes de tocar this. Si se te olvida:
class Revista extends Producto {
constructor(nombre, precio, numero) {
this.numero = numero;
}
}
new Revista("Muy", 4, 512);salidaReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
Y el otro olvido clásico, crear el objeto sin new:
const cafe = Producto("café", 7.5);salidaTypeError: Class constructor Producto cannot be invoked without 'new'
extends son difíciles de seguir en cualquier lenguaje.this: a qué apunta
En la mayoría de los lenguajes, this (o self) es siempre el objeto al que pertenece el método. En JavaScript, no: this se decide en el momento de la llamada, según cómo se escriba esa llamada. Es la fuente de errores más conocida del lenguaje:
const tienda = {
nombre: "La Esquina",
saludar() {
return `Bienvenido a ${this.nombre}`;
},
};
console.log(tienda.saludar()); // con el objeto delante: bien
const saludar = tienda.saludar; // el método, suelto
console.log(saludar());salidaBienvenido a La Esquina
salidaTypeError: Cannot read properties of undefined (reading 'nombre')
Es la misma función. Pero en la segunda llamada no hay nada delante del punto, así que this vale undefined y leer .nombre falla. Esto pasa sin darte cuenta cada vez que pasas un método como callback: setTimeout(tienda.saludar, 100) o boton.addEventListener("click", tienda.saludar) separan el método de su objeto.
this en cada una. Las flechas no tienen this propio: usan el del sitio donde se escribieron. Esquema: decodigo.com.Hay dos soluciones, y las dos se usan mucho:
class Contador {
cuenta = 0;
sumarNormal() {
this.cuenta++;
}
sumarFlecha = () => { // campo con una flecha: this fijo
this.cuenta++;
};
}
const c = new Contador();
const pulsar = (callback) => callback(); // como haría un botón
pulsar(c.sumarFlecha);
pulsar(c.sumarNormal.bind(c)); // bind fija this
console.log("cuenta:", c.cuenta);
try {
pulsar(c.sumarNormal);
} catch (e) {
console.log(e.message);
}salidacuenta: 2 Cannot read properties of undefined (reading 'cuenta')
bind devuelve una copia de la función con this fijado para siempre. La flecha guardada como campo de la clase consigue lo mismo de otra forma: como las flechas no tienen this propio, usa el del constructor, que es el objeto. La contrapartida es que esa flecha se crea una vez por objeto, en lugar de compartirse en el prototipo; para la mayoría de los programas, da igual.
Dentro de un método, las flechas son también lo que quieres para los callbacks, porque conservan el this del método:
const pedido = {
cliente: "Ana",
lineas: ["pan", "café"],
resumen() {
return this.lineas.map((l) => `${l} para ${this.cliente}`);
},
};
console.log(pedido.resumen());salida[ 'pan para Ana', 'café para Ana' ]
Todo el código de esta lección se ejecutó con Node.js 24 en un contenedor limpio antes de publicarla; las salidas y los errores están copiados de esa ejecución.
