Herencia Prototípica en JavaScript
Aprende la herencia prototípica en JavaScript: el enlace oculto [[Prototype]], __proto__ vs Object.getPrototypeOf/setPrototypeOf, la cadena de prototipos, F.prototype, sombreado de propiedades y cómo se enlaza this — con ejemplos ejecutables.
En JavaScript, los objetos pueden heredar propiedades y métodos de otros objetos a través de un mecanismo llamado herencia prototípica. En lugar de copiar características de una clase (como en Java o C++), cada objeto mantiene un enlace oculto hacia otro objeto — su prototipo — y JavaScript sigue ese enlace cuando no puede encontrar una propiedad en el propio objeto. Esta página explica cómo funciona ese enlace oculto, la diferencia entre __proto__ y Object.getPrototypeOf/setPrototypeOf, cómo se recorre la cadena de prototipos, el papel de F.prototype en las funciones constructoras, el sombreado de propiedades y cómo se comporta this a lo largo de la cadena.
El enlace oculto [[Prototype]]
Todo objeto JavaScript tiene una propiedad interna oculta llamada [[Prototype]]. Puede ser null o una referencia a otro objeto, y ese objeto referenciado se denomina prototipo del objeto.
[[Prototype]] es una ranura interna de la especificación del lenguaje — no se puede leer con un acceso normal a una propiedad. En cambio, se trabaja con ella mediante dos APIs públicas:
- El accesor histórico
__proto__(un getter/setter expuesto porObject.prototype). - Los métodos modernos y recomendados
Object.getPrototypeOf(obj)yObject.setPrototypeOf(obj, proto).
La forma más sencilla de definir un prototipo es con __proto__ dentro de un literal de objeto. Aquí hacemos que animal sea el prototipo de rabbit, de modo que rabbit pueda leer las propiedades de animal:
__proto__ vs Object.getPrototypeOf / setPrototypeOf
__proto__ es un getter/setter que hace tiempo fue desaconsejado para uso general — solo está estandarizado por compatibilidad con navegadores. En código real, es preferible usar los métodos explícitos:
Nótese la diferencia: __proto__ es un accesor de propiedad, mientras que getPrototypeOf/setPrototypeOf son funciones. Dos reglas prácticas:
__proto__no es lo mismo que[[Prototype]].__proto__es simplemente el accesor que lee y escribe la ranura interna[[Prototype]].- Evita cambiar el prototipo de un objeto después de crearlo.
Object.setPrototypeOfy la asignación deobj.__proto__son operaciones lentas: los motores optimizan mucho los objetos cuyo prototipo se fija en el momento de la creación. Define el prototipo una sola vez, cuando construyes el objeto.
La cadena de prototipos
El prototipo de un prototipo también puede tener su propio prototipo, formando una cadena de prototipos. Cuando lees obj.prop, JavaScript:
- Busca
propcomo propiedad propia deobj. - Si no la encuentra, sigue
[[Prototype]]y busca en el prototipo. - Repite el paso 2 subiendo por la cadena hasta encontrar
propo llegar anull.
Si la cadena termina en null sin encontrar la propiedad, el resultado es undefined.
Existen dos limitaciones en la cadena: las referencias no deben formar un bucle (JavaScript lanza un error si intentas crear un ciclo) y [[Prototype]] debe ser un object o null.
Sombreado de propiedades: escritura vs lectura
El prototipo solo se consulta para leer. Cuando escribes o eliminas una propiedad, la operación siempre actúa sobre el propio objeto, nunca sobre su prototipo. Asignar una propiedad que también existe en el prototipo crea una propiedad propia que sombrea (oculta) la heredada:
La excepción son las propiedades de acceso (getters/setters): como un setter es una llamada a función, escribir a través de un setter heredado ejecuta ese setter en lugar de crear una nueva propiedad de datos propia.
this siempre es el objeto que realiza la llamada
Una fuente habitual de confusión: sin importar dónde viva un método en la cadena, el valor de this dentro de él es el objeto antes del punto cuando se llamó al método — nunca el prototipo en el que fue definido. Los métodos heredados operan, por lo tanto, sobre el estado propio del objeto que hereda:
Esto es lo que hace útiles los métodos compartidos en un prototipo: una sola definición de método, pero cada objeto guarda sus propios datos.
Funciones constructoras y F.prototype
Configurar [[Prototype]] manualmente para cada objeto resulta tedioso. El patrón clásico es una función constructora usada con new. Toda función tiene una propiedad regular llamada prototype (escrita como F.prototype). Cuando llamas a new F(), el [[Prototype]] del objeto recién creado se establece en F.prototype.
Distingue F.prototype del [[Prototype]] de un objeto: F.prototype es una propiedad ordinaria de la función constructora que suministra el [[Prototype]] a los objetos creados con new F(). Por defecto, F.prototype es un objeto con una sola propiedad no enumerable constructor que apunta de vuelta a la función.
Nota: La sintaxis moderna de
classen ES6 es azúcar sintáctica sobre exactamente este mecanismo. Una declaraciónclasscrea una función constructora, coloca sus métodos enConstructor.prototypey conecta la cadena con los mismos enlaces[[Prototype]]. Consulta Herencia de Clases en JavaScript para ver la forma conextends/super.
Creación de objetos con Object.create
Object.create(proto) construye un nuevo objeto con [[Prototype]] establecido directamente en proto — sin necesidad de un constructor. Es la forma más explícita de configurar la herencia y acepta un segundo argumento: un mapa de descriptores de propiedad, en el mismo formato que usa Object.defineProperties.
El mapa de descriptores permite controlar indicadores como writable, enumerable y configurable — consulta Indicadores y Descriptores de Propiedades en JavaScript para saber qué hace cada uno. Los objetos creados con Object.create(null) (los llamados objetos "muy planos") se tratan en Métodos de Prototipo, Objetos Sin __proto__.
Herencia multinivel
Como cada prototipo puede tener su propio prototipo, es posible construir cadenas de varios niveles de profundidad para modelar relaciones más específicas:
Inspección e iteración de la cadena
Para comprobar si un objeto está en algún lugar de la cadena de otro objeto, usa isPrototypeOf. Para separar las propiedades propias de las heredadas, usa hasOwnProperty — ten en cuenta que for...in recorre toda la cadena (solo propiedades enumerables), mientras que Object.keys devuelve únicamente las claves propias.
Los tipos integrados como arrays, funciones y fechas también se basan en esta cadena — sus métodos viven en Array.prototype, Function.prototype, etc. Consulta Prototipos Nativos de JavaScript para ver cómo están conectados los tipos integrados.
Resumen
- Todo objeto tiene un
[[Prototype]]oculto que es otro objeto onull. - Lee
[[Prototype]]conObject.getPrototypeOfy establécelo conObject.setPrototypeOf;__proto__es el accesor heredado y cambiar un prototipo tras la creación es lento. - Las lecturas recorren la cadena de prototipos hasta encontrar la propiedad o hasta que la cadena termina en
null; las escrituras y eliminaciones siempre actúan sobre el propio objeto y pueden sombrear propiedades heredadas. thisdentro de un método es el objeto sobre el que se llamó al método, no el prototipo donde fue definido.new F()establece el[[Prototype]]del nuevo objeto enF.prototype;classen ES6 es azúcar sintáctica sobre exactamente este mecanismo — consulta Herencia de Clases en JavaScript.