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ás defineProperty
  • Mensaje de var instanciaE = {} más defineProperties
Mensajes:
 

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ón var 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 pues writable: false
  • ¿Se puede instanciaG2.nota con el valor ? = Sí, nota es modificable pues writable: true
Mensajes:
 

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.