Propiedades de objetos JavaScript ES6
Los objetos contienen propiedades clave-valor
Las frases JavaScript está basado en objetos
, Los objetos se relacionan a través de los prototipos
y Los objetos contienen propiedades clave-valor
podrían servir para identificar este lenguaje de programación. La última podría parecer innecesaria, pero describe la estructura de los objetos en JavaScript, dado que una propiedad es una entidad como una pareja clave-valor. Podríamos pensar, por ejemplo, en objetos que contengan una lista de valores en lugar de una lista de propiedades clave-valor.
La notación literal de un Array en JavaScript podría ser como ["abc", 789]
, aparentando ser sólo una lista de valores. Pero ese Array es sobre todo un objeto que está almacenado como Array {0: "a", 1: 789, length: 2}
. Sus propiedades son parejas clave-valor. Las claves son numéricas y además el objeto tendrá una propiedad length
que indicará el total de elementos que tiene el Array.
El nuevo built-in Set
también es una lista de valores. Podemos crear un nuevo conjunto con new Set().add("abc").add(789)
. En la consola de los navegadores quizás lo veremos representado como Set {"abc", 789}
.
Es parecido a un Array pero realmente su estructura interna es diferente al Array, pues será algo como Set {size: 2, [[Entries]]: Array{0: "abc", 1: "789", length: 2}}
. Observe entonces que Set es también un objeto y no sólo una simple lista de valores. Sólo tiene una propiedad externa size
que indica el total de elementos, los cuales se almacenan a su vez en la propiedad interna [[Entries]]
por medio de, precisamente, un Array. Las propiedades internas se representan con corchetes dobles y no son directamente accesibles.
Por lo tanto cualquier objeto de JavaScript está constituido como una lista de propiedades, con cada propiedad constituida como parejas clave-valor. Sobre los métodos para crear y modificar esas propiedades nos ocuparemos en este tema.
Object.defineProperty(obj, prop, desc)
Con Object.defineProperty()
creamos una nueva propiedad o modificamos una existente, devolviéndonos el objeto que deberá pasarse en el primer argumento. El segundo argumento es el nombre de la propiedad. El último argumento es un descriptor de propiedad. Hay dos grupos de descriptores, por un lado el descriptor de datos y por otro el descriptor de acceso. Ambos grupos comparten los descriptores configurable
y enumerable
. Si definimos una propiedad con un descriptor de datos podríamos usar value
y writable
como en el siguiente ejemplo:
let obj = Object.defineProperty({a: 1}, "b", { //Para ambos // Valor predeterminado configurable: false, // false enumerable: false, // false //Sólo para descriptor de dato value: 2, // undefined writable: false // false }); console.log(obj); // Object {a: 1, b: 2}
En el apartado siguiente Object.defineProperties()
explicaremos que significa cada uno de los descriptores configurable
, enumerable
y writable
. En este apartado nos limitaremos a entender como definimos propiedades para un objeto.
Si definimos una propiedad con un descriptor de acceso haremos uso de los getter y setter. En el siguiente código vemos que pueden aparecer configurable
y enumerable
, pero en lugar de value
y writable
hacemos uso de los métodos get()
para obtener el valor de la propiedad y set()
para asignarle un nuevo valor. Observe como usamos una variable externa donde almacenamos el valor, pues realmente no lo estamos guardando en el objeto.
let x; let obj = Object.defineProperty({a: 1}, "b", { //Para ambos // Valor predeterminado configurable: false, // false enumerable: true, // false //Sólo para descriptor de acceso //Getter get: function (){ // undefined return x; }, //Setter set: function(valor){ // undefined x = valor; } }); console.log(obj); // Object {a: 1} //Estableciendo con el setter la propiedad "b" obj.b = 2; //Obteniendo con el getter la propiedad "b" console.log(obj.b); // 2 //La propiedad "b" formará parte de las claves del objeto //si se establece la propiedad como enumerable console.log(Object.keys(obj)); // ["a", "b"] //En la consola del navegador no aparecerá inicialmente //la propiedad "b". El motivo se explica en el texto. console.log(obj); // Object {a: 1}
Una asignación como obj.b = 2
se podría asimilar
a la ejecución de un método obj.set(2)
que modificara la variable externa. Luego con console.log(obj.b)
sería como
si ejecutáramos otro método get()
haciendo console.log(obj.get())
.
Pero no hay que olvidar que get()
y set()
no son métodos del objeto sino que son métodos del descriptor, por lo que no podrán ejecutarse directamente sobre el objeto:
let obj = Object.defineProperty({}, "a", { get : function (){return 1} }); console.log(obj.get()); //TypeError: obj.get is not a function
Vamos a explicar ahora cuál es el mótivo por el que no aparece la propiedad b
al hacer console.log(obj)
. Se debe a que la consola del navegador captura una instántanea
del objeto, no rellenando aquellas propiedades que no estén disponibles en ese momento. Entre ellas están las definidas con getter. En la primera captura de la consola adjunta verá como el navegador representa el objeto.
El objeto aparece como Object {a: 1}
, pero si desplegamos el objeto veremos la propiedad b
sin valor. Hay tres puntos consecutivos a modo de enlace. Si nos sitúamos encima veremos en la segunda imagen que si se pulsa invocará una propiedad getter. Así ya en la tercera imagen se habrá obtenido el valor de esa propiedad.
Siguiendo con defineProperty()
hay que remarcar que los dos grupos de descriptores de datos y de acceso son incompatibles. O usamos value
y writable
o usamos get
y set
.
let obj = Object.defineProperty({a: 1}, "b", { value: 2, get: function (){ return 2; } }); //TypeError: Invalid property descriptor. Cannot both specify //accessors and a value or writable attribute
Los tres argumentos de defineProperty()
son requeridos. En el siguiente código pasamos un objeto vacío, una nueva propiedad con clave a
y un objeto descriptor también vacío. Los descriptores tomarán los valores predeterminados que comentamos antes, tomándose el grupo descriptor de datos como predeterminado.
let obj = Object.defineProperty({}, "a", {}); console.log(obj); // Object {a: undefined}
Los símbolos también pueden ser nombres de propiedades. En este ejemplo creamos un nuevo símbolo y lo guardamos en una variable para posteriormente poder referirnos a él. Hacemos la propiedad writable
para poder modificarla con una asignación, como veremos posteriormente en este tema.
let sym = Symbol("a"); let obj = Object.defineProperty({}, sym, {value: 1, writable: true}); console.log(obj); // Object {Symbol(a): 1} obj[sym] = 99; console.log(obj); // Object {Symbol(a): 99}
Recuerde que podemos usar un símbolo global único en su alcance con Symbol.for(key)
con lo que cada vez que lo referenciemos nos apuntará al mismo. Así evitaremos guardarlo en una variable:
let obj = Object.defineProperty({}, Symbol.for("a"), {value: 1, writable: true}); console.log(obj); // Object {Symbol(a): 1} obj[Symbol.for("a")] = 99; console.log(obj); // Object {Symbol(a): 99}
Accesores de datos getter y setter
Expliquemos un poco más sobre los accesores de datos o también llamados los getter y setter de un objeto. No habría ningún problema en usar las claves get
y set
como métodos del objeto, pero esto no es lo mismo que getter y setter:
let x; let obj = { get: function(){return x}, set: function(v){x=v} }; obj.set(1); console.log(obj.get()); // 1
Sin embargo los getter y setter pueden aplicarse directamente sobre un objeto en lugar de agregarlos con defineProperty()
. En este caso no son métodos del objeto, sino verdaderos accesores
de datos. En el siguiente ejemplo la propiedad b
es tratada como accesor:
let x; let obj = { get b(){return x}, set b(v){x=v} }; //Establecemos valor de la propiedad obj.b = 1; //Obtenemos valor de la propiedad console.log(obj.b); // 1
¿Y qué ventaja aportan estos accesores en lugar de usar propiedades como hacemos siempre? Supongamos que tenemos un módulo de JavaScript con una función que permite agregar un par de elementos al final de la página.
function agregarElementos(){ document.body.innerHTML += ` <div class="res">texto...</div> <div class="res">otro texto...</div>`; }
Estos elementos tiene un nombre de clase "res"
que nos permitirá dotarlos de estilo resaltado en otro momento de ejecución de dicho módulo, lo que haríamos con otra función:
function resaltar(){ for (let elem of obj.res){ elem.style.backgroundColor = "cyan" } }
La variable obj
será un objeto que aglutina propiedades para gestionar el módulo, por lo que habrá que declarar ese objeto con alcance global con respecto a dicho módulo. Contendrá una propiedad con una recopilación de todos los elementos con clase "res"
obtenida con el método del DOM querySelectorAll()
.
let obj = { res: document.querySelectorAll(".res") // ... otras propiedades para gestionar el módulo };
Es obvio que si la función agregar elementos se va a ejecutar posteriormente a esta declaración del objeto obj
, la propiedad res
no va a contener nada y nada será resaltado. Se soluciona usando un getter en la declaración del objeto:
let obj = { get res(){ return document.querySelectorAll(".res") } // ... otras propiedades para gestionar el módulo };
Cuando declaramos el objeto no se ejecutará el método get
. Así cuando ejecutemos la función para resaltar, la propiedad obj.res
será rellenada en ese momento. En el siguiente ejemplo interactivo podrá verlo en ejecución:
Ejemplo: Usando accesor getter
delete obj.res
, a continuación agregaremos elementos con document.querySelectorAll(".res")
sin o con un getter según la opción elegida y, finalmente, agregaremos los elementos al DOM. Así será como simular que estamos en el punto de carga de la página y el script está cargado pero los elementos aún no existen en el DOM.obj.res.length =
"res"
obtenido del navegador:Cada vez que ejecutemos resaltar se intentará llenar la lista de nodos en cualquiera de las dos primeras opciones. Con la primera opción la lista siempre estará vacía, por lo que no se resaltará ningún elemento. Con la segunda opción en cada ejecución se recupera la lista. Consultando obj.res.length
observamos que contiene los dos elementos. Con la última opción que cachea la lista de nodos, ésta sólo será recuperada la primera vez, apareciendo primero el descriptor de acceso get
y en las siguientes veces que pulsemos el botón de Resaltar
aparecerá el descriptor de datos value
.
Se podría argumentar que porque no usamos una propiedad res: null
y rellenamos la lista de nodos obj.res
antes de cada ejecución de la función para resaltar:
function resaltar(){ obj.res = document.querySelectorAll(".res"); for (let elem of obj.res){ elem.style.backgroundColor = "cyan" } }
Pero esto no siempre será posible. Por ejemplo, si es una lista de nodos muy larga que ocupa mucho tiempo obtenerla, quizás no nos interese ejecutar esa acción sino sólo la primera vez. O incluso esa lista de nodos posiblemente no llegue a usarse y por tanto no tenemos que estar invirtiendo tiempo en cargarla hasta que la necesitemos.
Si la lista de nodos no va a cambiar entre dos ejecuciones de la función resaltar()
que la utiliza, podría ser conveniente cachearla. Así no tendríamos que cargarla nuevamente. Este cacheado de valor, que se conoce en inglés como memoization, se usa cuando el valor que devuelve una función no cambiará entre ejecuciones. En el siguiente ejemplo la primera vez que se ejecuta el getter se elimina la propiedad get res()
(el propio getter). A continuación se asigna el valor a una propiedad con descriptor de datos, es decir, una propiedad con clave clásica. Así en los siguientes accesos ya no hay que volver a cargar la lista de nodos en el ejemplo interactivo anterior.
let obj = { get res(){ delete this.res; return this.res = document.querySelectorAll(".res") } // ... otras propiedades para gestionar el módulo };
Note que podemos eliminar la propiedad get res()
porque la hicimos configurable, lo que explicaremos más adelante en un apartado sobre el descriptor configurable
.
Object.defineProperties(obj, props)
El método genérico Object.defineProperties(obj, prop)
agrega o modifica propiedades a un objeto que se pasará en el primer argumento. El segundo argumento es un objeto cuyas claves son los nombres de las propiedades a agregar o modificar y cuyos valores son objetos descriptores tal como se expuso en el apartado anterior Object.defineProperty()
.
let obj = Object.defineProperties({a: 1}, { b: {value: 2, enumerable: true}, c: {get: function(){return 3}, enumerable: true} }); console.log(Object.keys(obj)); // ["a", "b", "c"]
No hay diferencia de comportamiento con el método Object.defineProperty()
, sólo que ahora podemos definir más de una propiedad.
Aprovecharemos este apartado para explicar los descriptores configurable
, enumerable
y writable
. En primer lugar nos preguntaremos cuáles son los descriptores predeterminados. Los visualizaremos para un objeto donde creamos dos propiedades con defineProperties()
, pero usando sólamente el descriptor de datos value
:
//Objeto con definicion de valores let obj = Object.defineProperties({}, { a: {value: 1}, b: {value: 2} });
En la otra opción del ejemplo usaremos un objeto literal con los mismas claves y valores de propiedades:
//Objeto literal let obj = {a: 1, b: 2};
Por cada opción podremos recuperar los descriptores de las propiedades con el método getOwnPropertyDescriptor()
que veremos con más detalle en un apartado más adelante. Basta decir ahora que ese método recupera los descriptores de una propiedad. El descriptor es un objeto del cual obtenemos las claves con el metodo de Array keys()
. Usando el método también de Array reduce()
obtenemos una representación de código del objeto descriptor de cada una de las dos propiedades.
document.getElementById("ver-descriptores").addEventListener("click", () => { let arr = ["a", "b"], cad = ""; for (let clave of arr){ cad += `"${clave}": {`; let descriptor = Object.getOwnPropertyDescriptor(obj, clave); cad += Object.keys(descriptor).reduce((p, v) => `${p}\n${" ".repeat(3)}${v}: ${descriptor[v]}`, "") cad += '\n}\n'; } document.getElementById("lista-descriptores").innerHTML = wxRC.resaltar(cad, "js"); });
Si ejecuta el ejemplo obtendrá estos descriptores, donde se observa que para un objeto literal estarán activados por defecto los descriptores configurable
, enumerable
y writable
, mientras que usando defineProperties()
estarán desactivados.
Objeto literal | Con defineProperties |
---|---|
"a": { value: 1 writable: true enumerable: true configurable: true } "b": { value: 2 writable: true enumerable: true configurable: true } | "a": { value: 1 writable: false enumerable: false configurable: false } "b": { value: 2 writable: false enumerable: false configurable: false } |
Ejemplo: Descriptores predeterminados
//Objeto literal let obj = {a: 1, b: 2};
Por lo tanto si queremos definir una propiedad con descriptores y que se comporten como si hubiésemos usado una notación literal hemos de especificarlos explicítamente y valorarlos a true
.
Object.getOwnPropertyDescriptor(obj, prop), Object.getOwnPropertyDescriptors(obj)
El método getOwnPropertyDescriptor(obj, prop)
nos devuelve los descriptores de una propiedad de un objeto.
let obj = {a: 1}; let desc = Object.getOwnPropertyDescriptor(obj, "a"); console.log(desc); // Object { // value: 1, // writable: true, // enumerable: true, // configurable: true}
En un tema anterior vimos como usar Object.create() para agregar un prototipo desde un objeto con una propiedad a la que también le pasábamos sus descriptores usando getOwnPropertyDescriptor()
:
let obj1 = {a: 1}; let obj2 = Object.create({b: 2}, {a: Object.getOwnPropertyDescriptor(obj1, "a")}); console.log(obj1 === obj2); // false
Observará que los objetos son distintos. El segundo tiene como prototipo una copia del primero, copiando no sólo el objeto sino también los descriptores de su única propiedad.
Si el primer objeto tuviese más de una propiedad tendríamos que usar getOwnPropertyDescriptors(obj)
. Pero el método estará disponible en ES7 (2017), por lo que por ahora podemos usar lo siguiente para implementarlo:
if (!Object.hasOwnProperty("getOwnPropertyDescriptors")){ Object.getOwnPropertyDescriptors = function(obj) { let opd = {}; let keys = Object.getOwnPropertyNames(obj). concat(Object.getOwnPropertySymbols(obj)); for (let key of keys){ opd[key] = Object.getOwnPropertyDescriptor(obj, key); } return opd; } } let obj1 = {a: 1, x: 5, y: 6}; obj2 = Object.create({b: 2}, Object.getOwnPropertyDescriptors(obj1)); console.log(obj1 === obj2); // false console.log(obj2); // Object {a: 1, x: 5, y: 6} console.log(Object.getPrototypeOf(obj2)); // Object {b: 2}
El descriptor enumerable
En el tema sobre iterar por un Array expusimos diversas formas de iterar por un Array usando métodos de Object. Pues en cualquier caso un Array también es un objeto. El método Object.keys()
nos devuelve un Array de claves sólo si tienen el descriptor enumerable activado. Mientras que getOwnPropertyNames()
nos devuelve ambas, enumerables y no enumerables. El bucle for-in
, con o sin hasOwnProperty()
, sólo devuelve las enumerables.
let obj = Object.defineProperties({}, { "a": {value: 1, enumerable: true}, "b": {value: 2, enumerable: false} }); //Con el método keys() obtenemos sólo las enumerables console.log(Object.keys(obj)); // ["a"] //Con getOwnPropertyNames() obtenemos enumerables y no enumerables console.log(Object.getOwnPropertyNames(obj)); // ["a", "b"] //Un bucle for-in con hasOwnProperty sólo accede a enumerables let keys = []; for (let prop in obj){ keys.push(prop); } console.log(keys); // ["a"]
En el siguiente tema veremos que hay un método {}.propertyIsEnumerable()
con el que podremos consultar si una propiedad tiene el descriptor enumerable activado. Consulte ese apartado para ver más detalles sobre el método.
let obj = Object.defineProperties({}, { "a": {value: 1, enumerable: true}, "b": {value: 2, enumerable: false} }); console.log(obj.propertyIsEnumerable("a")); // true console.log(obj.propertyIsEnumerable("b")); // false //Obtenemos lo mismo con getOwnPropertyDescriptor console.log(Object.getOwnPropertyDescriptor(obj, "a"). enumerable); // true console.log(Object.getOwnPropertyDescriptor(obj, "b"). enumerable); // false
El descriptor configurable
Ya vimos en un apartado anterior que si no especificamos writable
, enumerable
o configurable
éstos son declarados como falsos. En la consola del navegador obtenemos con getOwnPropertyDescriptor()
el objeto descriptor de un objeto al que le incorporamos una propiedad con defineProperty()
:
let obj = Object.defineProperty({}, "a", {value: 1}); console.log(Object.getOwnPropertyDescriptor(obj, "a")); //Object {value: 1, // writable: false, // enumerable: false, // configurable: false}
Si el descriptor no es configurable no podremos modificar ninguno de los descriptores de esa propiedad, ni siquiera el valor o el propio descriptor configurable. Nos lanzará un error de que no puede redefinir la propiedad
:
//No podemos modificar los descriptores try { obj = Object.defineProperty(obj, "a", {value: 0}); } catch(e){ console.log(e.message); // Cannot redefine property: a } try { obj = Object.defineProperty(obj, "a", {writable: true}); } catch(e){ console.log(e.message); // Cannot redefine property: a } try { obj = Object.defineProperty(obj, "a", {enumerable: true}); } catch(e){ console.log(e.message); // Cannot redefine property: a } try { obj = Object.defineProperty(obj, "a", {configurable: true}); } catch(e){ console.log(e.message); // Cannot redefine property: a }
En cambio si hacemos la propiedad configurable
let obj = Object.defineProperty({}, "a", { value: 1, configurable: true }); console.log(Object.getOwnPropertyDescriptor(obj, "a")); //Object {value: 1, // writable: false, // enumerable: false, // configurable: true}
Entonces si que podemos modificar todos los descriptores, incluso el propio descriptor configurable.
obj = Object.defineProperty(obj, "a", { value: 0, writable: true, enumerable: true, configurable: false }); console.log(Object.getOwnPropertyDescriptor(obj, "a")); //Object {value: 0, // writable: true, // enumerable: true, // configurable: false}
Otro efecto del descriptor configurable es que cuando está desactivado nos impide eliminar una propiedad usando el operador delete
, no advirtiendo como error este hecho. En el siguiente ejemplo la primera propiedad es configurable y podemos eliminarla. La segunda no lo es y el borrado no tiene ningún efecto ni es advertido.
let obj = Object.defineProperties({}, { "a": {value: 1, configurable: true}, "b": {value: 2, configurable: false} }); console.log(obj); // Object {a: 1, b: 2} //Eliminamos la propiedad "a" delete obj.a; console.log(obj); // Object {b: 2} //No se eliminará la propiedad "b" y no se advertirá delete obj.b; console.log(obj); // Object {b: 2}
El descriptor writable
El descriptor writable
indica si la propiedad puede ser modificada a través de una asignación. En el siguiente ejemplo tenemos que la propiedad b
no es writable
. Una reasignación del valor no será recogido y además no causará error. Si b
fuera configurable podríamos modificar el descriptor value
, como se observa en el ejemplo. Si no es configurable nos daría un error al intentar cambiar ese valor, como sucede con la propiedad c
, que no es writable
ni configurable
. Es una propiedad inmutable pues no podrá ser eliminada (dado que no es configurable) ni modificada.
let obj = Object.defineProperties({}, { "a": {value: 1, writable: true, configurable: false}, "b": {value: 2, writable: false, configurable: true}, "c": {value: 3, writable: false, configurable: false} }); //"a" es writable y puede reasignarse obj.a = 99; console.log(obj); // Object {a: 99, b: 2, c: 3} //"b" no es writable y no puede reasignarse obj.b = 88; console.log(obj); // Object {a: 99, b: 2, c: 3} //"b" es configurable y puede modificarse usando el descriptor obj = Object.defineProperty(obj, "b", {value: 88}); console.log(obj); // Object {a: 99, b: 88, c: 3} //"c" no es writable ni configurable y no puede //modificarse de ninguna forma. Es inmutable. obj.c = 77; console.log(obj); // Object {a: 99, b: 88, c: 3} try { obj = Object.defineProperty(obj, "c", {value: 77}); } catch(e){ console.log(e.message); // Cannot redefine property: c }
Por lo tanto un objeto inmutable es áquel en el que todas sus propiedades son inmutables y además no permite agregar nuevas propiedades. En el siguiente apartado veremos que el método Object.freeze()
nos permitirá hacer inmutable un objeto.
Object.freeze(obj), Object.isFrozen(obj)
Congelar un objeto supone desactivar los descriptores writable
y configurable
, tal como vimos en el apartado anterior. El método Object.freeze()
se encarga de hacer esto y bloquear el objeto para impedir agregar nuevas propiedades.
let obj = Object.defineProperty({}, "a", { value: 1, writable: true, enumerable: true, configurable: true }); obj = Object.freeze(obj); obj.a = 2; console.log(obj); // Object {a: 1} console.log(Object.getOwnPropertyDescriptor(obj, "a")); // Object { // value: 1, // writable: false, // enumerable: true, // configurable: false //}
El método Object.freeze()
convierte en inmutable un objeto. El objeto queda congelado
, lo que podemos consultar con Object.isFrozen()
. En el siguiente ejemplo congelamos un objeto comprobando que no podemos modificar el valor de una propiedad con una asignación. Si no estamos en modo estricto no lanzará ningún error. Con defineProperty
si que nos emitirá un error de que no puede redefinir la propiedad.
let obj = {a: 1} obj = Object.freeze(obj); //El objeto está congelado console.log(Object.isFrozen(obj)); // true //No podemos modificar el valor obj.a = 99; console.log(obj); // Object {a: 1} //Ni con defineProperty try { obj = Object.defineProperty(obj, "a", {value: 99}) } catch(e){ console.log(e.message); // Cannot redefine property: a }
Tampoco podemos agregar nuevas propiedades. Lanzará error en modo estricto y en modo normal con defineProperty
:
let obj = {a: 1} obj = Object.freeze(obj); //No podemos agregar nuevas propiedades obj.b = 2; console.log(obj); // Object {a: 1} //Ni con defineProperty try { obj = Object.defineProperty(obj, "b", {value: 2}) } catch(e){ console.log(e.message); // Cannot define property:b, // object is not extensible. }
La eliminación de propiedades no tendrá ningún efecto:
let obj = {a: 1} obj = Object.freeze(obj); //No podemos eliminar propiedades delete obj.a; console.log(obj); // Object {a: 1}
Sin embargo si un valor es a su vez otro objeto y aunque no podamos reasignar ese valor si que podemos modificar el interior de ese otro objeto. Siempre que no esté congelado también. En el siguiente ejemplo congelamos un objeto. El valor de la primera propiedad es un Array, por tanto un objeto. Al no estar congelado, sus elementos pueden ser modificados:
let obj = {a: [1]} obj = Object.freeze(obj); obj.a[0] = 2; console.log(obj); // Object {a: [2]}
Por lo tanto con freeze()
conseguimos una congelación superficial. Para hacer una congelación profunda podríamos usar un recursivo que itere por el objeto buscando valores que sean a su vez objetos para congelarlos también. Usamos getOwnPropertyNames()
que nos devuelve un Array de las claves de las propiedades enumerables y no enumerables. Lo concatenamos con getOwnPropertySymbols()
pues las claves que sean símbolos no se incluyen en el anterior Array.
function congelar(obj){ let keys = Object.getOwnPropertyNames(obj). concat(Object.getOwnPropertySymbols(obj)); for (let key of keys){ congelar(obj[key]); } return Object.freeze(obj); } let obj = {a: [1, {b: 2}], [Symbol.for("c")]: 3} obj = congelar(obj); //Modificación de valores no surtirán efecto, obj.a[0] = 99; obj.a[1].b = 88; obj[Symbol.for("c")] = 77; //pues todo el objeto está congelado console.log(obj); // Object {a: [1, {b: 2}], Symbol(c): 3}
En el código anterior podría ser aconsejable preguntar en cada propiedad si es un objeto con typeof obj[key] == "object"
para no intentar congelar algo que no sea un objeto. Pero el método Object.freeze(algo)
funciona sea cual sea el tipo del argumento. Si no es un objeto, o incluso si es nulo, devolverá el mismo valor. Además si el objeto estuviera congelado previamente lo devolverá como está.
//Congelar tipos primitivos no tiene ningún efecto console.log(Object.freeze(null)); // null console.log(Object.freeze(undefined)); // undefined console.log(Object.freeze(123)); // 123 console.log(Object.freeze("abc")); // "abc" //Congelar varias veces no tiene ningún efecto let obj = Object.freeze({a: 1}); console.log(Object.freeze(obj)); // Object {a: 1} console.log(Object.isFrozen(obj)); // true
También podemos congelar un Array, pues es un objeto:
let arr = Object.freeze([1, 2, 3]); //Modificar un elemento no tiene efecto arr[1] = 99; console.log(arr); // [1, 2, 3] //Eliminar un elemento no tiene efecto delete arr[1]; console.log(arr); // [1, 2, 3] //Agregar un elemento no tiene efecto arr[arr.length] = 4; console.log(arr); // [1, 2, 3] //Agregar con métodos de Array tampoco //tiene efecto pero acusa error try { arr.push(4); } catch(e){ console.log(e.message); // Can't add property 3, // object is not extensible }
Object.seal(obj), Object.isSealed(obj)
El método Object.seal()
sella el objeto desactivando el descriptor configurable
de todas sus propiedades. Esto tiene como principal efecto el impedimento para agregar nuevas propiedades. Aunque en modo normal agregando una propiedad no acusará error. Con el método Object.isSealed()
comprobamos si un objeto está sellado. Recordando lo visto en apartados anteriores, configurable
desactivado no permite modificar los descriptores, pero no impide una asignación directa si writable
esté activado.
let obj = {a: 1, b: 2}; obj = Object.seal(obj); console.log(Object.isSealed(obj)); // true //El descriptor configurable será ahora falso console.log(Object.getOwnPropertyDescriptor(obj, "a")); //Object {value: 1, writable: true, enumerable: true, // configurable: false} //No podemos agregar nuevas propiedades obj.b = 2; console.log(obj); // Object {a: 1} //Pero si podemos cambiar el valor de las //propiedades existentes con writable activado obj.a = 99; console.log(obj); // Object {a: 99}
Si el argumento de seal()
es un tipo primitivo (algo que no sea un objeto) el método lo devuelve tal cual. Además isSealed()
considera siempre sellado un tipo primitivo aunque previamente no le apliquemos seal()
:
//Sellar un tipo primitivo no tiene ningún efecto console.log(Object.seal(null)); // null console.log(Object.seal(undefined)); // null console.log(Object.seal(123)); // 123 //Los tipos primitivos están siempre sellados console.log(Object.isSealed("abc")); // true
Al igual que con freeze()
, es un sellado superficial. En el siguiente ejemplo tenemos objetos anidados, observando que sellar el objeto exterior no sella los interiores:
let obj = {x: {y: {z: [1]}}}; obj = Object.seal(obj); console.log(Object.isSealed(obj)); // true console.log(Object.isSealed(obj.x)); // false console.log(Object.isSealed(obj.x.y)); // false console.log(Object.isSealed(obj.x.y.z)); // false
Con un recursivo podemos sellar en profundidad:
function sellar(obj){ let keys = Object.getOwnPropertyNames(obj). concat(Object.getOwnPropertySymbols(obj)); for (let key of keys){ sellar(obj[key]); } return Object.seal(obj); } let obj = {x: {y: {z: [1]}}}; obj = sellar(obj); console.log(Object.isSealed(obj)); // true console.log(Object.isSealed(obj.x)); // true console.log(Object.isSealed(obj.x.y)); // true console.log(Object.isSealed(obj.x.y.z)); // true
Como los Array también son objetos, podríamos sellar un Array. En este ejemplo sellamos el Array y no podemos agregar nuevos elementos. Con una asignación no da error en modo normal. Con métodos como push()
si acusa error:
let arr = [1, 2, 3]; arr = Object.seal(arr); //Agregar elementos no tiene ningún efecto arr[arr.length] = 4; console.log(arr); // [1, 2, 3] //Con método push() da error try { arr.push(4); } catch(e){ console.log(e.message); // Can't add property 3, // object is not extensible }
Object.preventExtensions(obj), Object.isExtensible(obj)
El método Object.preventExtensions()
previene extensiones impidiendo que se agreguen nuevas propiedades a un objeto, lo que lo hace no extensible. Sabremos que un objeto es extensible con el método Object.isExtensible()
. Una vez que hagamos no extensible un objeto no hay forma de revertir la situación.
En el siguiente ejemplo hacemos no extensible un objeto, lo cual no actúa sobre los descriptores. No podremos agregar nuevas propiedades con una asignación aunque no acusará error en modo normal. En cambio usando defineProperty
si acusará error.
let obj = {a: 1}; obj = Object.preventExtensions(obj); console.log(Object.isExtensible(obj)); // false //Prevenir extensiones no modifica los descriptores console.log(Object.getOwnPropertyDescriptor(obj, "a")); //Object {value: 1, writable: true, enumerable: true, // configurable: true} //Pero no podemos agregar nuevas propiedades obj.b = 2; console.log(obj); // Object {a: 1} //Ni siquiera usando defineProperty try { obj = Object.defineProperty(obj, "b", {value: 2}); } catch(e){ console.log(e.message); // Cannot define property:b, // object is not extensible. }
Congelando, sellando o haciendo no extensible un objeto impiderá agregar nuevas propiedades, por lo que en principio hacen lo mismo. El congelado y el sellado tienen su descriptor configurable
desactivado, mientras que el no extensible no afecta a sus descriptores. En el congelado o sellado no podemos actuar con defineProperty
, pero en el no extensible si podríamos modificar los descriptores de las propiedades existentes.
En el siguiente ejemplo partimos de un objeto literal con lo que configurable
estará activado. Haciendo no extensible el objeto no impedirá modificar la propiedad existente con defineProperty
. Si estuviera congelado o sellado no podríamos hacerlo:
let obj = Object.preventExtensions({a: 1}); obj = Object.defineProperty(obj, "a", { value: 2, enumerable: false }); console.log(obj); // Object {a: 2}
Los objetos inicialmente son extensibles de forma predeterminada. Si el argumento que pasamos a preventExtensions()
no es un objeto, es decir, es un tipo primitivo, la ejecución no tiene ningún efecto devolviendo el valor tal cual.
//Un objeto literal es extensible de forma predeterminada console.log(Object.isExtensible({})); // true //Pero no lo son los tipos primitivos console.log(Object.isExtensible(null)); // false console.log(Object.isExtensible(undefined)); // false console.log(Object.isExtensible(123)); // false //Hacer no extensible un tipo primitivo no tiene efecto let str = Object.preventExtensions("abc"); console.log(str); // "abc" console.log(Object.isExtensible(undefined)); // false
Como en casos anteriores, el método preventExtensions()
es de tratamiento superficial. Para hacer no extensible en profundidad necesitamos un recursivo. En el siguiente ejemplo todos los objetos anidados resultarán no extensibles:
function hacerNoExtensible(obj){ let keys = Object.getOwnPropertyNames(obj). concat(Object.getOwnPropertySymbols(obj)); for (let key of keys){ hacerNoExtensible(obj[key]); } return Object.preventExtensions(obj); } let obj = {x: {y: {z: [1]}}}; obj = hacerNoExtensible(obj); console.log(Object.isExtensible(obj)); // false console.log(Object.isExtensible(obj.x)); // false console.log(Object.isExtensible(obj.x.y)); // false console.log(Object.isExtensible(obj.x.y.z)); // false
Un Array es un objeto que podemos hacer no extensible. En este ejemplo no podremos agregar nuevos elementos al Array. Con una asignación no acusará error en modo normal, pero usando métodos del prototipo de Array push()
o unshift()
que agregan elementos si acusará error en modo normal:
//Hacemos no extensible un Array let arr = [1, 2, 3]; arr = Object.preventExtensions(arr); //No podemos agregar elementos al Array arr[arr.length] = 4; console.log(arr); // [1, 2, 3] //Ni con métodos del prototipo de Array try { arr.push(4); } catch(e){ console.log(e.message); // Can't add // property 3, object is not extensible } try { arr.unshift(4); } catch(e){ console.log(e.message); // Can't add // property 3, object is not extensible }
Relación entre los descriptores de un objeto y los métodos freeze(), seal() y preventExtensions()
En este apartado veremos la relación existente entre los descriptores configurable
y writable
con los métodos freeze()
, seal()
y preventExtensions
. Si partimos de un objeto declarado con notación literal sabemos que los descriptores writable
y configurable
estarán activados por defecto. La ejecución de los métodos vistos sobre un objeto como ése conducirá al resumen de la siguiente tabla:
Método | configurable | writable |
---|---|---|
let obj = {a: 1}; | true | true |
obj = Object.freeze(obj) | false | false |
obj = Object.seal(obj) | false | true |
obj = Object.preventExtensions(obj) | true | true |
En el siguiente ejemplo interactivo podemos recrear esa situación. Partimos de un objeto con una única propiedad y le aplicamos uno de los metodos vistos. Tras eso probamos a modificar el objeto de varias formas y ver qué pasa.
Ejemplo: Relaciones entre descriptores y métodos de congelación
let obj = {a: 1};
obj.a = 99;