Prototipos y Herencia en JavaScript: La cadena que conecta tus objetos
JavaScript es un lenguaje orientado a objetos, pero funciona diferente a lo que probablemente conoces.
Mientras lenguajes como Java o C++ usan clases tradicionales, JavaScript usa algo llamado herencia prototípica 🔗.
Y aunque ES6 introdujo la palabra class, en el fondo sigue siendo el mismo sistema de prototipos con una sintaxis más amigable.
🧬 ¿Qué es un prototipo?
Piensa en un prototipo como el “molde genético” de un objeto. Cada objeto en JavaScript tiene una propiedad interna llamada [[Prototype]] que apunta a otro objeto del cual hereda propiedades y métodos.
Puedes acceder a esta propiedad mediante __proto__ o usando Object.getPrototypeOf().
const persona = {
saludo: function() {
return "¡Hola!";
}
};
const juan = Object.create(persona);
console.log(juan.saludo()); // "¡Hola!"
¿Qué acaba de pasar? 🤔
Aunque juan no tiene una propiedad llamada saludo, puede usarla porque la hereda de persona a través de su prototipo. Es como si juan le dijera a JavaScript: “No tengo eso, pero pregúntale a mi prototipo”.
🔗 La cadena de prototipos
Esta es donde se pone interesante. Cuando intentas acceder a una propiedad en un objeto, JavaScript hace una búsqueda en cadena:
- 🔍 Primero busca en el objeto mismo - ¿Tiene la propiedad? Si sí, la devuelve.
- 🔍 Si no, busca en su prototipo - Sube un nivel en la cadena.
- 🔍 Si tampoco, sigue subiendo - Va al prototipo del prototipo.
- 🔍 Continúa hasta llegar a
Object.prototype- El “ancestro común” de todos los objetos. - 🚫 Si no la encuentra - Devuelve
undefined.
Veamos un ejemplo práctico:
const animal = {
tipo: "Animal",
respirar() {
return "Respirando...";
}
};
const perro = Object.create(animal);
perro.raza = "Labrador";
const miPerro = Object.create(perro);
miPerro.nombre = "Max";
// Ahora observa la magia 🪄
console.log(miPerro.nombre); // "Max" (lo tiene él mismo)
console.log(miPerro.raza); // "Labrador" (del prototipo perro)
console.log(miPerro.respirar()); // "Respirando..." (del prototipo animal)
console.log(miPerro.toString()); // "[object Object]" (de Object.prototype)
La cadena sería:
miPerro → perro → animal → Object.prototype → null
Cada objeto tiene acceso a las propiedades de toda su cadena de prototipos. Es como una herencia familiar donde puedes usar los “superpoderes” de todos tus ancestros.
⚙️ Funciones constructoras: la forma antigua
Antes de ES6, la forma estándar de crear objetos con prototipos era mediante funciones constructoras:
function Persona(nombre, edad) {
this.nombre = nombre;
this.edad = edad;
}
// Los métodos se agregan al prototipo, no al objeto
Persona.prototype.saludar = function() {
return `Hola, soy ${this.nombre}`;
};
const maria = new Persona("María", 30);
console.log(maria.saludar()); // "Hola, soy María"
¿Por qué agregar métodos al prototipo? 🤓
Si pusieras el método saludar dentro del constructor, cada nueva instancia tendría su propia copia del método. Esto consume más memoria.
Al ponerlo en el prototipo, todas las instancias comparten el mismo método. Más eficiente, ¿no?
function Persona(nombre) {
this.nombre = nombre;
// ❌ Mala práctica - cada instancia tiene su propia copia
this.saludar = function() {
return `Hola, soy ${this.nombre}`;
};
}
// vs
function Persona(nombre) {
this.nombre = nombre;
}
// ✅ Buena práctica - método compartido
Persona.prototype.saludar = function() {
return `Hola, soy ${this.nombre}`;
};
🆕 Clases en ES6: azúcar sintáctica
Las clases en JavaScript son básicamente una forma más limpia y familiar de escribir funciones constructoras:
class Vehiculo {
constructor(marca) {
this.marca = marca;
}
acelerar() {
return `${this.marca} está acelerando`;
}
}
class Auto extends Vehiculo {
constructor(marca, modelo) {
super(marca); // Llama al constructor del padre
this.modelo = modelo;
}
detalles() {
return `${this.marca} ${this.modelo}`;
}
}
const miAuto = new Auto("Toyota", "Corolla");
console.log(miAuto.acelerar()); // "Toyota está acelerando"
console.log(miAuto.detalles()); // "Toyota Corolla"
Por debajo, esto sigue siendo prototipos. Puedes comprobarlo:
console.log(miAuto.__proto__ === Auto.prototype); // true
console.log(Auto.prototype.__proto__ === Vehiculo.prototype); // true
Las clases son solo una sintaxis más bonita. El motor de JavaScript sigue usando prototipos internamente.
🔍 Herramientas para inspeccionar prototipos
JavaScript te da varias formas de investigar la cadena de prototipos:
const arr = [1, 2, 3];
// ¿Quién es su prototipo?
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
// ¿Es instancia de...?
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true (porque hereda de Object)
// ¿Tiene esta propiedad propia o es heredada?
console.log(arr.hasOwnProperty('length')); // true (es propia)
console.log(arr.hasOwnProperty('map')); // false (está en el prototipo)
🎯 Propiedad propia vs heredada
Es importante distinguir entre propiedades propias y heredadas:
const obj = {
nombre: "JavaScript"
};
// Propiedad propia
console.log(obj.hasOwnProperty('nombre')); // true
// Propiedad heredada
console.log(obj.hasOwnProperty('toString')); // false
console.log('toString' in obj); // true (pero viene del prototipo)
Para iterar solo sobre propiedades propias:
const persona = Object.create({ edad: 30 });
persona.nombre = "Ana";
// Mal - incluye propiedades del prototipo
for (let key in persona) {
console.log(key); // "nombre", "edad"
}
// Bien - solo propiedades propias
for (let key in persona) {
if (persona.hasOwnProperty(key)) {
console.log(key); // "nombre"
}
}
// Mejor - directamente las propias
Object.keys(persona).forEach(key => {
console.log(key); // "nombre"
});
⚠️ Advertencia: no toques Object.prototype
Modificar Object.prototype es extremadamente peligroso:
// 🚨 NUNCA HAGAS ESTO
Object.prototype.miMetodo = function() {
return "Peligroso";
};
const obj = {};
console.log(obj.miMetodo()); // "Peligroso"
// Ahora TODOS los objetos tienen este método
// Incluso los que crees en el futuro
Esto contamina el namespace global y puede romper código de terceros. Es considerado una muy mala práctica.
💡 Casos de uso prácticos
1. Crear objetos sin prototipo
A veces quieres un objeto “puro” sin herencia de Object.prototype:
const mapa = Object.create(null);
mapa.toString = "No es un método, es una propiedad";
console.log(mapa.toString); // "No es un método, es una propiedad"
// No hay conflicto con Object.prototype.toString
Útil para crear diccionarios o mapas puros.
2. Simular métodos privados
Antes de los campos privados (#), se usaban closures:
function Contador() {
let count = 0; // Variable privada
this.incrementar = function() {
count++;
};
this.obtener = function() {
return count;
};
}
const c = new Contador();
c.incrementar();
console.log(c.obtener()); // 1
console.log(c.count); // undefined (privada)
3. Verificar tipos personalizados
class Usuario {}
class Admin extends Usuario {}
const admin = new Admin();
console.log(admin instanceof Admin); // true
console.log(admin instanceof Usuario); // true
console.log(admin instanceof Object); // true
🎓 Tips para dominar los prototipos
✅ Usa Object.create(null) para objetos sin herencia.
✅ Prefiere clases ES6 para código más legible.
✅ Nunca modifiques prototipos nativos (Array.prototype, Object.prototype).
✅ Usa hasOwnProperty() para distinguir propiedades propias.
✅ Entiende que class es solo sintaxis, por dentro son prototipos.
🚀 El verdadero superpoder
Entender prototipos te da ventajas reales:
- Debugging más efectivo - Sabrás por qué un método no existe o de dónde viene.
- Optimización de memoria - Entenderás cómo compartir métodos entre instancias.
- Comprensión profunda - Podrás leer y entender código de frameworks y librerías.
- Entrevistas técnicas - Es una pregunta frecuente en entrevistas de JavaScript.
La herencia prototípica es el corazón de JavaScript 💙. Aunque las clases modernas hacen el código más bonito, conocer cómo funcionan los prototipos te convierte en un desarrollador más completo.
JavaScript no usa herencia clásica, usa herencia prototípica. Y una vez que lo entiendes, todo cobra sentido. 🧩