Metaprogramación en Javascript
Como podemos construir objetos que definan el comportamiento de otros objetos.
Javascript es un lenguaje de programación orientado a objetos basado en prototypes. Esto quiere decir que las diferentes formas de composición se producen por vías distintas a como lo haría un lenguaje basado en clases tradicional.
Que la implementación sea diferente no lo hace menos potente ya que Javascript es un lenguaje reflexivo, es decir, permite cambiar su propia estructura y comportamiento en tiempo de ejecución.
Al permitir esa manipulación, los lenguajes reflexivos habilitan un tipo de programación llamada “metaprogramación” que según la wikipedia se define como:
La metaprogramación consiste en escribir programas que escriben o manipulan otros programas (o a sí mismos).
Dentro de este marco encontramos el concepto de meta-objects que en la misma linea se define como un objeto que crea o manipula otros objetos, incluido el mismo. En el artículo de hoy profundizaremos en los mecanismos de composición más importantes basados en meta-objects.
Este es el primer artículo de una serie sobre composición de meta-objects.
Anatomia de un meta-object
Como ya comentamos en un artículo pasado, el meta-object más sencillo es un mixin:
El objeto Person describe el comportamiento de un supuesto objeto de dominio que tenga name, lastname y age.
Lo llamamos mixin, porque en un futuro lo “mezclaremos” con un objeto para proporcionarle funcionalidad. Esa mezcla, podemos hacerla con Object.assign:
Este mecanismo de composición llamado comunmente “concatenación” establece una relación N-M — muchos a muchos — entre mixins y objetos:
Muchos objetos pueden mezclar el mismo objeto.
Un objeto puede ser mezclado en muchos objetos.
Object.assign realiza un shallow-copy de primitivas y referencias. Por tanto las identidades entre las funciones originales y las “copiadas” se cumplen:
Esto es porque a diferencia de las primitivas que son copiadas por contenido, las no-primitivas o tipos referenciados (Object, Arrays, Function, etc) se copian sus referencias.
En cuanto a estado se refiere, el objeto de dominio conserva su estado y el meta-object solo le proporciona la funcionalidad.
Como hemos dicho, Object.assign copia primitivas y referencias. Si el meta-object define una propiedad con un objeto, la referencia de este nested-object será copiada llevandonos a compartir estado entre instancias. Puede ser lo que queramos o no.
Enlazamiento de propiedades
Para entender bien el enlazamiento, tenemos que remontarnos a un concepto tan básico como el de una variable.
Las variables en Javascript son simplemente una forma de enlazar un nombre a un valor. Igual que en internet un enlace es un identifiacdor a una nueva página, las variables enlazan nombres a valores:
Las variables n1 y n2 representan el valor 1 y 2 respectivamente. Los números en Javascript son primitivas, esto quiere decir que su identidad se la proporciona su contenido. Por eso un ‘1’ simpre es igual a otro ‘1’.
En cambio cuando hablamos de no-primitivas, es un poco más complicado:
Aunque a y b parezcan guardar el mismo Array, en realidad son objetos distintos. Esto es porque la identidad de las no primitivas la establece su referencia — algo como la dirección de memoria del contenido real de ese objeto — no su contenido. Por tanto las variables a/b enlazan una referencia. De tal forma que si hacemos esto:
La variable ‘a’ enlaza la referencia a un nuevo array. A su vez, ‘b’ y ‘c’ están enlazando la misma referencia de ‘a’, por lo que la igualdad se cumple.
Enlazamiento con Object.assign
Esta es una de las carácteristcas fundamentales de los métodos de composición con meta-objects que encontramos en Javascript. Para entenderlo, vamos a retomar la concatenación de nuestro mixin con el objeto de dominio Lisa:
En el momento en que se produce la “mezcla” — esto es en la ejecución del assign — es cuando se crean las nuevas propiedades de Perosn dentro de Lisa y se enlanzan a los primitivas y no primitivas de Person. Así como hemos visto las primitivas serán copiadas sin problemas (un 2 siempre es un 2) y con las no-primitivas se copiaran sus referencias.
Si tuvieramos que implementar el assign nosotros, acabaríamos con un código similiar a este:
Y si miramos a la línea:
target[prop] = obj[prop]
Es ahí donde se produce el enlace, de la nueva propiedad del source en el target con el valor del source. Si fuese por ejemplo un número 2, copiariamos ese contenido tal cual. En cambio, si fuese una función, copiariamos la referencia a la función original.
Por eso cuando comparamos la nueva función de lisa con la función original de Person, nos encontramos que es la misma ya que sus referencias se han copiado:
Person.fullName === lisa.fullName // true
Así cuando usamos herramientas como Object.assign, no creamos nuevas referencias para cada una de las propiedas, si no que copiamos literalmente las referencias y primitivas que tuviese el objeto fuente.
Enlazamiento temprano/tardío
¿Qué ocurre ahora si después de haber mezclado el meta-object Person sobre el objeto de domino lisa, quisiéramos modificar la función original de Person?
El Object.assing ha realizado la copia de las propiedades de Person en lisa antes de la modificación, por tanto lo que ha copiado ha sido las propiedades originales. Así, al reasignar después la propiedad fullName
del objeto Person a una función nueva, lo que estamos haciendo es enalzar la variable fullName
a una nueva función — no-primitiva — que estamos creando en el momento. Sin embargo, el objeto lisa sigue conservando en su propiedad fullName
una referencia a la función original:
Esto es lo que se conoce como enlazamiento temprano o early bound. Es decir, justo en el momento de hacer la composición, lisa obtiene todas las nuevas propiedades enlazadas a los valores en ese momento de Person. Por tanto los cambios efectuados a posteriori sobre Person, no afectan al objeto de dominio lisa.
Decimos entonces que la concatenación de mixins está cerrada a la extensión y a la modificación.
En cambio, veamos que sucede sin en lugar de realizar una concatenación del mixin, lo delegamos sobre un objeto de dominio:
Object.create recibe un objeto — también llamado prototype — y lo delega sobre un nuevo objeto vacío. Finalmente devuelve ese objeto al que manualmente le estamos proporcionando el modelo de datos.
Ahora vemos como al imprimir lisa por una consola, obtenemos una respuesta similar a esta:
{ name: ‘Lisa’, lastname: ‘Simpson’ }
¿Dónde están las propiedades de Person? Pues bien, este método de composición funciona por mecanismos diferentes a la concatenación.
Cuando delegamos un objeto sobre otro utilizando el sistema de prototypes construido dentro de Javascript, lo que internamente hace el lenguaje es guardar el prototipo delegado sobre una propiedad especial y de solo lectura llamada __proto__:
Sin embargo y al contrario de como ocurría con la concatenación del mixin, lisa no tiene una propiedad fullName dentro de si misma, por tanto cuando hacemos lisa.fullName
el runtime de Javascript irá a la propiedad __proto__
a buscar dentro ese método que estamos requiriendo.
Como esa búsqueda se produce en el mismo momento en el que invocamos el método y como __proto__
guarda una referencia al objeto original Person, si después de haber hecho la delegación hacemos:
Cuando Javascript vaya a buscar el método fullName en __proto__
se encontrará con nuestra nueva versión.
Esto es lo que se conoce como enlazamiento tardío o late bound. Por tanto la delegación de prototipos es una técnica que está abierta a extensión y modificación.
En contraposición con los mixins, la delegación establece una realación N-1 — Muchos a 1 —:
Un objeto puede ser delegado sobre muchos objetos.
Pero un objeto solo puede tener delegado un objeto.
Por tanto es un método un poco más rigido que los mixins.
Conclusión
Hemos empezado a vislumbrar los mecanismos de composición que tenemos disponibles cuando hablamos de objetos que describen el comportamiento de otros objetos. Además hemos visto como se produce el enlace de variables en Javascript y como las distintas técnicas de composición se aprovechan de ellas. Por ejemplo, un mixin nos permite de forma fácil y rápida proporcionar varios comportamientos a varios objetos pero no nos permite actualizar las instancias después. Si queremos un proceso menos opaco deberemos recurrir a la delegación.
Sin embargo nos hemos dejado en el tintero otras cuestiones muy interesantes, como quien tiene el control sobre el estado y como podemos combinar las ventajas e incovenientes de la delegación y concatenación para crear técnicas más potentes.
Si te gustaron mis artículos de El rincón del front déjame un like, si te encantaron compártelos y si te fliparon suscríbete para no perderte el siguiente! :)
[portada]: Foto de 愚木混株 cdd20
Gracias!! el artículo me ha parecido super interesante.