Seleccionar página

Cuando nos planteamos analizar las propiedades de un objeto descubrimos que existen varios tipos de propiedades y que se comportan de forma diferente. La primera característica que tenemos que tener en cuenta es la diferencia entre propiedades enumerables y no enumerables. El segundo lugar tenemos que tener en cuenta las propiedades propias y heredadas. También tenemos que considerar las propiedades que se puede establecer como de sólo lectura o no configurables. Por último, hay que tener en cuenta alguna convención para definir propiedades privadas. Todos estos aspectos son importantes a la hora de trabajar con las propiedades de un objeto.

Con la excusa del desarrollo de una función universal para comparar cualquier tipo de dato en Javascript, de la que venimos tratando en los últimos artículos, ahora vamos a dar un repaso a todas las características de las propiedades, intentando explicar cómo funcionan y que tenemos que tener en cuenta para trabajar de forma consistente.

Propiedades enumerables y no enumerables

La diferencia entre las propiedades enumerables o no enumerables es:

  • las propiedades enumerables son aquellas que se obtienen al recorrer un bucle for in (que procesa las propiedades enumerables propias y heredadas) o las devueltas por Object.keys() (sólo las enumerables propiedades propias).
  • las propiedades no enumerables son las propiedades existentes que no se incluyen en la definición anterior y por lo tanto son ignoradas en los bucles for in y por Object.keys().

Por defecto, Javascript define las propiedades de los objetos predefinidos como no enumerables y de esta forma, aunque son parte del objeto, no se procesan cuando se recorren en un bucle. En nuestros objetos también podemos definir propiedades no enumerables por medio de los descriptores de las siguientes funciones:

  • Object.create(prototipo, descriptores)
  • Object.defineProperty(objeto, "propiedad", descriptor)
  • Object.defineProperties(objeto, descriptores)

El motivo para definir una propiedad como no enumerable es evitar que este valor sea relevante al recorrer el objeto con un bucle for in, pero esto no quiere decir que no sean importantes. Las propiedades no enumerables pueden ser muy importantes, por ejemplo, a la hora de comparar dos objetos y confirmar si son realmente equivalentes.

Un caso bastante interesante es la comparación de matrices. Tenemos que recordar que las matrices son un tipo de objeto y como propiedades enumerables tienen todos sus elementos, es decir, son enumerables el índice de cada uno de los elementos: 0, 1, etc. Una propiedad propia no enumerable que tienen las matrices es length, que no tiene que aparecer al recorrer la matriz, pero es una propiedad importante a la hora de hacer comparaciones. Si sólo comparamos las propiedades no enumerables estas dos matrices parecen iguales, pero realmente no lo son:

var arr1 = ['a','b','c'];
var arr2 = ['a','b','c'];
arr2.length = 10;
console.log(arr1);                 // [ 'a', 'b', 'c' ]
console.log(Object.keys(arr1));    // [ '0', '1', '2' ]
console.log(arr2);                 // [ 'a', 'b', 'c', , , , , , ,  ]
console.log(Object.keys(arr2));    // [ '0', '1', '2' ]

Para obtener las propiedades propias enumerables y no enumerables se utiliza Object.getOwnPropertyNames(). Este método nos devuelve los nombres de todas las propiedades propias del objeto, pero no las heredadas (que veremos a continuación).

// jshints version: 6
var priv = new WeakMap();
class Parent  {
    constructor(a, b) {
        this.a = a;
        priv.set(this, {b});
    }
    get b() {
        return priv.get(this).b;
    }
    set b(value) {
        var tmp = priv.get(this);
        tmp.b = value;
        priv.set(this, tmp);
    }
}
class Child extends Parent {
    constructor(x, b, c) {
        super(x);
        priv.set(this, {});
    }
    get c() {
        return priv.get(this).c;
    }
    set c(value) {
        var tmp = priv.get(this);
        tmp.c = value;
        priv.set(this, tmp);
    }
}
var c1 = new Child(10, 20, 30);
console.log(Object.getOwnPropertyNames(c1));        // [ 'a' ]
var proto1 = Object.getPrototypeOf(c1);
console.log(Object.getOwnPropertyNames(proto1));    // [ 'constructor', 'c' ]
var proto2 = Object.getPrototypeOf(proto1);
console.log(Object.getOwnPropertyNames(proto2));    // [ 'constructor', 'b' ]

Propiedades propias y heredadas

Las las propiedades propias y heredadas se diferencian en que:

  • las propiedades que están definidas en el objeto son propias, es decir, aquellas que se han creado con this o se han incluido como parte de un objeto literal del tipo {a: 1}.
  • las propiedades heredadas son las que dispone el objeto procedentes de su cadena de prototipos.

Por ejemplo, todos los objetos disponen de una serie de métodos que heredan de Object.prototype. De igual forma, las matrices heredan todas las propiedades de Array.propototy y este a su vez de Object.prototype, por lo que tienen las propiedades de las matrices y de los objetos. Estas son propiedades heredadas.

