Tabla de Contenidos

Notación de objetos

Módulo perteneciente al curso Programación avanzada con JavaScript y ECMAScript.

Introducción

JavaScript es un lenguaje orientado a objetos cuya base no son las clases (como otros lenguajes como C# o Java) sino los propios objetos. Crear objetos es una actividad fundamental en JavaScript y hay básicamente dos maneras de hacerlo:

Función constructora

Una función constructora es una función que se llama con new y que inicializa un objeto. Es una manera de crear objetos en JavaScript que es muy cómoda para desarrolladores que vienen de lenguajes basados en clases como C# o Java, ya que la sintaxis es familiar. El problema es que solo es similar la sintaxis, porque el funcionamiento por debajo es radicalmente distinto. La verdad es que JavaScript no tiene clases y el operador new funciona de forma distinta por completo en JavaScript.

ECMAScript 2015 añade clases (las veremos más adelante), pero las clases de ECMAScript 2015 son syntax sugar sobre la herencia por prototipos clásica. No tienen nada que ver con las “clases” tal y como las entendemos tradicionalmente (las de Java o C#).

Syntactic sugar: se dice que es una característica del lenguaje es syntactic sugar, cuando no añade ninguna capacidad nueva al lenguaje, sino tan solo una nueva forma (generalmente más sencilla) de realizar una tarea. Es pues como una nueva “sintaxis edulcorada” para hacer algo que ya era posible antes, facilitando las cosas al programador.

Una función constructora no tiene ninguna característica especial. Es más, este concepto realmente no existe en JavaScript. Lo que llamamos funciones constructoras son funciones normales, como todas las demás. Lo que las convierte en constructoras no es ninguna palabra clave, o el hecho de que tengan un nombre determinado. Lo que las convierte en funciones constructoras es que usamos new para invocarlas.

Y ¿qué hace new? Pues tres cosas principales. Supongamos una función llamada Foo y que la invocamos con new. En este caso lo que new hace es:

  1. Crea un objeto nuevo vacío y le asigna como prototipo el objeto Foo.prototype
  2. Invoca la función que se le indica con el contexto (el valor de this) establecido a este nuevo objeto
  3. Si (y solo si) la función devuelve undefined, entonces devuelve el objeto creado.

Se puede ver que el new de JavaScript poco tiene que ver con el new de Java o C#. Si vienes de alguno de esos lenguajes ten presente que aunque la sintaxis se parece, el funcionamiento nada tiene que ver.

Object notation

La segunda manera de crear objetos en JavaScript es usando object notation (notación de objeto). Es una sintaxis que seguro que habrás visto multitud de veces:

var foo = { value: 42 };

Eso es la notación de objetos. Hemos definido un objeto foo que tiene una propiedad, llamada value con el valor 42. Es una sintaxis muy útil para definir pequeños objetos a pesar de que tiene ciertas limitaciones. ECMAScript 2015 potencia la notación de objetos añadiéndole más capacidades, que son el objeto de este módulo.

Además de estas dos formas principales (función constructora y notación de objeto) existen más maneras para crear objetos en JavaScript, como p. ej. usar alguna función como Object.create.

JSON y notación de objeto

JSON significa literalmente JavaScript Object Notation, así que como puedes suponer algo tienen que ver. Pero no son lo mismo. Object notation es la notación para definir objetos en JavaScript y JSON es un formato para serializar objetos basado en esta sintaxis. A pesar de ser parecidos, tienen diferencias. P.ej. compara cómo es la notación de objeto para un objeto con dos propiedades en JavaScript:

{value: 42, desc:"answer to everything"}

Y cómo es el JSON que representa a este objeto:

"{"value":42,"desc":"answer to everything"}"

Efectivamente se parecen, pero no son lo mismo. Observa las comillas en los nombres de las propiedades que hay en el JSON y que no existen en la notación de objeto. Por supuesto un objeto JavaScript puede contener propiedades que sean funciones, pero nunca vas a ver funciones en JSON. JSON es un formato para serializar datos, no hay lugar para “funciones” en él.

No uses nunca eval para convertir una cadena JSON en un objeto JavaScript. Es cierto que, dada una cadena JSON, puedes transformarla a un objeto JavaScript usando var obj = eval("(" + str +")"); pero esto es un foco de vulnerabilidades. Primero porque no tienes garantía de que str contenga realmente una cadena JSON y eval ejecutará todo el código que le pases dentro. Si la cadena str contiene una llamada a función, esa será ejecutada. Usa siempre JSON.parse para convertir una cadena JSON a un objeto JavaScript: es seguro en tanto que no ejecuta código alguno y se asegura que la cadena sea realmente un JSON. Este método está disponible en cualquier navegador medianamente moderno (IE8 incluido).

En resumen: Si tienes una cadena en JSON y quieres obtener el objeto JavaScript equivalente usa siempre JSON.parse. Si quieres pasar un objeto JavaScript a notación JSON usa JSON.stringify.

DEMO: new en JavaScript

Veamos brevemente cómo funciona new.

Primero creamos una función constructora:

var Perro = function(r) {
    this.raza = r;
    this.ladrar = function() {
        console.log("guau!");
    }
}

Ahora podemos usarla para crear objetos:

var p = new Perro("Terrier"); // -> Object { raza: "Terrier", ladrar: Perro/this.ladrar() }
var p2 = new Perro("Bulldog"); // -> Object { raza: "Bulldog", ladrar: Perro/this.ladrar() }

Es posible llamar a una función constructora sin utilizar new. Realmente JavaScript no sabe que una función será constructora. Es cuando la usamos con new que le damos ese rol.

var p3 = Perro(); 

Al no usar new, el valor de this será del contexto global, es decir, que existirá raza y ladrar a nivel global.

Para saber si un objeto ha sido creado a partir de una función constructora tenemos que usar instanceof.

p instance of Perro; // -> true

Prototipos

Se dice que JavaScript es un lenguaje orientado a objetos basado en prototipos. La herencia en JavaScript funciona a través de la “cadena de prototipos”. Cada objeto tiene asociado un prototipo y hereda todas las propiedades y métodos que existan en el prototipo.

Si llamamos a una propiedad o método de un objeto y ésta no existe, se busca dicha propiedad en su prototipo. Y dado que el prototipo no deja de ser un objeto, el prototipo también tiene su propio prototipo asociado, lo que crea una cadena de prototipos. Ascendiendo por esta cadena es como el motor de JavaScript busca un método o una propiedad determinados. Al final la cadena termina en un prototipo, Object.prototype, que es el prototipo raíz (este objeto no tiene prototipo).

En ECMAScript 5 el prototipo de un objeto se define en su creación. Existen tres posibilidades:

  1. Si creamos el objeto con una función constructora, llamémosla Foo, el prototipo del objeto creado con new Foo es el objeto Foo.prototype
  2. Si creamos el objeto con notación de objeto, el prototipo del objeto creado es Object.prototype
  3. Si creamos el objeto usando la función Object.create, el prototipo del objeto es el objeto que pasemos como primer parámetro.

Cadena estática de prototipos

Oficialmente en ECMAScript 5 la cadena de prototipos es estática. Eso significa que una vez asignado el prototipo de un objeto ya no lo podemos cambiar. Podemos, por supuesto, añadir propiedades y métodos al objeto prototipo pero lo que no podemos hacer es que, si el prototipo de un objeto X era el objeto Y, Y deje de serlo.

En el caso del uso de funciones constructoras, el tema es un poco confuso porque si Foo es una función constructora entonces Foo.prototype puede asignarse a cualquier objeto en cualquier momento. Y recuerda que Foo.prototype es el prototipo de todos los objetos creados mediante la función constructora Foo. Pero si sustituyes Foo.prototype por otro objeto, este cambio solo afecta a los nuevos objetos que crees con new Foo, no con los ya creados:

En la imagen anterior el valor de f1.name es undefined porque f1 tiene el valor inicial de Foo.prototype que no define la propiedad name. Por su parte f2.name tiene valor porque f2 está creado después de que hayamos cambiado Foo.prototype por otro objeto que sí define la propiedad name.

La propiedad __proto__

La propiedad __proto__ es una característica no incorporada en el estándar ECMAScript 5, a pesar de que se encuentra en muchas implementaciones. Esta propiedad apunta en todo momento al prototipo del objeto. Y en algunas implementaciones dicha propiedad se puede asignar, lo que nos da un mecanismo no oficial de cambiar el prototipo de un objeto por otro.

Por ejemplo el siguiente código imprime true si el uso de __proto__ está soportado:

var Foo = function() {};
var f1 = new Foo();
console.log(f1.__proto__ === Foo.prototype)

Recuerda que __proto__ no es oficial en ECMAScript 5. Eso significa que en distintas implementaciones puede tener comportamientos distintos (p. ej. en algunas implementaciones __proto__ es de solo lectura).

Ahora bien, ten presente una cosa: incluso en aquellas implementaciones que permiten modificar el prototipo de un objeto una vez creado, eso no es una buena idea, y debes evitarlo. La razón es que cambiar el prototipo de un objeto una vez creado, rompe todas las posibles optimizaciones que puede realizar el motor JavaScript.

DEMO: Prototipos en funciones constructoras

Veamos qué ocurre con los prototipos cuando utilizamos funciones constructoras.

var Animal = function() {
};
var a = new Animal();
 
a; // -> Object

Veamos su prototipo con la propiedad __proto__:

a.__proto__; 
a.__proto__ === Animal.prototype; // -> true

El prototipo creado mediante una función constructora es el nombre de la función constructora punto prototype.

No debemos confundir la propiedad __proto__ con la propiedad prototype. La propiedad prototype existe tan solo para las funciones constructuras. __proto__ existe para los objetos y apunta al prototipo de cada uno de estos objetos.

DEMO: Prototipos en funciones constructoras - herencia

Veamos cómo utilizar el prototipo en funciones constructoras para crear relaciones de herencia.

Partimos del siguiente código:

var Animal = function() {
};
 
var a = new Animal();

Animal.prototype es el prototipo para todos los objetos creados con new Animal y se comparte entre ellos.

var b = new Animal();
 
console.log(a.__proto__ === b.__proto__); // -> true

Modificamos ahora la función constructora:

var Animal = function() {
    this.comer = function() {
        console.log("comiendo");
    }
};

Ahora crearemos una función constructura que permita crear objetos derivados de la otra función Animal:

var Perro = function() {
    this.ladrar = function() {
        console.log("guau");
    }
};
Establecemos una relación de jerarquía / herencia entre Perro y Animal
Perro.prototype = new Animal();
 
var a = new Animal();
 
console.log(a.__proto__ === Animal.prototype); // -> true
 
var p = new Perro();
 
console.log(p.__proto__ === Perro.prototype); // -> true

Al haber establecido una relación de jerarquía / herencia gracias a que JavaScript utiliza la cadena de prototipos, podemos hacer lo siguiente:

p.comer();

JavaScript no lo encontrará en Perro, pero la buscará en el prototipo del objeto p que es Perro.prototype que es un Animal y sí tiene ese método.

Prototipos en ECMAScript 2015

Una de las novedades de ECMAScript 6 es que soporta oficialmente la propiedad __proto__. Curiosamente dicha propiedad se incluye en el estándar pero se declara obsoleta.

Puede parecer raro que algo que no existía en la versión 5 pase a existir directamente como obsoleto en la versión 6 del estándar. La razón es que el estándar incorpora __proto__ para asegurar la compatibilidad con mucho código existente: a pesar de no ser oficial hay tanto código que lo usa que al incorporarlo se asegura que este código pueda ejecutarse en cualquier implementación de ECMAScript 6.

La razón de que se considere obsoleta es que ECMAScript 6 incorpora sus propios mecanismos para obtener y establecer el prototipo de un objeto una vez creado éste.

Importante: A pesar de que en ECMAScript 6 cambiar el prototipo de un objeto sea una operación soportada, debemos tener presente que el impacto en rendimiento es importante. Mejor evitarlo siempre que sea posible.

Para obtener el prototipo de un objeto podemos usar Object.getPrototypeOf o bien Reflect.getPrototypeOf. Ambas funciones son equivalentes:

var Foo = function() {};
var f1 = new Foo();
console.log(Object.getPrototypeOf(f1) === Foo.prototype)     // true
console.log(Reflect.getPrototypeOf(f1) === Foo.prototype)    // true

Y para establecer el prototipo de un objeto se puede usar Object.setPrototypeOf o bien Reflect.setPrototypeOf. Como antes, da igual usar la versión de https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create1Object o de Reflect ya que son equivalentes:

var obj = Object.create({name: 'campusMVP'});
console.log(obj.name);   // campusMVP
Object.setPrototypeOf(obj, {age: 42});
console.log(obj.name);   // Undefined
console.log(obj.age);    // 42

Establecer el prototipo en notación de objeto

Una de las novedades de ECMAScript 6 es que permite establecer el prototipo de un objeto creado con notación de objeto. Para ello basta con declarar una propiedad __proto__ en el momento en que declaramos el objeto:

var a={answer: 42};
var obj={
  question: 'what is the sense of life, the universe and everything?',
  __proto__: a
}
console.log(obj.question, obj.answer);

Se puede ver que obj.answer es 42 porque el objeto a es el prototipo del objeto obj. Para conseguir el mismo efecto en ECMAScript 5 debíamos crear el objeto obj usando Object.create.

Propiedades con nombre dinámico

Desde siempre hemos podido construir objetos con una propiedad cuyo nombre fuese dinámico, es decir dependiese de cualquier operación en tiempo de ejecución. El siguiente código crea un objeto con un método que se llama t_ seguido del número del mes actual:

var a = {};
a["t_" + new Date().getMonth() + 1] = 10;

Una vez se ha creado esa propiedad podemos acceder a ella con la notación de punto o de array, siendo ambas equivalentes:

Ahora bien, en ECMAScript 5 solo podemos crear esas propiedades, añadiéndolas al objeto una vez creado. Observa que en el código anterior declaramos el objeto vacío, y luego le añadimos la propiedad. Eso es porque la notación de objeto no nos permite declarar esas propiedades con nombre dinámico. Por supuesto en una función constructora no hay problema (dado que de hecho en la función constructora lo que realmente hacemos es añadir propiedades a this).

Una de las novedades en ECMAScript 2015 consiste en que también podemos declarar esas propiedades utilizando notación de objeto:

var a={
    ["t_" + new Date().getMonth() + 1]: 10
};

Observa como el nombre de la propiedad es realmente una cadena y se coloca entre corchetes. Debes usar los corchetes porque sino obtendrás errores de sintaxis. Los corchetes sirven para indicarle al parser dónde empieza y termina la expresión que genera el nombre de la propiedad.

Nombres de propiedades duplicadas

En ECMAScript 5, en modo estricto, declarar un objeto con nombres de propiedades duplicadas generaba un error de sintaxis. Es lógico puesto que no tiene mucho sentido declarar un objeto con una propiedad definida más de una vez:

var a= {p: 20, p:30};

Pero si pruebas este código en un entorno ECMAScript 2015 incluso con el modo estricto habilitado, verás que no produce error alguno.

A partir de ECMAScript 2015 esta restricción se ha eliminado. La razón es que ahora, al poder usar nombres de propiedades dinámicos, puede ocurrir que una propiedad con nombre dinámico genere el mismo nombre que otra propiedad, digamos “normal”. Entonces en lugar de prohibir nombres de propiedad duplicados, se ha optado por definir qué ocurre en caso de que existan nombres duplicados: la propiedad declarada en segundo lugar sobrescribe a la primera:

var a={p:20, p:30};
console.log(a.p);   // 30

Métodos y propiedades simplificadas

Acostúmbrate a la “nueva” sintaxis de ECMAScript para declarar métodos y/o propiedades en objetos, porque es tan sencilla y cómoda que te preguntarás por qué no existía antes.

Imagina un objeto hello que tiene un método greetings. Hasta ahora este objeto lo podíamos declarar de la siguiente manera:

var hello = {
    greetings: function(name) {
        return "hello " + name;
    }
}

Pues bien, ahora tenemos disponible una sintaxis simplificada para métodos, que te permite declarar exactamente este mismo objeto sin necesidad de la palabra clave function:

var hello = {
    greetings(name) {
        return "hello " + name;
    }
}

Esa sintaxis simplificada puedes usarla también para getters y setters de propiedades:

var hello = {
    get salute() {return "hello"}
}
console.log(hello.salute);      // hello

Un ejemplo un poco más completo, donde muestra un getter y un setter simulando una variable privada cuyo valor debe ser siempre positivo:

var hello = (() => {
    var _private = 42;
    return {
        get value() {return _private},
        set value(v) { 
            if (v > 0) {
                _private = v;
            }
            return _private;
        }
    }
})();
console.log(hello.value);           // 42;
hello.value = -10;
console.log(hello.value);           // 42;
hello.value = 20;
console.log(hello.value);           // 20;

DEMO: Novedades en notación de objetos

Repaso de las 4 novedades importantes de ECMAScript 6 en notación de objetos.

Posibilidad de especificar el prototipo del objeto en notación del objeto a la vez que declaramos el objeto:

var a = {name: "campusmvp"}
 
var b = {
    __proto__: a,
    cursos: ["javascript"]
}
 
b.name; // -> 'campusmvp'

Antes no se podía hacer, era necesario el uso de Object.create para obtener el mismo efecto.

Otra de las novedades son las propiedades con nombre dinámico:

var b = {
    __proto__: a,
    cursos: ["javascript"],
    ["i_" + Math.random()]: 42
}

Lo ejecutamos:

b; // -> Object { cursos: Array[1], i_0.123412341234: 42}

En ECMAScript 5 podíamos hacer:

b["i_" + Math.random()]: 42

Pero no podíamos hacerlo en la declaración de un objeto.

Otra novedad más es una sintaxis simplificada para declarar funciones.

var b = {
    __proto__: a,
    cursos: ["javascript"],
    ["i_" + Math.random()]: 42,
    foo(p) {return p + 1} // No es necesario usar 'function' ni los dos puntos (:)
}

Finalmente, otra característica nueva son los shorthand properties names:

var url = "https://campusmvp.es";
 
var options = {
    url, // en lugar de url: url
    method: "GET"
});