Object.create en JavaScript: fijar el prototipo de un objeto a mano

En esta página
Enterate del próximo post
Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.

Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.
El post anterior cerró señalando el regreso a la cadena de prototipos. Ese regreso empieza revisando qué hace new en cada paso: crea un objeto vacío, lo enlaza con el prototipo del constructor, ejecuta la función con ese objeto como this, y devuelve el resultado. El segundo de esos pasos tiene nombre propio y una forma de hacerlo aparte, sin pasar por ningún constructor ni por new: Object.create.
function Persona(nombre) {
this.nombre = nombre;
}
Persona.prototype.saludar = function () {
return `Hola, soy ${this.nombre}`;
};
const porNew = new Persona("Ana");
const porCreate = Object.create(Persona.prototype);
Object.getPrototypeOf(porNew) === Object.getPrototypeOf(porCreate); // true
porNew.saludar(); // "Hola, soy Ana"
porCreate.saludar(); // "Hola, soy undefined"Object.create(Persona.prototype) repite exactamente el segundo paso de new: crea un objeto y lo enlaza a Persona.prototype. La cadena de prototipos de porCreate queda indistinguible de la de porNew, y por eso las dos encuentran saludar en el mismo lugar.
Lo que Object.create no hace es ejecutar Persona. Nadie corrió this.nombre = nombre para porCreate, así que su propiedad nombre nunca se asignó, y el método la busca sin encontrarla. Object.create aísla el enlace con el prototipo del resto de la receta: sin ejecutar nada, sin decidir qué es this, sin devolver otra cosa que el objeto recién creado.
Como Object.create no depende de ninguna función ni de new, el objeto que recibe como prototipo puede ser cualquier objeto, no necesariamente el .prototype de un constructor.
const comportamientoAnimal = {
moverse() {
return `${this.nombre} se mueve`;
},
};
function crearAnimal(nombre) {
const animal = Object.create(comportamientoAnimal);
animal.nombre = nombre;
return animal;
}
const perro = crearAnimal("Rex");
const gato = crearAnimal("Michi");
perro.moverse(); // "Rex se mueve"
gato.moverse(); // "Michi se mueve"
Object.getPrototypeOf(perro) === comportamientoAnimal; // truecrearAnimal no usa new ni class en ningún lado. Cada llamada crea un objeto nuevo enlazado directamente a comportamientoAnimal, un objeto literal común. perro y gato comparten moverse de la misma forma en que dos instancias de una class comparten un método puesto en su prototipo, pero acá no hay ninguna función constructora detrás: solo un objeto que hace de prototipo y una función que arma el enlace a mano.
Object.create también acepta null como argumento. El resultado es un objeto que no delega en nada, ni siquiera en Object.prototype.
const literal = {};
const puro = Object.create(null);
typeof literal.toString; // "function"
typeof puro.toString; // "undefined"
puro; // [Object: null prototype] {}literal.toString existe porque {} se enlaza a Object.prototype apenas se crea, tal como se vio en el post de herencia y prototipos. puro.toString no existe en ningún escalón, porque no hay ningún escalón: la cadena de puro termina ahí mismo, en null. La consola lo marca con [Object: null prototype] para distinguirlo de un objeto común.
Ese vacío tiene una consecuencia concreta: JavaScript convierte un objeto a texto llamando a su toString heredado, y un objeto creado con Object.create(null) no tiene ninguno del que heredarlo.
const puro = Object.create(null);
`${puro}`;
// TypeError: Cannot convert object to primitive valueEl mismo vacío es, además, una defensa real contra el problema que cerraba el post de herencia: contaminar Object.prototype agregándole una propiedad a mano.
Object.prototype.esVip = false;
({}).esVip; // false ← lo hereda como cualquier literal
Object.create(null).esVip; // undefined ← no tiene ningún prototipo del que heredarloUn literal común hereda esa contaminación sin que nadie se lo pida, porque delega en Object.prototype como cualquier otro objeto creado con {}. Un objeto creado con Object.create(null) no tiene forma de heredarla: no delega en nada.
Object.create fija el prototipo en el momento de crear el objeto, no después. Cambiar el prototipo de un objeto que ya existe es una operación distinta: Object.setPrototypeOf.
const persona = { nombre: "Fernando" };
const conSaludo = {
saludar() {
return `Hola, soy ${this.nombre}`;
},
};
Object.getPrototypeOf(persona) === Object.prototype; // true
Object.setPrototypeOf(persona, conSaludo);
Object.getPrototypeOf(persona) === conSaludo; // true
persona.saludar(); // "Hola, soy Fernando"persona se creó como un literal común, enlazado a Object.prototype igual que cualquier otro. Object.setPrototypeOf lo reenlaza a mano, después de creado, a un objeto distinto. A partir de ahí la búsqueda de saludar sigue el mismo mecanismo de siempre: no lo encuentra en persona, sube un escalón, lo encuentra en conSaludo.
Entre las dos formas de fijar un prototipo a mano, Object.create es la directa: el objeto nace ya enlazado, sin un paso intermedio. Object.setPrototypeOf sirve para el caso distinto, cuando el objeto ya existe y no se lo puede volver a crear.
Object.createno es un atajo denew: es la operación quenewusa por dentro para enlazar un objeto con su prototipo, separada del resto de la receta y disponible para usarse sola, sin constructor, sinclass, y hasta sin ningún prototipo si el objeto lo pide.
Queda pendiente, más adelante en la serie, profundizar la herencia real con polimorfismo y reuso de código entre varios niveles de la cadena.