Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.
La primera vez que escribí un objeto en JavaScript sentí que estaba haciendo trampa.
Venía de cursar Programación con Java, donde para tener un objeto hay que declarar una clase, sus atributos, su constructor, sus getters y sus setters. Recién ahí, después de todo ese ritual, aparece el new. En JavaScript abrí dos llaves y ya tenía un objeto:
Tardé bastante en entender que no era trampa. Era otro modelo, con sus propias reglas, y confundirlo con el de Java me costó varios bugs.
Un objeto sin clase y, sin embargo, un objeto
En la cursada definimos un objeto por tres cosas: identidad, estado y comportamiento. El literal de arriba tiene las tres. Tiene una referencia propia, tiene datos y tiene una función que opera sobre esos datos. No le falta nada para ser un objeto en el sentido de la teoría, aunque no exista ninguna clase detrás.
Y encima es más flexible de lo que uno espera, porque la forma se define en tiempo de ejecución:
persona.js
persona.edad = 24; // agrego una propiedad que no existíadelete persona.edad; // y la sacopersona["nombre"]; // acceso con corchetes: la clave es un string
Esa flexibilidad es la parte linda y la parte peligrosa. Linda porque modelar algo lleva diez segundos. Peligrosa porque nadie te avisa cuando escribís persona.nombbre y obtenés undefined en vez de un error. Es exactamente el motivo por el que después terminé usando TypeScript en este mismo portfolio.
Entonces, ¿dónde está la POO acá?
Está en los prototipos, que es la parte que a mí no me entraba.
Ese objeto que parece suelto en realidad ya está heredando. Todo literal tiene como prototipo a Object.prototype, y por eso podés llamar a persona.toString() sin haber escrito nunca ese método. La herencia en JavaScript no es "esta clase extiende a esta otra", es "este objeto delega en este otro objeto":
prototipos.js
const base = { saludar() { return `Hola, soy ${this.nombre}`; },};// nuevoObjeto NO copia saludar: lo busca en base cuando lo necesitaconst alumno = Object.create(base);alumno.nombre = "Fernando";alumno.saludar(); // "Hola, soy Fernando"Object.getPrototypeOf(alumno) === base; // true
Cuando pedís alumno.saludar(), el motor busca la propiedad en el objeto; si no la encuentra, sube por la cadena de prototipos hasta encontrarla o llegar a null. Toda la herencia de JavaScript es esa búsqueda.
Lo importante, y esto es lo que ordena todo el tema: class no cambió ese mecanismo. Cuando llegó en ES2015 no trajo un modelo nuevo de objetos, le puso una fachada conocida al mismo motor de siempre.
class-es-azucar.js
class Persona { constructor(nombre) { this.nombre = nombre; } saludar() { return `Hola, soy ${this.nombre}`; }}typeof Persona; // "function", no "class"Persona.prototype.saludar; // el método vive en el prototipo
Los métodos de la clase terminan exactamente donde terminarían si los hubieras puesto a mano en el prototipo. Por eso digo que en JavaScript el objeto literal no es el pariente pobre de la clase: es el ladrillo con el que está hecha.
El detalle que más me hizo tropezar
En Java, this dentro de un método siempre es la instancia. En JavaScript this depende de cómo se llama a la función, no de dónde está escrita. Con métodos normales funciona como uno espera, pero si escribís el método como función flecha se rompe, porque la flecha no tiene this propio y se queda con el del contexto de afuera:
this.js
const contador = { total: 0, sumarBien() { this.total++; // this es contador }, sumarMal: () => { this.total++; // this NO es contador },};
Y hay otra trampa relacionada: si sacás el método del objeto y lo pasás como callback —algo tan común como setTimeout(contador.sumarBien, 100)— el objeto queda atrás y this se pierde. Ahí sí la flecha ayuda, pero envolviendo la llamada, no reemplazando el método.
Cuando quiero varios objetos parecidos sin escribir una clase, uso una función fábrica, que es simplemente una función que devuelve un literal. Para el nivel de complejidad en el que trabajo la mayor parte del tiempo, alcanza y sobra.
El literal fue creciendo con el lenguaje
Lo que sí cambió muchísimo es lo que se puede escribir adentro de las llaves. Vale la pena ver el recorrido, porque mucho código que uno encuentra en tutoriales viejos hace en cinco líneas cosas que hoy se resuelven en una.
ES5 (2009): el objeto se vuelve configurable
Antes de ES5 el objeto era básicamente una bolsa de pares clave-valor. ES5 trajo la maquinaria para controlarlo: Object.keys, Object.create, Object.freeze y los descriptores de propiedad con Object.defineProperty. También permitió declarar getters y setters directamente en el literal, algo que hoy usamos sin pensarlo:
es5.js
const cuenta = { _saldo: 0, get saldo() { return `$${this._saldo}`; }, set saldo(valor) { this._saldo = Math.max(0, valor); },};
Ese _saldo con guion bajo era la única forma de decir "esto es privado, no lo toques". Una convención, no una regla. Guardá el dato, porque vuelve más abajo.
ES2015: el salto grande
Es la versión que más cambió la forma de escribir objetos. Tres cosas concretas:
Propiedades abreviadas. Si la variable se llama igual que la clave, escribís la clave una sola vez.
Métodos abreviados. Adiós al saludar: function () {}.
Claves computadas. El nombre de la propiedad puede salir de una variable, dentro del propio literal.
es2015.js
const nombre = "Fernando";const campo = "rol";// Antesconst antes = { nombre: nombre, saludar: function () { return "Hola"; },};// Despuésconst despues = { nombre, [campo]: "Full Stack", saludar() { return "Hola"; },};
En la misma versión llegaron Object.assign para combinar objetos, los Symbol como claves únicas que no chocan con nada, y la sintaxis class de la que hablábamos.
ES2017 a ES2019: el objeto se vuelve recorrible
Acá el objeto se acercó al array. Object.values y Object.entries (ES2017) permitieron recorrerlo con las herramientas que ya usábamos para listas, y Object.fromEntries (ES2019) cerró el círculo para volver:
Ojo con esto: el spread hace una copia superficial. Si el objeto tiene otro objeto adentro, los dos van a compartir esa referencia. Cuando necesito una copia real uso structuredClone.
ES2020 a ES2022: leer sin miedo y encapsular de verdad
Trabajar con respuestas de una API era una cadena de if para no explotar en un nivel intermedio. El encadenamiento opcional y el operador de coalescencia nula lo resolvieron:
es2020.js
const ciudad = usuario?.direccion?.ciudad ?? "Sin definir";
Y en ES2022 llegaron dos cosas que cierran el tema de la POO. Una es Object.hasOwn, el reemplazo prolijo de hasOwnProperty. La otra son los campos privados con numeral, que son el primer encapsulamiento real del lenguaje: no una convención como el _saldo de ES5, sino un error si intentás acceder desde afuera.
es2022.js
class Cuenta { #saldo = 0; // privado de verdad depositar(monto) { this.#saldo += monto; return this; }}new Cuenta().#saldo; // SyntaxError, ni siquiera compila
De 2024 en adelante
ES2024 sumó Object.groupBy, que agrupa una colección por criterio y devuelve un objeto, algo que antes se hacía a mano con reduce. Y de ahí para acá las ediciones nuevas se fueron para otro lado: helpers de iteradores, Temporal para fechas, manejo explícito de recursos con using. Lo leo como una buena señal: al objeto literal ya no le falta nada urgente.
Lo que me llevo
Después de dar vueltas con esto, lo que me ordenó la cabeza fue dejar de traducir Java a JavaScript.
En la práctica uso literales para casi todo: configuraciones, respuestas de API, datos que solo necesitan viajar de un lado a otro. Saco una clase cuando de verdad necesito muchas instancias con estado propio y comportamiento compartido, que es bastante menos seguido de lo que pensaba cuando arranqué.
En JavaScript el objeto no es el resultado de una clase: es el punto de partida. La clase, cuando aparece, no hace más que ordenar lo que el literal ya sabía hacer.
Si estás estudiando POO en Java o en Python y saltás a JavaScript, mi consejo es ese: no busques la clase. Buscá el prototipo. Cuando entendés que todo se reduce a un objeto que delega en otro, el resto de la sintaxis es azúcar.