Usando C# para entender los constructores de Javascript

En estos posts estoy intentando usar C# para ayudar a comprender mejor cómo funciona el sistema de herencia basado en prototipos que se usa en javascript. En el anterior post vimos las diferencias entre un lenguaje basado en clases y uno basado en prototipos y cómo simular herencia prototípica usando objetos dinámicos en C#.

Acabamos implementando una clase, ProtoObject, que contenía una propiedad __proto__ que se comportaba de forma análoga a la propiedad __proto__ de los objetos javascript:

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);

    }
}

Con eso podíamos simular algo parecido a los objetos literales de javascript, pero en javascript existe otra forma de crear objetos: las funciones constructoras. Este tipo de funciones se invocan usando el operador new y funcionan de una forma un poco especial.

La propiedad prototype de los objetos Function

Empecemos con un ejemplo en javascript (aunque el ejemplo sea sólo con propiedades, con métodos sería exactamente igual):

function Person() {
  this.name = "Lucas";
}

Person.prototype.age = 17;

var p = new Person();
// Se cumplen ambas cosas:
// p.name === "Lucas"
// p.age === 17

¿Qué tenemos aquí? Tenemos dos formas distintas de añadir propiedades al objeto que estamos creando, una desde el cuerpo de la función y otra a través de la propiedad prototype de la función (es importante recordar que en javascript una función es un objeto más y, como tal, puede tener propiedades definidas).

Cuando se usa el operador new, lo que pasa es (más o menos) lo siguiente:

  1. Se crea un nuevo objeto
  2. Se asigna la propiedad prototype del objeto que representa la función Person a la propiedad __proto__ del objeto creado. Ojo, no se copia, se asigna, por lo que si modificamos p.__proto__ estamos modificando a la vez Person.prototype y todas las instancias que se hayan construido con esa función.
  3. Se ejecuta el cuerpo de la función sobre el objeto creado (this === objeto_creado)

Vamos a intentar implementarlo en C# pasando el siguiente test (las sintaxis no es tan clara como en javascript, pero nos sirve):

dynamic Person = new ProtoFunction(@this =>
{
  @this.Name = "Lucas";
});

Person.prototype.Age = 17;

// Simula el new Person() en javascript
dynamic p = Person();
Assert.That(p.Name, Is.EqualTo("Lucas"));
Assert.That(p.Age, Is.EqualTo(17));
Assert.That(p.__proto__, Is.SameAs(Person.prototype));

Para simular el cuerpo de la función, en el constructor del objeto ProtoFunction pasamos un Action<ProtoObject> que se ejecutará sobre el objeto recién creado. Además, para usar la función como constructor no estamos usando el operador new (no podemos redefinirlo en C#), sino que la invocamos directamente.

La implementación de esto la podemos hacer con una clase derivada de ProtoObject:

public class ProtoFunction : ProtoObject
{
  public dynamic prototype = new ProtoObject();
  private Action<object> init;

  public ProtoFunction(Action<dynamic> init = null)
  {
    this.init = init ?? new Action<dynamic>(x => { });
  }

  public override bool TryInvoke(InvokeBinder binder, object[] args, out object result)
  {
    dynamic p = new ProtoObject();
    p.__proto__ = prototype;
    init(p);
    result = p;
    return true;
  }
}

Como véis, el cuerpo de TryInvoke (el método que invoca C# cuando se invoca el objeto como si fuera una función) es calcado a los pasos descritos anteriormente: se crea un nuevo objeto, se asigna la propiedad prototype de la función a la propiedad __proto__ del objeto, y se ejecuta el cuerpo de la función sobre el objeto.

Conclusión

Espero que estos dos posts sobre los prototipos en javascript y los constructores hayan servido para entender mejor dos aspectos tan importantes de este lenguaje y sobre todo a eliminar la confusión tan habitual entre lo que significan las propiedades __proto__ y prototype.

En realidad, igual que veíamos en las conclusiones del post anterior, se trata de conceptos bastante simples que esconden un enorme potencial.

Aunque hay muchos frameworks de javascript que tratan de adaptar este tipo de herencia a una herencia más tradicional, simulando clases, creo que merece la pena comprender realmente cómo funciona un lenguaje y la filosofía que hay detrás de él, en lugar de intentar moldearlo para que se parezca a cosas que ya conoces.

Un comentario en “Usando C# para entender los constructores de Javascript

  1. Pingback: Enlaces semana 26 | Dev attitude

Comentarios cerrados.