Todos los objetos tienen el método hasOwnProperty() que indica si una propiedad es propia o heredada. Como al recorrer un objeto con una instrucción for in obtenemos las propiedades enumerables propias y heredadas, es muy común utilizar este método para trabajar solo con las propiedades enumerables propias, y no tratar las propiedades heredadas.

Para comprender un poco más vamos a revisar otro caso muy común: la herencia por medio de clases en ES6. Este ejemplo vamos a obtener las propiedades propias de un objeto por medio de Object.getOwnPropertyNames(), ignorando las propiedades heredadas desde las clases de las que hereda:

var priv = new WeakMap();
class Parent  {
    constructor(a, b) {
        this.a = a;
        priv.set(this, {b});
    }
    get b() {
        return priv.get(this).b;
    }
    set b(value) {
        var tmp = priv.get(this);
        tmp.b = value;
        priv.set(this, tmp);
    }
}
class Child extends Parent {
    constructor(x, b, c) {
        super(x);
        priv.set(this, {});
    }
    get c() {
        return priv.get(this).c;
    }
    set c(value) {
        var tmp = priv.get(this);
        tmp.c = value;
        priv.set(this, tmp);
    }
}
var c1 = new Child(10, 20, 30);
var c2 = new Child(10, 20, 40);
console.log(Object.getOwnPropertyNames(c1));    // [ 'a' ]
console.log(Object.getOwnPropertyNames(c2));    // [ 'a' ]

Aunque parezca sorprendente, sólo a es una propiedad propia del objeto, ya que ha sido creada en el constructor de Parent por medio de la instrucción this y por lo tanto se ha definido como una propiedad del objeto. Las propiedades b y c (y los constructores) son propiedades heredadas desde las clases Parent y Child y no son devueltas por Object.getOwnPropertyNames().

Descriptores de las propiedades

Vamos a ampliar un poco más nuestro análisis a las propiedades de los objetos con el resto de valores con los que podemos configurarlas. Como hemos visto, las propiedades pueden ser definidas como enumerables y no enumerables, pero las propiedades tienen un descriptor bastante completo donde también pueden definirse como no configurables ({configurable: false}) y de sólo lectura ({writeable: false}). Esta definición se realiza, al igual que en el caso de no enumerables, por medio de los descriptores que podemos utilizar con los métodos:

  • Object.create(prototipo, descriptores)
  • Object.defineProperty(objeto, "propiedad", descriptor)
  • Object.defineProperties(objeto, descriptores)

Cuando definimos una propiedad como {writeable: false}, estamos indicando que es una propiedad de sólo lectura y cuando intentamos cambiar su contenido, aunque no recibimos un error, no se cambia su valor.

Cuando definimos una propiedad como {configurable: false}, estamos indicando que no se puede cambiar su configuración con Object.defineProperty().

Podemos obtener los descriptores de las propiedades propias por medio de Object.getOwnPropertyDescriptor(), lo cual es bastante útil a la hora de conocer con que tipo de propiedad nos estamos enfrentando.

Propiedades privadas

Por último vamos a recordar que hay varios mecanismos para gestionar información privada y pública en objetos en Javascript. Todos los datos definidos como privados no están accesibles desde fuera de la clase o función constructora, y por lo tanto no vamos a tenerla en cuenta para comparar los valores de dos objetos.

No obstante, habitualmente se utiliza la convención de nombre iniciado con _ para definir propiedades privadas, pero realmente estas propiedades son accesibles. Para ser respetuosos con esta convención no deberíamos tener en cuenta este tipo de propiedades.

Implementación en equal()

Vamos a ver cómo incorporar todo lo que hemos visto sobre los distintos tipos de propiedades en nuestra función de comparación universal de cualquier valor en Javascript, que venimos desarrollando en los últimos artículos. Este es un breve resumen de lo que hemos decidido hacer en cada caso en la comparación de las propiedades de los objetos:

  • Por defecto vamos a comparar las propiedades enumerables propias. Es el caso más habitual y común.
  • Por defecto vamos a ignorar las propiedades cuyo nombre empieza por _. De esta forma respetamos el uso de esta convención para definir propiedades privadas.
  • Por medio de la opción {privateProperties: true} vamos a ser capaces de incluir en la comparación las propiedades privadas cuyo nombre empieza por _.
  • Por medio de una opción {nonEnumerableProperties: true} vamos a ser capaces de incluir las propiedades no enumerables en la comparación.
  • Por medio de una opción {allProperties: true} vamos a ser capaces de incluir todas las propiedades propias y heredadas, enumerables y no enumerables, en la comparación.

Para poder obtener los diferentes grupos de propiedades, dependiendo de las opciones que pasemos, vamos a implementar una función auxiliar que llamaremos getProperties y a la que pasaremos el objeto del que queremos obtener sus propiedades y un segundo parámetro con las opciones que se deben aplicar:

