Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.
En el post anterior quedó una pregunta sin responder: persona.toString() funciona aunque nadie escribió ese método en ningún lado. Esa pregunta es la puerta de entrada a cómo JavaScript modela la herencia.
Todo objeto literal queda enlazado a Object.prototype apenas se crea, sin que lo pidas. toString no está en persona: está en ese otro objeto, y persona lo alcanza por el enlace.
propiedad-propia.js
persona.hasOwnProperty("toString"); // false ← no es propiapersona.hasOwnProperty("nombre"); // true
hasOwnProperty distingue las dos cosas: lo que el objeto tiene puesto de verdad, y lo que consigue delegando en otro. nombre es propio. toString no.
Object.create(base) crea un objeto nuevo y le asigna base como prototipo. alumno no copia saludar: cuando lo llamás, el motor no lo encuentra en alumno, sube un escalón, lo encuentra en base, y lo ejecuta con alumno como this. Esa búsqueda, subiendo de objeto en objeto hasta encontrar la propiedad o quedarse sin escalones, es toda la herencia de JavaScript.
La cadena tiene un final. base delega a su vez en Object.prototype, que es donde vive toString, y Object.prototype no delega en nada: su prototipo es null. Cuando el motor llega ahí sin encontrar la propiedad, devuelve undefined.
__proto__ como clave dentro de un literal fija el prototipo en el momento de la creación, sin un segundo paso con Object.create. Ojo con no confundirlo con obj.__proto__ leído o escrito después de creado el objeto: ese es un accesor heredado de una época anterior a Object.getPrototypeOf/Object.setPrototypeOf, formalmente desaconsejado aunque los motores lo sigan soportando por compatibilidad. La clave __proto__ dentro del literal, en cambio, tiene su propia sintaxis en la especificación y no está desaconsejada.
for...in recorre también lo heredado
for-in.js
const base = { especie: "humano" };const persona = Object.create(base);persona.nombre = "Fernando";for (const clave in persona) { console.log(clave);}// nombre// especie
for...in recorre todas las propiedades enumerables que la búsqueda por la cadena encuentra, no solo las propias. Object.keys, en cambio, se queda solo con las propias:
solo-propias.js
Object.keys(persona); // ["nombre"]for (const clave in persona) { if (!Object.hasOwn(persona, clave)) continue; console.log(clave); // "nombre", una sola vez}
Por eso el patrón clásico adentro de un for...in es filtrar con Object.hasOwn cuando solo importa lo propio. Sin ese filtro, se termina iterando también lo heredado sin darse cuenta, algo que sorprende bastante la primera vez que el prototipo tiene algo enumerable.
Modificar un prototipo compartido afecta a todo lo que ya delega en él
prototipo-compartido.js
const uno = Object.create(base);const dos = Object.create(base);base.despedirse = function () { return "Chau";};uno.despedirse(); // "Chau" ← lo tienen los dos, sin tocarlosdos.despedirse(); // "Chau"
uno y dos ya existían cuando se agregó despedirse a base. No hizo falta recrearlos ni copiarles nada: la búsqueda pasa por base en el momento en que se llama al método, no en el momento en que el objeto se creó. Por eso un método agregado después aparece igual en todo lo que delega ahí.
shadowing.js
uno.despedirse = function () { return "Nos vemos";};uno.despedirse(); // "Nos vemos" ← propia, tapa la del prototipodos.despedirse(); // "Chau" ← sigue yendo a buscarla arriba
Asignarle despedirse directo a uno no toca base: le crea una propiedad propia, que la búsqueda encuentra antes de subir. uno tapa la del prototipo sin borrarla. dos ni se entera, porque nunca tuvo una propiedad propia con ese nombre.
Modificar Object.prototype afecta a todo el programa
pollution.js
const inocente = {};inocente.esVip; // undefinedObject.prototype.esVip = false;inocente.esVip; // false ← ahora TODO objeto tiene esta propiedad({}).esVip; // false, hasta un literal recién creado después del cambio
Object.prototype es el techo de la cadena de casi todo lo que se crea con {} en el programa entero. Agregarle algo ahí no afecta a un objeto puntual: afecta a todos, los que ya existían y los que todavía no se crearon. Es la razón por la que nunca hay que tocar Object.prototype a mano, y la base de una clase real de bug de seguridad, conocida como "prototype pollution": lograr que datos que vienen de afuera (un JSON del usuario, por ejemplo) terminen escribiendo una clave ahí arriba, sin que el programa lo haya pedido.
Un array hereda igual que cualquier objeto
Ya vimos en el post de valor y referencia que un array es un objeto por dentro. Acá está la otra mitad de esa idea: también delega, con la misma cadena que venimos usando.
cadena-de-un-array.js
const lista = [1, 2, 3];Object.getPrototypeOf(lista) === Array.prototype; // trueObject.getPrototypeOf(Array.prototype) === Object.prototype; // truetypeof lista.push; // "function", viene de Array.prototypetypeof lista.toString; // "function", dos escalones arriba, de Object.prototype
lista no tiene push propio: lo encuentra en Array.prototype, un escalón arriba. Y Array.prototype tampoco tiene toString propio: lo encuentra en Object.prototype, otro escalón más arriba. Es la misma búsqueda de siempre, con una cadena un poco más larga.
Lo que me llevo
La herencia en JavaScript no es una relación entre clases: es una cadena de objetos que se consultan en el momento en que se los llama, armada cuando cada uno se crea, no copiada de antemano.
Si estás empezando, quedate con esto: Object.create para delegar sin copiar, Object.hasOwn para saber si algo es propio o prestado dentro de un for...in, y cuidado extremo con modificar un prototipo compartido, sobre todo Object.prototype, porque el cambio se ve en todo el programa al mismo tiempo.
El próximo post de la serie es sobre class. A esta altura ya se puede adelantar que no inventa un modelo nuevo: le pone una sintaxis conocida a esta misma cadena. Ahí vemos qué hace new en cada paso, y qué es instanceof en términos de esta búsqueda.