UD 4: Objetos y Programación Orientada a Objetos (RA3)
Presentación de clase · 55 diapositivas para seguir la unidad
Referencia Módulo DWEC: Resultado de Aprendizaje 3 (RA3) — Desarrolla aplicaciones web utilizando el modelo de objetos de JavaScript, integrando estructuras clave-valor, la cadena de prototipos nativa, clases ES6+, herencia y mecanismos modernos de encapsulamiento, y aprovechando los objetos predefinidos del lenguaje.
1. Dónde estamos: de datos sueltos a modelos con comportamiento
| Unidad | Qué aprendimos | Qué aporta hoy |
|---|---|---|
| UD1 – UD2 | Sintaxis, tipos, funciones, this, closures |
this es la base de los métodos |
| UD3 | Arrays, map / filter / reduce, desestructuración, spread |
Transformar objetos con lo que ya sabes de arrays |
| UD4 (hoy) | Objetos y POO | Modelar el dominio: datos + comportamiento |
| UD5 | DOM y eventos | Pintar ese modelo en la página |
La pregunta de la unidad: ¿cómo represento en código las cosas de mi aplicación (usuarios, productos, pedidos) con sus datos y lo que saben hacer?
La programación orientada a objetos (POO) organiza el código agrupando estado (datos) y comportamiento (funciones) en objetos que representan conceptos del problema. En una tienda online, carrito.añadir(producto) o pedido.calcularTotal() se leen casi como el propio negocio.
JavaScript es distinto a Java o C#
Por debajo, JavaScript no tiene clases: tiene objetos enlazados con otros objetos (prototipos). La palabra class existe desde 2015, pero es una forma cómoda de escribir ese modelo. Por eso esta unidad va de abajo arriba: objetos → prototipos → clases → herencia y composición → objetos nativos.
2. Objetos literales: pares clave → valor
Un objeto es una colección de propiedades; cada propiedad es un par clave → valor. Las claves son siempre string (o Symbol); los valores pueden ser cualquier cosa. Cuando el valor es una función, la propiedad se llama método.
const usuario = {
nombre: "Ana",
edad: 31,
activo: true,
roles: ["editor", "admin"], // un array también es un objeto
direccion: { ciudad: "Sevilla" }, // objeto anidado
saludar() { // método (sintaxis corta, ES2015)
return `Hola, soy ${this.nombre}`;
},
};
Todo lo que no es primitivo es un objeto
Los primitivos son string, number, boolean, null, undefined, bigint y symbol. Todo lo demás (arrays, fechas, funciones…) es un objeto: typeof [] devuelve "object".
2.1. Punto o corchetes
| Notación | Ejemplo | Cuándo |
|---|---|---|
| Punto | usuario.nombre |
La clave es un identificador válido. Es la habitual. |
| Corchetes | usuario[clave] |
La clave está en una variable, tiene guiones o espacios, o se calcula al vuelo. |
const campo = "email";
usuario.campo; // undefined → busca una clave llamada literalmente "campo"
usuario[campo]; // como usuario.email → lo de dentro se EVALÚA
usuario["fecha-alta"]; // con guion: solo con corchetes
usuario.telefono = "600 000 000"; // añade
usuario.edad = 32; // cambia
delete usuario.activo; // borra
usuario.dni; // undefined: leer algo que no existe NO da error
usuario.dni.length; // ✗ TypeError: el error llega al acceder a undefined.algo
Atajos al crear (ES2015):
const nombre = "Luis";
const p = { nombre, [campo]: "l@x.es" }; // propiedad abreviada + clave calculada
// { nombre: "Luis", email: "l@x.es" }
Ya lo conoces de la UD3
usuario?.direccion?.ciudad no revienta si falta direccion, y const { nombre, edad } = usuario saca varias propiedades a la vez (desestructuración, con valores por defecto y alias).
3. Métodos y this
Dentro de un método, this es el objeto que está a la izquierda del punto en la llamada. Se decide al llamar, no al escribir el método.
const contador = {
valor: 0,
incrementar() {
this.valor++;
return this.valor;
},
};
contador.incrementar(); // 1 → this === contador
const otro = { valor: 100, incrementar: contador.incrementar };
otro.incrementar(); // 101 → mismo método, otro this
Trampa: los métodos no se escriben con flecha
Las funciones flecha no tienen this propio: usan el del ámbito donde se escribieron. incrementar: () => this.valor++ no funciona (en un módulo, this es undefined fuera de cualquier función). Los métodos de objetos y clases se escriben con la sintaxis corta metodo() { … }. Las flechas son ideales, en cambio, como callbacks dentro de un método (UD2).
4. Referencias y copias
La variable no guarda el objeto: guarda una referencia (una flecha) hacia él.
flowchart LR
a["const a"] --> O1["{ precio: 99 }"]
b["const b = a"] --> O1
c["const c = { ...a }"] --> O2["{ precio: 99 → 5 } (objeto nuevo)"]
const a = { precio: 10, dir: { ciudad: "Sevilla" } };
const b = a; // copia la FLECHA: mismo objeto
b.precio = 99;
a.precio; // 99
const c = { ...a }; // objeto NUEVO, copia SUPERFICIAL
c.precio = 5; // a no cambia
c.dir.ciudad = "Cádiz"; // ¡a.dir.ciudad también! dir sigue compartido
| Operación | Qué hace | Úsala para |
|---|---|---|
b = a |
Copia la referencia (mismo objeto) | Compartir a propósito |
{ ...a } |
Objeto nuevo, copia de un nivel | Cambiar propiedades sin tocar el original |
structuredClone(a) |
Copia profunda (todos los niveles) | Clonar datos anidados. Lanza error si hay funciones; las instancias de clase pierden su clase |
a === b |
¿Es la misma referencia? | Ojo: { x: 1 } === { x: 1 } es false |
Recuerda la UD2
Primitivos por valor, objetos por referencia. Si pasas un objeto a una función, la función recibe una copia de la flecha y puede modificar el original. Por eso en la UD3 preferíamos devolver objetos nuevos en vez de mutar.
5. Recorrer y proteger objetos
5.1. Object.keys, values y entries
Un objeto no tiene map ni filter, pero tres métodos estáticos lo convierten en array:
const stock = { teclado: 12, raton: 0, monitor: 4 };
Object.keys(stock); // ["teclado", "raton", "monitor"]
Object.values(stock); // [12, 0, 4]
Object.entries(stock); // [["teclado", 12], ["raton", 0], ["monitor", 4]]
for (const [prod, uds] of Object.entries(stock)) {
console.log(`${prod}: ${uds}`);
}
const agotados = Object.keys(stock).filter((k) => stock[k] === 0); // ["raton"]
// Transformar un objeto entero: entries → map → fromEntries
const precios = { teclado: 50, raton: 15 };
const conIva = Object.fromEntries(
Object.entries(precios).map(([k, v]) => [k, v * 1.21])
);
¿Y for…in?
for…in recorre también las propiedades heredadas del prototipo (sección 6). Por eso hoy se prefiere Object.keys / Object.entries con for…of. Y nunca for…in sobre arrays (UD3).
5.2. const, Object.seal y Object.freeze
| Reasignar la variable | Cambiar valores | Añadir propiedades | Borrar propiedades | |
|---|---|---|---|---|
const obj |
✗ | ✓ | ✓ | ✓ |
Object.seal(obj) |
— | ✓ | ✗ | ✗ |
Object.freeze(obj) |
— | ✗ | ✗ | ✗ |
const CONFIG = Object.freeze({ iva: 0.21, moneda: "EUR" });
CONFIG.iva = 0; // en un módulo (Vite): TypeError; fuera de modo estricto: se ignora en silencio
CONFIG = {}; // TypeError: Assignment to constant variable
const datos = Object.freeze({ dir: { cp: "41001" } });
datos.dir.cp = "28001"; // ¡funciona! freeze es SUPERFICIAL
Modo estricto
Los módulos ES (<script type="module">, todos los ficheros de Vite) y el cuerpo de las clases van siempre en modo estricto, que convierte muchos errores silenciosos en excepciones. Varios ejemplos de esta unidad solo lanzan error por eso.
6. Prototipos: lo que hay debajo
6.1. El problema: muchos objetos con los mismos métodos
Una función fábrica crea objetos sin repetir el literal…
function crearUsuario(nombre) {
return {
nombre,
saludar() {
return `Hola, ${this.nombre}`;
},
};
}
const u1 = crearUsuario("Ana");
const u2 = crearUsuario("Luis");
u1.saludar === u2.saludar; // false → cada objeto lleva SU copia de la función
…pero cada objeto lleva su propia copia de saludar: mil usuarios, mil funciones idénticas. JavaScript resuelve esto con los prototipos.
6.2. [[Prototype]] y la cadena de prototipos
Todo objeto tiene un enlace interno a otro objeto, su prototipo. En la especificación (y en DevTools) se llama [[Prototype]] y se consulta con Object.getPrototypeOf(obj).
flowchart TD
U["usuario<br/>nombre, edad"] -->|"[[Prototype]]"| OP["Object.prototype<br/>toString, hasOwnProperty, valueOf"]
OP -->|"[[Prototype]]"| N["null"]
L["lista = [1, 2]"] -->|"[[Prototype]]"| AP["Array.prototype<br/>map, filter, reduce…"]
AP -->|"[[Prototype]]"| OP
const usuario = { nombre: "Ana", edad: 31 };
usuario.toString(); // "[object Object]" — ¿de dónde sale?
Object.getPrototypeOf(usuario) === Object.prototype; // true: de ahí
Object.getPrototypeOf([1, 2]) === Array.prototype; // true: por eso todos los arrays tienen map
__proto__
Es la forma antigua de acceder a ese enlace. Sigue funcionando por compatibilidad, pero en código nuevo se usa Object.getPrototypeOf / Object.create.
6.3. Cómo busca JavaScript una propiedad
- ¿Es propiedad propia del objeto? → la usa.
- Si no, la busca en su prototipo.
- Si tampoco, sube al prototipo del prototipo…
- Si llega a
nullsin encontrarla →undefined.
"nombre" in usuario; // true
"toString" in usuario; // true (heredada): in mira toda la cadena
Object.hasOwn(usuario, "nombre"); // true
Object.hasOwn(usuario, "toString"); // false: hasOwn solo mira el propio objeto
Sombreado (shadowing): si el objeto tiene una propiedad propia con el mismo nombre que una heredada, se encuentra la propia primero. Así se «sobrescribe» un método heredado.
Leer sube por la cadena; escribir, no
usuario.toString = () => "Soy Ana" no cambia Object.prototype: crea una propiedad propia en usuario.
6.4. Funciones constructoras y new (la forma clásica)
Antes de 2015 se usaban funciones constructoras (con mayúscula por convención) llamadas con new:
function Persona(nombre, edad) {
this.nombre = nombre;
this.edad = edad;
}
Persona.prototype.saludar = function () {
return `Hola, soy ${this.nombre}`;
};
const p = new Persona("David", 30);
p.saludar(); // "Hola, soy David"
Lo que hace new Persona("David", 30):
- Crea un objeto vacío
{}. - Lo enlaza al prototipo: su
[[Prototype]]pasa a serPersona.prototype. - Ejecuta
Personaconthis= ese objeto (this.nombre = "David"…). - Lo devuelve.
Sin new no funciona
Persona("David", 30) ejecuta la función con this = undefined (modo estricto) y lanza TypeError. En código antiguo, sin modo estricto, this era el objeto global y se creaban variables globales por accidente.
6.5. Los métodos viven una vez en el prototipo
flowchart BT
p1["p1<br/>nombre: David · edad: 30"] -->|"[[Prototype]]"| PP["Persona.prototype<br/>saludar ƒ (una sola)<br/>constructor: Persona"]
p2["p2<br/>nombre: Eva · edad: 25"] -->|"[[Prototype]]"| PP
p3["p3<br/>nombre: Iker · edad: 41"] -->|"[[Prototype]]"| PP
| Fábrica | Prototipo | |
|---|---|---|
| Datos (nombre, edad) | en cada objeto | en cada objeto |
| Métodos | copiados en cada uno | una vez, compartidos |
| 1 000 objetos | 1 000 funciones | 1 función |
Regla: los datos van en cada objeto (this.x en el constructor); el comportamiento, en el prototipo.
No modifiques los prototipos nativos
Array.prototype.miMetodo = … funciona, pero rompe librerías (monkey patching). El método Array.prototype.flat se llama así, y no flatten, porque una librería antigua ya había añadido un flatten incompatible.
prototype y [[Prototype]] no son lo mismo
Persona.prototype es una propiedad de la función: el objeto que se usará como prototipo de lo que cree new Persona(). [[Prototype]] es el enlace que tiene cada objeto. Se cumple Object.getPrototypeOf(p) === Persona.prototype.
6.6. Object.create: herencia entre objetos
const animal = {
describir() {
return `${this.nombre} hace ${this.sonido}`;
},
};
const gato = Object.create(animal); // gato → animal
gato.nombre = "Misi";
gato.sonido = "miau";
gato.describir(); // "Misi hace miau"
| Herramienta | Para qué |
|---|---|
Object.getPrototypeOf(obj) |
Ver el prototipo de un objeto |
Object.create(proto) |
Crear un objeto con ese prototipo (Object.create(null): sin prototipo) |
obj instanceof Clase |
¿Está Clase.prototype en la cadena de obj? |
console.dir(obj) |
Ver el objeto desplegable con su [[Prototype]] |
6.7. class es azúcar sintáctico
function Persona(nombre) {
this.nombre = nombre;
}
Persona.prototype.saludar = function () {
return `Hola, soy ${this.nombre}`;
};
class Persona {
constructor(nombre) {
this.nombre = nombre;
}
saludar() {
return `Hola, soy ${this.nombre}`;
}
}
En los dos casos typeof Persona === "function" y saludar está en Persona.prototype. Azúcar sintáctico = otra forma de escribir algo que ya existía. Lo que class añade de verdad:
- Obliga a usar
new(sinnew:TypeError). - El cuerpo va siempre en modo estricto.
- Los métodos no son enumerables (no salen en
for…in). - Permite
#privados,staticyextends/supercómodos.
7. Clases
7.1. Anatomía de una clase
class Producto {
categoria = "general"; // ① campo público (uno por instancia)
constructor(nombre, precio) { // ② constructor: se ejecuta solo con new
this.nombre = nombre;
this.precio = precio;
}
precioConIva() { // ③ método: va al prototipo (compartido)
return this.precio * 1.21;
}
}
const teclado = new Producto("Teclado", 50); // ④ instancia
classDiagram
class Producto {
+categoria
+nombre
+precio
+precioConIva()
}
En UML (diagrama de clases): nombre arriba, atributos en medio, métodos abajo; + público, - privado, subrayado o static para lo estático.
7.2. El constructor deja el objeto en un estado válido… o no lo crea
class Producto {
constructor(nombre, precio = 0, stock = 0) {
if (!nombre?.trim()) {
throw new Error("El nombre es obligatorio");
}
if (precio < 0) {
throw new RangeError("Precio negativo");
}
this.nombre = nombre.trim();
this.precio = precio;
this.stock = stock;
}
}
new Producto("Ratón", 15); // ✓
new Producto("", 15); // ✗ Error: El nombre es obligatorio
Producto("Ratón", 15); // ✗ TypeError: Class constructor Producto cannot be invoked without 'new'
Las clases no se elevan (hoisting)
Usar una clase antes de la línea donde se declara da ReferenceError (zona muerta temporal, UD2). En la práctica, cada clase va en su fichero y se importa arriba.
7.3. Getters y setters
get y set son métodos que se usan como si fueran propiedades: sirven para validar al escribir y calcular al leer.
class Producto {
#precio = 0;
constructor(nombre, precio) {
this.nombre = nombre;
this.precio = precio; // ← pasa por el set: validación en un solo sitio
}
get precio() {
return this.#precio;
}
set precio(valor) {
if (valor < 0) throw new RangeError("Precio negativo");
this.#precio = valor;
}
get precioConIva() { // propiedad calculada, solo lectura
return this.#precio * 1.21;
}
}
const p = new Producto("Teclado", 50);
p.precio; // 50 → get precio()
p.precio = 20; // ✓ → set precio(20)
p.precio = -5; // ✗ RangeError
p.precioConIva; // 24.2 → sin paréntesis
p.precioConIva = 9; // ✗ TypeError en modo estricto: solo tiene getter
Solo cuando aporten algo
Un get/set que solo copia el valor sobra. Úsalos para validar, calcular o proteger. Y el getter no debe tener efectos secundarios ni ser lento. Ojo: get precio() { return this.precio; } se llama a sí mismo sin fin (RangeError: Maximum call stack size exceeded); por eso el dato va en #precio.
7.4. Encapsulación: campos y métodos privados con #
Encapsular es esconder el estado interno y exponer solo una interfaz pública que garantiza que el estado siempre es válido. Desde ES2022, todo lo que empieza por # solo existe dentro del cuerpo de la clase.
class CuentaBancaria {
#saldo = 0; // campo privado (hay que declararlo)
constructor(titular) {
this.titular = titular; // público
}
ingresar(cantidad) {
this.#validar(cantidad);
this.#saldo += cantidad;
}
get saldo() {
return this.#saldo;
}
#validar(cantidad) { // método privado
if (cantidad <= 0) throw new RangeError("Cantidad no válida");
}
}
const cuenta = new CuentaBancaria("Marcos");
cuenta.ingresar(100);
cuenta.saldo; // 100
cuenta.#saldo = 1e6; // ✗ SyntaxError: Private field '#saldo' must be declared in an enclosing class
7.5. Tres formas de «privado»
_saldo (convención) |
closure (UD2) | #saldo (ES2022) |
|
|---|---|---|---|
| ¿Accesible desde fuera? | Sí, cualquiera | No | No |
| ¿Quién lo impide? | Nadie: es un aviso | El ámbito de la función | El propio lenguaje |
| Error al intentarlo | Ninguno | Es undefined |
SyntaxError al cargar |
¿Sale en Object.keys / JSON? |
Sí | No | No |
| Dónde lo verás | Código antiguo | Fábricas, módulos | Clases modernas |
Es un SyntaxError, no un error normal
Se detecta al leer el fichero, antes de ejecutarlo: si escribes obj.#saldo fuera de la clase, no se ejecuta ninguna línea de ese script o módulo. Tampoco se puede capturar con try/catch.
La consola de Chrome sí te deja verlo
Para depurar, la consola de DevTools permite leer cuenta.#saldo. No significa que sea público: en tu código fuente fallará.
Dentro de la clase, #saldo in obj comprueba si obj tiene ese campo privado (ES2022).
7.6. static: lo que pertenece a la clase
class Pedido {
static #contador = 0; // privado y de la clase
static IVA = 0.21; // constante de la clase
constructor(lineas) {
this.id = ++Pedido.#contador;
this.lineas = lineas;
}
static desdeJSON(texto) { // método fábrica
const datos = JSON.parse(texto);
return new Pedido(datos.lineas);
}
}
Pedido.IVA; // 0.21
new Pedido([]).IVA; // undefined: no es de la instancia
Pedido.desdeJSON('{"lineas":[]}').id;
Usos típicos: constantes, contadores, métodos fábrica, utilidades. Math.max, Date.now, Object.keys o Number.isNaN son métodos estáticos.
¿Método normal o static?
¿Necesita los datos de una instancia concreta (this)? → método normal. ¿No? → static.
7.7. El this perdido
Si pasas un método como callback sin su objeto, pierde su this.
class Temporizador {
segundos = 0;
tic() { this.segundos++; }
}
const t = new Temporizador();
const f = t.tic;
f(); // ✗ TypeError: Cannot read properties of undefined (reading 'segundos')
[1, 2].forEach(t.tic); // ✗ el mismo TypeError
setInterval(t.tic, 1000); // ✗ sin error… y no cuenta
t.tic (sin paréntesis) es solo la función, sin el t. delante. Quien la llame después decide this: en una llamada suelta es undefined; setInterval la llama con this = window, así que suma en window.segundos (que acaba valiendo NaN) y t.segundos se queda en 0. Falla en silencio. En la UD5 pasará lo mismo con addEventListener.
| Solución | Código | Cuándo |
|---|---|---|
| Flecha al pasarla | setInterval(() => t.tic(), 1000) |
La habitual |
bind |
setInterval(t.tic.bind(t), 1000) |
Fijar this en una función reutilizable |
| Campo flecha | tic = () => { this.segundos++; }; |
Manejadores de eventos (crea una función por instancia) |
8. Herencia
8.1. extends y super
La herencia permite que una clase (subclase) obtenga las propiedades y métodos de otra (superclase) y añada o cambie comportamiento. En JavaScript es simple: un único padre.
classDiagram
Usuario <|-- Administrador : extends
class Usuario {
+nombre
+email
+saludar()
+puede(accion)
}
class Administrador {
+permisos
+puede(accion)
}
class Usuario {
constructor(nombre, email) {
this.nombre = nombre;
this.email = email;
}
saludar() { return `Hola, ${this.nombre}`; }
puede(accion) { return accion === "leer"; }
}
class Administrador extends Usuario {
constructor(nombre, email, permisos) {
super(nombre, email); // constructor del padre
this.permisos = permisos;
}
puede(accion) { return this.permisos.includes(accion); }
}
const admin = new Administrador("Eva", "e@x.es", ["leer", "borrar"]);
admin.saludar(); // heredado: "Hola, Eva"
admin.puede("borrar"); // propio: true
8.2. super() antes de this
El objeto lo crea la clase base: al hacer new Administrador(…), el constructor de Administrador llama a super(…), el de Usuario crea el objeto y su this, y solo entonces Administrador puede completarlo.
class Administrador extends Usuario {
constructor(nombre, email, permisos) {
this.permisos = permisos; // ✗ antes de super
super(nombre, email);
}
}
// ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
- Si la subclase no escribe constructor, JavaScript pone uno:
constructor(...args) { super(...args); }. - Si lo escribe, debe llamar a
super()antes de usarthis.
8.3. Sobrescritura y polimorfismo
class Figura {
area() { return 0; }
describir() {
return `${this.constructor.name}: ${this.area().toFixed(2)}`;
}
}
class Circulo extends Figura {
constructor(r) { super(); this.r = r; }
area() { return Math.PI * this.r ** 2; }
}
class Rectangulo extends Figura {
constructor(a, b) { super(); this.a = a; this.b = b; }
area() { return this.a * this.b; }
}
const figuras = [new Circulo(1), new Rectangulo(2, 3)];
figuras.map((f) => f.describir()); // ["Circulo: 3.14", "Rectangulo: 6.00"]
- Sobrescribir (override): la subclase define un método con el mismo nombre que el heredado.
super.metodo(): reutiliza la versión del padre y la amplía (describir() { return super.describir() + " (admin)"; }).- Polimorfismo: el código que recorre
figurasno pregunta qué tipo es cada una; cada objeto responde a su manera al mismo mensaje.
Señal de que quieres polimorfismo
Si te descubres escribiendo if (tipo === "circulo") … else if (tipo === "rectangulo") …, probablemente cada tipo debería ser una clase con su propio método.
8.4. La cadena con clases e instanceof
extends enlaza prototipos: es la misma cadena de la sección 6.
flowchart TD
A["admin<br/>nombre, email, permisos"] -->|"[[Prototype]]"| AP["Administrador.prototype<br/>puede ƒ"]
AP -->|"[[Prototype]]"| UP["Usuario.prototype<br/>saludar ƒ · puede ƒ"]
UP -->|"[[Prototype]]"| OP["Object.prototype"]
OP --> N["null"]
admin instanceof Administrador; // true
admin instanceof Usuario; // true
admin instanceof Object; // true
Object.getPrototypeOf(Administrador.prototype) === Usuario.prototype; // true
instanceof no mira «con qué clase se creó» el objeto: mira si Clase.prototype aparece en algún punto de su cadena.
9. Composición frente a herencia
9.1. «Es un» o «tiene un»
Herencia (extends) |
Composición | |
|---|---|---|
| Relación | es un: Administrador ES UN Usuario | tiene un: Coche TIENE UN Motor |
| Acoplamiento | Fuerte: si cambia el padre, cambian los hijos | Débil: las piezas se cambian |
| Riesgo | Jerarquías rígidas y profundas | Algo más de código para conectar |
| Encaja si | La jerarquía es real, estable y poco profunda | Se combinan comportamientos o pueden cambiar |
«Favor object composition over class inheritance» — Gamma, Helm, Johnson y Vlissides, Design Patterns (1994). No significa «nunca heredes»: significa preguntarse cada vez si la relación es de verdad «es un».
9.2. Composición en práctica: delegar
class Motor {
constructor(cv) { this.cv = cv; }
arrancar() { return `Motor de ${this.cv} CV en marcha`; }
}
class MotorElectrico {
arrancar() { return "Zumbido silencioso"; }
}
class Coche {
constructor(modelo, motor) {
this.modelo = modelo;
this.motor = motor; // TIENE UN motor
}
arrancar() {
return `${this.modelo}: ${this.motor.arrancar()}`; // delega
}
}
new Coche("León", new Motor(150)).arrancar();
new Coche("Born", new MotorElectrico()).arrancar();
- Cambiar de motor es pasar otro objeto, sin tocar
Coche. Cocheacepta cualquier objeto con un métodoarrancar()(duck typing).- Con herencia harían falta
CocheGasolina,CocheElectrico,CocheHibrido… y se multiplican con cada variante nueva.
9.3. El problema del pingüino: combinar capacidades (mixins)
Si Ave sabe volar() y Pinguino extends Ave, el pingüino hereda algo que no puede hacer. Y pato y pez nadan, pero no son de la misma familia. Solución: capacidades sueltas que se copian donde hacen falta (mixins).
const puedeVolar = {
volar() { return `${this.nombre} vuela`; },
};
const puedeNadar = {
nadar() { return `${this.nombre} nada`; },
};
const pato = Object.assign({ nombre: "Pato" }, puedeVolar, puedeNadar);
const pinguino = Object.assign({ nombre: "Pingu" }, puedeNadar);
pinguino.nadar(); // "Pingu nada"
Los métodos usan this, así que funcionan en cualquier objeto que tenga nombre. Object.assign(destino, ...origenes) copia las propiedades propias de los orígenes en el destino (copia superficial) y lo devuelve.
9.4. Cuatro preguntas antes de escribir class
flowchart TD
Q1{"¿Solo agrupa datos,<br/>sin comportamiento?"} -->|sí| R1["Objeto literal { }"]
Q1 -->|no| Q2{"¿Muchos objetos con<br/>los mismos métodos?"}
Q2 -->|sí| R2["class"]
R2 --> Q3{"¿Relación «es un»<br/>real y estable?"}
Q3 -->|sí| R3["extends (1–2 niveles)"]
Q3 -->|no| Q4{"¿Comparte capacidades con<br/>otros que no son familia?"}
Q4 -->|sí| R4["Composición / mixin"]
Lo que no vale nunca: heredar solo «para reutilizar código» sin una relación real.
10. Objetos nativos del lenguaje
JavaScript trae objetos listos para usar. Antes de escribir una función, comprueba si ya existe.
| Objeto | Para qué | Ejemplos |
|---|---|---|
Math |
Cálculos | Math.round, Math.random, Math.max |
Number |
Números | Number(), Number.parseFloat, Number.isNaN, toFixed |
String |
Texto | trim, includes, padStart, split, replaceAll |
Date |
Fechas y horas | new Date(), Date.now(), getFullYear |
Intl |
Formato según idioma | Moneda en euros, fechas en español |
Array |
Listas (UD3) | map, filter, reduce |
JSON |
Texto ↔ objeto (UD6) | JSON.stringify, JSON.parse |
Error |
Errores | throw new Error(…) |
Math, JSON e Intl no se instancian con new: son objetos con métodos estáticos. Date, Error o Map son clases.
¿Por qué \"hola\".toUpperCase() funciona si \"hola\" es un primitivo?
JavaScript lo envuelve un instante en un objeto String (autoboxing) y busca el método en String.prototype. Otra vez la cadena de prototipos. Nunca crees envoltorios a mano (new String("x")).
10.1. Math y Number
| Valor | round |
floor |
ceil |
trunc |
|---|---|---|---|---|
2.5 |
3 | 2 | 3 | 2 |
-2.5 |
-2 | -3 | -2 | -2 |
2.49 |
2 | 2 | 3 | 2 |
// Entero aleatorio entre min y max (ambos incluidos)
const azar = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
Math.max(...[4, 9, 2]); // 9 (spread, UD3)
Math.min(); // Infinity (¡ojo con arrays vacíos!)
0.1 + 0.2; // 0.30000000000000004
(0.1 + 0.2).toFixed(2); // "0.30" ← STRING
"0.30" + 1; // "0.301" (concatena)
Number("12.5"); // 12.5
Number("12,5"); // NaN: la coma no es decimal en JS
Number.parseFloat("12.5€"); // 12.5: lee hasta donde puede
Number.isNaN(NaN); // true
NaN === NaN; // false
Dinero
Los decimales se guardan en binario (IEEE 754) y muchos no son exactos: pasa en todos los lenguajes. Redondea solo al mostrar (toFixed / Intl) o trabaja en céntimos enteros (1999 en vez de 19.99). Nunca compares decimales con ===.
10.2. Date
const ahora = new Date();
const examen = new Date(2026, 10, 27); // ¡27 de NOVIEMBRE! (meses 0–11)
const iso = new Date("2026-11-27T09:00"); // ISO con hora: hora local
const utc = new Date("2026-11-27"); // ISO SIN hora: medianoche UTC (01:00 en España)
examen.getFullYear(); // 2026
examen.getMonth(); // 10 (0 = enero)
examen.getDate(); // 27 (día del mes)
examen.getDay(); // 5 (día de la SEMANA, 0 = domingo)
const dias = Math.ceil((examen - ahora) / 86_400_000); // restar fechas da milisegundos
Date.now(); // ms desde el 1-1-1970 (marca de tiempo)
| Trampa | Explicación |
|---|---|
| Meses de 0 a 11 | new Date(2026, 10, 27) es noviembre |
getDay ≠ getDate |
getDay = día de la semana; getDate = día del mes |
| Restar da ms | 1 día = 86 400 000 ms |
| Es mutable | setDate() cambia el objeto original |
| ISO sin hora = UTC | new Date("2026-11-27") es medianoche UTC: en América cae el día 26. Añade la hora ("2026-11-27T00:00") o usa números |
| Formatos no ISO | new Date("27/11/2026") no está estandarizado: usa números o ISO |
Temporal
Temporal es la nueva API de fechas del lenguaje (inmutable y sin estas trampas). Está llegando a los navegadores, pero Date seguirá en todo el código existente durante años.
10.3. String e Intl
const entrada = " Ana@Correo.ES ";
entrada.trim().toLowerCase(); // "ana@correo.es"
"Teclado".includes("cla"); // true
"7".padStart(3, "0"); // "007"
"a-b-c".split("-"); // ["a", "b", "c"]
"2026-11-27".slice(0, 4); // "2026"
"x.y.z".replaceAll(".", "/"); // "x/y/z"
// Los strings son INMUTABLES
entrada.trim(); // devuelve uno nuevo…
entrada; // …y entrada sigue con espacios
Intl formatea números, monedas, fechas y plurales según el idioma:
const eur = new Intl.NumberFormat("es-ES", { style: "currency", currency: "EUR" });
eur.format(1234.5); // "1234,50 €"
eur.format(12345.5); // "12.345,50 €"
examen.toLocaleDateString("es-ES", { weekday: "long", day: "numeric", month: "long" });
// "viernes, 27 de noviembre"
Sin punto de millares en 4 cifras
1234,50 € sale sin punto: la norma española no agrupa los números de cuatro cifras. No es un error. Y nada de concatenar " €" ni cambiar puntos por comas a mano.
10.4. Error es una clase… y se puede heredar
classDiagram
Error <|-- TypeError
Error <|-- RangeError
Error <|-- ValidacionError
class Error {
+name
+message
+stack
}
class ValidacionError {
+campo
}
class ValidacionError extends Error {
constructor(campo, mensaje) {
super(mensaje);
this.name = "ValidacionError";
this.campo = campo;
}
}
try {
throw new ValidacionError("email", "Email no válido");
} catch (e) {
if (e instanceof ValidacionError) {
console.warn(e.campo, e.message); // error esperado: se muestra al usuario
} else {
throw e; // un bug: que siga subiendo
}
}
Herencia bien usada: un ValidacionError es un Error. Lanza siempre new Error(…) o una subclase, nunca throw "texto" (sin stack ni instanceof). Lo usarás en formularios (UD5) y con fetch (UD6).
11. Verificar: DevTools
| Herramienta | Para qué |
|---|---|
console.log(obj) |
Vista rápida. Muestra el estado al desplegar, no al imprimir |
console.dir(obj) |
Vista como objeto, con [[Prototype]] desplegable |
console.table(lista) |
Array de objetos como tabla |
Object.getPrototypeOf(x) |
Subir un eslabón de la cadena a mano |
debugger; |
Pausa en esa línea (con DevTools abierto) para ver this y las variables |
> console.dir(admin)
▼ Administrador
nombre: "Eva"
email: "e@x.es"
permisos: (2) ['leer', 'borrar']
▼ [[Prototype]]: Usuario
constructor: class Administrador
puede: ƒ puede(accion)
▼ [[Prototype]]: Object
saludar: ƒ saludar()
puede: ƒ puede(accion)
▸ [[Prototype]]: Object
Cómo rotula Chrome los prototipos
El primer «[[Prototype]]: Usuario» es en realidad Administrador.prototype (dentro se ve constructor: class Administrador): Chrome etiqueta cada prototipo con el nombre de la clase de la que hereda.
Pruébalo con el DOM: Object.getPrototypeOf(document.body) y sigue subiendo. Un botón es HTMLButtonElement → HTMLElement → Element → Node → EventTarget → Object: la UD5 es usar esa cadena.
12. Errores comunes
| Error | ✗ Mal | ✓ Bien |
|---|---|---|
Olvidar new |
Producto("Ratón", 15) |
new Producto("Ratón", 15) |
this antes de super() |
this.permisos = p; super(n, e); |
super(n, e); this.permisos = p; |
| Heredar «para reutilizar» | class Carrito extends Producto |
class Carrito { #lineas = []; } |
| Sobrescribir y perder al padre | describir() { return "Admin"; } |
describir() { return super.describir() + "!"; } |
| Método flecha en un objeto | saludar: () => this.nombre |
saludar() { return this.nombre; } |
| Pasar el método sin su objeto | lista.forEach(t.tic) |
lista.forEach(() => t.tic()) |
_privado que no es privado |
this._saldo = 0; |
#saldo = 0; |
Meses de Date desde 1 |
new Date(2026, 11, 27) // ¿nov? |
new Date(2026, 10, 27) // nov |
Copiar con = |
const copia = original; |
const copia = { ...original }; |
Sumar tras toFixed |
total.toFixed(2) + envio |
(total + envio).toFixed(2) |
13. Ejercicio práctico guiado: modelo de una tienda con clases
Proyecto Vite (como en la Práctica 1) con un fichero por clase en src/modelo/ y un src/main.js que solo crea objetos y muestra resultados con console.log / console.table. Sin DOM todavía: es la base del modelo de la Práctica 2.
classDiagram
Producto <|-- ProductoDigital : extends
Carrito o-- Producto : tiene 0..*
class Producto {
+nombre
-precio
-stock
+IVA$
+get/set precio
+get precioConIva
+hayStock(uds)
+retirar(uds)
}
class ProductoDigital {
+urlDescarga
+hayStock() true
}
class Carrito {
-#lineas
+creado: Date
+añadir(producto, uds)
+quitar(nombre)
+get total
+resumen()
}
Producto:nombreobligatorio,preciocon setter que lanzaRangeErrorsi es negativo (el constructor también pasa por el setter),#stockprivado,static IVA = 0.21,get precioConIva,hayStock(uds)yretirar(uds)(lanzaErrorsi no hay stock).ProductoDigital extends Producto: añadeurlDescarga, llama bien asuper()y sobrescribehayStock()(siempretrue) yretirar()(no hace nada).Carrito: tiene productos (no hereda de nadie). Lista privada#lineasde{ producto, uds };añadir(producto, uds)llama aproducto.retirar(uds)(así falla si no hay stock) y, si el producto ya está, suma unidades;quitar(nombre),get totalredondeado a céntimos yget unidades.- Formato:
resumen()devuelve"3 artículos · 129,98 €"usandoIntl.NumberFormat;creadoguarda la fecha conDate. - Comprobar en
main.js:
const teclado = new Producto("Teclado", 49.99, 5);
const raton = new Producto("Ratón", 15, 3);
const ebook = new ProductoDigital("Ebook JS", 30, "https://ejemplo.es/js.pdf");
const c = new Carrito();
c.añadir(teclado, 2);
c.añadir(ebook, 1);
console.log(c.total); // 129.98
console.log(c.resumen()); // "3 artículos · 129,98 €"
// Un error no capturado para el módulo entero: cada prueba de error, en su try/catch
try { teclado.precio = -1; } catch (e) { console.log(e.name, e.message); } // RangeError
try { c.añadir(raton, 99); } catch (e) { console.log(e.name, e.message); } // Error: sin stock
console.log(Object.keys(teclado)); // ["nombre"] → los # no salen
¿Te sale 129.98000000000002?
Es el problema de los decimales de la sección 10.1 (2 * 49.99 + 30). Redondea en get total: Math.round(t * 100) / 100.
Criterio de éxito
Precio negativo → RangeError; #stock inaccesible desde main.js; total en formato 129,98 €; y Carrito no hereda de nadie.
Si terminas: descuento por composición
Crea SinDescuento y DescuentoPorcentaje (ambos con un método aplicar(total)) y haz que el Carrito tenga uno y lo use al calcular el total. Cambiar de descuento no debe obligar a tocar Carrito.
14. Comprueba lo aprendido
1. ¿Por qué se dice que class es azúcar sintáctico? ¿Qué hay debajo?
Porque class crea una función constructora y pone los métodos en su prototype; los objetos creados con new enlazan con él mediante [[Prototype]]. No añade un modelo de objetos nuevo: es otra forma de escribir constructoras y prototipos.
2. p1 y p2 son instancias de Persona. ¿Cuántas copias de saludar existen? ¿Dónde?
Una, en Persona.prototype. Las dos instancias la encuentran subiendo por su cadena de prototipos.
3. ¿Qué diferencia real hay entre #saldo y _saldo? ¿Qué pasa si escribes cuenta.#saldo fuera de la clase?
_saldo es una convención: la propiedad es pública. #saldo es privada y lo hace cumplir el lenguaje. Fuera de la clase es un SyntaxError que se detecta al cargar el fichero, así que no se ejecuta nada de ese script (en la consola de DevTools sí se puede leer, solo para depurar).
4. setInterval(t.tic, 1000) no cuenta: t.segundos sigue a 0. ¿Por qué? Da dos arreglos.
t.tic pasa la función sin su objeto; setInterval la llama con this = window, así que incrementa window.segundos. Arreglos: setInterval(() => t.tic(), 1000), setInterval(t.tic.bind(t), 1000), o declarar tic = () => { … } como campo flecha.
5. Carrito y Producto: ¿herencia o composición?
Composición: el carrito tiene productos; no es un producto. Heredar le daría precio o hayStock, que no tienen sentido para un carrito.
6. ¿Qué devuelve new Date(2026, 0, 31).getMonth()? ¿Y getDay() de esa fecha?
getMonth() devuelve 0 (enero). getDay() devuelve el día de la semana: el 31 de enero de 2026 fue sábado → 6.
7. ¿Qué imprime este código y por qué?
const a = { n: 1, sub: { m: 1 } };
const b = { ...a };
b.n = 2; b.sub.m = 2;
console.log(a.n, a.sub.m);
1 2. El spread hace una copia superficial: n se copia, pero sub es la misma referencia en a y b.
8. ¿Cuándo usarías un método static?
Cuando no necesita los datos de una instancia concreta (this): constantes, contadores, utilidades o métodos fábrica como Pedido.desdeJSON(texto).
15. Glosario
| Término | Definición |
|---|---|
| Objeto | Colección de propiedades clave → valor. |
| Propiedad / método | Par clave–valor / propiedad cuyo valor es una función. |
this |
Objeto sobre el que se llama el método (el que está a la izquierda del punto). |
| Referencia | La variable apunta al objeto; no lo contiene. |
| Copia superficial / profunda | { ...obj } copia un nivel / structuredClone(obj) copia todos. |
Prototipo / [[Prototype]] |
Objeto del que otro hereda / enlace interno a ese objeto. |
| Cadena de prototipos | Prototipo del prototipo… hasta null. |
| Propiedad propia | La que está en el propio objeto (Object.hasOwn). |
| Función constructora | Función que se llama con new para crear objetos. |
Object.create(proto) |
Crea un objeto con el prototipo indicado. |
| Azúcar sintáctico | Sintaxis más cómoda para algo que ya existía. |
| Clase / instancia | Plantilla para crear objetos / objeto creado con new Clase(). |
| Constructor | Método que inicializa la instancia al hacer new. |
| Getter / setter | Métodos que se usan como si fueran propiedades (get / set). |
| Encapsulación | Ocultar el estado y exponer solo una interfaz pública. |
#privado |
Campo o método accesible solo dentro de la clase (ES2022). |
static |
Miembro de la clase, no de cada instancia. |
Herencia (extends) |
Una subclase obtiene propiedades y métodos de su superclase. |
super |
Llama al constructor (super()) o a un método (super.m()) del padre. |
| Sobrescritura | Redefinir en la subclase un método heredado. |
| Polimorfismo | Mismo método, distinta respuesta según el objeto. |
| Composición / delegación | Un objeto TIENE otros y les pide parte del trabajo. |
| Mixin | Objeto de métodos que se copian en otros (Object.assign). |
| Objetos predefinidos | Objetos que trae el lenguaje: Math, Date, String, Intl, Error… |
| Modo estricto | Variante del lenguaje que convierte errores silenciosos en excepciones; activo en módulos y clases. |
16. Resumen y conexión con la siguiente unidad
- Debajo de
classhay funciones y una cadena de prototipos. - Los datos van en cada instancia; los métodos, en el prototipo, compartidos.
- En una subclase,
super()antes dethis. #privadolo hace cumplir el lenguaje;_privadoes solo un aviso.- Composición o herencia: decisión consciente, «tiene un» frente a «es un».
Math,Date,StringeIntlantes que reinventar.- La POO es una herramienta, no un dogma: a veces un objeto literal o una función son mejor opción.
Siguiente — UD5: DOM nativo y gestión de eventos. Pintarás este modelo en la página con createElement, classList o dataset, responderás a la interacción con addEventListener y te reencontrarás con el this perdido de la sección 7.7. Todo junto es la Práctica 2.
17. Referencias y fuentes
- MDN — Trabajar con objetos
- MDN — Herencia y la cadena de prototipos
- MDN — Clases · Propiedades privadas · static
- MDN — this · Function.prototype.bind
- MDN — Math · Date · Intl · Error
- javascript.info — Prototipos, herencia · Clases
- Gamma, Helm, Johnson y Vlissides (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.