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

¿Qué pasa con import y los web components?

¿Qué pasa con import y los web components?

Uno de los más controvertidos pilares de los componentes web ha sido el HTML Import. Considerado en estos momentos como una funcionalidad discontinuada, debemos conocer como sacar el máximo partido la instrucción import de Javascipt para gestionar la carga de nuestros componentes.
Template a fondo

Template a fondo

Hay dos formas estándar de crear contenido en un componente de forma flexible: la etiqueta template, que se considera como uno de los pilares de los Web Components y las template string de Javascript, que son una buena alternativa para generar el Shadow DOM con interpolación de datos.
Light DOM a fondo

Light DOM a fondo

El Light DOM es un espacio compartido entre nuestro componente web y el DOM general, que podemos utilizar para insertar contenido o configurar nuestro componente. Es una muy interesante característica que debemos conocer.
Shadow DOM a fondo

Shadow DOM a fondo

Para que los componentes web no colisionen unos con otros es muy útil utilizar el Shadow DOM para aislar el DOM y el CSS de cada componente. Esta característica se puede aplicar también a elementos HTML sin necesidad de utilizar Custom Elements, pero es con estos donde cobra todo su potencial. Demos un repaso profundo a las capacidades del Shadow DOM.