Volver al inicio
POO en JavaScript

Programación Orientada a Objetos en JS

Aprende a usar clases con constructores, getters/setters y métodos. Aunque las clases no son obligatorias en JavaScript, son la forma recomendada de organizar código orientado a objetos.

1. ¿Qué son las Clases?

Las clases en JavaScript son "azúcar sintáctica" sobre el sistema de prototipos. Fueron introducidas en ES6 para hacer la programación orientada a objetos más familiar, especialmente para desarrolladores de Java.

¿Por qué usar clases?

  • Código más organizado y reutilizable
  • Sintaxis más clara y similar a otros lenguajes
  • Encapsulación de datos y comportamiento
  • Facilita la herencia y el polimorfismo

Sintaxis Básica: Comparación con Java

Java

Java - Clase tradicionaljava
1public class Persona {
2 private String nombre;
3 private int edad;
4
5 public Persona(String nombre, int edad) {
6 this.nombre = nombre;
7 this.edad = edad;
8 }
9
10 public String getNombre() {
11 return this.nombre;
12 }
13
14 public void setNombre(String nombre) {
15 this.nombre = nombre;
16 }
17}

JavaScript

JavaScript - Sintaxis modernajavascript
1class Persona {
2 constructor(nombre, edad) {
3 this.nombre = nombre;
4 this.edad = edad;
5 }
6
7 // Getter
8 get nombre() {
9 return this._nombre;
10 }
11
12 // Setter
13 set nombre(valor) {
14 this._nombre = valor;
15 }
16}

2. Constructor

El constructor es un método especial que se ejecuta automáticamente cuando creas una instancia de la clase con new.

Diferencia con Java: Solo puedes tener UN constructor. No hay sobrecarga de constructores.

Constructor en acciónjavascript
1class Coche {
2 constructor(marca, modelo, año) {
3 this.marca = marca;
4 this.modelo = modelo;
5 this.año = año;
6 this.kilometraje = 0; // Valor por defecto
7 }
8}
9
10// Crear instancias
11const miCoche = new Coche("Toyota", "Corolla", 2023);
12console.log(miCoche.marca); // "Toyota"
13console.log(miCoche.kilometraje); // 0

Parámetros opcionales

Puedes usar parámetros por defecto como en funciones normales:

constructor(marca = "Genérica", modelo = "Estándar")

3. Getters y Setters

Los getters y setters permiten controlar cómo se accede y modifica una propiedad. Son especialmente útiles para:

  • Validación: Asegurar que los datos sean correctos
  • Propiedades computadas: Calcular valores dinámicamente
  • Encapsulación: Ocultar la implementación interna
Getters y Setters con validaciónjavascript
1class Persona {
2 constructor(nombre, edad) {
3 this._nombre = nombre;
4 this._edad = edad;
5 }
6
7 // Getter - se usa como propiedad, no como método
8 get nombre() {
9 return this._nombre.toUpperCase();
10 }
11
12 // Setter con validación
13 set edad(valor) {
14 if (valor < 0) {
15 console.log("❌ La edad no puede ser negativa");
16 return;
17 }
18 if (valor > 150) {
19 console.log("❌ La edad no es realista");
20 return;
21 }
22 this._edad = valor;
23 }
24
25 get edad() {
26 return this._edad;
27 }
28
29 // Propiedad computada (solo getter)
30 get mayorDeEdad() {
31 return this._edad >= 18;
32 }
33}
34
35const juan = new Persona("Juan", 25);
36
37// Usar getters (SIN paréntesis)
38console.log(juan.nombre); // "JUAN"
39console.log(juan.mayorDeEdad); // true
40
41// Usar setters (como asignación)
42juan.edad = 30; // ✅ Válido
43juan.edad = -5; // ❌ Error: edad negativa

Convención con guión bajo

Usamos _nombre como convención para indicar que es una propiedad "privada". El guión bajo es solo una convención, no hace la propiedad privada realmente.

4. Métodos de Instancia

