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

Clases: miembros estáticos

Los constructores pueden tener miembros estáticos, es decir, métodos y propiedades que residen en el objeto que es la función constructora. Veamos cómo funcionan.

Clases: métodos de acceso y datos privados

Los métodos get/set para controlar el acceso a los datos son uno de los mecanismos que nos ofrece Javascript para mantener nuestros datos fuera de miradas inadecuadas. Esta funcionalidad, junto con WeakMap nos permite implementar una protección bastante razonable de los datos. Veamos cómo…

Clases: métodos

Los métodos son una de las partes más importantes de las clases, en ellos incluimos las funciones que queremos que trabajen sobre nuestro objeto. Los métodos tienen un modelo específico de definición y funcionamiento.

Clases: propiedades

Las propiedades son un elemento básico de los objetos y las clases. Podemos definirlos de varias formas, tanto en el objeto como en el constructor y especificar su comportamiento con precisión. Veamos cómo.