Creando instancias y objetos en JavaScript
Introducción
Los objetos en JavaScript son realmente estructuras potentes para manipular datos en una aplicación web. Ya hice una primera aproximación en la serie de temas Cómo se hace un calendario con objetos en JavaScript. En estos temas volveré a repetir las ideas básicas allí expuestas pero usando conceptos sobre objetos que aportan un código más eficiente, tanto desde el punto de vista de los recursos que necesita la ejecución de JavaScript como del mantenimiento del código.
Algunos de los términos relacionados con objetos se especifican en el estándar ECMAScript 262:
- object (objeto)
- Un miembro del tipo Object. Un objeto es una colección de propiedades y tiene un único prototipo, que también es un objeto y que puede ser el valor null.
- constructor (constructor)
- Una función que crea e inicializa objetos.
- prototype (prototipo)
- Es un objeto que provee propiedades compartidas para otros objetos.
- built-in object (objeto intrínseco)
- Objetos que siempre están presentes en el inicio de la ejecución de JavaScript, como Object, Function, Number, String, Boolean y algunos otros.
JavaScript es un lenguaje OOP con una implementación basada en prototipos. A diferencia de otros lenguajes como Java o C++ que están basados en el concepto de clases, pues el comportamiento no es exactamente el mismo. Puede ampliar más sobre esto en ECMA-262-3 in detail. Chapter 7.1. OOP: The general theory de DmitrySoshnikov.
Por lo tanto en JavaScript no existe el concepto de clases y todas las instancias son creadas a partir del objeto intrínseco Object mediante funciones constructoras con el objeto intrínseco Function o inicializadores como new Object()
, objeto literal { ... }
o el método más reciente Object.create()
. Como en JavaScript todo son objetos, a veces usamos el término instancia para reflejar que es un nuevo objeto creado a partir de Object con cualquiera de los constructores o inicializadores mencionados antes. Realmente la "clase" o "plantilla" a partir de la cual se crean las instancias es Object.protoype
, es decir, el prototipo de Object.
Creando objetos en JavaScript
En este primer ejemplo veremos como construir nuevos objetos, aunque sería más apropiado decir inicializar nuevas instancias de Object:
Ejemplo: Creando objetos
- Mensaje de
var instanciaA = new Object()
- Mensaje de
var instanciaB = Object.create(null)
- Mensaje de
var instanciaC = {...}
- Mensaje de
var instanciaD = function(){}
másdefineProperty
- Mensaje de
var instanciaE = {}
másdefineProperties
Se presentan cinco instancias creadas a partir de Object o Function. En un par de ellas usamos el objeto literal de la forma {...}
. Este es el código:
Los dos primeros crean nuevas instancias del intrínseco Object. Una usando el operador new
y otra usando el método create
de Object. Este método no está disponible en versiones de navegadores como IE8, aunque podemos implementar una solución para que funcione como se expone en el último apartado de este tema. El tercer ejemplo usa el objeto literal que es una forma rápida de crear una instancia de Object y rellenarla con las propiedades y métodos. La instanciaD
usa el objeto intrínseco Function para crear una instancia. La creamos como función vacía y luego agregamos las propiedades y métodos con Object.defineProperty
, pues Function no es otra cosa que un Object.
Diferencias entre objeto literal de JavaScript y JSON
La expresiónvar x = {a: "abc", b: 123, c: false}
podemos denominarla como objeto con llaves. Se trata de un objeto literal, es decir, la expresión literal del intrínseco Object. Es una facilidad del lenguaje para crear una nueva instancia de Object y al mismo tiempo dotarla de propiedades y métodos. Algo parecido a crear un array con var arr = [12, 375, 4]
. El objeto literal no debe confundirse con la Notación de Objetos Javascript o JSON, que es un formato de intercambio de datos no sólo para JavaScript. Lo anterior en JSON sería incorporado en JavaScript con var x = eval('{"a":"abc", "b":123, "c":false}')
. Aunque no debería usarse eval
sino métodos parseadores que asegure que no se introduzcan cosas no esperadas, pues un dato del objeto podría también ser un método que ejecute algo. La cadena interior encerrada entre comillas simples son los datos en formato JSON que intercambiaríamos por la red. Es una forma alternativa a usar XML para intercambiar datos.La ventaja de agregar propiedades y métodos con defineProperty
es que podemos dotar a cada una, además del valor, de otras características como writable
o enumerable
. Con writable: false
impediremos que esa propiedad pueda ser modificada (aunque por defecto es false
y no hace falta explicitarlo). Con enumerable: true
le decimos al objeto que esa propiedad puede ser rescatada en un bucle for...in
o métodos que extraen las propiedades. Y más cosas que se pueden hacer, lo que nos lleva a pensar en usar esto en nuestras futuras implementaciones de objetos. El último ejemplo usa Object.defineProperties
que no es otra cosa que agrupar varios defineProperty
en una única estructura. Es soportado por los navegadores actuales Chrome 22.0, Firefox 16.0, Opera 12.02, Safari 1.5 e Internet Explorer 9. Con IE8 no podemos usarlo directamente, aunque en estos ejemplos he implementado una solución parcial que explico en el último apartado.
Constructores de objetos
En el apartado anterior creábamos instancias de los objetos intrínsecos de JavaScript. Es útil cuando sólo necesitamos una instancia de un objeto. En otro caso hemos de definir un constructor previo que sirve como plantilla para crear las diferentes instancias. Veámos estos ejemplos:
Ejemplo: Constructores de objetos
- Mensaje de construido con
function()
- Mensaje de construido con
function()
- Mensaje de con una función constructora y usando
defineProperties
- Mensaje de con una función constructora y usando
defineProperties
- ¿Se puede
instanciaG2.nombre
con el valor ? = No,nombre
no es modificable pueswritable: false
- ¿Se puede
instanciaG2.nota
con el valor ? = Sí,nota
es modificable pueswritable: true
El primer objeto lo denominados objetoF
. Con function objetoF(nombre){...}
declaramos una función que entra en el espacio de variables como objetoF
. Luego incorporamos las propiedades y métodos con la palabra reservada this
. Ahora podemos crear múltiples instancias del objetoF
, como var instanciaF = new objetoF("un valor")
. El argumento (y otros que se quieran agregar) nos servirán para construir el objeto, mejor dicho, cada instancia del objeto.
Podemos aprovechar la nueva característica para agregar propiedades y métodos que nos brinda Object.defineProperties
. Podemos disponer de una función constructora como la del ejemplo crearObjetoG(argumentos)
. Al crear una nueva instancia llamamos a esta función que, temporalmente, crea una nueva instancia de objeto intrínseco Object con new Object()
. Luego le agregamos las propiedades y métodos con defineProperties
. Al salir de la función constructora devolvemos el objeto temporal a la variable que declara la nueva instancia.
De cualquier forma la ventaja de usar defineProperties
está en los parámetros de las propiedades y métodos. En el ejemplo a la propiedad nombre
y al método mensaje
los hemos puesto como enumerable: true
. En cambio con la propiedad nota
no hemos especificado nada por lo que el valor por defecto será false. Aquí se hace una recopilación de las propiedades y métodos de instanciaG1
con un bucle for (var i in instanciaG1){...}
:
Ejemplo: Propiedades de instanciaG1
Vemos que presenta nombre
y mensaje
pero no presenta nota
. Esta propiedad está configurada en el constructor como no enumerable y por lo tanto no podrá ser relacionada. Aunque sí podrá ser leída como comprobamos al pulsar el siguiente botón simplemente haciendo alert(instanciaG1.nota)
en su evento onclick
:
Otro parámetro de las propiedades es writable
. Hemos declarado nombre
como no modificable (writable: false
) mientras que lo permitimos con nota
. En el ejemplo puede probar los botones para modificar los valores de esos campos y luego volver a presentar el mensaje de la instanciaG2.
Navegadores que no soportan Object.create
Navegadores como IE8 no soportan Object.create()
. Además Object
tiene los métodos defineProperty()
y defineProperties()
pero no actúan igual que los estándar. Para estos casos se suele implementar una solución parcial:
Digo que es parcial pues puede funcionar a excepción de la incorporación de los parámetros writable
o enumerable
para una propiedad. En todos los casos será como si ambas fueran true. Creo que no merece la pena perder más tiempo con versiones anteriores. Además la práctica de modificar objetos intrínsecos (o built-in) no es recomendable. Pensemos por ejemplo en una página donde actúan scripts de diversos orígenes, no esperando encontrar estos métodos modificados y trabajando en base a su funcionalidad original. Los resultados pueden ser imprevisibles.
En resumen, hay más de una forma de trabajar con objetos en JavaScript. Y es conveniente aprender cuáles son las mejores prácticas para conseguir un código eficiente. Esto empezaremos a verlo en el tema siguiente.