// get object properties with different scope
function getProperties(obj, options) {
  return (
    options.allProperties  ?                // All properties
      (function getAllProp(obj) {
        var proto = Object.getPrototypeOf(obj);
        return (
          typeof proto === 'object' && proto !== null ?
            getAllProp(proto) :
            []
        ).concat( Object.getOwnPropertyNames(obj) );
      })(obj) :
      options.nonEnumerableProperties ?
        Object.getOwnPropertyNames(obj) :   // All own properties (enumerable and nonenumerable)
        Object.keys(obj)                    // All own enumerable properties
  )
    .sort()
    .filter( function(prop, pos, arr) {
      if (prop[0] === '_' && !options.privateProperties) {
        return false;                       // Filter private properties (_)
      }                                     // Eliminate duplicates (for all properties)
      return !options.allProperties || pos === 0 || arr[pos - 1] !== prop;
    });
}

Puedes consultar el código completo de esta versión de esequal.js en GitHub y sus pruebas.

Parece que también sería importante poder comparar todos los descriptores a la hora de comparar dos objetos, ya que realmente no es completamente igual si dos objetos tienen una propiedad, pero, por ejemplo, en un caso es configurable y en otro caso no. Normalmente no es un caso muy relevante, pero sí podemos tratarlo con una opción

Por ello en equal() si pasamos la opción {checkPropertyDescritors; true} haremos que se comparen los descriptores de las propiedades y no sólo sus valores. Para ello vamos a incluir estas líneas de código en nuestra función:

if (options.checkPropertyDescritors) {      // Check property descriptor (optional)
    aDescriptor = Object.getOwnPropertyDescriptor(a, aKeys[i]);
    bDescriptor = Object.getOwnPropertyDescriptor(b, bKeys[i]);
    if (aDescriptor.enumerable   !== bDescriptor.enumerable ||
        aDescriptor.writable     !== bDescriptor.writable   ||
        aDescriptor.configurable !== bDescriptor.configurable )
    {
        return equal.NOT_EQUAL;
    }
}

Puedes consultar el código completo que incorpora esta funcionalidad en esequal.js en GitHub y sus pruebas.

Conclusiones

Aunque normalmente no nos preocupamos mucho de la forma en la que han sido definidas las propiedades, lo cierto es que tenemos una gran cantidad de opciones que condicionan el comportamiento las propiedades nuestros objetos . Aunque aquí las hemos revisado con la excusa de la comparación de objetos, lo cierto es que conocer y entender cómo podemos aprovechar y gestionar las propiedades de nuestros objetos tiene un gran interés, sobre todo a la hora de crear programas flexibles y a la vez robustos.

Aunque nuestra función equal() parece ya bastante completa, lo cierto es que es todavía tenemos que completarla con casos no previstos, lo cual vamos a abordar en los próximos artículos. Si no puedes esperar, descarga la versión final de esqueal.js con npm.

Novedades

Debate: Tecnologías de Front Web [vídeo]

Desde las principales comunidades de desarrollo de tecnologías de front (Madrid JS, Polymer Madrid, Angular Madrid y VueJS Madrid) se ha organizado este debate que pretende ser un ejercicio de sentido común en relación a las tecnologías de front actuales centradas en componentes.

El microservicio más grande del mundo [vídeo]

en esta interesante charla, Felipe Polo nos cuenta cómo un servicio puede crecer manteniendo su status “micro”, manteniendo su coherencia y orden, para resolver un problema de migración desde una aplicación monolítica hasta un sistema basado en microservicios.

Web Assembly workshop by Dan Callahan [video]

Este taller (en inglés) nos adentra en WebAssembly, cómo funciona y cuándo debe usarlo. También se describe cómo usar las herramientas de creación de perfiles. Esta nueva herramienta de bajo nivel y alto rendimiento está emergiendo con fuerza y debes conocerla.

breves

Descrubir algunas características de console

En el día a día nos encontramos muy a menudo utilizando console. Es una navaja multiusos que nos facilita la vida a la hora de depurar nuestro código. La mayoría de nosotros ha utilizado console.log(), pero tiene otras muchas funcionalidades.

Matrices dispersas o sparse arrays en JS

Una característica que puede producir algunos problemas, si no lo tenemos en cuenta, es la posibilidad de tener matrices con huecos, es decir, con algunos de sus elementos sin definir. Es lo que se suele denominar una matriz dispersa o sparse array. Veamos cómo trabajar con esta características de las matrices.

Algunos operadores de bits usados con asiduidad

Cada día más a menudo podemos encontrar operadores binarios utilizados como formas abreviadas de algunas operaciones que de otra forma sería algo menos compactas y, quizás, más comprensibles. Veamos algunos casos en detalle.

Cómo diferenciar arrow function de function

En un reciente artículo Javier Vélez Reyes hace patente las principales diferencias entre las funciones tradicionales y las funciones flecha, ya que ambos modelos no son equivalentes e intercambiables. Veamos cómo es posible saber si una función ha sido construida por medio de la instrucción function o como una arrow function.