Los métodos son funciones definidas dentro de una clase que operan sobre los datos de la instancia.

Métodos de instanciajavascript
1class CuentaBancaria {
2 constructor(titular, saldoInicial = 0) {
3 this.titular = titular;
4 this._saldo = saldoInicial;
5 }
6
7 // Métodos
8 depositar(cantidad) {
9 if (cantidad <= 0) {
10 console.log("❌ La cantidad debe ser positiva");
11 return false;
12 }
13 this._saldo += cantidad;
14 console.log(`✅ Depósito: $${cantidad}. Nuevo saldo: $${this._saldo}`);
15 return true;
16 }
17
18 retirar(cantidad) {
19 if (cantidad > this._saldo) {
20 console.log("❌ Saldo insuficiente");
21 return false;
22 }
23 this._saldo -= cantidad;
24 console.log(`✅ Retiro: $${cantidad}. Nuevo saldo: $${this._saldo}`);
25 return true;
26 }
27
28 get saldo() {
29 return this._saldo;
30 }
31
32 mostrarInfo() {
33 console.log(`Titular: ${this.titular}, Saldo: $${this._saldo}`);
34 }
35}
36
37const cuenta = new CuentaBancaria("Ana García", 1000);
38cuenta.depositar(500); // ✅ Depósito: $500. Nuevo saldo: $1500
39cuenta.retirar(200); // ✅ Retiro: $200. Nuevo saldo: $1300
40cuenta.mostrarInfo(); // Titular: Ana García, Saldo: $1300

5. El Contexto de 'this'

En JavaScript, this NO funciona como en Java. El valor de this depende de cómo se llama la función, no de dónde se define.

Problema común: Perder el contexto de this en callbacks y event listeners.

✅ Cuándo mantiene el contexto

✅ Contexto mantenido - Llamadas directasjavascript
1class Contador {
2 constructor() {
3 this.count = 0;
4 }
5
6 incrementar() {
7 this.count++;
8 console.log(`Count: ${this.count}`);
9 }
10
11 mostrarValor() {
12 console.log(`Valor actual: ${this.count}`);
13 }
14}
15
16const c = new Contador();
17
18// ✅ Llamada directa - this funciona correctamente
19c.incrementar(); // Count: 1
20c.mostrarValor(); // Valor actual: 1
21
22// ✅ Múltiples llamadas - this siempre apunta a 'c'
23c.incrementar(); // Count: 2
24c.incrementar(); // Count: 3

❌ Cuándo pierde el contexto

❌ Contexto perdido - Callbacks y asignacionesjavascript
1class Temporizador {
2 constructor(nombre) {
3 this.nombre = nombre;
4 }
5
6 saludar() {
7 console.log(`¡Hola! Soy ${this.nombre}`);
8 }
9}
10
11const temp = new Temporizador("Ana");
12
13// ✅ Llamada directa funciona
14temp.saludar(); // "¡Hola! Soy Ana"
15
16// ❌ Pasar el método como callback - PIERDE el contexto
17setTimeout(temp.saludar, 1000);
18// ❌ ERROR: Cannot read property 'nombre' of undefined
19// Razón: this es undefined, no apunta a 'temp'
20
21// ❌ Asignar método a variable - PIERDE el contexto
22const funcionSaludo = temp.saludar;
23funcionSaludo();
24// ❌ ERROR: Cannot read property 'nombre' of undefined

¿Por qué pierde el contexto?

Cuando pasas un método como callback, JavaScript solo pasa la función, no el objeto. Por eso this se vuelve undefined (o el objeto global en modo no estricto).

🔧 Cómo recuperar el contexto

Solución 1: Arrow Function en el callback

