7º octubre 2020

Indicadores y descriptores de propiedad

Como sabemos, los objetos pueden almacenar propiedades.

Hasta ahora, una propiedad era un simple par “clave-valor” para nosotros. Pero una propiedad de un objeto es algo más flexible y poderoso.

En éste capítulo vamos a estudiar opciones adicionales de configuración, y en el siguiente veremos como convertirlas invisiblemente en funciones ‘getter/setter’, de obtención y establecimiento.

Indicadores de propiedad

Las propiedades de objeto, a parte de un value, tienen tres atributos especiales (también llamados “indicadores”):

  • writable – si es true, puede ser editado, de otra manera es de solo lectura.
  • enumerable – si es true, puede ser listado en bucles, de otro modo no puede serlo.
  • configurable – si es true, la propiedad puede ser borrada y estos atributos pueden ser modificados, de otra forma no.

Aun no los vemos, porque generalmente no se muestran. Cuando creamos una propiedad “de la forma usual”, todos ellos son true. Pero podemos cambiarlos en cualquier momento.

Primero, veamos como conseguir estos indicadores.

El método Object.getOwnPropertyDescriptor permite consultar toda la información sobre una propiedad.

La sintaxis es ésta:

let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
obj
El objeto del que se quiere obtener la información.
propertyName
El nombre de la propiedad.

El valor de retorno también es llamado objeto “descriptor de propiedad”: éste contiene el valor de todos los indicadores.

Por ejemplo:

let user = {
  name: "Juan"
};

let descriptor = Object.getOwnPropertyDescriptor(user, 'name');

alert( JSON.stringify(descriptor, null, 2 ) );
/* descriptor de propiedad:
{
  "value": "Juan",
  "writable": true,
  "enumerable": true,
  "configurable": true
}
*/

Para cambiar los indicadores, podemos usar Object.defineProperty.

La sintaxis es ésta:

Object.defineProperty(obj, propertyName, descriptor)
obj, propertyName
el objeto y la propiedad con los que se va a trabajar.
descriptor
descriptor de propiedad a aplicar.

Si la propiedad existe, defineProperty actualiza sus indicadores. De otra forma, creará la propiedad con el valor y el indicador dado; en ese caso, si el indicador no es proporcionado, es asumido como false.

Por ejemplo, aqui se crea una propiedad name con todos los indicadores en false:

let user = {};

Object.defineProperty(user, "name", {
  value: "Juan"
});

let descriptor = Object.getOwnPropertyDescriptor(user, 'name');

alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
  "value": "Juan",
  "writable": false,
  "enumerable": false,
  "configurable": false
}
 */

Comparado con la creada “de la forma usual” user.name: ahora todos los indicadores son false. Si eso no es lo que queremos, entonces mejor los establecemos en true en el descriptor.

Ahora veamos los efectos de los indicadores con ejemplo.

Non-writable

Vamos a hacer user.name de solo lectura cambiando el indicador writable:

let user = {
  name: "Juan"
};

Object.defineProperty(user, "name", {
  writable: false
});

user.name = "Pedro"; // Error: No se puede asignar a la propiedad de solo lectura 'name'...

Ahora nadie puede cambiar el nombre de nuestro usuario, a menos que le apliquen su propio defineProperty para sobrescribir el nuestro.

Los errores aparecen solo en modo estricto

En el modo no estricto, no se producen errores al escribir en propiedades no grabables y demás. Pero la operación aún no tendrá éxito. Las acciones que infringen el indicador se ignoran silenciosamente de forma no estricta.

Aquí está el mismo ejemplo, pero la propiedad se crea desde cero:

let user = { };

Object.defineProperty(user, "name", {

  value: "Pedro",
  // para las nuevas propiedades se necesita listarlas explicitamente como true
  enumerable: true,
  configurable: true
});

alert(user.name); // Pedro
user.name = "Alicia"; // Error

Non-enumerable

Ahora vamos a añadir un toString personalizado a user.

Normalmente, un toString incorporado en objetos es no enumerable, no se muestra en un bucle for..in. Pero si añadimos nuestro propio toString, entonces por defecto, este se muestra en los bucles for..in, como sigue:

let user = {
  name: "Juan",
  toString() {
    return this.name;
  }
};

// Por defecto, nuestras propiedades se listan:
for (let key in user) alert(key); // name, toString

Si no nos gusta, podemos establecer enumerable:false. Entonces, no aparecerá en bucles for..in, exactamente como el incorporado:

let user = {
  name: "Juan",
  toString() {
    return this.name;
  }
};

Object.defineProperty(user, "toString", {
  enumerable: false
});

// Ahora nuestro toString desaparece:
for (let key in user) alert(key); // nombre

Las propiedades no enumerables también se excluyen de Object.keys:

