Nonsense

¿Por qué es tan feo mezclar getters/setters y prototipos?

Pregunta: La pregunta más exactamente, tal como me la trasladaba un amigo anónimo es “¿Por qué está feo usar como prototipo objetos que tienen getters o setters?”.

Óxido de Hierro (II)

Eso de la fealdad a veces es un tanto subjetivo, claro. En este caso no, claramente es feo. Pero intentemos primeramente entender la pregunta.

El problema visible es este: Cuando montamos una cadena de prototipos, ya sabemos cómo funciona el acceso a las propiedades. Aquello que ya todo el mundo sabe 1): Si intentamos leer objeto.propiedad (let v = objeto.propiedad;) entonces propiedad se busca en objeto y si no se encuentra, se busca en su prototipo, y si no se encuentra ahí, en el prototipo del prototipo, etc, hasta llegar al final de la cadena. Y cuando intentamos escribir objeto.propiedad (objeto.propiedad = v;) la cadena de prototipos no entra en juego para nada: Si no se encuentra propiedad en objeto entonces simplemente se le añade y se asigna el valor.

Hasta aquí es el comportamiento normal y conocido de la cadena de prototipos desde los tiempos de ECMAScript 3. Esto no es, en absoluto nuevo.

Por otra parte, están los getters y setters. Es algo más nuevo pero espero que también conocido por todos. Simplemente podemos definir una propiedad sobre un objeto como setter o getter:

let objeto = {
    get propiedad() { return "blah"; }
};

Lo que nos permite hacer…

let v = objeto.propiedad;

…con una sintaxis que aparenta que propiedad fuera una propiedad estática cuando en realidad estamos llamando a una función. De modo similar podemos declarar un setter con la palabra set 2) y funciona para cuando intentamos asignar un valor:

let objeto = {
    set propiedad(v) { console.log(v); }
};
objeto.propiedad = "Esto y lo otro";

De nuevo, esto, aunque es algo más reciente -ahora veremos exactamente de cuándo 3)-, también es un funcionamiento normal para el concepto general de getters y setters. Existe y se usa también en otros lenguajes desde hace tiempo. Así que, hasta aquí, todo bien.

Pero, ¿qué pasa cuando juntamos estas dos ideas aparentemente sencillas? Aquí es donde viene la fealdad. Porque al añadir los get/setters se modificaron ligeramente las reglas de búsqueda de la cadena de prototipos. Con los get/setters de por medio las reglas de búsqueda son algo menos sencillas:

let monkey = {
    get food() { return "I eat bananas"; }
};
let sophisticatedMonkey = Object.create(monkey);

console.log(sophisticatedMonkey.food); // Oh, no, bananas are so unsophisticated
sophisticatedMonkey.food = () => "I eat grapes and cheese"; // Let's evolve and be civilized

console.log(sophisticatedMonkey.food); // Sorry, monkey. You can't escape your nature!
console.log(sophisticatedMonkey.hasOwnProperty('food')); // -> false! false? Yes, false.

Lo que ocurre es que, a la hora de intentar asignar sophisticatedMonkey.food, aunque este no tenga tal propiedad, en su prototipo sí que la hay, y además resulta ser una propiedad que se ha definido como no escribible 4). Y lo que ocurre es que en lugar de intentar asignar food en sophisticatedMonkey, la existencia de tal propiedad no escribible en algún punto de su cadena de prototipos hace que, simplemente, no se asigne 5).

Ah, pero… no, añadir un setter (o hacer que la propiedad sea escribible, "tanto da, da lo mismo", que diría Arkontes) no solucionaría las cosas. Lo que haría sería que nuestro intento de asignación ejecutara dicho setter, pero tampoco nos permitiría asignar la propiedad en sophisticatedMonkey.

En la pregunta formulada anónimamente también acompañaba algún comentario sobre la posible “solución” de usar Object.defineProperty, que lamentablemente es igual de fea… o incluso más, porque en realidad lo que haríamos sería crear otra propiedad sobre sophisticatedMonkey que casualmente tiene el mismo nombre de food. Como solución no sólo es cansada, sino que además pasa de feo a horripilante.

¿Qué hay de malo en sepultar problemas?

O si no sepultarlos por lo menos ¿intentar evitarlos? Bueno, mucha gente toma esa opción y decide evitar el uso get/setters por el coste que supone para estos casos.

La huída no siempre es limpia y bueno lo cierto es que, en sí mismo, como concepto, el uso de getters y setters a veces es bastante práctico. En cualquier caso, parece claro que el coste es, en general, demasiado alto y no sólo en elegancia. A veces también en rendimiento.

Pero todo esto sólo explica un porqué bastante superficial. El porqué de la fealdad en sí, el porqué del resultado. Sería de ayuda poder entender por qué se hizo así. Lamentablemente, nos metemos en un terreno difícil de explorar y sobre todo en uno difícil de analizar.

Es interesante tener un poco de contexto primero. Decía antes que los getters y setters se añaden oficialmente en la especificación de ECMAScript 5, esto es, en Diciembre de 2009. Sin embargo, es relevante observar que, en realidad, fueron añadidos a JavaScript casi diez años antes, alrededor del 2000. Y es relevante también entender un poco 6) lo que pasó en esa época.