✅ Solución 1: Envolver en arrow functionjavascript
1class Temporizador {
2 constructor(nombre) {
3 this.nombre = nombre;
4 }
5
6 saludar() {
7 console.log(`¡Hola! Soy ${this.nombre}`);
8 }
9}
10
11const temp = new Temporizador("Ana");
12
13// ✅ Arrow function mantiene el contexto
14setTimeout(() => temp.saludar(), 1000);
15// ✅ Funciona después de 1 segundo: "¡Hola! Soy Ana"
16
17// ✅ También puedes usarlo múltiples veces
18setTimeout(() => temp.saludar(), 2000);
19setTimeout(() => temp.saludar(), 3000);

Solución 2: Método bind()

✅ Solución 2: Usar bind()javascript
1class Temporizador {
2 constructor(nombre) {
3 this.nombre = nombre;
4 }
5
6 saludar() {
7 console.log(`¡Hola! Soy ${this.nombre}`);
8 }
9}
10
11const temp = new Temporizador("Ana");
12
13// ✅ bind() crea una nueva función con 'this' fijado a 'temp'
14setTimeout(temp.saludar.bind(temp), 1000);
15// ✅ Funciona: "¡Hola! Soy Ana"
16
17// Puedes guardar la función con bind
18const saludoFijo = temp.saludar.bind(temp);
19setTimeout(saludoFijo, 2000); // ✅ También funciona

bind() crea una nueva función donde this siempre apunta al objeto que especifiques.

Solución 3: Arrow Function como propiedad de clase

✅ Solución 3: Arrow function en la clasejavascript
1class Temporizador {
2 constructor(nombre) {
3 this.nombre = nombre;
4 }
5
6 // Arrow function como propiedad (NO como método)
7 saludar = () => {
8 console.log(`¡Hola! Soy ${this.nombre}`);
9 }
10}
11
12const temp = new Temporizador("Ana");
13
14// ✅ Ahora puedes pasarlo directamente sin problemas
15setTimeout(temp.saludar, 1000);
16// ✅ Funciona: "¡Hola! Soy Ana"
17
18// Las arrow functions capturan 'this' del constructor
19const funcionSaludo = temp.saludar;
20funcionSaludo(); // ✅ También funciona directamente

Esta es la solución más moderna. Las arrow functions NO tienen su propio this, heredan el del contexto donde se crearon (el constructor).

Solución 4: Variable 'self' (antigua, pero válida)

✅ Solución 4: Variable self (patrón antiguo)javascript
1class Temporizador {
2 constructor(nombre) {
3 this.nombre = nombre;
4
5 // Guardar referencia a 'this'
6 const self = this;
7
8 this.saludar = function() {
9 console.log(`¡Hola! Soy ${self.nombre}`);
10 };
11 }
12}
13
14const temp = new Temporizador("Ana");
15
16// ✅ Funciona porque 'self' mantiene la referencia
17setTimeout(temp.saludar, 1000);
18// ✅ "¡Hola! Soy Ana"

Este patrón era común antes de ES6. Hoy es mejor usar arrow functions, pero aún lo verás en código legacy.

📊 Comparación con Java

Java

  • this siempre apunta al objeto
  • ✅ No pierdes el contexto en callbacks
  • ✅ Comportamiento predecible
  • ❌ Menos flexible

JavaScript

  • ⚠️ this depende de cómo se llama
  • ❌ Pierdes el contexto en callbacks
  • ❌ Debes usar bind() o arrow functions
  • ✅ Más flexible para programación funcional

Recomendación

Para evitar problemas con this, la mejor práctica es usar arrow functions como propiedades de clase (Solución 3) cuando definas métodos que se usarán como callbacks.

6. Métodos Estáticos

Los métodos estáticos pertenecen a la clase, no a las instancias. Se llaman directamente sobre la clase.

Similar a Java: Se usa la palabra clave static.

Métodos estáticosjavascript
1class Matematicas {
2 // Método estático
3 static sumar(a, b) {
4 return a + b;
5 }
6
7 static factorial(n) {
8 if (n <= 1) return 1;
9 return n * this.factorial(n - 1);
10 }
11
12 // Constante estática
13 static PI = 3.14159;
14}
15
16// Usar métodos estáticos (sobre la clase, NO sobre instancias)
17console.log(Matematicas.sumar(5, 3)); // 8
18console.log(Matematicas.factorial(5)); // 120
19console.log(Matematicas.PI); // 3.14159
20
21// ❌ ERROR: No puedes llamar métodos estáticos desde instancias
22const mat = new Matematicas();
23// mat.sumar(2, 3); // ❌ Esto dará error

