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

HTTP2 para programadores. Enviar mensajes del servidor al cliente con Server Sent Event (sin WebSockets)

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

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

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

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.

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.

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.