Cómo funciona la cadena de prototipos de JavaScript
Comprende la búsqueda en la cadena de prototipos de JavaScript, la diferencia entre prototype y [[Prototype]], cómo se ocultan propiedades heredadas y cómo inspeccionar los enlaces.
Cuando lees una propiedad, JavaScript comprueba primero las propiedades propias del objeto. Si no la encuentra, sigue su enlace interno [[Prototype]] hasta el siguiente objeto y repite el proceso hasta encontrar la propiedad o llegar a null. En ese último caso, la lectura devuelve undefined.
La regla es sencilla, pero cuesta más seguirla cuando ves prototype, __proto__ y [[Prototype]] juntos en un panel de DevTools o en una pregunta de entrevista. Firefox incluso etiqueta el enlace como <prototype> cuando despliegas un objeto en la consola.
Este artículo trata únicamente de cómo una búsqueda de propiedades recorre la cadena: qué objeto responde a una lectura, dónde se aplican las escrituras y qué API usar para inspeccionar un prototipo. Para funciones constructoras, clases y patrones de herencia con Object.create, consulta A Beginner’s Guide to OOP in JavaScript e Inheritance in JavaScript Object-Oriented Programming.
Puntos clave
- La lectura de una propiedad comprueba las propiedades propias y después cada
[[Prototype]]sucesivamente, y devuelveundefinedcuando la cadena termina ennull. - La propiedad
prototypede una función se convierte en el[[Prototype]]de las instancias creadas connew. No es el prototipo de la propia función. __proto__es un accessor obsoleto heredado deObject.prototype. UsaObject.getPrototypeOf()yObject.setPrototypeOf()en su lugar.- Asignar un valor a una propiedad heredada suele crear una propiedad propia que oculta (shadowing) la heredada y deja el prototipo sin cambios.
Object.create(null)produce un objeto sin cadena, lo que lo convierte en un diccionario seguro para claves de tipo string arbitrarias.
¿Cuál es la regla de búsqueda de la cadena de prototipos de JavaScript?
La cadena de prototipos de JavaScript es la secuencia de objetos, enlazados mediante [[Prototype]], que recorre la lectura de una propiedad hasta encontrar una coincidencia o llegar a null. Todo objeto ordinario tiene una ranura interna [[Prototype]] que apunta a otro objeto o a null. El algoritmo OrdinaryGet de la especificación define cómo funciona una lectura: busca una propiedad propia y, si no existe, repite la búsqueda en el prototipo. En la mayoría de los objetos, la cadena termina en Object.prototype, cuyo prototipo es null.
const animal = { eats: true };
const rabbit = Object.create(animal);
rabbit.jumps = true;
console.log(rabbit.jumps); // true (own property)
console.log(rabbit.eats); // true (found on animal)
console.log(rabbit.toString === Object.prototype.toString); // true
console.log(rabbit.flies); // undefined (not found anywhere)
console.log(Object.getPrototypeOf(Object.prototype)); // null
// rabbit -> animal -> Object.prototype -> null
Leer una propiedad inexistente no produce un error. Lo que lanza un TypeError es llamar a rabbit.flies(), porque la llamada se realiza sobre undefined. La guía Herencia y la cadena de prototipos de MDN explica este mismo modelo con más detalle.
¿Qué diferencia hay entre el prototype de una función y el [[Prototype]] de un objeto?
La propiedad prototype de una función es el objeto que se convierte en el [[Prototype]] de las instancias creadas con new. No es el prototipo de la propia función, que es Function.prototype. Para leer el enlace real de cualquier objeto, usa Object.getPrototypeOf().
class Greeter { hello() { return 'hi'; } }
const g = new Greeter();
console.log(Object.getPrototypeOf(g) === Greeter.prototype); // true
console.log(Object.getPrototypeOf(Greeter) === Function.prototype); // true
console.log(Object.getPrototypeOf(Greeter) === Greeter.prototype); // false
const arrow = () => {};
console.log(arrow.prototype); // undefined
console.log(Greeter.prototype.hello.prototype); // undefined
Las funciones flecha y los métodos de clase normales no tienen ninguna propiedad prototype, así que leer .prototype en ellos devuelve undefined.
| Nombre | Qué es | Dónde reside | Cómo leerlo |
|---|---|---|---|
[[Prototype]] | El enlace interno que sigue la búsqueda | En todos los objetos (como ranura interna) | Object.getPrototypeOf(obj) |
obj.__proto__ | Un getter/setter heredado para ese enlace | Object.prototype | Evítalo; usa getPrototypeOf |
Fn.prototype | El objeto asignado a las instancias creadas con new Fn() | Funciones construibles y clases | Fn.prototype |
¿Qué es proto?
__proto__ es una propiedad accessor definida en Object.prototype. No se almacena en cada objeto, por lo que un objeto que no hereda de Object.prototype no la tiene. MDN marca Object.prototype.__proto__ como obsoleto (Deprecated) y explica que solo forma parte del estándar para que los sitios web antiguos sigan funcionando. MDN recomienda usar en su lugar Object.getPrototypeOf() y Object.setPrototypeOf().
const proto = { greet() { return 'hello'; } };
const obj = Object.create(proto);
console.log(Object.getPrototypeOf(obj) === proto); // true
console.log(Object.hasOwn(obj, '__proto__')); // false
console.log(Object.hasOwn(Object.prototype, '__proto__')); // true
const bare = Object.create(null);
console.log(bare.__proto__); // undefined
const lit = { __proto__: proto };
console.log(Object.getPrototypeOf(lit) === proto); // true
La clave { __proto__: value } en un literal de objeto es sintaxis estándar e independiente del accessor. No está obsoleta. Solo la forma con dos puntos establece el prototipo; una clave calculada ["__proto__"] crea una propiedad propia ordinaria.
Incluso con la API adecuada, procura no cambiar el prototipo de un objeto después de crearlo. MDN advierte que hacerlo es “actualmente una operación muy lenta en todos los navegadores y motores de JavaScript”. Establece el prototipo al crear el objeto.
Shadowing: las propiedades propias ocultan las heredadas
La búsqueda se detiene en la primera coincidencia, así que una propiedad propia oculta cualquier propiedad con el mismo nombre situada más arriba en la cadena. La asignación funciona igual. Salvo que intervenga un setter heredado o una propiedad heredada no modificable (non-writable), el algoritmo OrdinarySet de la especificación crea o actualiza una propiedad propia en el objeto de destino. La propiedad heredada queda oculta, no modificada.
const defaults = { theme: 'light' };
const settings = Object.create(defaults);
console.log(settings.theme); // 'light' (inherited)
settings.theme = 'dark'; // creates an own property
console.log(defaults.theme); // 'light' (prototype untouched)
console.log(Object.hasOwn(settings, 'theme')); // true
delete settings.theme;
console.log(settings.theme); // 'light' (inherited again)
// settings -> defaults -> Object.prototype -> null
Object.hasOwn() te indica qué capa respondió a una lectura. MDN lo recomienda en lugar de hasOwnProperty().
¿Cómo llegan los métodos de Array.prototype a todos los arrays?
Todo array creado con un literal de array tiene Array.prototype como su [[Prototype]]. Por eso map, filter y push funcionan en cualquier array sin necesidad de copiarlos en él. Los objetos integrados (built-ins) también se basan en el shadowing: Array.prototype.toString oculta Object.prototype.toString.
const nums = [1, 2, 3];
console.log(Object.getPrototypeOf(nums) === Array.prototype); // true
console.log(Object.hasOwn(nums, 'map')); // false
console.log(nums.map === Array.prototype.map); // true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true
console.log(nums.toString()); // '1,2,3'
console.log(Object.prototype.toString.call(nums)); // '[object Array]'
// nums -> Array.prototype -> Object.prototype -> null
Object.create(null) para objetos sin cadena
Object.create(null) crea un objeto cuyo [[Prototype]] es null. No hereda nada: ni toString, ni hasOwnProperty, ni el accessor __proto__. Por tanto, un objeto con prototipo nulo es un contenedor seguro para claves procedentes de la entrada del usuario. Con un {} normal, nombres heredados como constructor ya devuelven un valor antes de que almacenes nada.
const words = ['a', 'constructor', 'a'];
const plain = {};
for (const w of words) plain[w] = (plain[w] ?? 0) + 1;
console.log(plain.constructor); // a string like "function Object() { [native code] }1"
const counts = Object.create(null);
for (const w of words) counts[w] = (counts[w] ?? 0) + 1;
console.log(counts.constructor); // 1
console.log(counts.a); // 2
console.log('toString' in counts); // false
// counts -> null
Un objeto con prototipo nulo no tiene métodos heredados, así que llama a Object.hasOwn(counts, key) en lugar de counts.hasOwnProperty(key). Interpolarlo en un template literal lanza un TypeError, porque no hay toString para convertirlo en string. Cuando las claves no sean strings, usa un Map.
¿Las clases usan la misma cadena de prototipos?
Sí. La sintaxis de clases no añade un nuevo modelo de herencia. Los métodos declarados en el cuerpo de una clase residen en ClassName.prototype, y las instancias acceden a ellos mediante la misma búsqueda por [[Prototype]] que cualquier otro objeto. extends enlaza el objeto prototype de una clase con el de la clase padre.
class Animal { speak() { return 'sound'; } }
class Dog extends Animal { bark() { return 'woof'; } }
const d = new Dog();
console.log(typeof Dog); // 'function'
console.log(Object.hasOwn(d, 'speak')); // false
console.log(Object.getPrototypeOf(d) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(d.speak()); // 'sound'
// d -> Dog.prototype -> Animal.prototype -> Object.prototype -> null
Conclusión
Todas las lecturas de propiedades siguen el mismo camino: JavaScript comprueba el propio objeto y después cada [[Prototype]] sucesivamente hasta encontrar la propiedad o llegar a null. Las escrituras se quedan en el objeto al que asignas el valor. La próxima vez que un valor parezca salir de la nada, ejecuta Object.hasOwn() sobre el objeto, llama después a Object.getPrototypeOf() en cada enlace y anota la cadena hasta encontrar el objeto que realmente contiene la propiedad.
Preguntas frecuentes
¿Qué diferencia hay entre el operador in y Object.hasOwn()?
El operador in comprueba toda la cadena de prototipos, por lo que 'toString' in {} devuelve true, ya que toString se hereda de Object.prototype. Object.hasOwn(obj, key) comprueba solo las propiedades propias del objeto y devuelve false para cualquier propiedad heredada. Usa in cuando una propiedad heredada deba contar, y Object.hasOwn() cuando necesites saber si el propio objeto contiene la clave.
¿Cómo utiliza instanceof la cadena de prototipos?
Por defecto, obj instanceof C devuelve true si C.prototype aparece en cualquier punto de la cadena de prototipos de obj. Esto no demuestra que C haya creado el objeto, porque cualquier objeto cuya cadena incluya C.prototype supera la comprobación. Una clase puede sustituir esta comprobación definiendo un método estático Symbol.hasInstance. Los valores primitivos en el lado izquierdo siempre devuelven false con la comprobación por defecto.
¿Los bucles for...in y Object.keys() incluyen las propiedades heredadas?
Un bucle for...in recorre las propiedades enumerables con clave de tipo string del objeto y de todos los objetos de su cadena de prototipos. Object.keys(), Object.entries() y el spread de objetos incluyen solo las propiedades enumerables propias del objeto. Los métodos de los prototipos integrados, como Object.prototype y Array.prototype, no son enumerables, por lo que no aparecen en for...in. Las propiedades enumerables de tus propios objetos prototipo sí aparecen.
¿Puede una cadena de prototipos de JavaScript contener un ciclo?
No con objetos ordinarios. Object.setPrototypeOf() lanza un TypeError si el nuevo prototipo crearía un ciclo, por ejemplo, al establecer b como prototipo de a cuando b ya hereda de a. Asignar mediante el setter __proto__ lanza el mismo error. Como los ciclos se rechazan, una cadena de objetos ordinarios siempre es finita y termina en null, de modo que la búsqueda de una propiedad siempre se detiene.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k