¿Cuándo usar métodos estáticos?

  • Utilidades que no dependen de datos de instancia
  • Métodos factory (para crear instancias)
  • Constantes compartidas

7. Herencia

La herencia permite crear clases basadas en otras clases. Usamos extends para heredar y super para llamar al constructor de la clase padre.

Similar a Java: Mismas palabras clave, misma sintaxis.

Herencia con extends y superjavascript
1// Clase padre
2class Animal {
3 constructor(nombre) {
4 this.nombre = nombre;
5 }
6
7 hacerSonido() {
8 console.log(`${this.nombre} hace un sonido`);
9 }
10}
11
12// Clase hija
13class Perro extends Animal {
14 constructor(nombre, raza) {
15 super(nombre); // Llama al constructor del padre
16 this.raza = raza;
17 }
18
19 // Sobrescribir método
20 hacerSonido() {
21 console.log(`${this.nombre} ladra: ¡Guau guau!`);
22 }
23
24 // Método propio
25 mostrarRaza() {
26 console.log(`${this.nombre} es un ${this.raza}`);
27 }
28}
29
30const miPerro = new Perro("Rex", "Labrador");
31miPerro.hacerSonido(); // Rex ladra: ¡Guau guau!
32miPerro.mostrarRaza(); // Rex es un Labrador

Puedes

  • Heredar de una clase
  • Sobrescribir métodos
  • Añadir nuevos métodos
  • Usar super para acceder al padre

No puedes

  • Herencia múltiple (solo una clase padre)
  • Modificadores de acceso reales (public, private)
  • Sobrecarga de métodos

Organizar Clases en Archivos Separados

Al igual que en Java, en JavaScript puedes organizar tus clases en archivos separados usando export e import.

Esto hace tu código más organizado, reutilizable y fácil de mantener.

Java

Java - package e importjava
1// Animal.java
2package animales;
3
4public class Animal {
5 protected String nombre;
6
7 public Animal(String nombre) {
8 this.nombre = nombre;
9 }
10}
11
12// Perro.java
13package animales;
14import animales.Animal;
15
16public class Perro extends Animal {
17 public Perro(String nombre) {
18 super(nombre);
19 }
20}

JavaScript

JavaScript - export e importjavascript
1// Animal.js
2export class Animal {
3 constructor(nombre) {
4 this.nombre = nombre;
5 }
6}
7
8// Perro.js
9import { Animal } from './Animal.js';
10
11export class Perro extends Animal {
12 constructor(nombre) {
13 super(nombre);
14 }
15}

Ejemplo Completo

📄 Animal.js
Animal.js - Clase padrejavascript
1// Exportar la clase Animal
2export class Animal {
3 constructor(nombre) {
4 this.nombre = nombre;
5 }
6
7 hacerSonido() {
8 console.log(`${this.nombre} hace un sonido`);
9 }
10}
11
12// También puedes exportar por defecto
13// export default Animal;
📄 Perro.js
Perro.js - Clase hijajavascript
1// Importar Animal desde Animal.js
2import { Animal } from './Animal.js';
3
4export class Perro extends Animal {
5 constructor(nombre, raza) {
6 super(nombre);
7 this.raza = raza;
8 }
9
10 hacerSonido() {
11 console.log(`${this.nombre} ladra: ¡Guau guau!`);
12 }
13}
📄 main.js
main.js - Usando las clasesjavascript
1// Importar las clases que necesitamos
2import { Animal } from './Animal.js';
3import { Perro } from './Perro.js';
4
5// Usar las clases
6const miAnimal = new Animal("Genérico");
7miAnimal.hacerSonido(); // "Genérico hace un sonido"
8
9const miPerro = new Perro("Rex", "Labrador");
10miPerro.hacerSonido(); // "Rex ladra: ¡Guau guau!"

