JavaScript
Tutorial JavaScript: Prototipos y cadena de prototipos
Descubre cómo funciona la herencia basada en objetos en JavaScript. Aprende sobre prototipos, cadenas de prototipos y manipulación de herencia para mejorar tu código.
Aprende JavaScript y certifícateFundamentos del sistema de prototipos: Herencia basada en objetos en JavaScript
JavaScript es un lenguaje basado en prototipos, lo que lo diferencia significativamente de otros lenguajes orientados a objetos como Java o C++. En lugar de utilizar clases como plantillas para crear objetos, JavaScript implementa la herencia a través de un mecanismo de cadena de prototipos que permite a los objetos heredar propiedades y métodos directamente de otros objetos.
El concepto de prototipo
En JavaScript, cada objeto tiene una propiedad interna llamada [[Prototype]]
(también conocida como __proto__
en navegadores antiguos) que apunta a otro objeto, su prototipo. Cuando intentamos acceder a una propiedad o método de un objeto, JavaScript primero busca en el propio objeto y, si no lo encuentra, continúa la búsqueda en su prototipo, luego en el prototipo del prototipo, y así sucesivamente hasta llegar al final de la cadena (generalmente Object.prototype
).
Esta estructura forma lo que conocemos como cadena de prototipos:
// Objeto literal básico
const animal = {
estaVivo: true,
comer() {
return "Comiendo...";
}
};
// Crear un objeto que hereda de animal
const perro = Object.create(animal);
perro.ladrar = function() {
return "Guau!";
};
console.log(perro.estaVivo); // true (heredado de animal)
console.log(perro.comer()); // "Comiendo..." (heredado de animal)
console.log(perro.ladrar()); // "Guau!" (propio de perro)
En este ejemplo, perro
hereda las propiedades y métodos de animal
a través de la cadena de prototipos. Cuando ejecutamos perro.estaVivo
, JavaScript no encuentra esta propiedad en perro
, así que busca en su prototipo (animal
) y la encuentra allí.
Accediendo y modificando prototipos
Existen varias formas de trabajar con prototipos en JavaScript:
- 1. Usando Object.create() - La forma más directa de establecer el prototipo de un objeto:
const vehiculo = {
tieneRuedas: true,
mover() {
return "Vehículo en movimiento";
}
};
const coche = Object.create(vehiculo);
coche.puertas = 4;
console.log(coche.tieneRuedas); // true
console.log(coche.mover()); // "Vehículo en movimiento"
- 2. Usando Object.getPrototypeOf() y Object.setPrototypeOf() - Para obtener o modificar el prototipo de un objeto existente:
// Obtener el prototipo
const prototipoCoche = Object.getPrototypeOf(coche);
console.log(prototipoCoche === vehiculo); // true
// Cambiar el prototipo (no recomendado por razones de rendimiento)
const nuevoVehiculo = {
volar() {
return "Volando...";
}
};
Object.setPrototypeOf(coche, nuevoVehiculo); // Cambia el prototipo de coche
console.log(coche.volar()); // "Volando..."
console.log(coche.tieneRuedas); // undefined (ya no hereda de vehiculo)
- 3. La propiedad prototype en funciones constructoras - Método tradicional antes de ES6:
function Animal(nombre) {
this.nombre = nombre;
}
// Añadir métodos al prototipo
Animal.prototype.respirar = function() {
return `${this.nombre} está respirando`;
};
// Crear instancia
const gato = new Animal("Felix");
console.log(gato.respirar()); // "Felix está respirando"
Cómo funciona la búsqueda en la cadena de prototipos
Cuando intentamos acceder a una propiedad o método, JavaScript sigue estos pasos:
- Busca la propiedad en el objeto mismo
- Si no la encuentra, busca en el prototipo del objeto
- Si aún no la encuentra, busca en el prototipo del prototipo
- Continúa hasta llegar a
Object.prototype
- Si no encuentra la propiedad en ningún lugar, devuelve
undefined
Este proceso se conoce como resolución de propiedades y es fundamental para entender cómo funciona la herencia en JavaScript:
// Ejemplo de cadena de prototipos extendida
const objetoBase = { a: 1 };
const objetoMedio = Object.create(objetoBase);
objetoMedio.b = 2;
const objetoFinal = Object.create(objetoMedio);
objetoFinal.c = 3;
console.log(objetoFinal.a); // 1 (heredado de objetoBase)
console.log(objetoFinal.b); // 2 (heredado de objetoMedio)
console.log(objetoFinal.c); // 3 (propio)
console.log(objetoFinal.d); // undefined (no existe en la cadena)
Object.prototype: el final de la cadena
Todos los objetos en JavaScript heredan de Object.prototype
, a menos que se especifique explícitamente lo contrario. Este objeto contiene métodos útiles como toString()
, hasOwnProperty()
y valueOf()
:
const miObjeto = { nombre: "Ejemplo" };
// Estos métodos están disponibles en todos los objetos
console.log(miObjeto.toString()); // "[object Object]"
console.log(miObjeto.hasOwnProperty("nombre")); // true
console.log(miObjeto.hasOwnProperty("edad")); // false
// Podemos crear un objeto sin prototipo
const objetoSinProto = Object.create(null);
console.log(objetoSinProto.toString); // undefined
Propiedades propias vs. heredadas
Es importante distinguir entre las propiedades que pertenecen directamente a un objeto y las que hereda:
const prototipo = { compartido: "Soy compartido" };
const instancia = Object.create(prototipo);
instancia.propio = "Soy único";
// Verificar propiedades propias
console.log(instancia.hasOwnProperty("propio")); // true
console.log(instancia.hasOwnProperty("compartido")); // false
// Iterar solo propiedades propias
for (let prop in instancia) {
if (instancia.hasOwnProperty(prop)) {
console.log(`Propiedad propia: ${prop}`);
} else {
console.log(`Propiedad heredada: ${prop}`);
}
}
// Obtener todas las propiedades propias
console.log(Object.keys(instancia)); // ["propio"] (solo propiedades propias)
Herencia múltiple y mixins
JavaScript no soporta la herencia múltiple directamente, pero podemos simularla mediante mixins (mezclas de objetos):
// Objetos con funcionalidades específicas
const nadador = {
nadar() {
return "Nadando...";
}
};
const volador = {
volar() {
return "Volando...";
}
};
// Función para combinar funcionalidades
function crearAnfibioVolador(nombre) {
// Crear objeto base
const animal = {
nombre,
comer() {
return `${this.nombre} está comiendo`;
}
};
// Añadir funcionalidades de otros objetos
Object.assign(animal, nadador, volador);
return animal;
}
const pato = crearAnfibioVolador("Donald");
console.log(pato.comer()); // "Donald está comiendo"
console.log(pato.nadar()); // "Nadando..."
console.log(pato.volar()); // "Volando..."
Ventajas del sistema de prototipos
El sistema de prototipos de JavaScript ofrece varias ventajas:
- Flexibilidad: Podemos modificar el comportamiento de objetos existentes en tiempo de ejecución.
- Eficiencia de memoria: Los métodos compartidos a través de prototipos solo se almacenan una vez en memoria.
- Dinamismo: Permite crear patrones de diseño adaptables y extensibles.
// Ejemplo de eficiencia de memoria
function Empleado(nombre) {
this.nombre = nombre;
}
// Un solo método compartido por todas las instancias
Empleado.prototype.saludar = function() {
return `Hola, soy ${this.nombre}`;
};
// Crear múltiples instancias
const empleado1 = new Empleado("Ana");
const empleado2 = new Empleado("Carlos");
const empleado3 = new Empleado("Elena");
// Todas comparten la misma implementación del método
console.log(empleado1.saludar === empleado2.saludar); // true
El sistema de prototipos es la base sobre la cual se construyen características más modernas de JavaScript, como las clases introducidas en ES6, que son en realidad una abstracción sintáctica (azúcar sintáctico) sobre este mecanismo fundamental de prototipos.
Manipulación y extensión de prototipos: Técnicas para modificar comportamiento heredado
La verdadera potencia del sistema de prototipos en JavaScript radica en su flexibilidad para modificar y extender el comportamiento de objetos existentes. A diferencia de lenguajes con sistemas de clases más rígidos, JavaScript permite alterar el comportamiento heredado de manera dinámica, incluso después de que los objetos han sido creados.
Modificación de prototipos nativos
JavaScript permite modificar los prototipos de objetos integrados como Array
, String
o Object
. Esta capacidad, aunque poderosa, debe usarse con precaución:
// Añadir un método a todos los arrays
Array.prototype.first = function() {
return this.length > 0 ? this[0] : undefined;
};
const numbers = [5, 10, 15];
console.log(numbers.first()); // 5
// Añadir un método a todas las cadenas
String.prototype.capitalize = function() {
return this.charAt(0).toUpperCase() + this.slice(1);
};
console.log("javascript".capitalize()); // "Javascript"
Esta técnica, conocida como monkey patching, permite extender la funcionalidad de tipos nativos. Sin embargo, presenta algunos riesgos importantes:
- 1. Colisiones de nombres: Si JavaScript añade en el futuro un método con el mismo nombre, tu código podría romperse.
- 2. Compatibilidad: Puede causar problemas con bibliotecas de terceros que no esperan estos métodos adicionales.
- 3. Mantenibilidad: Dificulta el seguimiento del origen de los métodos.
Por estas razones, la modificación de prototipos nativos generalmente se considera una mala práctica en código de producción, aunque puede ser útil para experimentación o polyfills temporales.
Polyfills: Implementando funcionalidad faltante
Un uso legítimo de la modificación de prototipos es la implementación de polyfills - código que proporciona funcionalidades modernas en entornos antiguos:
// Polyfill para Array.prototype.includes (añadido en ES7)
if (!Array.prototype.includes) {
Array.prototype.includes = function(element, fromIndex = 0) {
if (fromIndex < 0) {
fromIndex = Math.max(0, this.length + fromIndex);
}
for (let i = fromIndex; i < this.length; i++) {
if (this[i] === element || (Number.isNaN(this[i]) && Number.isNaN(element))) {
return true;
}
}
return false;
};
}
// Ahora podemos usar includes() incluso en navegadores antiguos
const fruits = ["apple", "banana", "orange"];
console.log(fruits.includes("banana")); // true
Los polyfills siguen una estructura común: verifican si la funcionalidad ya existe antes de implementarla, evitando sobrescribir implementaciones nativas.
Extensión de objetos mediante prototipos
Podemos extender objetos existentes añadiendo métodos a su cadena de prototipos:
// Función constructora base
function Vehículo(marca) {
this.marca = marca;
this.encendido = false;
}
// Métodos en el prototipo
Vehículo.prototype.encender = function() {
this.encendido = true;
return `${this.marca} encendido`;
};
Vehículo.prototype.apagar = function() {
this.encendido = false;
return `${this.marca} apagado`;
};
// Crear constructor derivado
function Coche(marca, modelo) {
// Llamar al constructor padre
Vehículo.call(this, marca);
this.modelo = modelo;
}
// Establecer la herencia prototípica
Coche.prototype = Object.create(Vehículo.prototype);
// Restaurar el constructor
Coche.prototype.constructor = Coche;
// Añadir métodos específicos de Coche
Coche.prototype.acelerar = function() {
if (!this.encendido) {
return `Primero debes encender el ${this.marca} ${this.modelo}`;
}
return `${this.marca} ${this.modelo} acelerando`;
};
// Crear instancia
const miCoche = new Coche("Toyota", "Corolla");
console.log(miCoche.encender()); // "Toyota encendido"
console.log(miCoche.acelerar()); // "Toyota Corolla acelerando"
Este patrón de herencia prototípica era común antes de ES6 y sigue siendo la base del funcionamiento interno de las clases en JavaScript moderno.
Sobrescritura de métodos heredados
Una técnica fundamental en la programación orientada a objetos es la capacidad de sobrescribir métodos heredados:
// Extender el método encender para Coche
Coche.prototype.encender = function() {
// Llamar al método original del prototipo padre
const resultadoOriginal = Vehículo.prototype.encender.call(this);
// Añadir comportamiento adicional
return `${resultadoOriginal} - Sistema de infoentretenimiento iniciado`;
};
console.log(miCoche.encender());
// "Toyota encendido - Sistema de infoentretenimiento iniciado"
La sobrescritura permite especializar el comportamiento de objetos derivados mientras se mantiene la funcionalidad base. El uso de call()
para invocar el método del prototipo padre es similar al super
en lenguajes basados en clases.
Mixins: Composición de comportamientos
Los mixins ofrecen una alternativa a la herencia para reutilizar código, permitiendo componer objetos con múltiples comportamientos:
// Definir comportamientos como objetos independientes
const conRadio = {
encenderRadio() {
return "Radio encendida";
},
cambiarEstacion(estacion) {
return `Sintonizando estación ${estacion}`;
}
};
const conNavegacion = {
iniciarGPS() {
return "GPS iniciado";
},
navegarA(destino) {
return `Navegando hacia ${destino}`;
}
};
// Función para aplicar mixins a un prototipo
function aplicarMixins(destino, ...mixins) {
mixins.forEach(mixin => {
Object.getOwnPropertyNames(mixin).forEach(prop => {
destino.prototype[prop] = mixin[prop];
});
});
}
// Aplicar mixins al prototipo de Coche
aplicarMixins(Coche, conRadio, conNavegacion);
// Ahora el coche tiene todas estas capacidades
console.log(miCoche.encenderRadio()); // "Radio encendida"
console.log(miCoche.navegarA("Madrid")); // "Navegando hacia Madrid"
Los mixins son especialmente útiles cuando necesitamos comportamientos compartidos entre objetos que no tienen una relación jerárquica clara.
Propiedades de acceso (getters y setters)
JavaScript permite definir propiedades con comportamientos personalizados mediante getters y setters:
// Añadir una propiedad calculada al prototipo
Object.defineProperty(Coche.prototype, "descripcion", {
get: function() {
return `${this.marca} ${this.modelo} (${this.encendido ? "Encendido" : "Apagado"})`;
}
});
// Añadir una propiedad con validación
Object.defineProperty(Coche.prototype, "velocidad", {
get: function() {
return this._velocidad || 0;
},
set: function(valor) {
if (valor < 0) {
throw new Error("La velocidad no puede ser negativa");
}
this._velocidad = valor;
}
});
miCoche.velocidad = 120;
console.log(miCoche.descripcion); // "Toyota Corolla (Encendido)"
console.log(miCoche.velocidad); // 120
// Esto lanzaría un error:
// miCoche.velocidad = -10;
Los getters y setters permiten encapsular la lógica de acceso a propiedades, añadiendo validación, cálculos o efectos secundarios cuando se leen o modifican valores.
Métodos de Object para manipular prototipos
JavaScript proporciona varios métodos para trabajar con prototipos de manera más controlada:
- 1. Object.create() - Crea un nuevo objeto con el prototipo especificado:
const vehiculoBase = {
tipo: "vehículo",
obtenerDescripcion() {
return `Soy un ${this.tipo}`;
}
};
const motocicleta = Object.create(vehiculoBase);
motocicleta.tipo = "motocicleta";
motocicleta.ruedas = 2;
console.log(motocicleta.obtenerDescripcion()); // "Soy un motocicleta"
- 2. Object.preventExtensions() - Evita que se añadan nuevas propiedades a un objeto:
const configuracion = { tema: "oscuro", idioma: "es" };
Object.preventExtensions(configuracion);
// Esto no tendrá efecto
configuracion.notificaciones = true;
console.log(configuracion.notificaciones); // undefined
- 3. Object.seal() - Impide añadir o eliminar propiedades, pero permite modificar las existentes:
const usuario = { nombre: "Ana", nivel: 1 };
Object.seal(usuario);
// Podemos modificar propiedades existentes
usuario.nivel = 2;
console.log(usuario.nivel); // 2
// Pero no añadir nuevas o eliminar existentes
usuario.premium = true; // No tendrá efecto
delete usuario.nombre; // No tendrá efecto
- 4. Object.freeze() - Hace un objeto completamente inmutable:
const constantes = { PI: 3.14159, GRAVEDAD: 9.8 };
Object.freeze(constantes);
// Ninguna modificación tendrá efecto
constantes.PI = 3.14; // No cambiará
console.log(constantes.PI); // 3.14159
Detección de propiedades en la cadena de prototipos
Es importante distinguir entre propiedades propias y heredadas:
const animal = { estaVivo: true };
const perro = Object.create(animal);
perro.ladra = true;
// Comprobar si una propiedad existe (propia o heredada)
console.log("estaVivo" in perro); // true
console.log("ladra" in perro); // true
// Comprobar si una propiedad es propia
console.log(perro.hasOwnProperty("estaVivo")); // false
console.log(perro.hasOwnProperty("ladra")); // true
// Obtener solo propiedades propias
console.log(Object.keys(perro)); // ["ladra"]
// Obtener propiedades propias y no enumerables
console.log(Object.getOwnPropertyNames(perro)); // ["ladra"]
Estas herramientas son esenciales para trabajar con objetos de manera segura y predecible, especialmente cuando manipulamos prototipos.
Patrones avanzados con prototipos
La flexibilidad de los prototipos permite implementar patrones de diseño sofisticados:
- Patrón Fábrica con prototipos compartidos:
// Prototipo compartido
const enemyPrototype = {
attack() {
return `${this.name} attacks with ${this.damage} damage`;
},
takeDamage(amount) {
this.health -= amount;
return this.health <= 0 ? `${this.name} is defeated!` : `${this.name} has ${this.health} health left`;
}
};
// Fábrica de enemigos
function createEnemy(type) {
let enemy;
if (type === "goblin") {
enemy = Object.create(enemyPrototype);
enemy.name = "Goblin";
enemy.damage = 10;
enemy.health = 30;
} else if (type === "dragon") {
enemy = Object.create(enemyPrototype);
enemy.name = "Dragon";
enemy.damage = 50;
enemy.health = 200;
// Método específico para dragones
enemy.breatheFire = function() {
return `${this.name} breathes fire!`;
};
}
return enemy;
}
const goblin = createEnemy("goblin");
const dragon = createEnemy("dragon");
console.log(goblin.attack()); // "Goblin attacks with 10 damage"
console.log(dragon.attack()); // "Dragon attacks with 50 damage"
console.log(dragon.breatheFire()); // "Dragon breathes fire!"
- Delegación de comportamiento:
// Objeto con comportamiento compartido
const calculadoraBase = {
sumar(a, b) { return a + b; },
restar(a, b) { return a - b; },
multiplicar(a, b) { return a * b; },
dividir(a, b) { return b !== 0 ? a / b : "Error: División por cero"; }
};
// Objeto que delega operaciones a calculadoraBase
const calculadoraCientifica = Object.create(calculadoraBase);
// Añadir métodos específicos
calculadoraCientifica.potencia = function(base, exponente) {
return Math.pow(base, exponente);
};
calculadoraCientifica.raizCuadrada = function(numero) {
return Math.sqrt(numero);
};
// Uso mediante delegación
console.log(calculadoraCientifica.sumar(5, 3)); // 8 (delegado a calculadoraBase)
console.log(calculadoraCientifica.potencia(2, 3)); // 8 (propio)
La manipulación y extensión de prototipos en JavaScript ofrece un sistema flexible para modificar comportamientos heredados, permitiendo patrones de diseño que serían difíciles de implementar en lenguajes con sistemas de clases más rígidos. Sin embargo, esta flexibilidad requiere disciplina para evitar código difícil de mantener o depurar.
Prototipos en JavaScript moderno: Integración con clases y patrones de diseño contemporáneos
JavaScript ha evolucionado significativamente con la introducción de ES6 (ECMAScript 2015) y versiones posteriores, ofreciendo una sintaxis moderna que facilita el trabajo con objetos y prototipos. Sin embargo, es crucial entender que las clases en JavaScript son simplemente una abstracción sintáctica (azúcar sintáctico) sobre el sistema de prototipos subyacente.
Clases como abstracción de prototipos
La sintaxis de clases introducida en ES6 proporciona una forma más familiar y estructurada de trabajar con prototipos:
// Implementación con prototipos (pre-ES6)
function Animal(name) {
this.name = name;
}
Animal.prototype.makeSound = function() {
return `${this.name} makes a sound`;
};
// Equivalente usando sintaxis de clase (ES6+)
class Animal {
constructor(name) {
this.name = name;
}
makeSound() {
return `${this.name} makes a sound`;
}
}
Bajo el capó, JavaScript sigue utilizando prototipos. Cuando definimos métodos en una clase, estos se añaden al prototipo del objeto, no a cada instancia individual:
const dog = new Animal("Rex");
const cat = new Animal("Whiskers");
// Ambos métodos apuntan a la misma función en el prototipo
console.log(dog.makeSound === cat.makeSound); // true
Herencia de clases y cadena de prototipos
La palabra clave extends
simplifica la implementación de herencia prototípica:
class Dog extends Animal {
constructor(name, breed) {
super(name); // Llama al constructor de Animal
this.breed = breed;
}
makeSound() {
return `${this.name} barks`;
}
fetch() {
return `${this.name} fetches the ball`;
}
}
const rex = new Dog("Rex", "German Shepherd");
console.log(rex.makeSound()); // "Rex barks"
console.log(rex.fetch()); // "Rex fetches the ball"
En este ejemplo, Dog.prototype
hereda de Animal.prototype
, creando la misma cadena de prototipos que habríamos configurado manualmente en JavaScript pre-ES6. La palabra clave super
proporciona acceso al constructor y métodos de la clase padre.
Métodos estáticos y prototipos
Los métodos estáticos pertenecen a la clase en sí, no a sus instancias, y no forman parte de la cadena de prototipos:
class MathUtils {
constructor() {
throw new Error("This utility class cannot be instantiated");
}
static sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
static average(...numbers) {
return this.sum(...numbers) / numbers.length;
}
}
console.log(MathUtils.sum(1, 2, 3, 4)); // 10
console.log(MathUtils.average(1, 2, 3, 4)); // 2.5
// No podemos acceder a métodos estáticos desde instancias
// const utils = new MathUtils(); // Error
Internamente, los métodos estáticos se implementan como propiedades de la función constructora, no de su prototipo.
Propiedades privadas y encapsulación
JavaScript moderno (desde ES2022) soporta propiedades privadas usando el prefijo #
, lo que permite una mejor encapsulación:
class BankAccount {
#balance = 0;
#transactionHistory = [];
constructor(initialDeposit = 0) {
if (initialDeposit > 0) {
this.deposit(initialDeposit);
}
}
deposit(amount) {
if (amount <= 0) throw new Error("Deposit amount must be positive");
this.#balance += amount;
this.#addTransaction("deposit", amount);
return this.#balance;
}
withdraw(amount) {
if (amount <= 0) throw new Error("Withdrawal amount must be positive");
if (amount > this.#balance) throw new Error("Insufficient funds");
this.#balance -= amount;
this.#addTransaction("withdrawal", amount);
return this.#balance;
}
#addTransaction(type, amount) {
this.#transactionHistory.push({
type,
amount,
date: new Date()
});
}
get balance() {
return this.#balance;
}
get accountStatement() {
return [...this.#transactionHistory];
}
}
const account = new BankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.balance); // 1300
// console.log(account.#balance); // Error: private field
Las propiedades privadas no son accesibles fuera de la clase, ni siquiera para clases derivadas, proporcionando un nivel de encapsulación más fuerte que las convenciones anteriores (como prefijos con guion bajo).
Patrones de diseño modernos con prototipos y clases
La sintaxis moderna facilita la implementación de patrones de diseño comunes:
- 1. Patrón Singleton - Garantiza una única instancia de una clase:
class DatabaseConnection {
static #instance = null;
constructor(host, username, password) {
if (DatabaseConnection.#instance) {
return DatabaseConnection.#instance;
}
this.host = host;
this.username = username;
this.password = password;
this.connected = false;
DatabaseConnection.#instance = this;
}
connect() {
if (this.connected) return "Already connected";
this.connected = true;
return `Connected to ${this.host} as ${this.username}`;
}
disconnect() {
if (!this.connected) return "Not connected";
this.connected = false;
return "Disconnected";
}
}
// Ambas variables referencian la misma instancia
const db1 = new DatabaseConnection("localhost", "admin", "password123");
const db2 = new DatabaseConnection("otherhost", "root", "different"); // Ignorado
console.log(db1 === db2); // true
console.log(db1.host); // "localhost" (de la primera instancia)
- 2. Patrón Factory - Centraliza la creación de objetos:
// Clase base
class Vehicle {
constructor(make, model) {
this.make = make;
this.model = model;
this.type = "generic";
}
getInfo() {
return `${this.make} ${this.model} (${this.type})`;
}
}
// Clases específicas
class Car extends Vehicle {
constructor(make, model, doors) {
super(make, model);
this.doors = doors;
this.type = "car";
}
}
class Motorcycle extends Vehicle {
constructor(make, model, engineSize) {
super(make, model);
this.engineSize = engineSize;
this.type = "motorcycle";
}
}
// Factory
class VehicleFactory {
static createVehicle(type, ...args) {
switch (type.toLowerCase()) {
case "car":
return new Car(...args);
case "motorcycle":
return new Motorcycle(...args);
default:
throw new Error(`Unknown vehicle type: ${type}`);
}
}
}
// Uso
const honda = VehicleFactory.createVehicle("motorcycle", "Honda", "CBR600RR", "600cc");
const toyota = VehicleFactory.createVehicle("car", "Toyota", "Corolla", 4);
console.log(honda.getInfo()); // "Honda CBR600RR (motorcycle)"
console.log(toyota.getInfo()); // "Toyota Corolla (car)"
- 3. Patrón Observer - Implementa el patrón publicador/suscriptor:
class EventEmitter {
#events = new Map();
on(eventName, listener) {
if (!this.#events.has(eventName)) {
this.#events.set(eventName, []);
}
this.#events.get(eventName).push(listener);
return this; // Para encadenamiento
}
off(eventName, listener) {
if (!this.#events.has(eventName)) return this;
const listeners = this.#events.get(eventName);
const index = listeners.indexOf(listener);
if (index !== -1) {
listeners.splice(index, 1);
}
return this;
}
emit(eventName, ...args) {
if (!this.#events.has(eventName)) return false;
const listeners = this.#events.get(eventName);
listeners.forEach(listener => listener(...args));
return true;
}
once(eventName, listener) {
const onceWrapper = (...args) => {
listener(...args);
this.off(eventName, onceWrapper);
};
return this.on(eventName, onceWrapper);
}
}
// Uso
const chat = new EventEmitter();
function messageHandler(user, message) {
console.log(`${user}: ${message}`);
}
chat.on("message", messageHandler);
chat.emit("message", "Alice", "Hello everyone!"); // "Alice: Hello everyone!"
// Evento que se ejecuta solo una vez
chat.once("join", user => console.log(`${user} joined the chat`));
chat.emit("join", "Bob"); // "Bob joined the chat"
chat.emit("join", "Charlie"); // No hace nada, el listener ya se eliminó
Mixins con sintaxis de clase
Los mixins siguen siendo útiles en JavaScript moderno para compartir funcionalidad entre clases no relacionadas:
// Definir mixins como objetos con métodos
const TimestampMixin = {
getCreatedAt() {
return this.createdAt;
},
setCreatedAt() {
this.createdAt = new Date();
return this;
}
};
const ValidationMixin = {
validate(schema) {
const errors = [];
for (const [field, rules] of Object.entries(schema)) {
if (rules.required && !this[field]) {
errors.push(`${field} is required`);
}
if (rules.minLength && this[field]?.length < rules.minLength) {
errors.push(`${field} must be at least ${rules.minLength} characters`);
}
}
this.isValid = errors.length === 0;
this.errors = errors;
return this.isValid;
}
};
// Función para aplicar mixins a una clase
function applyMixins(targetClass, ...mixins) {
mixins.forEach(mixin => {
Object.getOwnPropertyNames(mixin).forEach(prop => {
Object.defineProperty(
targetClass.prototype,
prop,
Object.getOwnPropertyDescriptor(mixin, prop)
);
});
});
}
// Aplicar mixins a una clase
class User {
constructor(username, email) {
this.username = username;
this.email = email;
this.setCreatedAt();
}
}
applyMixins(User, TimestampMixin, ValidationMixin);
// Uso
const user = new User("alice", "alice@example.com");
console.log(user.getCreatedAt()); // Date object
const isValid = user.validate({
username: { required: true, minLength: 3 },
email: { required: true }
});
console.log(isValid); // true
Composición sobre herencia
En JavaScript moderno, se favorece cada vez más la composición sobre la herencia. La composición permite construir objetos complejos combinando objetos más simples:
// Componentes funcionales
const hasPosition = (state) => ({
getPosition() {
return { x: state.x, y: state.y };
},
setPosition(x, y) {
state.x = x;
state.y = y;
},
move(dx, dy) {
state.x += dx;
state.y += dy;
}
});
const hasHealth = (state) => ({
getHealth() {
return state.health;
},
takeDamage(amount) {
state.health = Math.max(0, state.health - amount);
return state.health === 0;
},
heal(amount) {
state.health = Math.min(state.maxHealth, state.health + amount);
}
});
const canAttack = (state) => ({
attack(target) {
if (target.takeDamage) {
const isDead = target.takeDamage(state.attackPower);
return isDead ? "Target defeated!" : "Hit!";
}
return "Invalid target";
}
});
// Fábrica de entidades
function createGameEntity(name, options = {}) {
// Estado base
const state = {
name,
x: options.x || 0,
y: options.y || 0,
health: options.health || 100,
maxHealth: options.maxHealth || 100,
attackPower: options.attackPower || 10
};
// Componer la entidad con los comportamientos necesarios
return {
getName: () => state.name,
...hasPosition(state),
...hasHealth(state),
...canAttack(state)
};
}
// Uso
const player = createGameEntity("Hero", { health: 200, attackPower: 25 });
const enemy = createGameEntity("Goblin", { x: 10, y: 5, health: 50 });
player.move(5, 5);
console.log(player.getPosition()); // { x: 5, y: 5 }
console.log(player.attack(enemy)); // "Hit!"
console.log(enemy.getHealth()); // 25
Este enfoque de composición es más flexible que la herencia tradicional y evita muchos de los problemas asociados con jerarquías de clases profundas.
Prototipos y módulos ES6
Los módulos ES6 proporcionan una forma de organizar y encapsular código relacionado:
// file: shape.js
export class Shape {
constructor(color) {
this.color = color;
}
getColor() {
return this.color;
}
}
// file: circle.js
import { Shape } from './shape.js';
export class Circle extends Shape {
constructor(color, radius) {
super(color);
this.radius = radius;
}
getArea() {
return Math.PI * this.radius * this.radius;
}
getCircumference() {
return 2 * Math.PI * this.radius;
}
}
// file: main.js
import { Circle } from './circle.js';
const redCircle = new Circle("red", 5);
console.log(redCircle.getColor()); // "red"
console.log(redCircle.getArea()); // 78.54...
Los módulos permiten una mejor organización del código y evitan la contaminación del espacio de nombres global, un problema común en JavaScript anterior.
Patrones asíncronos con prototipos
La combinación de prototipos con características asíncronas modernas como async/await
permite patrones elegantes:
class DataService {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async fetchData(endpoint) {
try {
const response = await fetch(`${this.baseUrl}/${endpoint}`);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`Failed to fetch ${endpoint}:`, error);
throw error;
}
}
}
// Servicios específicos que heredan la funcionalidad base
class UserService extends DataService {
constructor(baseUrl) {
super(baseUrl);
}
async getUsers() {
return this.fetchData('users');
}
async getUserById(id) {
return this.fetchData(`users/${id}`);
}
}
class ProductService extends DataService {
constructor(baseUrl) {
super(baseUrl);
}
async getProducts(category) {
const products = await this.fetchData('products');
return category ? products.filter(p => p.category === category) : products;
}
}
// Uso
async function init() {
const userService = new UserService('https://api.example.com');
const productService = new ProductService('https://api.example.com');
try {
const users = await userService.getUsers();
const electronics = await productService.getProducts('electronics');
console.log(`Loaded ${users.length} users`);
console.log(`Found ${electronics.length} electronic products`);
} catch (error) {
console.error("Failed to initialize:", error);
}
}
init();
Rendimiento y optimización
A pesar de la sintaxis moderna, es importante recordar las implicaciones de rendimiento del sistema de prototipos:
- Acceso a propiedades: Cada acceso a una propiedad no encontrada en el objeto implica una búsqueda a través de la cadena de prototipos.
- Modificación de prototipos: Modificar prototipos de objetos existentes puede tener efectos en cascada y afectar el rendimiento.
- Creación de objetos: Los diferentes patrones de creación de objetos tienen distintas características de rendimiento.
// Comparación de rendimiento entre diferentes enfoques
// 1. Métodos en el prototipo (eficiente en memoria)
class EfficientShape {
constructor(x, y) {
this.x = x;
this.y = y;
}
move(dx, dy) {
this.x += dx;
this.y += dy;
}
}
// 2. Métodos en cada instancia (ineficiente en memoria)
function InefficientShape(x, y) {
this.x = x;
this.y = y;
// Este método se crea para cada instancia
this.move = function(dx, dy) {
this.x += dx;
this.y += dy;
};
}
// Prueba de memoria
const shapes1 = Array.from({ length: 10000 }, () => new EfficientShape(0, 0));
const shapes2 = Array.from({ length: 10000 }, () => new InefficientShape(0, 0));
// Las instancias de EfficientShape comparten el mismo método move
console.log(shapes1[0].move === shapes1[1].move); // true
// Cada instancia de InefficientShape tiene su propia copia del método
console.log(shapes2[0].move === shapes2[1].move); // false
Integración con APIs modernas
Las clases y prototipos de JavaScript se integran perfectamente con APIs modernas del navegador y Node.js:
// Extender elementos del DOM
class EnhancedElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
border: 1px solid #ccc;
padding: 16px;
}
</style>
<div>
<h2>${this.getAttribute('title') || 'No Title'}</h2>
<slot></slot>
</div>
`;
}
static get observedAttributes() {
return ['title'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'title' && this.shadowRoot) {
this.render();
}
}
}
// Registrar el componente personalizado
customElements.define('enhanced-box', EnhancedElement);
// Uso en HTML:
// <enhanced-box title="My Custom Element">
// <p>This is custom content</p>
// </enhanced-box>
El sistema de prototipos de JavaScript, combinado con la sintaxis moderna de clases, proporciona una base flexible y potente para el desarrollo de aplicaciones contemporáneas. Aunque la sintaxis ha evolucionado, los principios fundamentales del sistema de prototipos siguen siendo la base del modelo de objetos de JavaScript, permitiendo patrones de diseño elegantes y soluciones eficientes a problemas complejos.
Ejercicios de esta lección Prototipos y cadena de prototipos
Evalúa tus conocimientos de esta lección Prototipos y cadena de prototipos con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Funciones flecha
Polimorfismo
Array
Transformación con map()
Gestor de tareas con JavaScript
Manipulación DOM
Funciones
Funciones flecha
Async / Await
Creación y uso de variables
Excepciones
Promises
Funciones cierre (closure)
Herencia
Herencia
Estructuras de control
Selección de elementos DOM
Modificación de elementos DOM
Filtrado con filter() y find()
Funciones cierre (closure)
Funciones
Mapas con Map
Reducción con reduce()
Callbacks
Manipulación DOM
Promises
Async / Await
Eventos del DOM
Async / Await
Promises
Filtrado con filter() y find()
Callbacks
Creación de clases y objetos Restaurante
Reducción con reduce()
Filtrado con filter() y find()
Reducción con reduce()
Conjuntos con Set
Herencia de clases
Eventos del DOM
Clases y objetos
Modificación de elementos DOM
Mapas con Map
Introducción a JavaScript
Funciones
Tipos de datos
Clases y objetos
Array
Conjuntos con Set
Array
Encapsulación
Clases y objetos
Uso de operadores
Uso de operadores
Estructuras de control
Excepciones
Transformación con map()
Funciones flecha
Selección de elementos DOM
Encapsulación
Mapas con Map
Creación y uso de variables
Polimorfismo
Tipos de datos
Estructuras de control
Todas las lecciones de JavaScript
Accede a todas las lecciones de JavaScript y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Javascript
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Funciones Cierre (Closure)
Sintaxis
Arrays Y Métodos
Estructuras De Datos
Conjuntos Con Set
Estructuras De Datos
Mapas Con Map
Estructuras De Datos
Funciones Flecha
Programación Funcional
Filtrado Con Filter() Y Find()
Programación Funcional
Transformación Con Map()
Programación Funcional
Reducción Con Reduce()
Programación Funcional
Clases Y Objetos
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Manipulación Dom
Dom
Selección De Elementos Dom
Dom
Modificación De Elementos Dom
Dom
Eventos Del Dom
Dom
Callbacks
Programación Asíncrona
Promises
Programación Asíncrona
Async / Await
Programación Asíncrona
Certificados de superación de JavaScript
Supera todos los ejercicios de programación del curso de JavaScript y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender qué es y cómo funciona la herencia prototípica en JavaScript.
- Aprender a crear objetos mediante Object.create().
- Usar getters y setters para controlar el acceso a las propiedades.
- Diferen" "ciar entre propiedades propias y heredadas.
- Manipular y extender prototipos de manera eficiente.
- Implementar patrones de diseño con prototipos.