Es sorprendente como podemos hablar de mecanismos complejos de un lenguaje como Promesas o Iteradores y aun así incomodarnos cuando en alguna entrevista nos preguntan “cómo definirías un objeto” o “qué es un prototype”. Y es que son preguntas que atacan directamente a nuestras bases sobre el lenguaje y disciernen entre programadores que saben muy bien usar APIs de programadores que entiende los cimientos sobre las que están construidas.
Y es que, como todo en la vida, se nos puede llenar la boca hablando de un tema sin practicamente entender nada de sus bases. Es natural y no necesariamente malo. El problema viene cuando perdemos la habilidad de extrapolar 2 conceptos al ignorar el princpio básico que los une.
Si quieres masterizar conceptos más avanzados o dotarte de una caja de herramientas útiles para poder afrontar cualquier problema o masterizar los principios básicos de composición, tienes que entender los básicos. De nada sirve hablar del mecanismo por el cual podemos representar un valor futuro si ni siquiera entendemos las formas más esenciales de representación.
¿Qué es un Objeto?
Esta pregunta puede ser contestada de muchas formas y dependerá en mayor medida del prisma por el cual estemos mirando.
Si hablamos desde la perspectiva tradicional de Programación orientada a objetos, estos almacena un estado y un comportamiento. Así por ejemplo un Coche posee información sobre su motor, su mecanismo de tracción, sus atributos estéticos, etc. Y además contiene cierto comportamiento como moverse o “ser conducido” por un actor externo.
Esta descripción nos parece natural porque tendemos a pensar de la misma forma: estamos rodeados de objetos a los cuales le asignamos propiedades y acciones:
Sin embargo, también podríamos pensar que el objeto es solo un conjunto de propiedades que pueden ser manipuladas mediante una acción ejecutada por actores y dicha acción no tiene por qué formar parte del objeto sobre el que se ejecuta.
Así podríamos tener un Coche que almacenase información y a través de la acción “Conducir”, un actor modificase esa información directamente, sin necesidad de que el Coche supiera nada de la acción conducir en sí:
Sea como fuere, al final los objetos son estructuras que agrupan información bajo un mismo paraguas, podemos discutir si la funcionalidad debería estar ahí o no o si los objetos deberían de ser mutables o no, pero en última instancia es la forma que tenemos de almacenar información.
Por ejemplo, en el paradigma funcional los ‘objetos’ actúan simplemente de registros que se van regenerando (no mutando) cada vez que ocurre una actualización.
¿Qué es un objeto en Javascript?
Los objetos dentro de Javascript son estructuras que almacenan “entradas” públicas o privadas de pares clave/valor.
Estas claves pueden ser o bien un String
o un Symbol
y nos permiten identificar el valor que puede ser cualquier primitiva o no primitiva. Así podemos tener objetos “tontos” que solo representen información que pueda ser regenerada o mutada a través de unas acciones o podemos tener objetos stateful - con estado - que se apliquen acciones a si mismo mediante la modificación de sus propios atributos.
Todo en Javascript puede ser un objeto, a excepción de los tipos null
y undefined
.
typeof null === ‘object’
es un bug común en Javascript que no puede ser arreglado para no romper la retro compatibilidad de las numerosas aplicaciones que se apoyan en dicho bug.
Ahora, según como usemos los objetos en Javascript, se pueden clasificar en 2 tipos:
Objetos de esquema fijos.
Objetos diccionarios.
Los primeros hacen referencia a usar objetos con un número de propiedades fijas y cuyas claves se conocen en tiempo de desarrollo. Es la forma más común de uso y el ejemplo típico es pensar en como si fueran registros en una base de datos.
let user = { name: ‘Ana’, lastname: ‘Perez’ };
Los segundos se refieren a cuando las claves de las propiedades son calculadas en runtime — en tiempo de ejecución — de tal forma que los objetos almacenan las propiedades como un Map.
let map = { [‘january’]: ‘Enero’, [‘June’]: ‘Junio’ };
Habitualmente usar un Map es preferible a un Objeto diccionario por multiples razones explicadas aquí.
Hoy nos centraremos en los objetos de esquema fijos por ser los más utilizados.
¿Cómo se crean objetos en Javascript?
Cuando se empieza estudiar el sistema de objetos de Javascript, suele llamar la atención la siguiente sentencia:
“Cualquier función en Javascript puede devolver un objeto nuevo”.
Y es normal que la persona que estudia aquí se plantee el hecho de que realmente en cualquier lenguaje se pueden devolver nuevos objetos y es cierto, la diferencia aquí radica en que en Javascript no hace falta que estos se construyan.
El proceso de construcción de un objeto abarca muchas cosas. En primer lugar, el espacio en memoria ha de ser reservado, seguidamente el objeto ha de ser inicializado mediante la función del constructor. En algunos lenguajes el valor del contexto ha de ser establecido y especificado. Finalmente el puntero al nuevo objeto será devuelto al desarrollador.
Todo esto ocurre mediante el uso de una palabra reservada — normalmente llamada new
— y de un constructor que puede ser una función normal o una Clase.
Pues bien, nada de esto es necesario en Javascript, ya que podemos crear una nueva instancia así:
Esta sintaxis con llaves se conoce como un ‘literal object’ y nos permite crear objetos sin recurrir a procedimientos de construcción.
Por eso en Javascript el valor de this es tan variable. Al no existir un mecanismo explicito que calcule y especifique su valor, se han de recurrir a técnicas como el call-site.
En el objeto user
hemos definido dos propiedades:
Una primera propiedad llamada ‘name’ con el valor ‘Antonio’.
Una segunda propiedad llamada ‘lastname’ con el valor ‘Vivaldi’.
Las reglas para especificar claves son las mismas que las que usamos a la hora de declarar variables normales, con la excepción de que podemos usar palabras reservadas también:
Esto es porque internamente se reprensetan como un String
.
Atajos con variables: Si tenemos una variable cuyo nombre coincide con la clave del objeto, podemos establecerla directamente: Por ejemplo con la variable
const x = 1
, podemos usarla directamente como clave de un objeto así:{ x }
en lugar de hacer{ x: x }.
Para acceder a los valores simplemente usamos la notación punto:
o.const // 1
o.while // 2
o.for // 4
De igual forma para actualizar sus valores:
o.while = 5;
También podemos acceder con la notación
[‘keyName’]
, pero ya que estamos hablando de objetos con esquemas fijos, cuyas propiedades conocemos en tiempo de desarrollo, es preferible usar siempre la notación punto.
Descriptores de propiedades
Hemos visto que las claves pueden ser o bien un String
o bien Symbol
, pero ¿qué ocurre con los tipos de sus valores? Pues bien, las propiedades pueden ser de dos tipos:
Propiedades de datos: Guardan cualquier valor en Javascript (recuerda que una función también es un valor).
Propiedades de acceso: Los famosos getters y setters. Utilizan los modificadores set y get y nos permiten computar propiedades.
Esto es importante porque cuando creamos propiedades en nuestros objetos no guardamos el valor sin más, sino que además se crea lo que se conoce como un PropertyDescriptor
o un descriptor de propiedad que almacena metadatos sobre como diferentes procedimientos de Javascript operan sobre dicha propiedad.
Te has fijado que a veces cuando haces un Object.keys
sobre un objeto, no salen todas las propiedades? Eso es porque su PropertyDescriptor
define que ciertas propiedades son enumerables — es decir, que se ven afectadas por los mecanismos de iteración — y otras no.
La forma que tienen estos PropertyDescriptors
la establece una interface interna. Por ejemplo, esta es la interface para una propiedad de datos:
interface DataPropertyDescriptor {
value?: any;
writable?: boolean;
configurable?: boolean;
enumerable?: boolean;
}
Y esta para las propiedades de acceso:
interface AccessorPropertyDescriptor {
get?: (this: any) => any;
set?: (this: any, v: any) => void;
configurable?: boolean;
enumerable?: boolean;
}
Finalmente, el tipo PropertyDescriptor
es constituido por una unión entre los dos tipos anteriores:
type PropertyDescriptor = DataPropertyDescriptor | AccessorPropertyDescriptor;
Esto es solo una forma de representarlo. No es código Javascript válido, pero es interesante exponerlo para esclarecer conceptos.
Ahora, si usamos la función Object.getOwnPropertyDescriptor
podemos obtener el PropertyDescriptor
de la propiedad de cualquier objeto. Por ejemplo, de nuestro objeto User
de antes:
A continuación, se describen el significado de cada una:
writable: Define si el valor puede ser cambiado o no.
configurable: Determina si podemos reconfigurar una variable o no. Por ejemplo, si es ‘false’: No podemos eliminar la propiedad, ni cambiar el tipo (de datos a acceso o viceversa), no podemos cambiar ningún atributo del descriptor (solo value),
enumerable: Si se tiene en cuenta en las utilidades enumeración de propiedades como
Object.keys
Por defecto las propiedades configurable, enumerable y writable se especifican a true.
La propiedad más interesante es configurable, puesto que una vez definida a false
la propiedad no puede ser reconfigurada de nuevo — esto es, cambiar cualquier de las propiedades del descriptor comentadas anteriormente — por lo que hay que tener cuidado.
Cuando definimos un objeto con la propiedad configurable a
false
, el único cambio que puede hacerse es modificar la variable writable detrue
afalse
. Esto es debido a una transicción historica en la que la propiedad length de unarray
siempre ha sido no configurable pero se ha permitido cambiar la posibilidad de que pueda ser modificada para habilitar el poder ‘congelar’ arrays.
Definiendo propiedades mediante descriptores
Poco sentido tendrían estos descriptores si no pudiéramos definirlos. Este mecanismo suele utilizarse más en librerías donde las creadoras quieren controlar el cómo se accede o se extiende el comportamiento de sus objetos.
Para ello se utiliza la utilidad Object.defineProperty
que nos permite crear una propiedad mediante un descriptor y así poder configurar los valores arriba expuestos.
Object.defineProperty(obj, prop, descriptor)
Dónde ‘obj’ es el objeto donde vamos a definir la propiedad, ‘prop’ es una cadena con el nombre de la propiead y ‘descriptor’ es el descriptor de esa propiedad.
Recuerda que, si estamos definiendo una
DataProperty
o unaAccessorProperty
, necesitaremos proporcionar unDataDescriptorProperty
o unAccessorDescriptorProperty
respectivamente.
Así:
Los valores por defecto de writable, configurable y enumerable son
false
siempre que declaremos la propiedad usando defineProperty. Si agregamos una propiead por el procedimiento normal, los valores por defecto de estos sontrue
.
Alternativamente, podemos definir múltiples propiedades usando Object.defineProperties.
Tipos de claves y valores
Hemos comentado que las claves de los objetos pueden ser o bien un String
o bien un Symbol
. El primero será el que más utilicemos para definir propiedades y el segundo se suele usar por mecanismos internos (como Symbol.iterator) o para técnicas más avanzadas de privacidad de datos — que aprovechan la unicidad de los símbolos para mantener las propiedades privadas.
Una propiedad cuya clave es un Symbol por defecto no es enumerable. Por eso cuando hacemos por ejemplo
Object.keys
sobre un objeto, no aparecen esté tipo claves.
Performance tip: En V8 (el motor Javascript de nodejs y Chrome) se optimizan de forma nativa las propiedades enteras indexadas, por lo que para cadenas de texto numéricas (dentro de un rango especifico) como clave, la insercción e iteración del objeto puede ser más eficiente que otras soluciones como Map.
Sin embargo, hemos mencionado que las propiedades pueden almacenar cualquier tipo de valor, esto es, tanto primitivas como no primitivas. Aunque es importante mencionar que, en el caso de las no primitivas, lo que se almacena dentro de la propiedad es una referencia a la misma, por lo que hay que tener cuidado a la hora de actualizar/borrar elementos o copiar objetos:
Observamos como la mutación de las propiedades del objeto inner son las propiedades del objeto foo
, puesto que la propiedad inner
almacenó una referencia cuando se definió.
De igual forma una copia usando Object.assign
o el spread operator, provocará una copia de las referencias y no una copia en profundidad creando objetos nuevos.
Conclusión
Hemos cubierto los elementos más básicos que se encuentran alrededor de los objetos en Javascript, pero todavía nos falta mucho por ver: copia de accessor properties, cadena prototípica, objetos inicializados y mucho más.
Sin embargo, esta es la puerta para asentar conocimientos y poder entender mecanismos complejos sobre objetos como la composición de objetos.
En la siguiente entregar hablaremos en profundidad de las propiedades de acceso especiales, los getters y setters de los objetos planos.
Nada más por hoy :).