Named Exports (Recomendado)

Puedes exportar múltiples clases desde un archivo:

Múltiples exportsjavascript
1// animales.js
2export class Perro { }
3export class Gato { }
4export class Pajaro { }
5
6// main.js
7import { Perro, Gato } from './animales.js';

Default Export

Un solo export principal por archivo:

Export por defectojavascript
1// Animal.js
2export default class Animal { }
3
4// main.js
5import Animal from './Animal.js';
6// Puedes usar cualquier nombre
7import MiAnimal from './Animal.js';

Importante: Extensión .js

En JavaScript, debes incluir la extensión .js en los imports. En Java no se incluye la extensión .java.

8. Diferencias Clave con Java

Campos Privados con #

JavaScript no tenía campos privados reales hasta ES2022. Ahora puedes usar # para crear campos verdaderamente privados:

Campos privados con #javascript
1class Usuario {
2 #contraseña; // Campo privado (con #)
3
4 constructor(nombre, contraseña) {
5 this.nombre = nombre; // Público
6 this.#contraseña = contraseña; // Privado
7 }
8
9 verificarContraseña(intento) {
10 return this.#contraseña === intento;
11 }
12}
13
14const user = new Usuario("Ana", "secreto123");
15console.log(user.nombre); // ✅ "Ana"
16// console.log(user.#contraseña); // ❌ ERROR: Campo privado

No hay Sobrecarga de Métodos

En Java puedes tener múltiples métodos con el mismo nombre pero diferentes parámetros. En JavaScript, el último método sobrescribe al anterior:

Solución a la falta de sobrecargajavascript
1class Calculadora {
2 // ❌ NO funciona como en Java
3 sumar(a, b) {
4 return a + b;
5 }
6
7 // Este sobrescribe al anterior
8 sumar(a, b, c) {
9 return a + b + c;
10 }
11}
12
13const calc = new Calculadora();
14// calc.sumar(2, 3); // ❌ Error: falta el tercer parámetro
15
16// ✅ Solución: Usar parámetros opcionales
17class CalculadoraMejor {
18 sumar(a, b, c = 0) {
19 return a + b + c;
20 }
21}
22
23const calc2 = new CalculadoraMejor();
24console.log(calc2.sumar(2, 3)); // 5
25console.log(calc2.sumar(2, 3, 4)); // 9

9. Ejercicios Prácticos

Ejercicio 1: Clase Producto

Crea una clase Producto con:

  • Constructor que reciba nombre y precio
  • Getter y setter para precio (validar que sea positivo)
  • Método aplicarDescuento(porcentaje)
  • Getter precioConIVA que calcule el precio con 21% IVA
Ver solución
Solución Ejercicio 1javascript
1class Producto {
2 constructor(nombre, precio) {
3 this.nombre = nombre;
4 this._precio = precio;
5 }
6
7 get precio() {
8 return this._precio;
9 }
10
11 set precio(valor) {
12 if (valor < 0) {
13 console.log("❌ El precio no puede ser negativo");
14 return;
15 }
16 this._precio = valor;
17 }
18
19 aplicarDescuento(porcentaje) {
20 if (porcentaje < 0 || porcentaje > 100) {
21 console.log("❌ Porcentaje inválido");
22 return;
23 }
24 this._precio -= this._precio * (porcentaje / 100);
25 }
26
27 get precioConIVA() {
28 return this._precio * 1.21;
29 }
30}
31
32// Probar
33const laptop = new Producto("Laptop", 1000);
34console.log(laptop.precio); // 1000
35laptop.aplicarDescuento(10); // Descuento del 10%
36console.log(laptop.precio); // 900
37console.log(laptop.precioConIVA); // 1089

Ejercicio 2: Herencia de Vehículos

