class, new e instanceof en JavaScript

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 terminó con una promesa: class no inventa un modelo de herencia nuevo, le pone una sintaxis conocida a la cadena de prototipos que ya vimos. Antes de mostrar class, conviene ver la máquina que tiene abajo, escrita a mano.
new en cada pasofunction Persona(nombre) {
this.nombre = nombre;
}
Persona.prototype.saludar = function () {
return `Hola, soy ${this.nombre}`;
};
const persona = new Persona("Fernando");
persona.saludar(); // "Hola, soy Fernando"
Object.getPrototypeOf(persona) === Persona.prototype; // truenew Persona("Fernando") hace cuatro cosas en orden: crea un objeto vacío, lo enlaza con Persona.prototype como prototipo (exactamente lo que hacíamos a mano con Object.create en el post anterior), ejecuta Persona con ese objeto nuevo como this, y devuelve el objeto, salvo que la función devuelva explícitamente otro objeto por su cuenta.
"use strict";
function Persona(nombre) {
this.nombre = nombre;
}
const roto = Persona("Fernando");
// TypeError: Cannot set properties of undefined (setting 'nombre')// El mismo código en modo sloppy, sin "use strict"
function Persona(nombre) {
this.nombre = nombre;
}
const roto = Persona("Fernando");
roto; // undefined, Persona no devuelve nada
globalThis.nombre; // "Fernando" ← se filtró como variable globalOlvidarse del new no rompe la sintaxis: rompe el this. En modo estricto, this es undefined adentro de la función y explota apenas se intenta asignar una propiedad. En modo sloppy no explota nada, y ahí está lo peligroso: this pasa a ser el objeto global, así que nombre termina como una variable global filtrada, sin ningún error que avise.
class es la misma máquina con otra sintaxisclass PersonaClase {
constructor(nombre) {
this.nombre = nombre;
}
saludar() {
return `Hola, soy ${this.nombre}`;
}
}
typeof PersonaClase; // "function"
typeof PersonaClase.prototype.saludar; // "function", vive en el prototipoEl constructor termina siendo la función. saludar termina en PersonaClase.prototype, exactamente donde pusimos saludar a mano en el ejemplo anterior. No hay ningún mecanismo nuevo: es la misma cadena de prototipos, con una sintaxis que no obliga a escribir Persona.prototype.saludar = ... línea por línea.
PersonaClase("Fernando");
// TypeError: Class constructor PersonaClase cannot be invoked without 'new'Acá sí hay una diferencia real, y es una mejora deliberada. Una función constructora de las de antes no tiene forma de saber si la llamaron con new o no, y por eso el bug anterior es posible. Un class sí lo sabe, y si lo llamás sin new tira un error inmediato en vez de corromper this en silencio.
instanceof recorre la misma cadena que ya conocemospersona instanceof Persona; // true
persona instanceof Object; // true, la cadena sigue subiendo hasta ahí
const otraPersona = {};
otraPersona instanceof Persona; // falseinstanceof pregunta si Persona.prototype aparece en algún escalón de la cadena del objeto de la izquierda. Es la misma búsqueda del post anterior, pero comparando contra un prototipo puntual en vez de buscar una propiedad.
function Otro() {}
Otro.prototype = Persona.prototype;
const cosa = new Otro();
cosa instanceof Persona; // true, aunque nunca pasó por el constructor PersonaEsto es lo que se suele entender mal: instanceof no pregunta "¿te construyó esta función?". Pregunta "¿tu cadena de prototipos pasa por este objeto?". cosa se construyó con Otro, pero como Otro.prototype se reasignó para ser el mismo objeto que Persona.prototype, la cadena de cosa pasa por ahí igual. instanceof no tiene forma de distinguir un caso del otro.
class, no del objeto literalEn el post de objetos literales quedó anotado que _saldo con guion bajo era una convención de ES5, no una regla. Con class sí existe una regla real:
class Cuenta {
#saldo = 0;
depositar(monto) {
this.#saldo += monto;
return this;
}
get saldo() {
return this.#saldo;
}
}
const cuenta = new Cuenta();
cuenta.depositar(100);
cuenta.saldo; // 100, a través del getter
Object.keys(cuenta); // [] ← #saldo no aparece, ni ahí ni en ningún lado#saldo no es una propiedad más con un nombre raro: es harina de otro costal. No aparece en Object.keys, no se puede leer con corchetes, y escribir cuenta.#saldo fuera del cuerpo de la clase es un error de sintaxis, no un undefined. El programa ni siquiera compila.
function crearPersona(nombre) {
return {
nombre,
saludar() {
return `Hola, soy ${this.nombre}`;
},
};
}
const persona1 = crearPersona("Fernando");
const persona2 = crearPersona("Ana");
persona1.saludar === persona2.saludar; // false ← cada una con su propia funciónconst c1 = new PersonaClase("Fernando");
const c2 = new PersonaClase("Ana");
c1.saludar === c2.saludar; // true ← la misma función, compartida en el prototipoAhí está la diferencia real entre las dos formas. Una función fábrica crea un método nuevo por cada llamada: más memoria si hay muchas instancias, pero cada saludar es una función común, sin ningún misterio con this. class comparte un único saludar en el prototipo entre todas las instancias: más eficiente, pero reintroduce el problema de a qué se refiere this según cómo se llame al método, que es exactamente el tema del próximo post.
classno reemplaza la cadena de prototipos: la envuelve en una sintaxis que además agrega dos cosas que la función constructora de antes no tenía, el error inmediato si te olvidás delnewy los campos privados de verdad.
Si estás empezando, quedate con esto: new hace cuatro pasos concretos, no magia; instanceof compara cadenas de prototipos, no "quién te construyó"; y una función fábrica sigue siendo una opción legítima, no una versión pobre de una clase.
El próximo post de la serie es sobre this, call, apply y bind: qué decide, llamada por llamada, a qué apunta this dentro de un método compartido en un prototipo, que es justo el problema que la función fábrica de este post evita del todo.