Usando C# para entender los prototipos de Javascript

Que cada vez se usa más javascript no es ninguna novedad, pero por desgracia tampoco es novedad que muchas veces se usa javascript en el contexto de un framework sin entender realmente el lenguaje.

Aparte de ser un lenguaje dinámico, una característica que diferencia mucho a javascript de lenguajes como C# y Java es el mecanismo de herencia. En lugar de utilizar un sistema de herencia basado en clases, se usa herencia basada en prototipos.

Una buena forma de entender como funcionan las cosas es intentar implementarlas. No se trata de realizar una implementación «usable», sino de utilizar cosas que ya conocemos para entender mejor cosas nuevas. Voy a dedicar un par de posts para ver cómo simular con C# la herencia prototípica de javascript.

Clases vs Prototipos

No voy a entrar en conceptos complicados como sistemas de tipos, teoría de categorías y cosas por el estilo, pero vamos a intentar definir «más o menos» cómo funciona cada sistema.

En un lenguaje basado en clases se definen clases que agrupan datos y operaciones. A partir de esas clases, se crean objetos (instancias) que contendrán los datos y operaciones definidos en la clase a la que pertenecen. Además, podemos definir subclases, es decir, clases que heredan de otras clases y en las que podemos añadir o modificar el comportamiento de la clase base.

En la lenguaje basado en prototipos no existen las clases. Existen sólo objetos, pero cada objeto tiene un prototipo asociado. Este prototipo no es más que otro objeto (con su respectivo prototipo). Al invocar una operación sobre un objeto, primero se mira si el objeto ha definido esa operación. Si la ha definido, se invoca la operación y ya está. Si no, se intenta invocar la operación definida en el objeto que actúa como prototipo, y si no en el prototipo del prototipo y así sucesivamente.

Una consecuencia de esta diferencia es que en el caso de una lenguaje basado en clases el comportamiento del objeto queda definido por la clase a la que pertenece y, por tanto, queda definido en el momento de su creación, mientras que con prototipos el comportamiento del objeto puede cambiar siempre que cambiemos su prototipo, cosa que podríamos hacer en cualquier momento (incluso después de crearlo).

Insisto, ésta no es una definición en absoluto rigurosa y puede haber lenguajes que se comporten de manera diferente, pero para nuestro objetivo, que es entender cómo funciona la herencia en javascript con respecto a C# o Java, nos sirve.

Simulando un objeto Javascript con C#

Para nuestro experimento, lo primero que necesitamos es simular un objeto javascript usando C#. El objetivo es pasar este test:

dynamic p = new ProtoObject();
p.Name = "Lucas";
Assert.That(p.Name, Is.EqualTo("Lucas"));

Esto lo podríamos hacer directamente con un ExpandoObjet, pero desgraciadamente Microsoft tiene la mala costumbre de hacer muchas clases sealed (cuando no internal) y no podemos heredar de él, así que vamos a crear una clase que herede de DynamicObject y use un diccionario para almacenar las propiedades:

public class ProtoObject : DynamicObject
{
  private readonly IDictionary<string, object> properties = new Dictionary<string, object>();

  public override bool TrySetMember(SetMemberBinder binder, object value)
  {
    properties[binder.Name] = value;
    return true;
  }

  public override bool TryGetMember(GetMemberBinder binder, out object result)
  {
    return properties.TryGetValue(binder.Name, out result);
  }
}

Aunque el ejemplo sea con una propiedad, podríamos añadir métodos usando proiedades de tipo Acion o Func y funcionaría igual (aunque la sintaxis es un poco desagradable):

dynamic p = new ProtoObject();
p.Sum = new Func<int, int, int>((a, b) => a + b);
Assert.That(p.Sum(2, 3), Is.EqualTo(5));

Ahora que tenemos creado nuestro objeto dinámico, vamos a implementar la propiedad __proto__ de javascript.

Añadiendo una cadena de prototipos a través de __proto__

Al explicar las diferencias de un sistema basado en clases y uno basado en prototipos, decía que en el segundo cada objeto tiene un prototipo y, cuando no podemos resolver una propiedad dentro de un objeto, tratamos de hacerlo en su prototipo. Pues bien, en javascript el prototipo se almacena como una propiedad más del objeto, llamada __proto__.

Vamos intentar simularlo pasando este test:

dynamic p = new ProtoObject();

p.__proto__.Name = "Lucas";
Assert.That(p.Name, Is.EqualTo("Lucas"));

p.Name = "Tengo preferencia";
Assert.That(p.Name, Is.EqualTo("Tengo preferencia"));

Para pasar este test, necesitamos modificar nuestra clase ProtoObject:

public class ProtoObject : DynamicObject
{
    private readonly IDictionary<string, object> properties = new Dictionary<string, object>();

    private ProtoObject lazyInitializedProto;

    public ProtoObject __proto__
    {
        get { return lazyInitializedProto ?? (lazyInitializedProto = new ProtoObject()); }
        set { lazyInitializedProto = value ?? new ProtoObject(); }
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        properties[binder.Name] = value;
        return true;
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        var hasMember = properties.TryGetValue(binder.Name, out result);
        return hasMember || __proto__.TryGetMember(binder, out result);

    }
}

Hemos añadido una propiedad __proto__ y hemos hecho que cuando no se pueda resolver la propiedad en el diccionario de objetos local, se intente resolverla a través de __proto__. La parte más extraña es tener que inicializar de forma perezosa el prototipo, pero eso es únicamente para evitar una recursión infinita al crear el objeto (se crearía el __proto__, que a su vez crearía su __proto__ que a su vez…).

Conclusión

Aunque sólo sirva para fines didácticos (que a nadie se le ocurra intentar usar esto en el mundo real), esta implementación permite comprender fácilmente cómo funciona de forma aproximada el sistema de prototipos en javascript.

Falta un aspecto fundamental, que es el this, puesto que no tenemos una limpia de definir operaciones en los prototipos que actúen sobre las propiedades definidas en el propio objeto, pero sirve para hacernos una idea de qué es esa misteriosa propiedad __proto__ que encontramos en los objetos javascript cuando los inspeccionamos con un depurador.

Es muy atractiva la simplicidad de la idea. En el fondo todo se basa en composición y delegación, pero empleando únicamente esos dos conceptos se pueden conseguir resultados realmente interesantes.