JavaScript es un lenguaje basado en prototipos, no en clases. Aunque ES6 introdujo la sintaxis class, por debajo el motor sigue usando el mismo mecanismo de prototipos que existía desde el principio. Entender cómo funciona la cadena prototípica es clave para leer código legado, depurar herencia y comprender qué ocurre realmente cuando creas una "clase" en JavaScript.
Qué es [[Prototype]]
Cada objeto en JavaScript tiene una propiedad interna llamada [[Prototype]] (accesible con Object.getPrototypeOf() o la propiedad no estándar __proto__). Cuando el motor busca una propiedad en un objeto y no la encuentra, sube a su prototipo, luego al prototipo del prototipo, y así hasta llegar a null:
const animal = {
tipo: 'animal',
respirar() {
return `${this.nombre} respira`;
}
};
const perro = {
nombre: 'Rex',
ladrar() {
return `${this.nombre} ladra`;
}
};
// Asignar el prototipo de perro
Object.setPrototypeOf(perro, animal);
console.log(perro.ladrar()); // "Rex ladra" propiedad propia
console.log(perro.respirar()); // "Rex respira" heredada de animal
console.log(perro.tipo); // "animal" heredada
// Verificar la cadena
console.log(Object.getPrototypeOf(perro) === animal); // true
Object.create(): crear objetos con prototipo explícito
Object.create(proto) crea un nuevo objeto cuyo prototipo es exactamente el objeto que le pasas. Es la forma más clara de establecer herencia prototípica:
const vehiculo = {
arrancar() {
return `${this.marca} arranca`;
},
detener() {
return `${this.marca} se detiene`;
}
};
const coche = Object.create(vehiculo);
coche.marca = 'Toyota';
coche.ruedas = 4;
console.log(coche.arrancar()); // "Toyota arranca"
console.log(coche.hasOwnProperty('marca')); // true
console.log(coche.hasOwnProperty('arrancar')); // false está en el prototipo
// Object.create(null) crea un objeto sin prototipo (útil como diccionario puro)
const mapa = Object.create(null);
mapa.clave = 'valor';
console.log(mapa.toString); // undefined (no hereda de Object)
Funciones constructoras: el patrón pre-ES6
Antes de las clases, los objetos con métodos compartidos se creaban con funciones constructoras. Los métodos se añadían a Funcion.prototype para que todos las instancias los compartieran sin duplicarlos en memoria:
function Persona(nombre, edad) {
// Propiedades propias de cada instancia
this.nombre = nombre;
this.edad = edad;
}
// Métodos en el prototipo (compartidos por todas las instancias)
Persona.prototype.saludar = function() {
return `Hola, soy ${this.nombre}`;
};
Persona.prototype.mayorDeEdad = function() {
return this.edad >= 18;
};
const ana = new Persona('Ana', 25);
const luis = new Persona('Luis', 15);
console.log(ana.saludar()); // "Hola, soy Ana"
console.log(luis.mayorDeEdad()); // false
// Todos comparten el mismo método del prototipo:
console.log(ana.saludar === luis.saludar); // true
La cadena de prototipos completa
Cuando creas un objeto con new Persona(), la cadena de prototipos tiene varios eslabones:
// ana ? Persona.prototype ? Object.prototype ? null console.log(Object.getPrototypeOf(ana) === Persona.prototype); // true console.log(Object.getPrototypeOf(Persona.prototype) === Object.prototype); // true console.log(Object.getPrototypeOf(Object.prototype)); // null // instanceof recorre la cadena: console.log(ana instanceof Persona); // true console.log(ana instanceof Object); // true // toString() viene de Object.prototype: console.log(ana.toString()); // "[object Object]"
Las clases ES6 son azúcar sintáctico
La sintaxis class no introduce un nuevo sistema de herencia. Compila exactamente al mismo mecanismo prototípico:
class Animal {
constructor(nombre) {
this.nombre = nombre;
}
hablar() {
return `${this.nombre} hace un sonido`;
}
}
class Perro extends Animal {
hablar() {
return `${this.nombre} ladra`;
}
}
const d = new Perro('Rex');
console.log(d.hablar()); // "Rex ladra"
// Por debajo, sigue siendo prototipos:
console.log(typeof Perro); // 'function'
console.log(Object.getPrototypeOf(Perro.prototype) === Animal.prototype); // true
Conocer prototipos ayuda a entender por qué añadir métodos a Array.prototype afecta a todos los arrays, por qué el operador in incluye propiedades heredadas mientras hasOwnProperty no, y por qué modificar el prototipo en tiempo de ejecución tiene implicaciones de rendimiento.