Esto es antes de ES5 pero también es antes del fracaso de ES4. Es incluso antes del intento de ES4. Es incluso antes de la primera versión de Firefox o incluso la existencia de Mozilla. Es, más concretamente, el momento de clara dominación de Internet Explorer y esto, más allá de otras cosas, lo que significa para JavaScript es que es una situación en la que existen básicamente dos JavaScripts: Uno es lo que escribimos profesionalmente, que sí, es bastante horrible pero como debe funcionar en IE4/5 que es lo que usa todo el mundo, se limita básicamente a JavaScript 1.2/1.3. Por otro lado, existe SpiderMonkey donde Brendan Eich va añadiendo, sin demasiado interés por estandarizar, nuevas funcionalidades como los getters y setters.

Y no es tanto el problema de estandarizar o no. Lo que creo que más caracteriza ese momento en el desarrollo del lenguaje es que se van añadiendo esas funcionalidades pero realmente nadie las usa porque sólo están en Netscape (y luego Firefox). Así, no hay respuesta. El desarrollo que se hace en esos años 2000-2003 en JavaScript ignora por completo estas opciones. No surgen problemas con estas cosas no porque no los haya sino porque nadie las usa.

Mucho más adelante, pero mucho más, John Resig escribía esto sobre getters y setters y hablaba de cómo en 2007 ya era una opción usarlos porque finalmente estaban soportados no solo en Firefox sino *gasp* también en Safari y en Opera. Mirando 2007 en la gráfica de antes esto ya suponía un impresionante 25-30%. 7 años después, más o menos en la misma época en que se intentaba la especificación de ES4.

Yo personalmente no recuerdo que el entusiasmo de Resig por los getters y setters estuviera tan extendido o fuera compartido por una parte apreciable de la comunidad de desarrolladores. Sí, alguna conversación en alguna lista de correo, pero no muchísimo más. También es cierto, claro, que mis recuerdos no son siempre los más fiables así que admitamos que para entonces despertaran cierto interés. Un par de años más de discusiones y peleas, llegaría ES5 donde por fin, entre pequeños arreglos y ajustes, los getters y setters se colarían como nueva funcionalidad oficial.

Mientras tanto en otro lado de la ciudad...

El problema de la falta de uso y respuesta por parte de los desarrolladores se acentúa por otro motivo. No sólo ocurre que algunas funcionalidades no se usen la falta de soporte en IE. Además, es una época (2000-2009) en la que me temo que el conocimiento que existe de JavaScript es pobre, el rendimiento de los motores es frágil y las herramientas para usarlo mejor apenas están empezando a aparecer 7). Y recordemos que una de las jugadas más simpáticas de jQuery en 2006 fue aquello de poner jQuery.fn como alias de jQuery.prototype para que la gente hiciera plugins sin miedo a eso de los prototipos.

Es bastante especulativo por mi parte, claro, pero juntando todas estas ideas, temo que sólo mucho tiempo después de que las mecánicas de interacción de get/setters y el prototipo ya estuvieran solidificadas en el lenguaje, es cuando empezaron a usarse ambos más extensivamente y cuando, consecuentemente, empezamos a ver esos pequeños 8) detalles. Más aún, sospecho que en bastantes casos alguien puede haberse encontrado con esto en aquellos años (al menos en las fases finales 2006-2009) y puede haber decidido simplemente hacer las cosas de otra forma.

Es una especulación, sí, pero recordando también que es precisamente esa época, entre 2005 y 2009, cuando los intentos de crear sistemas de clases, de herencia, de extensión sobre los prototipos de JavaScript, florecían por todas partes. No había librería, blog, lista de correo, grande o pequeña, famosos o menos, donde no se intentara montar algo en ese sentido. Y muchos de los intentos eran… barrocos cuando no rococó o incluso churriguerescos. No me parece demasiado aventurado suponer falta de conocimiento.

En fin, creo que como último motivo del origen de esa fealdad, me quedaría con esa falta de uso hasta que ya ha sido demasiado tarde. Es más, sospecho que incluso hoy es un tema poco discutido porque una gente ha preferido seguir otras opciones y otra probablemente ni siquiera llegará a plantearse la situación.

1)
Por lo menos espero inocentemente que todo el mundo sepa esto.
2)
Existe otra sintaxis más explícita para definir getters y setters usando Object.defineProperty; el resultado es más o menos el mismo -luego veremos diferencias- pero la sintaxis es más cansada de escribir. ¡Más aún! En un momento dado existían Object.prototype.__defineGetter__ y Object.prototype.__defineSetter__, que ya están obsoletos y por tanto ignoraré alegremente
3)
Oficialmente los get/setters entran en la especificación de ES5, pero… no, no adelantemos acontecimientos xD
4)
Sólo tiene getter, no setter
5)
y además silenciosamente, sin dar ningún error
6)
De verdad que no me gusta nada tener que escribir sobre la historia de JavaScript :( pero en fin…
7)
Firebug es de 2006. jQuery también.
8)
o no tan pequeños