preloader

Indicadores y Descriptores de Propiedad en JavaScript

Indicadores y Descriptores de Propiedad

Como ya sabemos, los objetos en JavaScript pueden almacenar propiedades.

Hasta ahora, hemos tratado una propiedad como un simple par “clave-valor”. Sin embargo, una propiedad de un objeto puede ser mucho más flexible y poderosa.

En este capítulo, exploraremos opciones adicionales de configuración, y en el siguiente, veremos cómo transformarlas en funciones getter/setter para obtener y asignar valores.

Indicadores de Propiedad

Las propiedades de un objeto, además de un valor, tienen tres atributos especiales (también llamados “indicadores”):

  • writable: si es true, la propiedad puede ser modificada; de lo contrario, es de solo lectura.
  • enumerable: si es true, la propiedad puede ser listada en bucles; de otro modo, no.
  • configurable: si es true, la propiedad puede ser eliminada y sus atributos modificados; de lo contrario, no.

No hemos visto estos atributos hasta ahora porque, generalmente, no se muestran. Cuando creamos una propiedad “de la manera usual”, todos estos indicadores son true. Pero podemos cambiarlos en cualquier momento.

Primero, veamos cómo obtener estos indicadores.

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

La sintaxis es:

				
					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 devuelto es un objeto llamado “descriptor de propiedad” que 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 modificar los indicadores, podemos usar Object.defineProperty.

La sintaxis es:

				
					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 lo contrario, creará la propiedad con el valor y el indicador dado; en ese caso, si el indicador no es proporcionado, se asume como false.

En el ejemplo a continuación, 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 no es lo que queremos, es mejor establecerlos en true en el descriptor.

Veamos los efectos de los indicadores con ejemplos.

No-writable

Vamos a hacer que user.name sea 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 usen su propio defineProperty para sobrescribir el nuestro.

Los errores aparecen solo en modo estricto

En el modo no estricto, no se producen errores al intentar escribir en propiedades no editables; pero la operación no tendrá éxito. Las acciones que infringen el indicador se ignoran silenciosamente en el modo no estricto.

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 explícitamente como true
  enumerable: true,
  configurable: true
});

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

				
			

No-enumerable

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

Normalmente, en los objetos, un toString nativo no es enumerable, no se muestra en un bucle for..in. Pero si añadimos nuestro propio toString, por defecto éste se muestra en los bucles for..in:

				
					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 es lo que queremos, podemos establecer enumerable:false. Entonces no aparecerá en bucles for..in, exactamente como el toString nativo:

				
					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

				
			

No-configurable

El indicador “no-configurable” (configurable:false) a veces está preestablecido para los objetos y propiedades nativos.

Una propiedad no configurable no puede ser eliminada, y sus atributos no pueden ser modificados.

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í, un programador no puede cambiar el valor de Math.PI o sobrescribirlo.

				
					Math.PI = 3; // Error, porque tiene writable: false

// delete Math.PI tampoco funcionará

				
			

Tampoco podemos cambiar Math.PI a writable de vuelta:

				
					// Error, porque configurable: false
Object.defineProperty(Math, "PI", { writable: true });

				
			

No hay nada en absoluto que podamos hacer con Math.PI.

Convertir una propiedad en no configurable es irreversible. No podremos cambiarla de vuelta con defineProperty.

Nota que “configurable: false” impide cambios en los indicadores de la propiedad y su eliminación, pero permite el cambio de su valor.

Aquí user.name es “no 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 que user.name sea una constante “sellada para siempre”, tal como la constante nativa Math.PI:

				
					let user = {
  name: "John"
};

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

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

				
			

Único cambio de atributo posible: writable true → false

Hay una excepción menor acerca del cambio de indicadores.

Podemos cambiar writable: true a writable: false en una propiedad no configurable, impidiendo más la modificación de su valor (sumando una capa de protección). Aunque no hay vuelta atrás.

Object.defineProperties

Existe un método Object.defineProperties(obj, descriptors) que permite definir varias propiedades a la vez.

La sintaxis es:

				
					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 a la vez, podemos usar el método Object.getOwnPropertyDescriptors(obj).

Junto con Object.defineProperties, puede ser usado como una forma “consciente de los indicadores” de clonar un objeto:

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

				
			

Normalmente, cuando clonamos un objeto, usamos una asignación para copiar las propiedades:

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

				
			

… pero esto no copia los indicadores. Así que si queremos un “mejor” clon, entonces se prefiere Object.defineProperties.

Otra diferencia es que for..in ignora las propiedades simbólicas y las no enumerables, pero Object.getOwnPropertyDescriptors devuelve todos los descriptores de propiedades, incluyendo simbólicas y no enumerables.

Sellado de un objeto globalmente

Los descriptores de propiedad trabajan a 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/modificar propiedades, establece todas las propiedades existentes como configurable: false y writable: false.

También tenemos formas de probarlos:

  • Object.isExtensible(obj): Devuelve false si está prohibido añadir propiedades; de lo contrario, true.
  • Object.isSealed(obj): Devuelve true si está prohibido añadir/eliminar propiedades y todas las propiedades existentes tienen configurable: false.
  • Object.isFrozen(obj): Devuelve true si está prohibido añadir/eliminar/modificar propiedades y todas las propiedades son configurable: false y writable: false.

Estos métodos se usan rara vez en la práctica.

Related Post

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *