Seleccionar página

Javascript tiene desde hace mucho tiempo herencia, que gestiona por medio de la cadena de prototipos. En este sentido tenemos todos bastante claro que un objeto tiene miembros creados directamente en él (en this), y también tiene miembros creados en la cadena de prototipos, por ejemplo, el método .hasOwnProperty() que tienen todos los objetos y que proviene de Object.

var obj = {a: 1};

console.log( obj.a );              // 1
console.log( obj.hasOwnProperty ); // function

Muy pocos programadores se animan a trabajar directamente con la cadena de prototipos y en general nos conformamos con las herencias implícitas de los objetos que se van definiendo de forma transparente al utilizar unos objetos u otros.

function A () {
  this.sum = function (a, b) {
    return a + b;
  };
}

function B () {
  this.mult = function (a, b) {
    return a * b;
  };
}
B.prototype = new A();

var b = new B();

console.log(b.sum(5, 2));
console.log(b.mult(5, 2));

Con la aparición de la sintaxis de class se hace más fácil y sencillo realizar esta herencia en Javascript, ya que ahora con una simple cláusula extends una clase puede heredar de otra.

class A {
  sum(a, b) {
    return a + b;
  };
}

class B extends A {
  mult(a, b) {
    return a * b;
  };
}

var b = new B();

console.log(b.sum(5, 2));
console.log(b.mult(5, 2));

En este punto hay que aclarar que hayamos utilizado clases o funciones constructoras el mecanismo de herencia es el exactamente el mismo: la cadena de prototipos, por ello hay gente que a las clases en Javascript les llama de forma despectiva “azúcar sintáctico”, pero la verdad es que un poco de “azúcar” es muy agradable.

prototipos

Lo que nos ocupa hoy es averiguar cómo podemos recorrer la cadena de prototipos para inspeccionar los miembros (propiedades y métodos) que ha heredado el objeto de las diferentes clases.

Las funciones disponen de una propiedad prototype automáticamente, no hace falta que nosotros las creemos, y por lo tanto toda función es susceptible de ser utilizado con new al disponer de esta propiedad prototype. Si definimos una clase y preguntamos por su tipo descubriremos que devuelve function, por lo tanto las clases son, realmente, funciones, por lo tanto también tienen una propiedad prototype.

function foo() {
}
console.log(typeof foo);            // function
console.log(typeof foo.prototype);  // object

class bar {
}
console.log(typeof bar);            // function
console.log(typeof bar.prototype);  // object

Por su parte, los objetos tienen una propiedad __proto__ que corresponde a la propiedad prototype de la clase con el que se ha creado. Esta propiedad ha sido muy controvertida, la creó Firefox y hasta hace poco no formaba parte del estándar de EMACScript. Como la mayoría de los navegadores lo han implementado, finalmente en la especificación de 2015 (ES6) se incluyo en el estándar, pero dejando claro que sólo era incluido por compatibilidad hacia atrás.

En general debemos utilizar Object.getPrototypeOf() o Reflect.getPrototypeOf() para obtener el prototipo del objeto y Object.setPrototypeOf() o Reflect.setPrototypeOf() para asignar un nuevo prototipo al objeto. Debemos evitar el uso directo de __proto__.

En consecuencia, lo que devuelve Object.getPrototypeOf() (__proto__) corresponde con la propiedad prototype de la clase o función constructora con el que se ha creado el objeto. Es un poco confuso, pero no es tan complicado:

class C {
}

var c = new C();

console.log(c.__proto__ === C.prototype);               // true
console.log(Object.getPrototypeOf(c) === C.prototype);  // true

Antes de continuar, debemos tener claro que las clases y funciones constructoras tienen una propiedad prototype que incluye los miembros que los objetos heredarán. Los objetos tienen una propiedad __proto__, que obtenemos con Object.getPrototypeOf, que corresponde al prototipo de la clase con la que han sido construidos. Por lo tanto obj.__proto__ === Class.prototype cuando obj es del tipo Class. Como todo se llama «prototipo» es fácil hacerse un lio.

Analizar la herencia: recorrer la cadena de prototipos

Ahora que hemos repasado los conceptos básicos, ya estamos en disposición para conocer la cadena de prototipos, es decir, la herencia, de un objeto. Básicamente, sólo tenemos que ir consultando sucesivamente con Object.getPrototypeOf() e iremos obteniendo la cadena de prototipos.

class A {}

class AA extends A {}

class AAA extends AA {}

var aaa = new AAA();

for (let proto = Object.getPrototypeOf(aaa); proto !== null; proto = Object.getPrototypeOf(proto)) {
  console.log(proto.constructor.name);
}
AAA
AA
A
Object

En este caso vemos como el objeto aaa hereda de la clase AAA, que hereda de la clase AA, que hereda de la clase A, que hereda de Object, y aunque no lo mostramos, Object tiene a su vez un prototipo que es null, y aquí se acaba la cadena.

Con este procedimiento podemos hacer visible todas las cadenas de prototipos de los diferentes objetos. Por ejemplo, una función tiene como prototipo Function, que tiene a su vez como prototipo a Object y este a su vez a null. Como las clases son funciones, además de tener un prototype, también tienen un __proto__ como función. De nuevo, parece un lio, pero con un poco de cuidado podemos comprender que no es tan complejo como parece.

Miembros aportados por cada prototipo

Si tenemos curiosidad por saber que miembros del objeto han sido creados directamente en el objeto y cuales han sido aportados por cada uno de los prototipos de los que hereda, sólo tenemos que hacer uso de Object.getOwnPropertyNames() que nos dará las propiedades definidas en cada objeto.

class B {
  constructor(x) {
    this.x = x;
  }
  total1() {
    return this.x;
  }
}
class BB extends B {
  constructor(x, y) {
    super(x);
    this.y = y;
  }
  total2() {
    return super.total1() + this.y;
  }
}
class BBB extends BB {
  constructor(x, y, z) {
    super(x, y);
    this.z = z;
  }
  total3() {
    return super.total2() + this.z;
  }
}

var bbb = new BBB();

console.log('bbb', ':', ...Object.getOwnPropertyNames(bbb).map(p => `\n\t this.${p}`)
);
for (
  let proto = Object.getPrototypeOf(bbb);
  proto !== null;
  proto = Object.getPrototypeOf(proto)
) {
  console.log(
    proto.constructor.name, ':',
    ...Object.getOwnPropertyNames(proto).map(p => `\n\t ${proto.constructor.name}.prototype.${p}`)
  );
}
bbb :
	 this.x
	 this.y
	 this.z
BBB :
	 BBB.prototype.constructor
	 BBB.prototype.total3
BB :
	 BB.prototype.constructor
	 BB.prototype.total2
B :
	 B.prototype.constructor
	 B.prototype.total1
Object :
	 Object.prototype.constructor
	 Object.prototype.__defineGetter__
	 Object.prototype.__defineSetter__
	 Object.prototype.hasOwnProperty
	 Object.prototype.__lookupGetter__
	 Object.prototype.__lookupSetter__
	 Object.prototype.isPrototypeOf
	 Object.prototype.propertyIsEnumerable
	 Object.prototype.toString
	 Object.prototype.valueOf
	 Object.prototype.__proto__
	 Object.prototype.toLocaleString

En este caso vemos que el en objeto bbb están disponibles los métodos de cada uno de los prototipos de su cadena (BBB.prototype.total3(), BB.prototype.total2(), B.prototype.total1() y todos los miembros de Object) y se han definido sobre él mismo tres propiedades (this.x, this.y, this.z).

Todo en una función inspectObject()

Ya tenemos todos los elementos para crear una pequeña función de inspección de los objetos y su herencia a través de la cadena de prototipos:

