Seleccionar página

nombres de las clases

Las clases mantienen las mismas limitaciones que los nombres de las funciones o las variables. JavaScript tiene más o menos las siguientes reglas para los nombres:

  • Puede estar compuesto por letras, números un guión bajo (_) o el signo de dolar ($).
  • No se puede usar un número como primer carácter del nombre.
  • Se distingue entre mayúsculas y minúsculas. Por convención las clases y constructores suelen empezar en mayúsculas.
  • No hay límite a la longitud del nombre.
  • No se pueden usar las palabras reservadas de JavaScript.

nombres de los miembros

Una curiosa característica de los objetos es que pueden tener miembros (propiedades o métodos) cuyo nombre no es correcto como nombre de variable. Las propiedades y métodos no están realmente limitados por las reglas anteriores, y podemos crear nombres que contengan otro tipo de símbolos, correspondan a palabras reservadas o empiecen por números. En general no es una buena idea utilizar este tipo de nombres, pero vamos a ver como funcionan:

class function
class C {
  'foo bar' () {
    return 2;
  }
}

const c = new C();
console.assert( c[ 'foo bar' ]() === 2 );
function C () {
}
C.prototype[ 'foo bar' ] = function () {
  return 2;
};

const c = new C();
console.assert( c[ 'foo bar' ]() === 2 );

    Como estos nombres no se pueden utilizar con el nombre la estructura objeto.propiedad, tenemos que utilizar la forma objeto["propiedad"]. Es bastante engorroso y suele ser poco práctico utilizar este tipo de nombres, pero habrá ocasiones donde sean necesarios o no tengamos más remedio que utilizarlos.

    propiedades calculadas

    Los nombres de los miembros de la clase pueden proceder del contenido de variables o del resultado de cualquier otra expresión válida, es decir, no tenemos que escribir el nombre literal y podemos utilizar variables o expresiones que devuelvan los nombres de las propiedades a utilizar.

    class function
    const x = 'bar';
    function y () {
      return 'foo';
    }
    
    class C {
      [ x ] () {
        return 2;
      }
      get [ y() ] () {
        return 1;
      }
    }
    
    const c = new C();
    console.assert( c[ x ]() === 2 );
    console.assert( c[ y() ] === 1 );
    
    const x = 'bar';
    function y () {
      return 'foo';
    }
    
    function C () {
    }
    C.prototype[ x ] = function () {
      return 2;
    };
    Object.defineProperty( C.prototype, y(), {
      get () {
        return 1;
      }
    } );
    
    const c = new C();
    console.assert( c[ x ]() === 2 );
    console.assert( c[ y() ] === 1 );
    

    El nombre de la propiedad es el contenido de la variable o el resultado de la expresión, por lo que se suelen denominar propiedades calculadas.

      nombres de métodos especiales

      Hay algunos nombres de métodos que tienen un significado especial y son invocados por Javascript de forma implícita en algunas operaciones. Debemos conocerlos para evitar utilizarlos con otros fines y que tengamos efectos colaterales inesperados. Algunos de estos métodos son:

      valueOf()

      Cuando un objeto es un recubrimiento sobre un tipo estándar, por ejemplo new Number() sobre un tipo number, Javascript llama al método valueOf() para obtener el valor primitivo.

      En algunos cosos puede ser de nuestro interés devolver un valor simple desde una determinada clase, en cuyo caso podemos implementar un evento con el nombre valueOf() para convertir nuestro objeto en ese valor primitivo.

      toString()

      Si un objeto tiene un método toString(), este método será invocado cuando se necesite convertir el objeto a texto. Por defecto, Object provee de un método de estas características que devuelve la cadena [object Object]. Si escribimos un método con el nombre toString podemos personalizar ese resultado:

      class function
      class C {
        toString () {
          return '<Object C>';
        }
      }
      
      const c = new C();
      console.assert( (c + '') === '<Object C>' );
      function C () {
      }
      C.prototype.toString = function () {
        return '<Object C>';
      };
      
      const c = new C();
      console.assert( (c + '') === '<Object C>' );
      

      toJSON()

      Cuando se invoca a JSON.stringify() Javascript busca si en los objetos existe un método con el nombre toJSON, si existe, lo llama para realizar la conversión a JSON, lo cual podemos utilizarlo para personalizar el comportamiento de JSON.stringify().

      class function
      class C {
        constructor ( x, y, s ) {
          this.x = x;
          this.y = y;
          this.s = s;
        }
        toJSON () {
          return {x : this.x, y : this.y};
        }
      }
      
      const c = new C( 1, 2, 3 );
      console.assert( JSON.stringify( c ) === '{"x":1,"y":2}' )
      
      function C ( x, y, s ) {
        this.x = x;
        this.y = y;
        this.s = s;
      }
      C.prototype.toJSON = function () {
        return {x : this.x, y : this.y};
      };
      
      const c = new C( 1, 2, 3 );
      console.assert( JSON.stringify( c ) === '{"x":1,"y":2}' )
      

      Symbol

      ECMAScript 6 introduce los símbolos como un nuevo tipo primitivo (es decir, si se pregunta por el tipo con typeof dirá que es de tipo symbol). Los símbolos son tokens que sirven como identificadores únicos, que se crean llamando a Symbol() (siempre sin new). Aunque se pueden pasar una cadena descriptiva, dos símbolos son siempre diferentes, aunque tengan la misma descripción.

      const s1 = Symbol('A');
      const s2 = Symbol('A');
      
      console.assert( s1 !== s2 );
      

      Los símbolos son una excelente forma de gestionar meta datos en los objetos y clases, ya que las propiedades con un símbolo por nombre son ignoradas en los bucles for...in, no son devueltas por Object.keys() o Object.getOwnPropertyNames(). Como ya vimos en otro artículo, los símbolos no son un mecanismo efectivo de protección de los datos y se puede acceder a ellas sin demasiada dificultad, aunque, por lo general, pasan desapercibidas en un segundo plano.

      símbolos bien conocidos

      Hay una serie de símbolos bien conocidos y que se registran de forma global, que constituyen una interesante forma de añadir algunas funcionalidades a nuestras clases. Antes de la aparición de los símbolos, Javascript definía métodos “reconocibles” como los que hemos visto más arriba. Las nueva forma de realizar este tipo de implementación se basa en símbolos bien conocidos y, de esta forma, no se “ensucia” la interfaz de nuestras clases con métodos que se definen para usos internos en nuestra relación con las API de Javascript y que normalmente no van a ser invocadas por los otros programadores.

      Los símbolos bien conocidos se crean con Symbol.for() y quedan registrados como como propiedades del objeto Symbol. A día de hoy, los símbolos que se han registrado en Javascript son:

      • Personalización de operaciones básicas del lenguaje:
        • Symbol.hasInstance (método): permite que un objeto personalice el comportamiento de instanceof.
        • Symbol.toPrimitive (método): permite que un objeto personalice cómo se convierte a un valor primitivo.
        • Symbol.toStringTag (propiedad – texto): es llamado por Object.prototype.toString para componenr la descripción de cadena predeterminada de un objeto del tipo `[object ${obj [Symbol.toStringTag]}]`.
      • Bucles:
        • Symbol.iterator (método): hace que un objeto sea iterable devolviendo un objeto iterador.
      • Expresiones regulares:
        • Symbol.match (método): es utilizado por String.prototype.match.
        • Symbol.replace (método): es utilizado por String.prototype.replace.
        • Symbol.search (método): es utilizado por String.prototype.search.
        • Symbol.split (método): es utilizado por String.prototype.split.
      • Otros:
        • Symbol.unscopables (propiedad – objeto): permite que un objeto oculte algunas propiedades de la sentencia with.
        • Symbol.species (método): facilita con la copia de matrices e instancias de RegExp, ArrayBuffer y Promise.
        • Symbol.isConcatSpreadable (propiedad – booleana): indica si Array.prototype.concat debe concatenar cada uno de los elementos del objeto o el objeto como un sólo elemento.

      Vamos a ver un par de sencillos ejemplos:

      Symbol.hasInstance

      Un método estático que corresponda a este símbolo será invocado cuando se pregunta por la instrucción prototypeof. Gracias a este prototipo podemos responder true o false cuando pregunte si un determinado objeto es de nuestra clase.

      class function
      class C extends Array {
        static [ Symbol.hasInstance ] ( value ) {
          if (value instanceof Array) {
            return true;
          }
          return false;
        }
      }
      
      const c = new C();
      console.assert( c instanceof C );
      console.assert( [] instanceof C );
      
      function C ( ...args ) {
        return Reflect.construct(
          Object.getPrototypeOf(
            Object.getPrototypeOf( this )
          ).constructor,
          args,
          Object.getPrototypeOf( this ).constructor
        );
      }
      C.prototype = Object.create(
        Array.prototype,
        {constructor : {value : C}}
      );
      Object.setPrototypeOf( C, Array );
      
      Object.defineProperty( C, Symbol.hasInstance, {
        value : function ( value ) {
          if (value instanceof Array) {
            return true;
          }
          return false;
        }
      } );
      
      const c = new C();
      console.assert( c instanceof C );
      console.assert( [] instanceof C );
      

      En este ejemplo, todos los objetos de tipo Array también dicen ser de tipo C.

      Symbol.iterator

      Hemos creado una clase Stack a la que se le incorporan valores con un método .push() y se retiran valores con un método .pop():

      class function
      class Stack {
        constructor () {
          this.top  = undefined;
          this.size = 0;
        }
        push ( value ) {
          this.top = {value, previous : this.top};
          this.size++;
          return this;
        }
        pop () {
          if (typeof this.top === 'undefined') return;
          const value = this.top.value;
          this.top    = this.top.previous;
          this.size--;
          return value;
        }
      }
      
      const stack = new Stack();
      stack.push( 1 )
           .push( 2 )
           .push( 3 );
      console.assert( stack.pop() === 3 );
      console.assert( stack.pop() === 2 );
      console.assert( stack.pop() === 1 );
      
      function Stack () {
        this.top  = undefined;
        this.size = 0;
      }
      Stack.prototype.push = function ( value ) {
        this.top = {value, previous : this.top};
        this.size++;
        return this;
      };
      Stack.prototype.pop  = function () {
        if (typeof this.top === 'undefined') return;
        const value = this.top.value;
        this.top    = this.top.previous;
        this.size--;
        return value;
      };
      
      const stack = new Stack();
      stack.push( 1 )
           .push( 2 )
           .push( 3 );
      console.assert( stack.pop() === 3 );
      console.assert( stack.pop() === 2 );
      console.assert( stack.pop() === 1 );
      

      Si queremos que la pila se pueda vaciar por medio de un bucle for...in que obtenga cada uno de los valores y se detenga cuando ya no queden más elementos, podemos implementar un método con el valor Symbol.iterator.

      class function
      class Stack {
      class Stack {
        constructor () {
          this.top  = undefined;
          this.size = 0;
        }
        push ( value ) {
          this.top = {value, previous : this.top};
          this.size++;
          return this;
        }
        pop () {
          if (typeof this.top === 'undefined') return;
          const value = this.top.value;
          this.top    = this.top.previous;
          this.size--;
          return value;
        }
        [ Symbol.iterator ] () {
          return {
            next : () =&gt; ({
              done  : this.size === 0,
              value : this.pop()
            })
          }
        }
      }
      
      const stack = new Stack();
      stack.push( 1 )
           .push( 2 )
           .push( 3 );
      for (let v of stack) {
        console.log( v ); // 3, 2, 1
      }
      console.assert( stack.size === 0 );
      
      function Stack () {
        this.top  = undefined;
        this.size = 0;
      }
      Stack.prototype.push = function ( value ) {
        this.top = {value, previous : this.top};
        this.size++;
        return this;
      };
      Stack.prototype.pop = function () {
        if (typeof this.top === 'undefined') return;
        const value = this.top.value;
        this.top    = this.top.previous;
        this.size--;
        return value;
      };
      Stack.prototype[ Symbol.iterator ] = function () {
        return {
          next : () =&gt; ({
            done  : this.size === 0,
            value : this.pop()
          })
        }
      };
      
      const stack = new Stack();
      stack.push( 1 )
           .push( 2 )
           .push( 3 );
      for (let v of stack) {
        console.log( v ); // 3, 2, 1
      }
      console.assert( stack.size === 0 );
      

      El método Symbol.iterator debe devolver un objeto que contengan un método next() que a su vez devuelva un objeto con una propiedad done indicando si se han procesado todos los valores y una propiedad value con el valor obtenido en esa llamada. De esta forma podemos utilizar los objetos de nuestra clase Stack como fuente de un bucle for...in.

      símbolos propios

      También podemos definir nuestros propios símbolos para dar una cierta privacidad a nuestros datos, no es que queden ocultos, pero al menos no son visibles a primera vista y queda claro a otros programadores que son datos que se deben tratar con cuidado.

      En el ejemplo anterior hemos creado una propiedad top que hace referencia a último elemento de la pila y que no es muy elegante mantener tan visible. Por otro lado, la propiedad size es de lectura y escritura y quizás algún programador piense que puede modificarla para cambiar el tamaño de la pila. Vamos a cambiar estas propiedades por símbolos para dejar claro que son datos de configuración y por lo tanto reducimos su visibilidad:

      class function
      const TOP  = Symbol();
      const SIZE = Symbol();
      
      class Stack {
        constructor () {
          this[ TOP ]  = undefined;
          this[ SIZE ] = 0;
        }
        get size () {
          return this[ SIZE ];
        }
        push ( value ) {
          this[ TOP ] = {value, previous : this[ TOP ]};
          this[ SIZE ]++;
          return this;
        }
        pop () {
          if (typeof this[ TOP ] === 'undefined') return;
          const value = this[ TOP ].value;
          this[ TOP ] = this[ TOP ].previous;
          this[ SIZE ]--;
          return value;
        }
        [ Symbol.iterator ] () {
          return {
            next : () =&gt; ({
              done  : this[ SIZE ] === 0,
              value : this.pop()
            })
          }
        }
      }
      
      const stack = new Stack();
      stack.push( 1 )
           .push( 2 )
           .push( 3 );
      for (let v of stack) {
        console.log( v ); // 3, 2, 1
      }
      console.assert( stack.size === 0 );
      
      const TOP  = Symbol();
      const SIZE = Symbol();
      
      function Stack () {
        this[ TOP ]  = undefined;
        this[ SIZE ] = 0;
      }
      Object.defineProperty( Stack.prototype, 'size', {
        get () {
          return this[ SIZE ];
        }
      } );
      Stack.prototype.push = function ( value ) {
        this[ TOP ] = {value, previous : this[ TOP ]};
        this[ SIZE ]++;
        return this;
      };
      Stack.prototype.pop = function () {
        if (typeof this[ TOP ] === 'undefined') return;
        const value = this[ TOP ].value;
        this[ TOP ] = this[ TOP ].previous;
        this[ SIZE ]--;
        return value;
      };
      Stack.prototype[ Symbol.iterator ] = function () {
        return {
          next : () =&gt; ({
            done  : this[ SIZE ] === 0,
            value : this.pop()
          })
        }
      };
      
      const stack = new Stack();
      stack.push( 1 )
           .push( 2 )
           .push( 3 );
      for (let v of stack) {
        console.log( v ); // 3, 2, 1
      }
      console.assert( stack.size === 0 );
      

      Para acceder al tamaño de la pila se ha creado un método de acceso que devuelve el valor almacenado en la propiedad con el nombre del símbolo SIZE, de esta forma el dato es accesible con facilidad, pero evitamos que se cambie por un descuido. Si un programador se empeña en cambiar este valor, finalmente podría obtener los símbolos y cambiarlo, pero desde luego no sería algo fortuito.

      miembros estáticos Índice herencia