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)
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
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
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
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.
guau….!!!! que loco y que entusiasmado que te pone esto…!!!