function inspectObject ( obj ) {
  const getMembers = ( obj, name ) => {
    return Reflect.ownKeys( obj )
                  .map( p => {
                    let propName = typeof p === 'symbol' ? `[ ${p.toString()} ]` : `.${p}`;
                    return `\n\t ${name}${propName}`;
                  } );
  };
  console.log(
    'obj', ':',
    ...getMembers( obj, 'this' )
  );
  for (
    let proto = Object.getPrototypeOf( obj );
    proto !== null;
    proto = Object.getPrototypeOf( proto )
  ) {
    let objName = proto.name || proto.constructor.name;
    console.log(
      objName, ':',
      ...getMembers( proto, `${objName}.prototype` )
    );
  }
}

Ahora podemos ir aplicando con facilidad esa función a cualquier objeto que tengamos a mano, incluso sobre elementos del DOM, y descubrir su cadena de prototipos:

inspectObject(document.querySelector('div'));
obj :
//…
HTMLDivElement :
//…
HTMLElement :
//…
Element :
//…
Node :
//…
EventTarget :
//…
Object :
//…
 

Conclusión

Os animamos a profundizar por vosotros mismos en las cadenas de prototipos de todo tipo de objetos accesibles desde Javascript: vuestros objetos, objetos predefinidos de Javascript, objetos de DOM, incuso funciones (que también son objetos). Os podéis sorprender en bastantes casos y siempre resulta muy instructivo.

Una pequeña función como la que os hemos planteado os puede ayudar en esta labor exploratoria. También podéis ampliar o desarrollar vuestra propia función de inspección recorriendo la cadena de prototipos. Seguro que muchos utilizáis editores o depuradores que dan información similar a esta. Lo importante no es que herramienta utilicéis, la claves está en conocer la herencia, comprender las cadenas de prototipos y aprovechar este conocimiento para crear programas más completos.

Novedades

HTTP2 para programadores. Enviar mensajes del servidor al cliente con Server Sent Event (sin WebSockets)

HTTP2 para programadores. Enviar mensajes del servidor al cliente con Server Sent Event (sin WebSockets)

En esta charla, organizada por MadridJS, Pablo Almunia nos muestra cómo la mayoría de nosotros cuando oímos hablar por primera vez de HTTP2 nos ilusionamos con las posibilidades que presumiblemente se abrían para el desarrollo de soluciones web avanzadas y cómo muchos nos sentimos defraudados con lo que realmente se podía implementar.

En esta charla podemos ver cómo funciona el HTTP2, que debemos tener en cuenta en el servidor para hace uso de este protocolo y, sobre todo, cómo podemos enviar información desde el servidor al cliente de forma efectiva y fácil. Veremos con detenimiento cómo por medio de los Server-Sent Events (SSE) podemos recibir en el cliente datos enviados desde el servidor sin utilizar websocket, simplificando enormemente la construcción de aplicaciones con comunicación bidireccional.

Observables en Javascript con Proxies

Observables en Javascript con Proxies

En esta charla, organizada por MadridJS, Pablo Almunia nos habla de la observación reactiva de objetos en Javascript por medio de Proxy. Se describe paso a paso cómo funcionan los Proxies y en que casos pueden ser nuestro mejor aliado. Veremos que no hay que tenerles miedo, son bastante sencillos de utilizar, y nos ofrecen una gran abanico de posibilidades.

Aplicaciones JAMStack, SEO friendly y escalables con NextJS

Aplicaciones JAMStack, SEO friendly y escalables con NextJS

En esta charla de Madrid JS, Rafael Ventura nos describe las funcionalidades clave de NextJS, nos muestra en vivo cómo desarrollar una completa aplicación JAMStack con Server Side Rendering (SSR) y Static Site Generation (SSG) y termina mostrando como publicar esta aplicación en Vercel.

Stencil JS: mejora el Time To Market de tu producto, por Rubén Aguilera

Stencil JS: mejora el Time To Market de tu producto, por Rubén Aguilera

En esta charla Rubén Aguilera nos cuenta los problemas que tienen muchas empresas a la hora de sacar productos accesibles, vistosos y usables en el Time To Market que requiere Negocio y cómo podemos minimizar este tiempo gracias al DevUI con StencilJS para adecuar una aplicación de Angular a las exigencias del mercado en tiempo record.