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 estrue
, puede ser editado, de otra manera es de solo lectura.enumerable
– si estrue
, puede ser listado en bucles, de otro modo no puede serlo.configurable
– si estrue
, 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.
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
:
- No se puede cambiar el indicador
configurable
. - No se puede cambiar el indicador
enumerable
. - No se puede cambiar
writable: false
atrue
(al revés funciona). - 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 notrue
. - Object.isSealed(obj)
- Devuelve
true
si añadir/eliminar propiedades está prohibido, y todas las propiedades existentes tienenconfigurable: false
. - Object.isFrozen(obj)
- Devuelve
true
si añadir/eliminar/cambiar propiedades está prohibido, y todas las propiedades sonconfigurable: false, writable: false
.
Estos métodos son usados rara vez en la práctica.
Comentarios
<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…)