Seleccionar página

Una de las características más interesantes de los objetos en Javascript es la herencia por medio de la cadena de prototipos, es decir, las clases pueden heredar de otras clases para adaptar o completar el comportamiento. Una clase hija puede incluir nuevos miembros, sobreescribir los ya existentes, etc.

classfunction
class C {
  x () {
    return 1;
  }
}

class CC extends C {
  y () {
    return 2;
  }
}

const cc = new CC();
console.assert( cc.x() === 1 );
console.assert( cc.y() === 2 );
console.assert( cc instanceof C );
  function C () {
  }
  C.prototype.x = function () {
    return 1;
  };
  
  function CC () {
  }
  C.prototype.y = function () {
    return 2;
  };
  Object.setPrototypeOf( CC.prototype, C.prototype );
  
  const cc = new CC();
  console.assert( cc.x() === 1 );
  console.assert( cc.y() === 2 );
  console.assert( cc instanceof C );

Para definir la herencia con class utilizamos la instrucción extends seguida de la clase padre de la que heredamos. Esto modifica la cadena de prototipos y hace que el prototipo de la clase hija apunte al constructor de la clase padre.

En el caso de function tenemos que hacer nosotros explícitamente el cambio de prototipo, para ello lo más sencillo es utilizar Object.setPrototypeOf(). Lo que hacemos es que el prototipo de la clase hija tenga como prototipo el prototipo de la clase padre. Puede parecer un galimatías, pero la herencia por prototipos funciona de esta manera.

Como resultado de esta herencia obtenemos este conjunto de prototipos enlazados:

    Por último, tenemos que recordar que todos los objetos de una clase hija son también instancias de la clase padre y, por lo tanto si utilizamos instanceof devolverá true si preguntamos por cualquier clase de la cadena de prototipos. En este ejemplo se cumple cc instanceof CC && cc instanceof C && cc instanceof Object, ya que hereda de todos ellos.

    herencia en varios niveles

    La herencia se puede hacer en tantos niveles de profundidad como sea necesario, es decir, una clase puede heredar de una y esta de otra y así sucesivamente.

    classfunction
    class C {
    }
    
    class CC extends C {
    }
    
    class CCC extends CC {
    }
    
    const ccc = new CCC();
    console.assert( ccc instanceof C );
    
    function C () {
    }
    
    function CC () {
    }
    Object.setPrototypeOf (CC.prototype, C.prototype);
    
    function CCC () {
    }
    Object.setPrototypeOf (CCC.prototype, CC.prototype);
    
    const ccc = new CCC ();
    console.assert (ccc instanceof C);
    

      La única exigencia para la herencia es que las clases de las que se heredan ya deben haber sido definidas antes de proceder a su incorporación en la cadena de prototipos y, como consecuencia, no es posible realizar herencias circulares.

      herencia múltiple

      Javascript no soporta la herencia múltiple, es decir, heredar directamente desde dos clases padre. Las clases sólo tienen un prototipo y por lo tanto no pueden heredar de dos clases al mismo tiempo.

      Han aparecido varias soluciones más o menos completas e ingeniosas de combinar (vía mixin) prototipos y simular la herencia múltiple. Aquí presentamos un posible aproximación para la herencia múltiple:

      function multi ( ...baseClasses ) {
      
        function copy ( target, source ) {
          for (let key of Reflect.ownKeys( source )) {
            if ([ 'constructor', 'prototype', 'name' ].indexOf( key ) === -1) {
              Object.defineProperty( target, key, Object.getOwnPropertyDescriptor( source, key ) );
            }
          }
        }
      
        function inherit ( ...args ) {
          for (let b of baseClasses) {
            copy( this, new b( args ) );
          }
        }
      
        for (let base of baseClasses) {
          copy( inherit, base );
          copy( inherit.prototype, base.prototype );
        }
        return inherit;
      }
      

      Con esta función podemos simular herencia desde varias clases, tanto si usamos class como function:

      classfunction
      class A {
        constructor ( x ) {
          this.x = x;
        }
        increase () {
          return ++(this.x);
        }
      }
      
      class B {
        constructor ( x ) {
          this.x = x;
        }
        duplicate () {
          return this.x = this.x * 2
        }
      }
      
      class C extends multi( A, B ) {
        constructor ( x ) {
          super( x );
        }
      }
      
      var c = new C( 10 );
      console.assert( c.increase() === 11 );
      console.assert( c.duplicate() === 22 );
      console.assert( c.increase() === 23 );
      
      function A (x) {
        this.x = x;
      }
      
      A.prototype.increase = function () {
        return ++(this.x);
      };
      
      function B (x) {
        this.x = x;
      }
      
      B.prototype.duplicate = function () {
        return this.x = this.x * 2
      };
      
      function C (x) {
        return Reflect.construct (
          Object.getPrototypeOf (Object.getPrototypeOf (this)).constructor,
          [ x ],
          Object.getPrototypeOf (this).constructor
        );
      }
      
      Object.setPrototypeOf(C.prototype, multi (A, B).prototype);
      
      const c = new C (10);
      console.assert (c.increase () === 11);
      console.assert (c.duplicate () === 22);
      console.assert (c.increase () === 23);
      

      La función multiple() recibe las clases que se quieren heredar y esta función devuelve una sola clase combinación de los elementos de las dos anteriores. Aunque no es evidente, esta aproximación tiene una importante limitación, ya que las clases A y B no pueden heredar a su vez de otras clases, ya que la estrategia utilizada es la de clonar los miembros de las clases en un nuevo constructor, pero no se gestiona la cadena de prototipos de las mismas.

      No hay una solución universal y robusta para la herencia múltiple. Debemos adecuarnos a la naturaleza del lenguaje y aceptar que no dispone de herencia múltiple, aunque podamos usar algunas estrategias para simularla.

      heredar de clases predefinidas

      Una interesante posibilidad es heredar desde objetos predefinidos de Javascript o del DOM, como HTMLElement. Con esta posibilidad podemos extender y adaptar las capacidades de los objetos por medio de la herencia.

      Date

      Como ejemplo vamos a ampliar el objeto Date para incluir un método .diff() con el que comparar con otra fecha y obtener los milisegundos entre ambas.

      classfunction
      class D extends Date {
        constructor ( ...args ) {
          super( ...args );
        }
        diff ( date ) {
          return this.getTime() - date.getTime();
        }
      }
      
      const d = new D( 2019, 0, 1 );
      console.assert( d instanceof Date );
      console.assert(
        typeof d.diff( new Date() ) === 'number'
      );
      
      function D (...args) {
        return Reflect.construct (
          Object.getPrototypeOf (Object.getPrototypeOf (this)).constructor,
          args,
          Object.getPrototypeOf (this).constructor
        );
      }
      D.prototype.diff = function (date) {
        return this.getTime () - date.getTime ();
      };
      
      Object.setPrototypeOf (D.prototype, Date.prototype);
      
      const d = new D (2019, 0, 1);
      console.assert (d instanceof Date);
      console.assert (
        typeof d.diff (new Date ()) === 'number'
      );
      
      

        En el caso de los objetos predefinidos de Javascript hay cierta controversia sobre si esta es o no una buena práctica. Hay transpiladores como Babel que directamente no dan soporte a la herencia desde objetos predefinidos de Javascript por que no se ajustan a la forma en la que han planteado la creación de los mismos con function.

        La herencia es una de las opciones que tenemos a nuestra disposición y, tratada con el adecuado conocimiento y cuidado, puede ser una excelente solución para muchos problemas con los que nos podemos encontrar en nuestro día a día como programadores.

        HTMLElement

        No hay duda sobre la importancia de heredar de los objetos de DOM, ya que la herencia desde HTMLElement es la base de la construcción de Web Components y, por lo tanto, son la clave para los desarrollos modernos basados en componentes.

        classfunction
        class Counter1 extends HTMLElement {
          constructor() {
            super();
            this.x = 0;
            this.addEventListener('click', () => {
              this.clicked()
            });
          }
        
          clicked() {
            this.x++;
            this.render();
          }
        
          connectedCallback() {
            this.render();
          }
        
          render() {
            this.textContent = this.x.toString();
          }
        }
        window.customElements.define(
          'num-counter-1',
          Counter1
        );
        
        function Counter ( ...args ) {
          const newThis = Reflect.construct(
            Object.getPrototypeOf(Object.getPrototypeOf(this)).constructor,
            args,
            Object.getPrototypeOf(this).constructor
          );
          newThis.x     = 0;
          newThis.addEventListener( 'click', () => {
            newThis.clicked()
          });
          return newThis;
        }
        Object.setPrototypeOf(Counter.prototype, HTMLElement.prototype);
        Counter.prototype.clicked = function () {
          this.x++;
          this.render();
        };
        Counter.prototype.connectedCallback = function () {
          this.render();
        };
        Counter.prototype.render = function () {
          this.textContent = this.x.toString();
        };
        window.customElements.define(
          'num-counter',
          Counter
        );
        

          Las posibilidades de utilización de Web Componentes heredados de HTMLElement, o de algunos de los componentes expuestos por el DOM, abre un amplio e interesante mundo de posibilidades que podemos explotar gracias a la herencia en Javascript.

          heredar de null

          Un caso singular de herencia es hacerlo directamente desde null. En Javascript el objeto Object hereda del objeto null y a partir el resto de objetos. Cuando creamos un objeto con Create.object(null) ese nuevo objeto no tiene ninguna de las propiedades de Object ya que hereda directamente de null.

          En nuestras clases podríamos optar por heredar desde null y eliminar las propiedades y métodos que nos aporta Object y que, habitualmente, no utilizamos.

          classfunction
          class C extends null {
            constructor () {}
          }
          
          const c = new C();
          console.assert(
            typeof c.hasOwnProperty === 'undefined'
          );
          console.assert( ! (c instanceof Object) );
          
          function C () {
          }
          Object.setPrototypeOf(C.prototype, null);
          
          const c = new C();
          console.assert(
            typeof c.hasOwnProperty === 'undefined'
          );
          console.assert( ! (c instanceof Object) );
          

          Lamentablemente, la sintaxis basada en class tiene un bug en los motores de Javascript Firefox, Chrome y Node (quizás también en otras implementaciones) y no funciona correctamente. La versión basada en function no tiene ningún problema y funciona en todos los motores que hemos podido utilizar. Este no es un caso muy habitual, y tampoco debe preocuparnos especialmente.

          Conclusiones

          Gestionar la herencia por medio de los prototipos resulta bastante confuso, e incluso, con años de experiencia es fácil confundir unos prototipos con otros. Es comprensible que hasta la aparición de class no se haya extendido el uso de la herencia en Javascript. Ahora, realmente, no tenemos excusas y podemos explotar esta posibilidad.

          nombres y Symbol Índicesuper