====== Notación de objetos ====== Módulo perteneciente al curso [[informatica:programacion:cursos:programacion_avanzada_javascript|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: Implica usar ''new'' sobre una función específica (la función constructora) que crea, inicializa y devuelve el objeto. * //Object notation//: Se usa la notación de objeto para definir, crear e inicializar un objeto. ==== 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: - Crea un objeto nuevo vacío y **le asigna como prototipo el objeto ''Foo.prototype''** - Invoca la función que se le indica **con el contexto (el valor de ''this'') establecido a este nuevo objeto** - 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 ''[[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create|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: - 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'' - Si creamos el objeto con notación de objeto, el prototipo del objeto creado es ''Object.prototype'' - 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: {{ :informatica:programacion:cursos:programacion_avanzada_javascript:14-cambio-foo-prototype.png |}} 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 [[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect|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''. {{ :informatica:programacion:cursos:programacion_avanzada_javascript:14-usando-proto-notacion-objeto.png |}} ===== 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: {{ :informatica:programacion:cursos:programacion_avanzada_javascript:14-propiedades-dinamicas.png |}} 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" });