alert(Object.keys(user)); // name

Non-configurable

Los indicadores no configurables (configurable:false) a veces es un preajuste para los objetos propiedades incorporadas.

Una propiedad no configurable no puede ser eliminada ni cambiada por defineProperty.

Por ejemplo, Math.PI es de solo lectura, no enumerable y no configurable:

let descriptor = Object.getOwnPropertyDescriptor(Math, 'PI');

alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
  "value": 3.141592653589793,
  "writable": false,
  "enumerable": false,
  "configurable": false
}
*/

Así que, un programador es incapaz de cambiar el valor de Math.PI o sobrescribirlo.

Math.PI = 3; // Error

// delete Math.PI tampoco funcionará

Convertir una propiedad en no configurable es hacer una calle de una vía. No podremos cambiarla de vuelta, porque defineProperty no funciona en propiedades no configurables.

Para ser precisos, la no configurabilidad impone varias restricciones a defineProperty:

  1. No se puede cambiar el indicador configurable.
  2. No se puede cambiar el indicador enumerable.
  3. No se puede cambiar writable: false a true (al revés funciona).
  4. No se puede cambiar get/set por una propiedad accesoria (pero puede asignarlos si está ausente).

La idea de “configurable: false” es prevenir cambios en los indicadores de la propiedad y su eliminación mientras que permite el cambio de su valor.

Aquí user.name es “non-configurable”, pero aún puede cambiarse (por ser “writable”):

let user = {
  name: "John"
};

Object.defineProperty(user, "name", {
  configurable: false
});

user.name = "Pete"; // funciona
delete user.name; // Error

Y aquí hacemos user.name una constante “sellada para siempre”:

let user = {
  name: "John"
};

Object.defineProperty(user, "name", {
  writable: false,
  configurable: false
});

// No seremos capaces de cambiar usuario.nombre o sus identificadores
// Nada de esto funcionará:
user.name = "Pedro";
delete user.name;
Object.defineProperty(user, "name", { value: "Pedro" });

Object.defineProperties

Hay un método Object.defineProperties(obj, descriptors) que permite definir varias propiedades de una sola vez.

La sintaxis es esta:

Object.defineProperties(obj, {
  prop1: descriptor1,
  prop2: descriptor2
  // ...
});

Por ejemplo:

Object.defineProperties(user, {
  name: { value: "Juan", writable: false },
  surname: { value: "Perez", writable: false },
  // ...
});

Entonces, podemos asignar varias propiedades al mismo tiempo.

Object.getOwnPropertyDescriptors

Para obtener todos los descriptores al mismo tiempo, podemos usar el método Object.getOwnPropertyDescriptors(obj).

Junto con Object.defineProperties puede ser usado como una forma de “indicadores-conscientes” al clonar un objeto:

let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));

Normalmente cuando clonamos un objeto, usamos una sentencia para copiar las propiedades, como esta:

for (let key in user) {
  clone[key] = user[key]
}

…Pero eso no copia los identificadores. Así que si queremos un “mejor” clon entonces se prefiere Object.defineProperties.

Otra diferencia es que for..in ignora propiedades simbólicas, pero Object.getOwnPropertyDescriptors retorna todos los descriptores de propiedades incluyendo los simbolicos.

Sellando un objeto globalmente

Los descriptores de propiedad trabajan al nivel de propiedades individuales.

También hay métodos que limitan el acceso al objeto completo:

Object.preventExtensions(obj)
Prohíbe añadir propiedades al objeto.
Object.seal(obj)
Prohíbe añadir/eliminar propiedades, establece todas las propiedades existentes como configurable: false.
Object.freeze(obj)
Prohíbe añadir/eliminar/cambiar propiedades, establece todas las propiedades existentes como configurable: false, writable: false.

Y también hay pruebas para ellos:

Object.isExtensible(obj)
Devuelve false si esta prohibido añadir propiedades, si no true.
Object.isSealed(obj)
Devuelve true si añadir/eliminar propiedades está prohibido, y todas las propiedades existentes tienen configurable: false.
Object.isFrozen(obj)
Devuelve true si añadir/eliminar/cambiar propiedades está prohibido, y todas las propiedades son configurable: false, writable: false.

Estos métodos son usados rara vez en la práctica.

Mapa del Tutorial

Comentarios

lea esto antes de comentar…
  • Si tiene sugerencias sobre qué mejorar, por favor enviar una propuesta de GitHub o una solicitud de extracción en lugar de comentar.
  • Si no puede entender algo en el artículo, por favor explique.
  • Para insertar algunas palabras de código, use la etiqueta <code>, para varias líneas – envolverlas en la etiqueta <pre>, para más de 10 líneas – utilice una entorno controlado (sandbox) (plnkr, jsbin, codepen…)