Crea una clase Vehiculo y una clase Moto que herede de ella:

  • Vehiculo: constructor(marca, modelo), método info()
  • Moto: añade propiedad cilindrada, sobrescribe info()
  • Crea un método estático en Vehiculo llamado comparar(v1, v2)
Ver solución
Solución Ejercicio 2javascript
1class Vehiculo {
2 constructor(marca, modelo) {
3 this.marca = marca;
4 this.modelo = modelo;
5 }
6
7 info() {
8 return `${this.marca} ${this.modelo}`;
9 }
10
11 static comparar(v1, v2) {
12 return v1.marca === v2.marca && v1.modelo === v2.modelo;
13 }
14}
15
16class Moto extends Vehiculo {
17 constructor(marca, modelo, cilindrada) {
18 super(marca, modelo);
19 this.cilindrada = cilindrada;
20 }
21
22 info() {
23 return `${super.info()} - ${this.cilindrada}cc`;
24 }
25}
26
27// Probar
28const moto1 = new Moto("Yamaha", "R1", 1000);
29const moto2 = new Moto("Honda", "CBR", 600);
30
31console.log(moto1.info()); // "Yamaha R1 - 1000cc"
32console.log(Vehiculo.comparar(moto1, moto2)); // false

Ejercicio 3: Clase Wallet con propiedades privadas

Crea una clase Wallet (billetera) con:

  • Propiedad privada (convención) _saldo
  • Métodos depositar(cantidad) y retirar(cantidad)
  • Getter saldo que devuelva el saldo
  • El saldo nunca debe poder ser negativo
Ver solución
Solución Ejercicio 3javascript
1class Wallet {
2 constructor(saldoInicial = 0) {
3 this._saldo = saldoInicial >= 0 ? saldoInicial : 0;
4 }
5
6 depositar(cantidad) {
7 if (cantidad <= 0) {
8 console.log("❌ La cantidad debe ser positiva");
9 return false;
10 }
11 this._saldo += cantidad;
12 console.log(`✅ Depositado: $${cantidad}`);
13 return true;
14 }
15
16 retirar(cantidad) {
17 if (cantidad > this._saldo) {
18 console.log("❌ Saldo insuficiente");
19 return false;
20 }
21 this._saldo -= cantidad;
22 console.log(`✅ Retirado: $${cantidad}`);
23 return true;
24 }
25
26 get saldo() {
27 return this._saldo;
28 }
29}
30
31// Probar
32const miWallet = new Wallet(1000);
33miWallet.depositar(500); // ✅ Depositado: $500
34console.log(miWallet.saldo); // 1500
35miWallet.retirar(200); // ✅ Retirado: $200
36miWallet.retirar(2000); // ❌ Saldo insuficiente
37
38// Técnicamente podrías acceder a _saldo, pero por convención no deberías
39console.log(miWallet._saldo); // 1300 (funciona, pero es mala práctica)

Ejercicio 4: Factory con Métodos Estáticos

Crea una clase Usuario con métodos estáticos factory:

  • Método estático crearAdmin(nombre) que cree un usuario con rol "admin"
  • Método estático crearInvitado() que cree un usuario con nombre "Invitado"
Ver solución
Solución Ejercicio 4javascript
1class Usuario {
2 constructor(nombre, rol = "usuario") {
3 this.nombre = nombre;
4 this.rol = rol;
5 this.fechaCreacion = new Date();
6 }
7
8 // Factory methods
9 static crearAdmin(nombre) {
10 return new Usuario(nombre, "admin");
11 }
12
13 static crearInvitado() {
14 return new Usuario("Invitado", "invitado");
15 }
16
17 mostrarInfo() {
18 console.log(`Usuario: ${this.nombre}, Rol: ${this.rol}`);
19 }
20}
21
22// Probar
23const admin = Usuario.crearAdmin("María");
24const invitado = Usuario.crearInvitado();
25
26admin.mostrarInfo(); // Usuario: María, Rol: admin
27invitado.mostrarInfo(); // Usuario: Invitado, Rol: invitado