Tabla de Contenidos
Programación Orientada a Objetos con ES5
Módulo perteneciente al curso Programación avanzada con JavaScript y ECMAScript.
Introducción
A la hora de programar sistemas complejos, una de las abstracciones más útiles que se han inventado es la de la programación orientada a objetos. Ésta ha dominado el mundo de la programación desde hace décadas y raro es el lenguaje que no soporta las partes más importantes de este paradigma.
Como ya sabrás, la programación orientada a objetos trata de establecer una comparación entre el mundo físico (con objetos) y el del software (inmaterial), trabajando con entidades de código que representan objetos “reales”, y que nos permiten agrupar datos y funcionalidad para facilitarnos la escritura de código. Los objetos nos proporcionan muchas cosas, pero sobre todo orden y un modelo claro de trabajar con la información.
JavaScript ofrece un buen soporte para crear código orientado a objetos, si bien posee diversas limitaciones frente a otros lenguajes puramente orientados a objetos (como Java o C#) y a veces el código puede ser complicado de entender.
En este módulo vamos a conocer todo lo que necesitas saber sobre la programación orientada a objetos con JavaScript. Por completitud partiremos completamente de cero para que el contenido sea útil a todo tipo de alumnos, incluso los que no han tenido contacto previo con la POO.
Conceptos básicos de POO
En la realidad pocas cosas están construidas desde cero, sin tener partes constituyentes. Generalmente, todos los objetos que utilizamos o tocamos a diario están constituidos a su vez por otros objetos más pequeños. Éstos tienen una funcionalidad determinada e independiente del conjunto al que pertenecen, aunque luego se vea potenciada por la interacción sobre los otros elementos que forman parte de aquél.
Por ejemplo, tu ordenador está constituido por multitud de piezas u objetos que están especializados en unas funciones muy determinadas. Cada pieza por separado no nos es muy útil, pero todas juntas hacen que funcione el sistema. Cada objeto tiene unas características muy determinadas y unas funcionalidades concretas que lo identifican. La tarjeta de vídeo tiene una cierta cantidad de memoria, una velocidad de proceso, etc.. y dispone de funciones para transformar vectores, o para calcular superficies 3D complejas. Estas piezas se pueden reutilizar, no sólo porque una tarjeta gráfica de un ordenador se puede colocar en otro, sino porque el propio concepto de tarjeta es lo que reutilizamos cuando fabricamos dos tarjetas iguales.
A la hora de programar resulta de enorme utilidad poder definir una serie de “objetos software” especializados en determinadas funciones y con unas determinadas propiedades, que se gestionen a sí mismos y que pudiésemos reutilizar. De este modo, si por ejemplo estamos programando un juego de “marcianitos”, en lugar de crear una función que se encargue de manejar los datos de un “marcianito” e ir aplicándola uno a uno a todos ellos, podemos definir un objeto Marcianito y que él mismo sepa cómo autogestionarse. En lugar de modificar en una matriz la posición del invasor podemos simplemente decirle que se mueva, y él sabrá cómo hacerlo. También sabrá determinar si choca contra otro y evitarlo, o lanzar un rayo para disparar al defensor.
Con esta abstracción hemos pasado de un mundo en el que una inteligencia central lo controlaba todo, a otro en el que existen diferentes objetos que son independientes y saben gestionarse a sí mismos.
Lo primero que debemos conocer sobre programación orientada a objetos es la diferencia que existe entre un objeto y una clase.
Una clase define aquellas propiedades y métodos que conceptualmente determinan a un ente o cosa. Se trata de algo genérico, no particular. Es decir, no definimos un marcianito en particular, sino que definimos una clase “Marcianito” que determina cómo debe ser cualquier marcianito que tengamos, qué características tienen todos ellos (color, posición, disparos disponibles…) y qué cosas son capaces de hacer (moverse, disparar, pintarse en pantalla, desaparecer…).
Cuando una clase adquiere entidad física con valores concretos para sus propiedades se dice que estamos ante un objeto o, mejor aún, ante una instancia de la clase. Así, un marcianito concreto que hayamos creado, con un determinado color, una posición, etc.. es un objeto o una instancia de la clase “Marcianito”.
Es muy importante tener clara esta distinción.
Los tres pilares básicos de la Programación Orientada a Objetos son:
- Encapsulación: es el empaquetamiento de datos y funciones dentro de un único elemento (una clase), accesibles siempre a través de éste. Por ejemplo, para acceder a las propiedades de un objeto solamente es posible hacerlo a través de dicho objeto. De esta forma, el objeto y sus datos y acciones forman un todo. Está también relacionado con la ocultación de datos que se refiere a que los datos internos de un objeto no son accesibles desde el exterior y sólo pueden tocarse a través de una interfaz determinada que se haya definido. Por ejemplo, puedes almacenar internamente el valor de la estatura de una clase
Personaen centímetros, pero exponerla al exterior a través de una propiedad que devuelve metros, y a la hora de escribirlo que solamente se pueda hacer a través de una determinada función que valida múltiples condiciones y la convierte a centímetros antes de almacenarla de nuevo. - Herencia: cuando tenemos diferentes variantes de una clase, en lugar de tener que definir una clase nueva que replique los datos y la funcionalidad de la primera, es más fácil crear una nueva clase que “herede” de la clase preexistente todas sus características. Por ejemplo, si tenemos un objeto
Personaque tiene información como nombre, apellidos, edad, dirección, etc… y ahora queremos manejar otro tipo de personas comoClientes,Proveedores,Familiares, etc… que tienen los mismos datos pero que, a mayores añaden otros, y redefinen la manera de usar algunos otros, podemos crear clases derivadas de la clase Persona que heredarán toda su funcionalidad y permitirán también particularizarla. Este mecanismo es muy potente y nos permite escribir código más conciso, fácil de gestionar y menos propenso a errores. - Polimorfismo: cuando varios objetos heredan de la misma clase base, los podremos utilizar de la misma manera considerando que son de dicha clase base común. Por ejemplo si tenemos las clases especializadas que mencionábamos:
Cliente,Proveedor,Familiar… y todas heredan dePersona, podemos hacer uso de ellas en una misma función, sin importarnos su tipo concreto, ya que todas ellas son Personas también. En lenguajes más avanzados que JavaScript que definen el concepto de Interfaz (un contrato que define cómo interaccionar con una clase), el polimorfismo se refiere también al uso de interfaces. En JavaScript eso no existe.
En este vídeo te cuento detalladamente todo lo anterior para que puedas entenderlo mejor si partes de cero en esto de la POO.
Ahora vamos a ir viendo poco a poco cómo JavaScript soporta las principales características del paradigma de programación orientada a objetos, aunque tenga sus propias particularidades al respecto.
Métodos clásicos de definición de objetos en JavaScript
Redefiniendo objetos genéricos
La manera más sencilla de crear objetos propios en JavaScript es instanciando objetos genéricos de tipo Object y dotarlos de propiedades.
var marcianito1 = new Object(); // Definimos propiedades marcianito1.name = "invasor del espacio #1"; marcianito1.color = "Azul"; marcianito1.x = "100"; marcianito1.y = "20"; marcianito1.disparos = 30; alert(marcianito1.name); // -> invasor del espacio #1 alert(marcianito1.disparos ); // -> 30 marcianito1.disparos--;
Esto funciona porque JavaScript es un lenguaje dinámico y al añadir propiedades que no existían, JavaScript las crea.
De la misma manera, también podemos crear nuevos métodos:
// Función anónima o 'lambda': marianito1.disparar = function() { this.disparos--; alert(this.name + " ha disparado."); } // Usamos el método previamente definido: marcianito1.disparar();
this se refiere al objeto actual. Veremos esto más adelante.
Esta manera de crear objetos tiene el inconveniente de que si queremos definir un nuevo objeto, marcianito2, tendremos que redefinir tanto sus propiedades como métodos. No podemos reutilizar. Tampoco tenemos forma de identificar que estos objetos que vamos a usar son de cierto tipo ya que son de la clase genérica Object.
Sintaxis JSON para objetos
Otra sintaxis alternativa para crear objetos en JavaScript es usar la notación JSON (JavaScript Object Notation). También crea objetos de la instancia Object.
var marcianito1 = { name : "invasor del espacio #1", color : "Azul", x : 100, y : 20, disparos : 30, disparar : function() { this.disparos--; alert(this.name + " ha disparado."); } };
Esta notación es una de las formas más populares de intercambiar información entre servicios.
// Todo funciona igual que con la otra manera // de definir objetos, propiedades y métodos alert(marcianito1.name); // -> invasor del espacio #1 alert(marcianito1.disparos ); // -> 30
Patrón factoría
Una tercera estrategia para crear objetos en JavaScript es usar el patrón Factoría. Recibe este nombre porque “fabrica” objetos.
Una factoría es un objeto (o una función) cuya funcionalidad es la de crear otros objetos. Se suele usar cuando la creación del objeto no es trivial y/o bien requiere personalizaciones posteriores a la creación del objeto pero necesarias antes de poder usarlo.
Hasta ahora, el mayor problema que teníamos era que había que definir de nuevo propiedades y métodos para cada nuevo objeto.
Podemos escribir una función que se encargue de crear objetos.
function crearMarcianito(nombre, color, posX, posY, disparos) { return { nombre : nombre, color : color, x : posX, y: posY, disparos : disparos, disparar : function () { this.disparos--; alert(this.name + " ha disparado."); } }; }
Creamos objetos:
var marcianito1 = crearMarcianito("Invasor #1", "Azul", 0, 0, 30); var marcianito2 = crearMarcianito("Invasor #2", "Verde", 100, 300, 50); alert(marcianito1.name); alert(marcianito2.disparos);
Aunque es una ventaja respecto a las otras maneras de crear objetos, seguimos creando objetos genéricos (clase 'Object') y no aplicamos Programación Orientada a Objetos.
Constructores de clases
En módulos anteriores hemos visto que para crear un nuevo objeto de una clase propia de JavaScript –como una fecha o una matriz- se empleaba la palabra clave new seguida del nombre del tipo de objeto, por ejemplo:
var Fecha = new Date(2014, 09, 02);
Así es como deberíamos poder crear nuestros propios objetos, especificando el tipo correcto para crear un verdadero objeto de esa clase.
La cláusula new en JavaScript se utiliza para crear nuevas instancias de clases.
Lo que hace new es llamar a una función especial que poseen todas las clases, denominada constructor, y que como en todos los lenguajes orientados a objetos, tiene el mismo nombre que la clase que queremos instanciar.
Como se ve en la línea anterior, un constructor toma opcionalmente una serie de parámetros para inicializar el objeto que se está creando, de forma similar al patrón factoría que acabamos de ver. En el ejemplo de la línea anterior hemos creado un objeto de la clase Date, especializada en albergar y trabajar con fechas, que tiene como valores de creación un determinado día, mes y año. Este objeto, gracias a lo que determina su clase, maneja una determinada información (en este caso la fecha y la hora) y tiene métodos que le permiten hacer cosas con ella, como descomponerla en sus partes, transformarla en texto y todo lo que hemos visto que puede hacer esta clase.
Además podemos determinar si la variable asignada pertenece a la clase Date fácilmente, por ejemplo si comprobamos su tipo con instanceof como ya hemos visto en un tema anterior.
Para conseguir un efecto similar con nuestras propias clases y hacer programación orientada a objetos de forma correcta lo que debemos hacer es definir nuestros propios constructores.
Constructores reales de objetos
Un constructor es una función con el mismo nombre que la clase que queramos definir.
function Marcianito(nombre, color, posX, posY, disparosIniciales) { this.name = nombre; this.color = color; if (posX < 0) { posX = 0; } if (posX > 100) { posX = 100; } this.x = posX; if (posY < 0) { posY = 0; } if (posY > 100) { posY = 100; } this.y = posY; if (disparosIniciales < 0) { disparosIniciales = 0; } if (disparosIniciales > 100) { disparosIniciales = 100; } this.disparos = disparosIniciales; this.disparar = function () { this.disparos--; alert(this.name + " ha disparado."); }; }
Usamos el constructor:
var marcianito1 = new Marcianito("Invasor #1", "Azul", 100, 20, 30); alert(marcianito1.name); var marcianito2 = new Marcianito("Invasor #2", "Verde", 140, 10, 50); marcianito2.disparar(); alert(marcianito2.disparos);
Fundamentos de la variable especial this
Siempre que en JavaScript se llama a una función, JavaScript define un valor concreto para la palabra clave this.
Al usar el constructor de la sección anterior, JavaScript crea un nuevo objeto de tipo genérico vacío this. Al hacer this.name, estamos definiendo la propiedad name. Estamos haciendo lo mismo que en secciones anteriores.
var marcianito1 = Marcianito("Invasor #1", "Azul", 100, 20, 30); alert(marcianito1.name);
Dará un error el alert porque no hay nada definido. Observemos que no hemos usado new. En este caso, JavaScript crea el objeto this de tipo Window (objeto global de JavaScript). Para ver alguna propiedad tendríamos que hacer:
alert(window.disparos);
this es el objeto por defecto dentro de una función:
function Marcianito(nombre, color, posX, posY, disparosIniciales) { // Esto this.name = nombre; // es lo mismo que: name = nombre; ... }
Recordemos, el valor de this cambia según el contexto de la llamada.
Determinando el tipo del objeto
function Marcianito(nombre, color, posX, posY, disparosIniciales) { this.name = nombre; this.color = color; if (posX < 0) { posX = 0; } if (posX > 100) { posX = 100; } this.x = posX; if (posY < 0) { posY = 0; } if (posY > 100) { posY = 100; } this.y = posY; if (disparosIniciales < 0) { disparosIniciales = 0; } if (disparosIniciales > 100) { disparosIniciales = 100; } this.disparos = disparosIniciales; this.disparar = function () { this.disparos--; alert(this.name + " ha disparado."); }; } var marcianito1 = new Marcianito("Invasor #1", "Azul", 100, 20, 30);
Queremos ver el tipo de dato de marcianito1:
alert(typeof(marcianito1)); // -> object
Todos los objetos que no son nativos de JavaScript, devuelven siempre “object” al consultar su tipo con typeof.
// La propiedad "constructor" contiene el código de la // función constructora alert(marcianito.constructor);
Aprovecharemos la propiedad constructor para saber el tipo de objeto que tenemos:
alert(marcianito.constructor == Marcianito); // -> true
Pero JavaScript nos ofrece otra herramienta más legible: la función instanceof:
alert(marcianito1 instanceof Marcianito); // -> true
Algo curioso es lo siguiente:
alert(marcianito1 instanceof Object); // -> true
Todos los objetos de JavaScript son de tipo 'object'. Un objeto puede pertenecer a más de un tipo.
Prototipos
Funciones compartidas en prototipos
Todas las funciones constructoras de JavaScript poseen una propiedad especial llamada prototype. Esta propiedad contiene una referencia a un objeto especial que JavaScript utiliza como prototipo de todos los métodos que va a tener esa clase.
Cualquier función que esté asignada a prototype, estará disponible para cualquier instancia de mi clase.
function Marcianito(elNombre, elColor, posX, posY, disparosIniciales) { (...) if (!Marcianito.prototype.disparar) { Marcianito.prototype.disparar = function () { this.disparos--; //codigo para pintar el disparo alert(this.name + " ha disparado."); }; } }
var marcianito1 = new Marcianito("Invasor #1", "Azul", 100, 20, 30); var marcianito2 = new Marcianito("Invasor #2", "Verde", 140, 10, 50); alert(marcianito1.disparar == marcianito2.disparar); // -> true
Todas las clases de JavaScript tienen un prototipo, lo que nos permite definir funciones de manera dinámica sobre clases que ya existen.
Partamos de una función que nos diga si un valor es numérico:
function isNumeric(n) { return !isNan(parseFloat(n)); }
En lugar de tenerlo como una función global, podríamos meterla en la clase nativa String:
String.prototype.isNumeric = function () { return !isNaN(parseFloat(this)); }
Y la usaríamos de la siguiente manera:
alert("1234".isNumeric()); // -> true alert("abcd".isNumeric()); // -> false
Búsqueda de miembros y cadena de prototipos
En el vídeo anterior hemos visto cómo definir funciones pertenecientes a todas las instancias de una clase mediante el uso de prototipos. Su uso práctico está claro, pero ¿por qué funciona el código que hemos visto?
En realidad no estamos definiendo métodos en nuestra clase sino en el prototipo de ésta, así que a priori, al no pertenecer a nuestra clase verdaderamente debería producirse un error al hacer la llamada. ¿Acaso se replican todos los métodos del prototipo en los objetos que instanciemos?
Nada de eso. El modo de funcionamiento de JavaScript a la hora de acceder a los miembros de un objeto consiste en seguir la cadena de prototipos. Esto implica que cuando se llama a una propiedad o método de un objeto, la búsqueda se realiza desde el objeto hacia arriba en esa cadena:
- Primero se busca en el objeto.
- Si no está, se busca en su prototipo.
- Si no está, se busca en el prototipo del prototipo (de haberlo) y así sucesivamente.
- Si no se encuentra en el último prototipo, entonces se produce un error porque no existe el miembro buscado.
Por eso, cuando definimos un miembro en el prototipo, podemos usarlo como si fuera parte del objeto. Al escribir:
marcianito1.disparar();
Se busca el método disparar en el objeto, como no está se busca en su prototipo y al encontrarlo allí se ejecuta desde éste usando el contexto del objeto como hemos visto (por eso this sigue apuntando al objeto original). Esto último es muy importante.
En el paso 3 de la lista anterior se busca también en el prototipo del prototipo. ¿Y esto, cómo se entiende?. Bien, un prototipo no es más que otro objeto normal de JavaScript, y por lo tanto tiene su propio prototipo también.
De hecho, todos los objetos de JavaScript tienen como prototipo de mayor nivel (el último en el que se busca) un objeto genérico de la clase Object. La clase Object ofrece algunos métodos de base que, precisamente por lo anterior, están disponibles en todos los demás objetos del lenguaje.
La siguiente figura muestra de manera simplificada la cadena de prototipos de nuestra clase Marcianito:
Como vemos, el prototipo de nuestra clase es otra clase cuyo constructor apunta a la función constructora de la clase (la que nosotros hemos definido). A su vez posee como prototipo un objeto genérico Object. Éste tiene un constructor que apunta a la función de creación de la clase Object (definida por el sistema) y como prototipo se tiene a sí mismo. Estudia la figura detenidamente para asegurarte de que ves claras las relaciones, pues es muy importante.
Cada una de las instancias de la clase (marcianito1 y marcianito2 en la figura de ejemplo) tiene definidas sus propias propiedades y utilizan la función “disparar” del prototipo. Además, como por encima de la cadena de prototipos tienen la clase Object también están a su disposición todos los métodos que ofrece ésta: toString, toLocaleString y algunos otros reflejados en la figura anterior.
Así, por ejemplo, el método toString que convierte el objeto en una representación del mismo como cadena de texto, lo podemos llamar en todos los objetos porque todos tienen a Object como extremo de la cadena de prototipos.
Esto posee unas implicaciones muy interesantes que se ramifican mucho para dar lugar a todo tipo de técnicas avanzadas de programación con JavaScript.
Una consecuencia inmediata es que podemos ocultar (en inglés se usa generalmente el verbo shadow) cualquier miembro de nivel superior en la cadena de prototipos definiéndolo en un nivel inferior.
Por ejemplo, si definimos un método toString, propio en el prototipo de nuestra clase, se encontrará antes que el de Object y por lo tanto sustituirá a este. Si en el constructor añadimos esta línea:
Marcianito.prototype.toString = function() { return this.name + " - " + this.color; };
más tarde cuando escribamos alert(marcianito1); o transformemos el objeto en una cadena, en lugar de devolver el habitual "[object Object]" que ofrece por defecto el toString del prototipo de Object, veremos el nombre y el color del marcianito, tal y como hemos definido.
Esto tiene un efecto secundario pernicioso, que a veces puede provocar errores difíciles de detectar, y es que si tenemos un miembro (propiedad o método) definido en el prototipo de nuestra clase y posteriormente creamos inadvertidamente un miembro del mismo nombre en una instancia de la clase (en un objeto ya instanciado, vamos), éste se encontrará antes que el del prototipo, y por lo tanto podríamos sustituir al del prototipo sin darnos cuenta, con efectos no deseados.
Aunque cuando añadimos o modificamos un miembro en el prototipo automáticamente éste pasa a estar disponible en todas las instancias de la clase que use ese prototipo, no es posible cambiar de golpe el prototipo entero (es decir, asignarle otro objeto diferente como prototipo a nuestra clase) y que todas lo tengan accesible de repente. Esto tiene que ver con el modo interno de funcionamiento de JavaScript y no funcionará.
Dado que el prototipo se comparte entre todos los objetos de una determinada clase, si creamos propiedades asociadas a éste las tendremos disponibles en todas las instancias de la clase a la vez: a efectos prácticos lo que tenemos son variables compartidas entre todos los objetos, lo que se conoce en otros lenguajes como miembros o variables estáticas.
El uso de este tipo de variables es interesante para casos muy puntuales, pero no se recomienda en general ya que puede producir conflictos y bugs difíciles de detectar.
Todas las instancias de una clase disponen de una propiedad interna denominada __proto__ que está apuntando al prototipo de la clase, como se ve en la figura. Internet Explorer impide el acceso a esta propiedad y no podremos manipularla. Sin embargo, el resto de los navegadores sí que permiten acceder a ella si la escribimos en nuestro código. Generalmente, más allá de la curiosidad que pueda despertarnos el examinarla con un depurador, no necesitaremos tocarla en absoluto.
Controlando el valor de contexto con call y apply
Vamos a darle una pequeña vuelta de tuerca más a lo que conocemos de la palabra clave this.
Como hemos visto, su valor se determina en función de sobre qué objeto se esté haciendo la llamada o, lo que es lo mismo, a qué objeto pertenece la función que hace uso de esta palabra clave. Hemos visto también que new cambia ese contexto para que se refiera al objeto concreto que estamos creando.
¿Podemos controlar de alguna manera nosotros ese valor?
La respuesta, como cabría esperar, es que sí podemos.
Para ello nos apoyaremos en los métodos especiales call y apply disponibles para todas las funciones.
Consideremos por ejemplo el siguiente código:
function asignarNombreYEdad(nombre, edad) { this.Nombre = nombre; this.Edad = edad; } var miObjeto = new Object(); asignarNombreYEdad.call(miObjeto, "Pepe", "40"); alert(miObjeto.Nombre + " - " + miObjeto.Edad);
Definimos una función asignarNombreyEdad, tras lo cual creamos un nuevo objeto genérico miObjeto. Si llamásemos a la función directamente, como hemos visto antes, this se referiría a la ventana del navegador (el objeto global). Sin embargo, en este caso lo que hemos hecho es llamar a la función a través de su método especial call. Éste toma como primer parámetro el objeto que queremos usar como contexto para la ejecución de la función, es decir, aquel al que va a apuntar this. El resto de parámetros son los que se le pasarán a la función para llamarla. Gracias a esto, en este fragmento nuestro objeto genérico obtiene las propiedades Nombre y Edad y podemos mostrarlas en la última línea.
El método apply funciona exactamente igual, sólo que los parámetros en lugar de pasárselos a continuación separados con comas se los pasamos como elementos de una matriz. En el ejemplo anterior sería:
asignarNombreYEdad.apply(miObjeto, ["Pepe", "40"]);
Por lo demás son idénticas.
Estas dos funciones nos permiten controlar con precisión el valor del contexto actual, es decir, el valor de this, y nos permiten realizar algunas cuestiones avanzadas, por lo que conviene conocer su existencia.
Existe una tercera variante de estas funciones denominada bind que funciona exactamente igual que call pero en lugar de ejecutar la función inmediatamente, devuelve una referencia a la función ya preparada para ser llamada con el contexto y los parámetros indicados. De este modo se puede reutilizar la misma función, ya parametrizada, en múltiples ocasiones, lo cual puede resultar de gran utilidad en algunos casos.
Herencia
Encadenamiento de prototipos
El lenguaje JavaScript no soporta la forma “clásica” de herencia que se espera en la mayor parte de los lenguajes de programación orientados a objetos. Sin embargo, la implementa de manera muy eficiente gracias a lo que acabamos de aprender sobre el encadenamiento de prototipos.
En la práctica, el hecho de que los miembros se busquen en toda la cadena de prototipos significa que podemos hacer que una clase disponga de los mismos miembros que cualquier otra, que es algo muy parecido a la herencia tradicional de otros lenguajes.
Existen multitud de técnicas para crear objetos que hereden de otros: simple encadenado de prototipos, robo de constructores, herencia por combinación, herencia prototípica (inventado por Douglas Crockford, conocido programador JavaScript), herencia parasitaria… pero todas ellas se basan en los mismos conceptos refinando los resultados, y todas tienen sus puntos positivos y también problemáticos.
Al final, todas las técnicas de herencia se basan en el mismo concepto: si hacemos que el prototipo de nuestra clase sea una instancia de otra, entonces la nueva clase dispondrá de los mismos miembros que la original, ya que se encontrarán en ese prototipo cuando se realice la búsqueda.
Veamos un primer intento de crear herencia entre dos clases. Supongamos que definimos una clase simple como esta:
function ClaseBase() { this.Nombre = ""; ClaseBase.prototype.mostrarNombre = function () { alert(this.Nombre); } }
Como vemos consta de una propiedad Nombre y de un método en su prototipo para mostrar el valor de esa propiedad.
Para crear una clase derivada de esta podemos escribir lo siguiente:
function ClaseDerivada() { this.Apellidos = ""; } ClaseDerivada.prototype = new ClaseBase(); ClaseDerivada.prototype.constructor = ClaseDerivada;
Es decir, definimos la nueva clase y ahora, fuera de su constructor, reasignamos el prototipo para que utilice un objeto de la clase anterior, que será de la que herede.
OJO: Además no debemos olvidar que es necesario asignar el constructor del prototipo a la función correcta o tendremos problemas para identificar el tipo de la nueva clase si necesitamos crear nuevos objetos de la misma. Esto se explicará más a fondo en una lección posterior.
Gracias a esto, ahora podemos instanciar un nuevo objeto de la clase derivada y disfrutaremos de todos los métodos y propiedades de la clase base:
var cd = new ClaseDerivada(); cd.Nombre = "Perico"; cd.Apellidos = "de los Palotes"; cd.mostrarNombre(); alert(cd instanceof ClaseDerivada); alert(cd instanceof ClaseBase);
Al ejecutar este fragmento veremos que todo funciona como era de esperar y el método mostrarNombre muestra el valor asignado, y los dos últimos alert devuelven true, ya que instanceof determina que el objeto es tanto una instancia de la clase base como de la derivada.
Es más fácil de entender si echamos un vistazo a cómo queda la jerarquía de prototipos de estas clases (analízala con atención):
Nuestra clase derivada y la clase base comparten el mismo prototipo, que cambia de constructor según sea una clase o la otra la que instanciemos (arriba de todo el prototipo en el extremo de la cadena es Object, como siempre).
Cuando creamos un nuevo objeto de la clase derivada éste tiene sus propios miembros (la propiedad Apellidos en este caso) y hereda los del prototipo, o sea, los de la clase base (o al menos eso parece).
Esta estrategia de herencia simple es la que se ha usado tradicionalmente en JavaScript y es perfectamente válida pero presenta varios problemas:
- El código está desplazado, ya que primero definimos el constructor de la clase derivada y luego le asignamos, ya fuera, un nuevo prototipo. Es feo, añade complejidad y es propensa a fallos por omitir algún paso.
- No tenemos forma directa de crear constructores con parámetros que funcionen bien así.
- Las propiedades de instancia del prototipo, aunque sí que se heredan, en realidad no se llegan a utilizar ni a inicializar. El motivo es que cuando escribimos por ejemplo
cd.Nombre = "Perico";en realidad se crea esa propiedad en la nueva instancia, y luego al leerla se coge de ahí, no del prototipo, ya que está antes en la ruta de búsqueda. Puedes comprobarlo viendo el valor decd.Nombre y de cd.__proto__.Nombreen un navegador que no sea Internet Explorer. Comprobarás que son diferentes y sólo el primero contiene el valor que esperabas. Es fácil ver el porqué si examinamos la jerarquía con un depurador:
Como vemos al asignar el valor de Nombre se crea esta propiedad en el objeto cd, mientras que en el prototipo también hay una propiedad idéntica con el valor original (una cadena en blanco).
Este comportamiento es apropiado de todos modos ya que si realmente se usase la propiedad Nombre del prototipo, como todas las instancias de la clase utilizan el mismo objeto como prototipo, estaríamos compartiendo ese valor, pisando unos objetos a otros.
A continuación vamos a estudiar el método más recomendable para simular la herencia en JavaScript.
Robo de constructores
Entonces, tras todo lo que hemos visto sobre herencia, ¿cuál es el mejor método para crear herencia entre clases en JavaScript?
Como se ha dicho antes, existen muchas técnicas diferentes y cada gran biblioteca de funciones (jQuery, Dojo, Prototype, ExtJS…) tiene su propia variante. Sin embargo, existe un buen consenso en considerar que, por regla general, la manera más sencilla y menos problemática de crear herencia para usos normales del lenguaje es mediante la técnica conocida como robo de constructores con sustitución de prototipos.
Esta técnica es una derivada de la que hemos visto en la lección anterior, combinándola con otra denominada robo de constructores.
Consiste en llamar al constructor de la clase base desde el constructor de la derivada, pasándole como contexto el objeto que se está creando, de manera que se recreen todas las propiedades en éste y se pueda inicializar. En general sería así:
function ClaseBase(nom) { this.Nombre = nom; ClaseBase.prototype.mostrarNombre = function () { alert(this.Nombre); }; } function ClaseDerivada(nom, apell) { ClaseBase.call(this, nom); this.Apellidos = apell; } ClaseDerivada.prototype = new ClaseBase(); ClaseDerivada.prototype.constructor = ClaseDerivada;
Al llamar al constructor de la clase base mediante call, le pasamos como contexto el mismo objeto que tenemos en nuestro constructor (this). Lo que provocamos es que se ejecute el código del constructor de la clase base en el mismo objeto que tenemos en el constructor de la clase derivada.
A efectos prácticos estamos ejecutando los dos constructores sobre nuestro objeto.
Por desgracia es necesario seguir asignando el prototipo y el constructor del prototipo en código externo, así que no nos libramos del primer problema que indicábamos antes. En cambio, conseguimos dos beneficios muy importantes eliminando los puntos 2 y 3 de la lista anterior:
- Podemos crear constructores con parámetros.
- Nuestro objeto tendrá definidos desde el principio todos sus miembros, con las validaciones pertinentes y la lógica que sea necesaria.
Es un gran avance y generalmente es el patrón que deberíamos utilizar para hacer herencia en JavaScript.
Estas técnicas de herencia se pueden combinar para emular herencia múltiple también, cada clase apuntando en su prototipo a un objeto de la anterior en la jerarquía.
Si buscas en Internet verás que existen decenas de métodos diferentes para conseguir simular la herencia en JavaScript, cada uno con sus ventajas y sus inconvenientes. De hecho, casi cada biblioteca de funciones como jQuery y similares utiliza sus propias técnicas para implementar la herencia. En cualquier caso, sea como sea el método que utilicemos, lo más importante es entender cómo funcionan los prototipos y los constructores, y visualizar mentalmente la jerarquía que forman de modo similar a lo que se ha mostrado en las figuras anteriores. De esta manera, cuando veamos herencia aplicada en el código de otros, aunque se utilice una variante extraña de las muchas existentes, tendremos una idea clara enseguida del modo en que funcionan.
Creando variantes de nuestra clase base
////// Herencia function Marcianito(elNombre, elColor, posX, posY, disparosIniciales) { this.name = elNombre; this.color = elColor; if (posX < 0) posX = 0; if (posX > 100) posX = 100; this.x = posX; if (posY < 0) posY = 0; if (posY > 100) posY = 100; this.y = posY; if (disparosIniciales < 0) disparosIniciales = 0; if (disparosIniciales > 100) disparosIniciales = 100; this.disparos = disparosIniciales; if (!Marcianito.prototype.disparar) { Marcianito.prototype.disparar = function () { this.disparos--; //codigo para pintar el disparo alert(this.name + " ha disparado."); }; } Marcianito.prototype.toString = function () { return this.name + " - " + this.color; }; } //var marcianito1 = new Marcianito("Invasor del espacio 1", "Azul", 100, 20, 30);
Vamos a definir una nueva variante de Marcianito creando un nuevo constructor:
function NaveEstelar(elNombre, elColor, posX, posY) { // Usamos ''call'' para que "herede" lo que habíamos // definido en 'Marcianito'. Esto es lo que conocemos // como robo de constructores Marcianito.call(this, elNombre, elColor, posX, posY, 30); this.tipo = "Nave estelar"; } NaveEstelar.prototype = new Marcianito(); // Indicamos cuál es el verdadero constructor NaveEstelar.prototype.constructor = NaveEstelar; // Ahora definiremos otro tipo de Marcianito function NaveNodriza(elNombre, elColor, posX, posY) { Marcianito.call(this, elNombre, elColor, posX, posY, 50); this.tipo = "Nave Nodriza"; } NaveNodriza.prototype = new Marcianito(); NaveNodriza.prototype.constructor = NaveNodriza; var marcianito1 = new NaveEstelar("Invasor del espacio 1", "Azul", 100, 20); var marcianito2 = new NaveNodriza("Invasor del espacio 2", "Verde", 400, 10); alert(marcianito1); //alert(marcianito1.tipo); //alert(marcianito2); //alert(marcianito2.tipo);
Detalles internos sobre creación de objetos
Si quieres entrar a detalle y saber qué hace JavaScript internamente cuando instancia un nuevo objeto usando una función constructora, puedes recurrir al estándar. Sin embargo, esa documentación es muy árida y difícil de leer y comprender.
Por suerte es posible simular lo que hace JavaScript internamente de manera relativamente fácil. Y es lo que vamos a hacer a continuación para que quede más claro todavía cómo funciona.
Vamos a crear nuestro propio comando new para instanciar un objeto a partir de una función constructora. Le llamaremos NEW para distinguirlo bien. Su código sería el siguiente:
function NEW(fConstructora){ var nObjeto = { '__proto__': fConstructora.prototype }; return function(){ fConstructora.apply(nObjeto, arguments); return nObjeto; }; };
Esta función funciona exactamente igual. Se le pasa el identificador de la función constructora. Como este NEW es una función normal de JavaScript y no una palabra clave, tenemos que pasárselo entre paréntesis (es la única diferencia con el new nativo, que se puede saltar esto)
Vamos a ver qué hace con más detalle:
- Crea un nuevo objeto definido directamente con formato JSON (Object Notation). Este objeto (y esto es muy importante) contiene una propiedad
__proto__a la cual se le asigna el prototipo de la función constructora. Ya hemos visto que todos los objetos tienen una propiedad como esta que contiene el objeto “patrón” de la clase. En el caso de clases simples, el prototipo será Object directamente. En el caso de clases derivadas este prototipo será el que le corresponda, y esto es lo que le asignamos. - Definimos una función dentro, que es la que vamos a devolver como resultado de la llamada a
NEW. Recordemos que una funciónen JavaScript es como un objeto más, así que podemos devolverla como resultado de otra función sin ningún problema. De hecho, es algo que se hace de forma muy habitual. Lo que lleva a cabo esta subfunción que vamos a devolver es lo siguiente:- Llamaa la función constructora que le pasamos como argumento y usando
applyle asigna como contexto (o sea, el valor dethis) el objeto que acabamos de crear (al cual tenemos acceso porque estamos enuna clausura), y como argumentos los diferentes argumentos que se le pasen a esta función (la palabra claveargumentsdevuelve esto, como veremos un poco más adelante con detalle). Con esto estamos llamando al constructor de la manera que lo llamanew. - Devuelve el resultado de esa llamada, que será el objeto
nObjetoya habiendo pasado por el constructor, así que tendrá todas las propiedades que se le asignen en éste, etc…
¡Listo!
Probemos a utilizarlo. Primero vamos a definir una clase y a instanciarla de la manera convencional para ver en la consola del navegador qué obtenemos. El código sería este:
function Animal(sNombre, nPatas) { this.nombre = sNombre; this.patas = nPatas; } var bicho = new Animal("Perro", 4); console.log(bicho);
Si vamos a la consola y examinamos el objeto creado, pulsando sobre él, veremos lo siguiente (en Firefox):
Tal y como cabría esperar: el prototipo es Object y el constructor Animal. Perfecto.
Vale, ahora vamos a instanciar otro animal pero esta vez con la función que hemos creado hace un momento:
var bicho2 = NEW(Animal)("Canario", 2); console.log(bicho2);
Ahora tenemos que poner el constructor entre paréntesis pero lo demás es idéntico. Veamos en la consola el “bicho” generado:
¡Exactamente igual! Podemos crear objetos como si fuésemos JavaScript.Hemos jugado a dioses y nos ha salido bien 😉
Hasta ahora todo ha ido bien. Pero vamos a ver si sigue funcionando bien con clases que heredan de otras…
Vamos a definir una clase Gato derivada de Animal por el método convencional:
function Gato(sNombre) { Animal.call(this, sNombre, 4); } Gato.prototype = new Animal(); Gato.prototype.constructor = Gato;
Ahora instanciamos un objeto de esta nueva clase siguiendo también el método convencional:
var gato1 = new Gato("Patucas"); console.log(gato1);
Y lo que veremos en la consola al inspeccionarlo es esto:
Ahora vamos a hacer lo mismo pero con nuestro “creador” simulado:
var gato2 = NEW(Gato)("Calcetines"); console.log(gato2); //Idéntico!
y, sorpresa:
¡Es idéntico! 😉 Así que funciona bien.
Herencia: Detalles importantes a recalcar
La Programación Orientada a Objetos en JavaScript en general, y la herencia en particular, es todo un reto porque como el soporte nativo es tan peculiar (basado en herencia de prototipos y no como se hace en el resto de lenguajes), es necesario “inventar” modos de conseguir cosassimilares a la POO tradicional, con las herramientas con las que contamos. Es normal que a estas alturas te esté costando mucho entenderlo bien.
Para tratar de simplificar un poco más la cuestión de la herencia, vamos a ver un resumen que nos ayudará a ver mejor toda la “foto” desde una perspectiva global, y aclarará muchos detalles un poco más.
Resumen de conceptos
Vamos a poner en perspectiva unos cuantos conceptos sobre la herencia que suele costar un poco entender. Para ello supongamos que disponemos de una clase base llamada ClaseBase, y una clase derivada de ésta a la que llamaremos ClaseDerivada. A efectos de ilustrar algunos detalles supondremos que la clase base dispone de un par de propiedades propA y propB, mientras que la clase derivada añade también otro par de propiedades propias a mayores llamadas propC y propD.
Veamos:
Los prototipos no se usan para instanciar objetos. Los prototipos son objetos (instancias concretas de clases, ¡ojo!)que JavaScript usa para localizar métodos y propiedades dentro de una jerarquía de clases. Si instancias un objeto de una clase derivada así:
var cd = new ClaseDerivada();
Lo que tendrás es un objeto con las propiedades y métodos específicos de la clase derivada (propC y propD), y en su prototipo otro objeto asignado de tipo ClaseBase, con unas propiedades y métodos específicos (propA y propB) que estarán disponibles también en el nuevo objeto derivado que se ha creado.
Si ahora escribimos:
cd.propA = "Hola";
JavaScript intenta buscar la propiedad propA en el objeto cd. Sin embargo, este objeto no posee dicha propiedad, así que sigue subiendo por la cadena de prototipos para comprobar si alguno de los objetos que están en la misma, a cualquier nivel, dispone de dicha propiedad. En este caso, el prototipo del objeto cd es otro objeto de tipo ClaseBase que sí tiene esa propiedad. Por lo tanto se encuentra y se asigna, y funciona bien.
Como JavaScript no es orientado a objetos de la manera en que otros lenguajes lo son, recurre a este artificio de la cadena de prototipos.
Que un objeto A de tipo ClaseBase sea el prototipo de otro objeto B de tipo ClaseDerivada, no quiere decir que se use el constructor del objeto A (o sea Clasebase()) para instanciar al objeto B. De hecho no es así. Lo que se puede hacer es “robar” el constructor como hemos visto anteriormente, de modo que se redefinan las mismas propiedades que tenía la clase base en el nuevo objeto B de la clase derivada, pero no es siempre necesario ni mucho menosobligatorio.
Hay que distinguir claramente entre las propiedad prototype y __proto__ de los objetos. Al inspeccionar un objeto con las herramientas del desarrollador de cualquier navegador (se estudia con detalle en el módulo de depuración), todos los objetos disponen de una propiedad __proto__ pero no todos (de hecho casi ninguno) disponen de la propiedad prototype.
¿Por qué es esto?. Muy sencillo: porque la propiedad __proto__ apunta al objeto que actúa como prototipo del objeto actual (que es una instancia concreta de la clase base), mientras que prototype apunta a un objeto que es el que se usará como patrón para meter en __proto__ cada vez que se cree un nuevo objeto de la clase correspondiente. Es decir, prototype es un “patrón” para __proto__.
prototype es una propiedad exclusiva de los tipos nativos de JavaScript y más en concreto de las funciones. Por eso puedes escribir ClaseDerivada.prototype = new ClaseBase(); ya que en la función ClaseDerivada() (¡que es el constructor!) sí que está disponible esa propiedad. Es necesario fijarse en que, cuando hablamos de una clase en JavaScript (y no de un objeto), estamos realmente hablando de su función constructora ya que las clases como tal no existen.
Cuando una función se usa con la palabra clave new delante (es decir, para crear/instanciar un objeto) lo que hace JavaScript es crear un objeto vacío, asignarle como prototipo (en __proto__) lo que haya en la propiedad prototype de la función y la llama estableciendo this a este nuevo objeto. En eso consiste la creación de un objeto.
¿Por qué y para qué asignamos una propiedad constructor al prototipo?
Lo primero que debemos tener en cuenta es que no es indispensable ni necesario asignar el constructor de la forma que hemos visto, pero sí que será muy recomendable como veremos pronto.
Es decir, si yo defino un par de clases, base y derivada, sin usar la línea ClaseDerivada.prototype.constructor = ClaseDerivada;, como hemos visto antes, no pasa absolutamente nada y el códigova a funcionar bien igual.
La propiedad constructor solamente se usa para tener asociada a cada objeto una referencia a la función que lo ha construido. Esto es a efectos de identificación y por si la queremos usar nosotros en nuestro código por algún motivo (para instanciar otro objeto de la misma clase sin saber cómo se llama, como veremos en breve). Pero en realidad JavaScript no utiliza la propiedad constructor para nada y de hecho no todos los objetos lo tienen.
Insisto: el constructor de una clase es simplemente la función a la que nosotros llamemos para crear el objeto, no lo que indique esa propiedad, que ni siquiera tiene porqué existir, y de hecho no existe si creamos un objeto con notación JavaScript (JSON) o si no se lo asignamos explícitamente.
Entonces si la herencia funciona igual sin eso y tampoco es necesario utilizarlo, ¿por qué es recomendable hacerlo de esta manera?.
Es una mera cuestión de coherencia, y de que la jerarquía de clases se muestre como realmente es.
Lo mejor es verlo gráficamente… Si definimos dos clases muy sencillas(base y derivada) de esta manera:
function ClaseBase(n){ this.nombre = n; } function ClaseDerivada(n){ ClaseBase.call(this, n) ; } ClaseDerivada.prototype = new ClaseBase(); var cd = new ClaseDerivada("Pepe");
Sin asignar el constructor al prototipo de la clase derivada como hemos hecho hasta ahora, lo que veremos si instanciamos un objeto de la clase derivada ylo examinamos con las herramientas del desarrollador del navegador es esto:
Como vemos, el objeto del que heredamos (que es de tipo ClaseBase lógicamente) tiene establecido el constructor adecuado (ClaseBase) pero nuestra clase derivada carece de esa propiedad. No pasa nada salvo que, por el motivo que sea, queramos averiguar con qué función se ha construido nuestra clase, algo que raramente querremos en realidad.
De acuerdo. Ahora observemos el mismo diagrama si usamos la línea del constructor a la que estamos acostumbrados:
ClaseDerivada.prototype.constructor = ClaseDerivada;
Como vemos, ahora la herencia se aprecia de manera más clara: vemos que el objeto tiene como constructor la clase derivada, y que su prototipo lo tiene, como antes, de la clase base. El funcionamiento es el mismo pero lo vemos más claro.
Se considera una buena práctica hacerlo así, pero no es indispensable ni obligatorio.
A continuación vamos a ver con un poco más de detalle cómo es todo el proceso de creación de objetos y herencia replicando nosotros lo que hace JavaScript internamente, y vamos a estudiar un caso práctico en el que usaremos la propiedad constructor.
Herencia: Uso práctico de prototype
Mirando nuestra clase Gato de antes, lo primero que realizamos en su constructor es un robo de constructores. Es decir, llamamos al constructor de la clase base de modo que se ejecute sobre nuestro objeto. De este modo forzamos a que se definan en él las mismas propiedades y métodos que se definen en el constructor del objeto base.
Por cierto, ten en cuenta que además de usar call puedes usar apply y pasar arguments como segundo argumento de modo que en clases que toman muchos parámetros no hay que repetirlos, ya que se pasarán todos los que haya automáticamente. Repasa esa lección si lo crees necesario.
Una vez hecho este robo de constructor, lo que tenemos que hacer es asignarle como prototipo un objeto cualquiera de la clase base. Como sabemos, JavaScript busca métodos y propiedades en toda la cadena de prototipos, por lo que haciendo esto, lo que conseguimos es que el nuevo objeto “herede” todos los métodos que tiene un objeto de la clase base, ya que JavaScript si no los encuentra en el nuevo objeto los buscará en su prototipo, y así sucesivamente.
Esto ya lo sabíamos, pero vamos a retomar una cuestión relacionada que es muy importante: existen dos propiedades relacionadas con el prototipo de una clase:
__proto__: esta propiedad la tienen todos los objetos, y apunta al objeto concreto que actúa como prototipo de la clase actual. Aunque no se trata de una propiedad estándar la implementan todos los intérpretes de JavaScript.prototype: esta propiedad la poseen únicamente algunos objetos nativos de JavaScript y en concreto las funciones. Cuando una función se usa como constructora connew, se recurre a este objeto para asignar la propiedad anterior (__proto__) del objeto que estamos creando, como hemos visto en la funciónNEWde antes que simulaba al new nativo. De este modo, el objeto que se crea “hereda” los miembros del objeto que esté asignado en la propiedadprototypede su función constructora.
Es decir, la propiedad prototype de la función constructora contiene un objeto “patrón” que se usará como prototipo (o sea, en la propiedad __proto__) de todas las clases que se creen con dicha función constructora. Este prototipo es el mismo, por lo que lo comparten todos los objetos de una misma clase.
Podemos aprovechar este hecho para crear propiedades y métodos estáticos con valores únicos compartidos entre todos los objetos de una clase.
Aclarado esto, disponemos de dos formas de asignar este objeto prototipo:
La manera tradicional, que es la que está en el ejemplo de la lección anterior, consiste simplemente en instanciar un nuevo objeto de la clase base y asignarlo. Sería así:
Gato.prototype = new Animal();
Esto tiene la ventaja de que es muy fácil y directo y además funciona en todos los navegadores y motores de JavaScript que han existido y existirán. Pero tiene la pega de que realmente el objeto que se crea tendrá todas sus propiedades y datos internos sin inicializar (que ocupan algo más de memoria de la necesaria), e incluso podría darnos errores si no le pasamos los valores adecuados al constructor.
Por supuesto podríamos usar una llamada completa, con parámetros, y funcionaría igual y evitaríamos este últimop osible problema:
Gato.prototype = new Animal("Patron", 0);
La manera “moderna” de asignar el prototipo es usar el método Object.create disponible en todos los navegadores modernos desde hace muchos años, y que se haría de la siguiente manera:
Gato.prototype = object.Create(Animal.proptotype);
Este método lo que hace es crear un nuevo objeto sin inicializar (“vacío”) y le asigna a su vez como prototipo el objeto que le digamos. En la línea anterior, por lo tanto, lo que hace realmente es asignarle al prototipo de la clase derivada (Gato) un nuevo objeto sin inicializar con el prototipo de la clase base (Animal), para que herede sus métodos.
La ventaja de hacerlo así frente al método tradicional es que no hay que llamar al constructor de la clase base otra vez y no quedan miembros inicializados que no se van a usar para nada. Es decir, está más optimizado, pero en situaciones normales no hay diferencia práctica alguna.
En la actualidad, si no tenemos que dar soporte a navegadores antiguos (especialmente Internet Explorer 8) es el método recomendado a utilizar para asignar el prototipo en la herencia.
Asignar el constructor y sacarle partido en la práctica
De acuerdo con lo anterior ya tenemos visto a fondo la herencia. Pero nos sigue faltando el detalle de asignar el constructor también como parte de este proceso.
¿Por qué hacemos esto?:
Gato.prototype.constructor = Gato;
En condiciones normales no nos aporta nada, pero se hace para lograr una coherencia con el proceso normal de instanciación, en el que sí se establece esta propiedad a título informativo, como ya hemos visto. Pero, en la práctica, ¿para qué podría usarse el constructor a partir de esa propiedad?
Imaginemos que queremos que los animales puedan tener “animalitos-hijo”.
Podemos definir en el prototipo de la clase base Animal el siguiente método (siempre se deben definir, en general, en el prototipo todas las funciones que queramos que se hereden):
Animal.prototype.procrear = function(){ return new Animal(this.nombre + " - Hijo", this.patas); };
Gracias a esta función, heredada por todos los objetos que hereden de Animal, si ahora queremos tener un gatito podemos escribir simplemente, en un objeto de la clase Gato:
gatito1 = gato1.procrear(); console.log(gatito1);
Si examinamos el nuevo objeto gatito1 veremos lo siguiente:
Nota: en versiones recientes de Firefox, cuando muestras un objeto por consola, visualiza la propiedad __proto__ con el nombre <prototype>. Supongo que lo hacen por facilitar la lectura, pero en realidad confunde. Si visualizas el objeto anterior usando este navegador ten en cuenta que ese <prototype> es en realidad __proto__ y que este es el nombre que debes usar para acceder a él mediante código, aunque la consola lo “rebautice” de esa manera.
Ha funcionado. Sin embargo hay un detalle importante que debemos tener en cuenta: el nuevo cachorro de gato es simplemente un Animal, no un Gato que sería lo lógico…
Aquí es donde entra la propiedad constructor que a priori no servía para gran cosa. Dado que la tenemos asignada, podemos cambiar nuestra definición de la función procrear a este otro código:
Animal.prototype.procrear = function(){ return new this.__proto__.constructor(this.nombre + " - Hijo", this.patas); };
Es decir, usamos el verdadero constructor de la clase actual para instanciar un nuevo hijo.
Veamos qué pasa ahora al instanciar el gatito:
Ahora es exactamente igual que un objeto cualquiera de la clase Gato. Y sin embargo hemos usado una función que está en el prototipo de su clase base.
Con esto hemos visto una aplicación práctica real de porqué es bueno asignar el constructor al prototipo. Todos los programadores profesionales de cierto nivel confían y esperan que lo hagas, precisamente por situaciones como esta.
Encapsulación en Javascript
Como hemos estudiado al principio, los tres pilares de la programación orientada a objetos son la herencia, el polimorfismo y la encapsulación. Hasta ahora hemos visto como los dos primeros son posibles gracias al uso de prototipos y funciones constructoras. ¿Pero qué pasa con el tercero?
La encapsulación o encapsulamiento es una característica de los lenguajes orientados a objetos que consiste en que las clases pueden definir miembros privados, ocultos al exterior, y que solamente pueden ser cambiados a través de los métodos establecidos a tal efecto.
Por ejemplo, en un lenguaje orientado a objetos puro como C# podemos definir una clase de este modo:
class MiClase { private string _nombre = ""; public string Nombre { get { return _nombre.ToUpper(); } set { _nombre = value; } } }
Lo que hace este código C# es definir una clase llamada MiClase que tiene una propiedad pública Nombre que controla el acceso a un miembro interno/privado denominado _nombre. Con el uso de la palabra private indicamos que esa propiedad interna no es accesible para el código exterior a la clase, así que no queda más remedio que utilizar la propiedad Nombre que encapsula el acceso a ésta, y por ejemplo en este caso devuelve siempre el valor en mayúsculas, independientemente de cómo se haya guardado.
En JavaScript de entrada no hay una forma sencilla de hacer lo mismo. Como hemos visto hasta ahora tenemos dos formas de definir variables dentro de una función constructora:
function MiClase() { var local = "local"; this.local = "De instancia"; }
En este caso, las variables locales definidas en la función sólo tienen validez durante la ejecución de la función, desapareciendo después, por lo que, en principio, no nos valen de mucho. Los miembros de instancia que asignamos mediante this, y que ya hemos visto a la hora de definir constructores, son públicamente accesibles y no tenemos forma de limitar el acceso a los mismos.
Entonces ¿cómo podemos conseguir un efecto de encapsulamiento similar al de otros lenguajes?
Antes de poder dar una solución tenemos que presentar el concepto de las clausuras o closures.
Clausuras y la verdadera regla de ámbito de variables
Este concepto de clausura es quizá el más difícil de entender para un programador que se aproxima por primera vez a lenguajes funcionales como JavaScript. Aunque la explicación es algo árida trata de dedicarle toda tu atención puesto que las clausuras son fundamentales para trabajar con JavaScript a cierto nivel.
Una clausura o closure es una función que es evaluada en un determinado ámbito pero que tiene acceso también a variables que están ubicadas en un ámbito diferente. Son como un puente entre dos mundos.
El concepto se inventó en los años 60 y se implementó por primera vez en 1975 en un lenguaje llamado Scheme, que es un dialecto del tradicional lenguaje Lisp. Está relacionado generalmente con los lenguaje funcionales como JavaScript, pero lo incorporan también otros lenguajes modernos como C#, el primer lenguaje orientado a objetos en soportarlo (curiosamente Java no ofrece closures).
Para entender lo básico sobre una clausura, consideremos el siguiente código de un constructor:
function pruebaClosure() { var loc = "¡Hola closure!"; this.muestraMensaje = function () { alert(loc); }; } var obj = new pruebaClosure(); obj.muestraMensaje(); //Muestra el mensaje de saludo
En este fragmento hemos definido una función/clase que dispone a su vez de otra función interna definida como método de la clase. Es similar a tener definida una función dentro de otra función (anidada) y ya lo hemos visto en las lecciones anteriores.
Desde esta función interna (muestraMensaje) se accede al valor de la variable local loc que tenemos definida dentro de la función. Lo curioso en este código es que funciona perfectamente y muestra el mensaje contenido en dicha variable local cuando lo llamamos desde cualquier instancia. Es decir, que en este caso la regla de ámbito que hemos estudiado en el módulo 2 del curso parece que se rompe ya que tenemos una función que es capaz de ¡acceder a una variable local de otra función diferente!.
En teoría, si no supiésemos nada sobre el verdadero funcionamiento de JavaScript, cuando llamamos a muestraMensaje en la última línea del fragmento anterior, el ámbito de la llamada sería el objeto global, pues se trata de una llamada normal y corriente a una función desde la propia ventana (no desde dentro de otra función). Por lo tanto, desde ésta no deberíamos tener acceso a la variable local loc, pues esta ya no existe (las variables locales se destruyen al terminar la ejecución de la función, y el constructor ya se ha ejecutado).
Sin embargo, no es así y la regla sigue siendo vigente. La verdadera regla de acceso a variables es que una línea de nuestro código sólo puede acceder a variables que tengan un ámbito mayor o igual que el suyo propio.
Y en este caso se está cumpliendo ya que la variable loc tiene un ámbito mayor que el de la línea de código que lanza la alerta, que tiene un ámbito (el de su función) todavía más restringido. Debemos pensarlo como una jerarquía donde los niveles inferiores siempre tienen acceso a los superiores.
Las clausuras se llaman así porque lo que representan son funciones que tienen acceso a una serie de variables liberadas ya en su propio ámbito original, pero que quedan confinadas (clausuradas) en el ámbito de la función que las utilizará más tarde, fuera de ámbito.
En el ejemplo anterior, si no existiese la función muestraMensaje, al terminar de llamar al constructor, las variables locales quedarían liberadas. Sin embargo, como ese método hace uso de ellas, quedan confinadas (clausuradas) dentro del ámbito de la función para que ésta las pueda utilizar.
¡Buff! Reflexiona con calma sobre el concepto. Detrás de su aparente sencillez se encuentran un sinfín de posibilidades de conseguir código avanzado. Por ejemplo, algo muy común es el uso de funciones autoejecutadas cuyo único propósito es mantener dentro de un ámbito cerrado nuestras variables para evitar que entren en conflicto con otras variables globales que pueda haber en el código.
Ejecución diferida de funciones gracias a las clausuras
Por ejemplo, analicemos ahora lo siguiente:
function concatenar(s1) { return function(s2) { return s1 + ' ' + s2; }; } var diHola = concatenar("Hola"); alert( diHola("visitante") );
Lo que se muestra por pantalla en la última línea es la frase “Hola visitante”.
Es decir, en la práctica gracias al uso de una clausura hemos definido una función que permite asignar valores para ejecución retardada. En la penúltima línea llamamos a concatenar pasándole un parámetro. Esto nos devuelve una función que tiene almacenado el valor de ese parámetro y que está esperando otro. Así que, lo que tenemos en la variable diHola es una función preparametrizada y lista para ser utilizada cuando queramos. El valor del primer parámetro s1 queda clausurado por el ámbito de la función.
¿Para qué sirve esto? Se trata de algo realmente útil, y no es la única aplicación práctica de las clausuras. Gracias a ello podemos habilitar todo un sistema de ejecución en diferido de funciones JavaScript. Esto nos permite crear punteros a funciones preparametrizadas y crear con ello un sistema de eventos, lanzar acciones periódicas complejas y particularizadas con setInterval o setTimeout… y muchas otras cosas.
Consideremos por ejemplo la idea de lanzar una función con parámetros cada cierto tiempo, con setInterval o con setTimeOut. Esta dos funciones, como hemos visto, toman como parámetros una referencia a la función a ejecutar y el tiempo en milisegundos al cabo del cual debe ejecutarse (periódicamente o sólo una vez, según sea uno u otro método). Pero tienen una limitación: no permiten en todos los navegadores pasar parámetros a la función a ejecutar. Así que no sirve para llamar a funciones que tomen parámetros para particularizar su funcionamiento. La solución chapucera (y común) sería definir una función sin parámetros que a su vez llame a la que queremos llamar nosotros con los parámetros requeridos.
Una solución mucho más elegante es usar una clausura, como en este ejemplo:
function moverElemento(elto, x, y) { return function(){ elto.style = "position:absolute; left:"+ x + ";top:" + "y;"; }; } var mover = moverElemento(getElementById("miDiv"), 0, 0); setTimeout(mover, 500);
Otra de sus muchas aplicaciones es la definición de variables privadas, pilar de la encapsulación, como veremos a continuación.
Definición de variables privadas
Por convención, las variables que comienzan por guion bajo son variables privadas.
////// Variables privadas function Marcianito(elNombre, elColor, posX, posY, disparosIniciales) { // Definición de la propiedad name, que encapsula el acceso a // la variable de instancia _name var _name = elNombre; // Funciones de clausura que sí tendrán acceso a la variable anterior Marcianito.prototype.getName = function(){ return _name.toUpperCase() + " (" + this.color + ")"; }; Marcianito.prototype.setName = function(nombre) { _name = nombre; }; this.color = elColor; if (posX < 0) posX = 0; if (posX > 100) posX = 100; this.x = posX; if (posY < 0) posY = 0; if (posY > 100) posY = 100; this.y = posY; if (disparosIniciales < 0) disparosIniciales = 0; if (disparosIniciales > 100) disparosIniciales = 100; this.disparos = disparosIniciales; if (!Marcianito.prototype.disparar) { Marcianito.prototype.disparar = function () { this.disparos--; //codigo para pintar el disparo alert(this.getName() + " ha disparado."); }; } Marcianito.prototype.toString = function () { return this.getName() + " - " + this.color; }; }
Lo probamos:
var marcianito1 = new Marcianito("Invasor del espacio 1", "Azul", 100, 20, 30); alert(marcianito1.getName()); // -> INVASOR DEL ESPACIO 1 (Azul) marcianito1.setName("Comecocos"); alert(marcianito1.getName()); // -> COMECOCOS (Azul)
De todos modos, el código tiene un problema y es que estamos usando la variable this dentro de las funciones de clausura. Como sabemos, el valor de this varía según el ámbito desde el que se llame la función.
Una posible solución para mantener el ámbito original de la variable this creamos una nueva variable que igualaremos al valor de this:
////// Variables privadas function Marcianito(elNombre, elColor, posX, posY, disparosIniciales) { var that = this; // Definición de la propiedad name, que encapsula el acceso a // la variable de instancia _name var _name = elNombre; // Funciones de clausura que sí tendrán acceso a la variable anterior Marcianito.prototype.getName = function(){ return _name.toUpperCase() + " (" + that.color + ")"; }; Marcianito.prototype.setName = function(nombre) { _name = nombre; }; this.color = elColor; if (posX < 0) posX = 0; if (posX > 100) posX = 100; this.x = posX; if (posY < 0) posY = 0; if (posY > 100) posY = 100; this.y = posY; if (disparosIniciales < 0) disparosIniciales = 0; if (disparosIniciales > 100) disparosIniciales = 100; this.disparos = disparosIniciales; if (!Marcianito.prototype.disparar) { Marcianito.prototype.disparar = function () { that.disparos--; //codigo para pintar el disparo alert(that.getName() + " ha disparado."); }; } Marcianito.prototype.toString = function () { return that.getName() + " - " + that.color; }; }
Así conseguimos que el valor original de this se mantenga da igual desde donde la llamemos.
var marcianito1 = new Marcianito("Invasor del espacio 1", "Azul", 100, 20, 30); var fNombre = marcianito1.getName; alert(fNombre()); // -> INVASOR DEL ESPACIO (Azul)
Si no lo hubiéramos hecho así, cuando llamásemos a la función fNombre obtendríamos:
INVASOR DEL ESPACIO (undefined)
Prototipos accediendo a variables de ámbito: un error grave
El código desarrollado en el vídeo anterior puede parecer que es correcto, pero sin embargo tiene un error muy grave. ¿Eres capaz de identificarlo?.
Antes de seguir leyendo dale una vuelta en la cabeza al código del ejemplo y trata de averiguar qué puede tener mal. Quizá lo hayas visto ya nada más empezar a ver el vídeo. Si no es así, antes de continuar piensa en cuál puede ser el fallo.
Más pistas…
¿Qué verás por pantalla si mantienes el código del vídeo y escribes lo siguiente?:
var marcianito1 = new Marcianito("Invasor del espacio 1", "Azul", 100, 20, 30); var marcianito2 = new Marcianito("Invasor del espacio 2", "Rojo", 400, 300, 30); alert(marcianito1.getName()); alert(marcianito2.getName());
Pues que, de repente, ambos marcianitos van a mostrar por pantalla ¡el mismo nombre y el mismo color!. Pruébalo para comprobar que es así…
El problema se produce al asignar desde dentro del constructor una función prototipo, que a su vez accede a variables privadas. Es decir, al hacer esto:
function Marcianito(elNombre, elColor, posX, posY, disparosIniciales) { var that = this; var _name = elNombre; Marcianito.prototype.getName = function(){ return _name.toUpperCase() + " (" + that.color + ")"; }; ..... }
Lo que estamos haciendo es que, cada vez que se crea un nuevo Marcianito se está redefiniendo la función getName del prototipo, la cual debido a las reglas de ámbito (creando una clausura en este caso) está accediendo a las variables privadas de la instancia en la que se define.
En otras palabras: cuando creas el marcianito1 la función del prototipo queda “atada” al valor de las variables privadas (_name y that) de esa instancia. Luego, al definir otro marcianito (el marcianito2 en el ejemplo) se sustituye la función getName de nuevo, esta vez apuntando al ámbito de la clausura de este segundo objeto. O sea, en cada instanciación de un marcianito estamos redefiniendo las funciones del prototipo, las cuales están accediendo a un ámbito diferente, lo cual está mal.
Una función de prototipo, por definición, se comparte entre todas las instancias de una clase. Por ello, si la redefinimos accediendo a unas variables de ámbito, todos los objetos de la clase accederán a las mismas variables privadas. Esto puede ser útil, por ejemplo, para crear variables privadas compartidas entre todas las instancias de una clase, es decir, variables privadas estáticas.
La manera correcta de definir estos métodos que acceden a variables privadas de instancia es definirlas directamente en el mismo ámbito, es decir, usando this como con el resto de las propiedades y métodos, así:
function Marcianito(elNombre, elColor, posX, posY, disparosIniciales) { var that = this; var _name = elNombre; this.getName = function(){ return _name.toUpperCase() + " (" + that.color + ")"; }; ..... }
De esta manera la función accede al ámbito adecuado en cada caso.
Las funciones del prototipo no pueden acceder a variables de instancia (como _name o that en el ejemplo). Mientras el prototipo no necesite acceder a variables privadas y solo acceda a propiedades de instancia (es decir, a través de this) es posible definirlo dentro del constructor.
Por todo ello, la definición correcta (una de ellas) de esta clase de ejemplo, Marcianito, sería la siguiente:
function Marcianito(elNombre, elColor, posX, posY, disparosIniciales) { var that = this; var _name = elNombre; this.getName = function(){ return _name.toUpperCase() + " (" + that.color + ")"; }; this.setName = function (nombre) { _name = nombre; }; this.color = elColor; if (posX < 0) posX = 0; if (posX > 100) posX = 100; this.x = posX; if (posY < 0) posY = 0; if (posY > 100) posY = 100; this.y = posY; if (disparosIniciales < 0) disparosIniciales = 0; if (disparosIniciales > 100) disparosIniciales = 100; this.disparos = disparosIniciales; if (!Marcianito.prototype.disparar) { Marcianito.prototype.disparar = function () { this.disparos--; //codigo para pintar el disparo alert(this.getName() + " ha disparado."); }; } Marcianito.prototype.toString = function () { return this.getName() + " - " + this.color; }; }
Fíjate en que, como los métodos getName y setName deben acceder a variables de ámbito, no pueden ir definidos en el prototipo. Sin embargo, los métodos disparar y toString, como solo acceden a variables de instancia (con this), sí que pueden definirse en el constructor como miembros del prototipo (incluso comprobando si ya están definidos o no, como se hace con disparar, para evitar definirlos en cada instanciación).
Otra forma muy habitual y más correcta aún de definir las propiedades usando verdaderas propiedades encapsuladas, como veremos a continuación.
Verdaderas propiedades encapsuladas en ECMAScript 5
La encapsulación conseguida con la técnica que acabamos de ver puede ser muy útil. Sin embargo, aún con ello, siguen sin ser verdaderas propiedades, sino que son dos funciones que nos permite leer y escribir el valor de una variable privada.
En POO, por regla general, se entiende por propiedad a un miembro que utiliza el mismo nombre tanto para leer como para escribir.
Conscientes de esta limitación los creadores del motor Mozilla (el que viene integrado en el navegador Firefox) definieron hace años unas extensiones de Object que permitían definir propiedades tradicionales como las descritas. Estos métodos, __defineGetter__ y __defineSetter__, permitían especificar respectivamente la función que posibilitaría leer una propiedad y escribirla. Poco después otros navegadores (Chrome, Safari y Opera) incluirían también soporte para estas extensiones, por lo que prácticamente se hicieron universales. Sólo Internet Explorer sigue sin soportarlas, aunque es comprensible porque no forman parte de estándar o especificación alguna, y además la propia Mozilla ha decidido hacerlas obsoletas.
Por otro lado, cuando se estaba gestando la versión 4 de la especificación ECMAScript (que regula a JavaScript) se consideraron multitud de cambios profundos en el lenguaje, entre ellos la posibilidad de definir propiedades y clases de la manera habitual en otros lenguajes orientados a objetos. Esa versión 4 nunca llegó a ver la luz porque se rompió el grupo de trabajo debido a las desavenencias existentes entre sus miembros. Así se salto a la versión 5, que era más conservadora en cuanto a las características nuevas a incorporar.
Entre las novedades del estándar ECMAScript 5 se ofrecía una manera oficial para conseguir propiedades, de la misma forma que se hacía con las extensiones propietarias de Mozilla, pero de manera ya estandarizada y aceptada por todos los navegadores: el método defineProperty.
Este método toma tres argumentos:
- El objeto sobre el que se va a definir la propiedad.
- El nombre de la propiedad.
- Un objeto descriptor que definirá mediante sus propiedades las características concretas que regirán el acceso a la propiedad.
El más interesante es el último ya que mediante diversas propiedades nos va a dar la posibilidad de controlar muchos aspectos de cómo funcionará luego la propiedad. Las propiedades de este objeto descriptor son las siguientes:
value: el valor inicial asociado con la propiedad.get: la función getter que se encargará de devolver el valor de la propiedadset: la función setter que se ocupará de almacenar el valor de la propiedad.writable: un booleano que servirá para indicar si será posible cambiar el valor de la propiedad o no. Ojo: el valor por defecto esfalse, así que si no lo cambiamos no podremos escribir la propiedad, solo leerla. Nos permite conseguir propiedades de solo lectura.configurable: determina si el tipo de propiedad definido inicialmente va a ser modificable o no. Si la establecemos afalseno se podrá cambiar el tipo de descriptor asociado a la propiedad, es decir, no se podrá asociar un nuevo descriptor con valores diferentes parawritable,configurableyenumerable. También controla si podremos eliminar la propiedad del objeto o no mediante el uso de la instruccióndelete. Generalmente no producirá un error si se intenta modificar, salvo que estemos en modo estricto de ejecución de JavaScript.enumerable: indica si la propiedad aparecerá en al enumeración de propiedades del objeto como un miembro normal. Veremos en qué consiste la enumeración en el próximo epígrafe de este tema.
Enumeración: Término derivado de la lógica matemática y de ciencia informática, que se refiere a una colección de elementos que forman parte de una lista y que están relacionados. Definen un número de valores relacionados con un significado concreto.
La instrucción delete nos permite eliminar cualquier miembro de un objeto mediante código. Por ejemplo, si escribimos delete marcianito1.name, estaremos eliminando su propiedad name, de existir y si está permitido. Su uso es bastante restringido y poco habitual.
Por ejemplo, podemos definir una propiedad simple así:
var objeto = new Object(); Object.defineProperty(objeto, "nombre", { value: "Mi nombre", writable: true, enumerable: true, configurable: true } ); objeto.nombre = "Un objeto"; //Escribimos alert(objeto.nombre); //Y leemos
O, de manera más habitual, definiendo simplemente el getter y el setter, como en nuestra clase Marcianito:
function Marcianito(elNombre, elColor, posX, posY, disparosIniciales) { var _name = elNombre; var that = this; function getName() { return _name.toUpperCase() + " (" + that.color + ")"; }; function setName(nombre) { _name = nombre; }; if (Object.defineProperty) { Object.defineProperty(this, "name", { get: getName, set: setName }); } . . . . //Resto del constructor }
Si usamos value no podemos utilizar las funciones get y set, y viceversa.
Existe una variante denominada defineProperties (en plural) que nos deja definir varias propiedades de golpe pasándoselas como propiedades del descriptor:
Object.defineProperties(objeto, { "Apellido1": { value: "López", writable:true, enumerable:true}, "Apellido2": { value: "Fernández", writable: true, enumerable: true } } );
El soporte de estos dos métodos por parte de los principales navegadores es el siguiente:
- Internet Explorer: a partir de la versión 9.0. En IE8, aunque el método está implementado, sólo permite actuar sobre objetos del navegador y no sobre objetos de JavaScript, por lo que el soporte es muy limitado y, en la práctica deberíamos considerar que no existe.
- Chrome: a partir de la versión 5.0. Como se actualiza automáticamente, en la práctica el soporte es universal en este navegador.
- Firefox: a partir de la versión 4.0.
- Safari: a partir de la versión 5.1. En Safari 5.0 hay un soporte parcial que es justo al revés del de IE8; funcionará en los de JavaScript pero no en los del navegador, así que mejor evitarlo también. La versión para móviles y tabletas (iPhone/iPad) lo soporta sin problemas.
- Android: el navegador por defecto de este sistema operativo móvil soporta esta característica.
Si queremos usarlo tenemos que tener en cuenta estas particularidades y realmente sólo le vamos a sacar partido en navegadores modernos. Si el soporte de navegadores viejos no nos importa, entonces es una gran herramienta que añadir a nuestra caja de trucos de programador JavaScript y deberíamos usarlo. Si debemos asegurar la compatibilidad con cualquier navegador, entonces la técnica de la lección anterior es la apropiada.
Podemos bloquear la modificación de un objeto usando para ello tres métodos diferentes de la clase base Object. Se trata de los métodos preventExtensions, seal y freeze. Aunque son métodos muy parecidos, el primero (preventExtensions) impide que añadan nuevas propiedades o métodos a un objeto preexistente. El método seal bloquea la modificación de un objeto impidiendo que se añadan o quiten propiedades o métodos, tampoco permite que se puedan redefinir (marca todas las propiedades como no configurables). Finalmente, el tercero (freeze) además de sellar el objeto impide que se cambien los valores de propiedades cuando estas son de tipo valor (se han especificado con value en el descriptor). Si la propiedad tiene un get o un set todavía funcionarán dando la sensación de que se ha modificado el valor, pero lo que se modifica es el estado interno del dato que manejan estos accesores. Es decir, freeze, a todos los efectos convierte un objeto en inmutable. Se puede comprobar la situación de cada uno de los estados provocados por estos métodos usando los correspondientes métodos booleanos de Object: isExtensible, isSealed y isFrozen.
Espacios de nombres
En los lenguajes orientados a objetos existen los espacios de nombres o namespaces. Se trata de una forma de agrupar funciones / objetos de manera que sea más fácil encontrarlos y que no interfieran unos con otros.
JavaScript carece de esta característica, pero es fácil de simular.
Imaginemos que queremos tener un espacio de nombres:
CampusMVP.Utilidades.Cookies
Veamos cómo hacer algo así en JavaScript:
var CampusMVP = { Utilidades: { Cookies: { getCookie : function(nombre){ alert("Obtener cookie " + nombre); }, setCookie : function(nombre, valor) { alert("Almacenar cookie - "+ nombre + ": "+ valor); } } }, Criptografia: { cifrar: function(datos, clave){ alert("Se ha llamado a cifrar"); } } }; // Uso: CampusMVP.Utilidades.Cookies.setCookie("Prueba", "Valor"); CampusMVP.Utilidades.Cookies.getCookie("Prueba"); CampusMVP.Criptografia.cifrar("Dato", "Clave");
Vamos a ver otra manera de definir espacios de nombres:
//Función para definición de espacios de nombres function definirNS(nomNS) { //Separo los espacios de nombres a través del punto var nombres = nomNS.split("."); var base = window; //Se empieza a definir a partir del objeto global //Se recorren para ir creando los sub-objetos que definen la ruta completa, variando el padre for(var i = 0; i < nombres.length; i++){ var nodoActual = nombres[i]; base[nodoActual] = base[nodoActual] || {}; //Lo añade solo si no existe previamente base = base[nodoActual]; } } //Extiendo Object con una función para añadir funciones //que compruebe previamente su existencia Object.prototype.defFunc = function(nombre, func){ //Lo dejamos solamente para funciones (aunque valdría para asignar cualquier miembro nuevo) if (typeof(func) != "function") throw new Error("¡Estás intentando asignar algo que no es una función!"); //Comprobamos primero que no exista if (this[nombre]) //Si existe ya, lanzamos un error throw new Error("¡Intento de redefinir una función existente!"); else this[nombre] = func; }; //Creo un espacio de nombres para agrupar funciones definirNS("CampusMVP.Utilidades.Cookies"); //Definimos las funciones que queremos añadir al espacio de nombres //(aunque en realidad en los espacios de nombres se suelen agrupar clases //que instanciamos, no solo funciones) CampusMVP.Utilidades.Cookies.defFunc("getCookie", function(nombre){ alert("Obtener cookie " + nombre); }); CampusMVP.Utilidades.Cookies.defFunc("setCookie", function(nombre, valor){ alert("Almacenar cookie - "+ nombre + ": "+ valor); }); //Otro espacio de nombres para agrupar funciones de criptografía definirNS("CampusMVP.Criptografia"); CampusMVP.Criptografia.defFunc("cifrar", function(datos, clave){ alert("Se ha llamado a cifrar"); }); // Uso CampusMVP.Utilidades.Cookies.setCookie("Prueba", "Valor"); CampusMVP.Utilidades.Cookies.getCookie("Prueba"); CampusMVP.Criptografia.cifrar("Dato", "Clave");
Podemos hacerlo más fácil de usar y encapsulado creando un módulo:
//Módulo que añade la funcionalidad de NameSpaces al objeto Global //y a los objetos la capacidad de añadir funciones sin problema de sobrescribirlas (function(){ //Función para definición de espacios de nombres (evito redefinición) window.definirNS = window.definirNS || function(nomNS) { //Separo los espacios de nombres a través del punto var nombres = nomNS.split("."); var base = window; //Se empieza a definir a partir del objeto global //Se recorren para ir creando los sub-objetos que definen la ruta completa, variando el padre for(var i = 0; i< nombres.length; i++){ var nodoActual = nombres[i]; base[nodoActual] = base[nodoActual] || {}; //Lo añade solo si no existe previamente base = base[nodoActual]; } } //Extiendo Object con una función para añadir funciones //que compruebe previamente su existencia Object.prototype.defFunc = Object.prototype.defFunc || function(nombre, func){ //Lo dejamos solamente para funciones (aunque valdría para asignar cualquier miembro nuevo) if (typeof(func) != "function") throw new Error("¡Estás intentando asignar algo que no es una función!"); //Comprobamos primero que no exista if (this[nombre]) //Si existe ya, lanzamos un error throw new Error("¡Intento de redefinir una función existente!"); else this[nombre] = func; }; })();
Reflexión: inspeccionando los objetos
Si hemos creado un objeto, normalmente conoceremos todas sus propiedades, tanto las originales como las que se hayan añadido después. Sin embargo, en ocasiones los objetos no los habremos creado nosotros (pueden estar en un script de otra página, por ejemplo) o es posible que no queramos escribir continuamente los nombres de todas las propiedades aunque las conozcamos.
Para ayudarnos en la tarea de conocer mejor cualquier objeto se encuentran las técnicas de reflexión. Gracias a ellas podremos conocer los miembros que tiene un objeto y acceder a sus valores de manera genérica.
Por ejemplo, vamos a escribir una función que resuma en un cuadro de alerta todas las propiedades de un objeto y sus correspondientes valores, independientemente del objeto personalizado que le pasemos.
Como es evidente, al desconocer el tipo de objeto que se nos pasará en cada llamada a la función, no podemos usar nombres de propiedades concretas para generar el informe de resumen de propiedades. Aquí es donde entra en juego el bucle for-in visto en el módulo dedicado a las estructuras de control de flujo. Este tipo de bucle permite recorrer de manera directa las propiedades de un objeto para consultar su nombre:
var marcianito1 = new Marcianito("Invasor del espacio 1", "Azul", 100, 20, 30); var informe = ""; for (var prop in marcianito1) { informe += prop + "\n"; } alert(informe);
El bucle for-in nos permite enumerar todas las propiedades y métodos de un objeto cualquiera. Con este código obtendremos un mensaje con cada nombre de una propiedad del objeto marcianito1, incluyendo las que se hayan añadido dinámicamente tras su definición inicial (asignándolas cuando no existen).
Fíjate en que también se enumeran los miembros de su prototipo (aunque no las del prototipo del prototipo, es decir, no se sigue la cadena de prototipos hacia arriba).
Sin embargo esto, aunque útil, nos permite obtener sólo el nombre de las propiedades, pero no su valor.
Los objetos que creamos exponen sus propiedades y métodos de forma similar a como lo hacen las matrices, es decir, con índices entre corchetes. Pero en su caso, los índices numéricos se sustituyen por índices de texto que coinciden con el nombre de estos métodos y propiedades.
De este modo es indistinto escribir:
marcianito1.disparos = 50; alert(marcianito1.name); marcianito1.disparar();
Que escribir esto:
marcianito1["disparos"] = 50;
alert(marcianito1["name"]);
marcianito1["disparar"]();
Nótese que se han añadido unos paréntesis después de indicar como índice disparar de forma que se ejecute la función y no se devuelva su contenido.
Recuerda como en el módulo dedicado a las matrices hablábamos sobre las matrices asociativas y que éstas no estaban soportadas en JavaScript, pero que se podían simular mediante una sintaxis similar que generaba propiedades en objetos. Ahora aquello tendrá mucho más sentido 😃
Siguiendo esta técnica es fácil escribir una función que permita obtener un resumen de las propiedades y métodos de un objeto cualquiera incluyendo sus valores:
function ResumeObjeto(obj) { var prop; var informe = ""; for (prop in obj) { informe += prop + ": " + obj[prop] + "\n"; } return informe; }
Ahora, para obtener un informe sobre las propiedades de cualquier objeto escribiremos:
alert(ResumeObjeto(marcianito1));
Con el resultado siguiente:
Fíjate en que las funciones, al no ejecutarse, pues no les hemos puesto un par de paréntesis al final, muestran su código completo, por lo que podemos verlo sin ningún problema.
En las descargas del curso he dejado un ejemplo cuyo código debes examinar. En él, además de mostrar estos datos básicos, se crea un objeto para cada propiedad ofreciendo sendas propiedades para el nombre, el tipo y su valor que nos muestran la información correspondiente a cada miembro del objeto.
En este ejemplo se ha afinado un poco más y, dado que como (hemos visto en el módulo correspondiente) typeof no nos devuelve el nombre de clase correcto para objetos no nativos, utiliza una función definida especial que he creado llamada getObjectClassName, la cual usa expresiones regulares para obtener una información más precisa sobre el tipo de cada propiedad.
Cuando usamos for-in para recorrer los elementos de una matriz es importante saber que solo recorrerá aquellos elementos de la matriz que estén explícitamente establecidos. Es decir, los elementos no inicializados no se obtendrán dentro de un bucle de este tipo, debiendo usar un bucle convencional (con una variable numérica) para recorrer todos los elementos en orden. Además, no existe garantía tampoco de que el bucle for-in nos devuelva los elementos de la matriz en el orden original. En realidad es dependiente de la implementación concreta del navegador o entorno en el que ejecutemos nuestro código, por lo que puede que en un entorno se devuelvan con un orden y en otro entorno con un orden diferente. El uso principal de for-in es el de recorrer las propiedades de un objeto y, según indica el propio estándar, el orden de recorrido es arbitrario. Usarlos para recorrer matrices no es considerado una buena práctica, aunque muchas veces se haga.
Funciones anónimas autoejecutables: el patrón Módulo
Un patrón muy utilizado a la hora de escribir código JavaScript son las funciones anónimas autoejecutables, que son la base de diversos patrones, fundamentalmente el patrón Module o Módulo.
La idea es la siguiente: aprovechar las propiedades de ámbito de las variables de JavaScript y el uso de clausuras para escribir código más limpio que no interfiera con otro código JavaScript que pudiera haber en la página. De hecho, es la técnica que utilizan muchas de las bibliotecas importantes para inicializarse.
Por ejemplo, consideremos el siguiente fragmento de código JavaScript:
var v1 = 0; function miFunc1(){ v1 = 5; alert(v1); } function miFunc2(){ v1 = 10; alert(v1); }
En este código tan sencillo estamos definiendo una variable y dos funciones. Dado que desde ambas funciones debemos poder acceder a la misma variable común v1, la declaramos de manera global, siendo accesible desde toda la página. Este código, si bien funcionará correctamente, presenta varios problemas, entre los que cabe destacar los dos siguientes:
- La variable, siendo global, será accesible desde cualquier función de la página, no solo desde las dos que la necesitan y tampoco solo desde nuestras funciones. Otras funciones de otros scripts que incluyamos en la página también tendrán acceso.
- Por mucho cuidado que tengamos, si los nombres de la variable o de las funciones coinciden con los de otras variables o funciones globales definidas en otro script, tendremos un conflicto y se producirán errores. Estamos polucionando innecesariamente el objeto global.
Lo que buscamos es la manera de poder definir funciones que compartan información entre sí, la almacenen para ejecución diferida y que no polucionen el código global de la página.
Obviamente podríamos definir una clase, la cual podríamos instanciar luego para ser utilizada. La clase nos permitiría definir miembros privados así como propiedades y métodos específicos de la misma. Posteriormente la instanciamos y hacemos uso de ella.
Eso puede estar bien en ocasiones, pero si el código es de un solo uso (como por ejemplo una inicialización) o son funciones que queremos usar de modo estático (sin instanciar nada explícitamente) estamos matando moscas a cañonazos. Y más, teniendo en cuenta que en JavaScript la orientación a objetos no es tan sencilla y directa como en otros lenguajes.
Así que vamos a hacer un pequeño cambio en el código anterior y lo envolveremos con una cierta estructura, como se muestra a continuación:
(function (){ var v1 = 0; function miFunc1(){ v1 = 5; alert(v1); }; function miFunc2(){ v1 = 10; alert(v1); }; })();
¿Qué hemos hecho aquí? Vamos a verlo con calma.
El código es casi idéntico al anterior, pero en este caso lo hemos rodeado con unas llaves y la palabra clave function.
Fíjate además que para evitar problemas para distinguir cada instrucción de código, a las funciones originales se les añade un punto y coma al final, después de la última llave. No es indispensable pero ayuda mucho con la legibilidad del código y evita posibles problemas de interpretación.
Con esta sintaxis estamos definiendo una función anónima (no estamos indicando nombre alguna para ésta). Es como la definición de una función cualquiera, solo que sin nombre:
function () {....}
Además, a esta función la rodeamos de paréntesis, con lo cual obtenemos una referencia a la misma (más sobre esto en breve), y posteriormente le colocamos dos paréntesis más, así:
(function (){....})();
¿Qué estamos haciendo? Los dos paréntesis finales se utilizan para llamar a la función anónima. En JavaScript una función se ejecuta cuando la llamamos usando los paréntesis. Sin paréntesis se obtiene una referencia a la misma. Por ejemplo:
var f1, f2; f1 = miFuncion; f2 = miFuncion();
Tras ejecutar este código, la variable f1 tendrá una referencia a la función miFuncion. La variable f2, sin embargo, almacenará el valor devuelto por la función miFuncion tras su ejecución. La diferencia está en los paréntesis. De hecho, podríamos llamar a la misma función escribiendo simplemente f1(); ya que f1 contiene una referencia a la función como he dicho.
Por lo tanto lo que hacemos con la estructura anterior es definir una función anónima y al mismo tiempo llamarla.
¡Con eso se crea, se ejecuta y desaparece!
De esta manera conseguimos que todo lo que hay dentro esté aislado del resto del código de la página. Mantiene su propia identidad pero no interfiere.
En un ejemplo real, aparte de realizar posibles tareas de inicialización dentro, crearíamos probablemente algunos objetos especializados que expondríamos al exterior a través del objeto global, creando así clases de una sola instancia (Singleton), que además tendrían acceso a ciertos datos internos los cuales jamás interferirían con el resto del código tampoco. En un vídeo posterior de ejemplo del curso podrás ver esto en la práctica.
Singleton: Patrón de orientación a objetos en el que existe una y solo una instancia de un objeto en particular.
¿Por qué hay que rodearla con paréntesis? ¿Funcionaría sin eso?
Vamos a probarlo: quitémosle los paréntesis que rodean a la definición de función y vamos a ver que pasa… Esto es lo que muestra la consola de los principales navegadores:
Es decir, que todos generan un error de sintaxis, aunque el mensaje que devuelven es diferente y en general no resulta de mucha ayuda.
El problema estriba en que el intérprete del lenguaje no sabe cómo interpretar el código que tenemos escrito, ya que después de function siempre va el nombre de la función, salvo que ésta sea anónima y por lo tanto la estemos asignando a alguna variable o la evaluemos. Por eso la rodeamos de paréntesis, de modo que la estamos forzando a que se evalúe.
Pero esta no es la única forma de hacerlo, y de hecho verás por ahí código similar que en lugar de paréntesis utiliza cualquier otro operador que actúe sobre la función, ya que así se obliga a ejecutarla y obtener su resultado, por ejemplo:
!function (){....}();
O bien:
+function (){....}();
E incluso cosas mucho más peregrinas e innecesarias, como:
!+-~function (){....}();
Que también funciona, pero son una tontería desde mi punto de vista.
A mí personalmente me parece que la forma más apropiada es la primera que he ilustrado, con los paréntesis, porque es clara, directa y no fuerza a realizar ninguna operación adicional, pero conviene reconocer otras formas por si las vemos por ahí.
Paso de parámetros de inicialización
Otra utilidad de uso muy común es pasarle parámetros de inicialización a la función, de modo que podamos referenciarlos desde la función anónima, modificarlos y mantener sus valores originales si son tipos por valor. Este patrón es muy común en algunas bibliotecas y de hecho, jQuery lo ha utilizado durante muchos años para inicializarse:
(function (w){ function miFunc1(){ alert(w.document.title); }; miFunc1(); })(window);
De este modo, por ejemplo, estamos pasando el objeto global como parámetro inicial y lo usamos desde dentro con un alias w que perdurará cuando llamemos a las diferentes funciones internas si las exponemos hacia el exterior. Además, pasando el objeto window de esta manera y almacenándolo en “w” estamos también ganando rendimiento. El motivo es que al estar definido en una variable local, a las funciones que definamos dentro de la función anónima, el intérprete las encuentra antes y es más eficiente en su uso. De hecho, es muy habitual pasar como parámetro de inicialización otros objetos comunes como document, por ejemplo:
(function (w, d, undefined){ function miFunc1(){ alert(d.title); }; miFunc1(); })(window, document);
Fíjate en un detalle importante en este código anterior. ¿Para qué es el parámetro undefined de la función anónima que además no se usa?
En JavaScript nada nos impide escribir código como el siguiente:
undefined = 1;
aunque undefined sea una palabra clave. Lo que hacemos es cambiar la definición de undefined y esto obviamente afecta al resto del código que estamos utilizando. ¿Por qué iba a hacer alguien esto? No lo sé, pero lo cierto es que ocurre (muchas veces incluso por error al hacer comparaciones incorrectamente usando = (asignando) en lugar de == para comparar).
En las versiones modernas de los navegadores, que implementan ECMAScript 5 o posterior, la instrucción anterior no tiene efecto. Pero en navegadores antiguos como Internet Explorer 8 o anterior, sí que funciona y puede causar estragos (pruébalo con el modo de compatibilidad de las herramientas del desarrollador y verás que así es).
Por ello incluir un tercer parámetro de nombre undefined, el cual no se le pasa a la llamada de la función anónima (fíjate en que solo se le pasan 2), lo que hace en la práctica es definir una variable local/parámetro de la función que se llama undefined y nos aseguramos de que tendrá el valor de undefined siempre. Por eso mismo, en código antiguo sobre todo, lo verás utilizado así en muchas ocasiones y era considerado una buena práctica.
En resumen
En definitiva, con las funciones anónimas autoejecutadas conseguimos ejecutar código y definir funciones y propiedades aisladas del resto de los elementos de script de la página, evitando posibles interferencias. También nos permite, entre otras cosas, precargar valores para ejecución tardía, aislar variables y métodos para uso interno de nuestro código o definir bien las variables de contexto (this) sobre las que se va a ejecutar el mismo.
Además, es la base de algunos patrones de diseño en JavaScript, siendo el más común el patrón Módulo, aunque también de otros como los patrones Singleton o Factoría.
Prácticas propuestas para el módulo
Este módulo es probablemente el más árido del curso, sobre todo para los principiantes que nunca hayan visto nada relacionado con la orientación a objetos. Sin embargo, su correcta comprensión es esencial para poder trabajar a nivel profesional con JavaScript (y en realidad con cualquier otro lenguaje moderno, aunque no depende del modelo de prototipos que hemos visto). Repasa el material todas las veces que sean necesarias para asegurarte de que has comprendido bien todos los conceptos y que has replicado en tu equipo desde cero los ejemplos que se desarrollan en el módulo, aunque parezcan sencillos.
Además, para reforzar y practicar te propongo los siguientes ejercicios:
Practica la creación de objetos anónimos básicos con la sintaxis JSON que hemos visto. JSON es un formato ligero y fácil de crear y leer que se usa constantemente en aplicaciones web que impliquen intercambio de información con el servidor, en especial en aplicaciones AJAX y SPA:
- Puedes aprender el formato (es bastante sencillo) en el sitio web oficial del mismo.
- Te propongo que crees primero un objeto sencillo (los datos básicos de una persona:
nombre,apellidos,fechaNacimiento,estatura, por ejemplo). - Muestra por pantalla la fecha de nacimiento de uno de los objetos JSON que hayas creado de esta manera.
- Añádele datos más avanzados que impliquen otros objetos, a su vez definidos como JSON. Por ejemplo, a tus objetos iniciales con datos básicos añádeles un campo
direccionque sea a su vez un objeto JSON que especifique los miembros de la dirección por separado:calle,cp,ciudadypais. - ¿Cómo accederías a mostrar por pantalla el nombre de la ciudad en la que vive una de las personas representadas por tu objeto JSON?
- Más tarde, en el módulo dedicado a AJAX, necesitarás trabajar más con este formato.
Define el constructor de un objeto de tipo Animal. Este tipo va a tener una serie de propiedades y métodos como: nombre (cadena), numPatas (numérico), tieneCola (booleano), tieneAlas (booleano), color (cadena), fechaNacimiento (tipo fecha), sonido (cadena de texto), posicion (numérico, valor por defecto 0) y los métodos andar (que sumará 1 a su posición), correr (que sumará 3) y hablar (que mostrará en un alert el sonido que hace el animal). Crea este constructor y crea a partir de él (con new) un par de animales diferentes y llama a alguno de sus métodos y muestra por pantalla alguna de sus propiedades.
- Una vez tengas la clase básica, ahora asegúrate de que los parámetros que se te pasan son del tipo adecuado. Para ello crea sendas propiedades (haz algunas usando métodos del prototipo, y otras usando el método
defineProperty, así practicarás ambos) y asegúrate de que no se puede acceder directamente al valor de la variable privada/local subyacente que almacena el dato. En el caso del número de patas, si tiene cola o no y si tiene alas o no, haz que sean propiedades de solo lectura, es decir, que no se pueda cambiar su valor una vez que se ha creado la clase (solo se pueden establecer esos valores en el constructor). - Haz que el constructor admita de manera opcional la fecha actual, de modo que si no se especifica, que se le ponga la fecha actual.
Ahora crea un par de clases nuevas para animales, pero más especializadas: un Gato y una Tortuga. Ambas deben heredar de la clase Animal. Asegúrate de que tienen la cadena de prototipos y los constructores adecuadamente definidos.
- En la clase
Gatoañade una propiedad nueva, solo disponible en este animal, que esesPeludoque será de tipo booleano (si lo es o no lo es). - Solo para la clase
Gatoredefine el métodocorrerpara que en lugar de sumar 3 a la posición, sume 5 (¡los gatos son muy rápidos!). - En la clase
Tortugaredefine el métodocorrery que le sume solamente 1 (¡las tortugas son muy lentas, correr y andar es lo mismo para ellas!). - Prueba que los métodos
correrde cada clase funcionan correctamente.
Ahora crea varios animales de tipo genérico y de tipo especializado como Gato o Tortuga, y mételos en una matriz junto con otros objetos que no sean de tipo Animal ni derivados de éste (por ejemplo, fechas, números, cadenas…). Procesa la matriz dentro de un bucle y, asegurándote de que solo utilizas los que sean animales (genéricos o especializados), haz llamadas dentro del bucle para cada animal a su método correr (mostrando por pantalla su posición después para ver cuánto han corrido). ¿Se llama correctamente al método correr en cada caso o se llama al método por defecto de la clase Animal y no el especializado a pesar de ser algunos de ellos objetos derivados de Animal?.
Crea una función esPar y añádela al tipo numérico de modo que, cuando quieras comprobar si un número es par o no, solo tengas que escribir algo como var n = 1234; n.esPar(); para obtener el resultado. Estás extendiendo la clase de los números para ello (pista: usa prototype).
Repasa las funciones que vimos en el módulo de matrices y que puedes descargar que extienden a las matrices con funcionalidad extra en navegadores antiguos. Ahora podrás entender todo ese código mucho mejor.
¿Qué pasa si intentas redefinir la función toString de una clase, pero quieres comprobar antes si ya está definida en el prototipo, para evitar que se defina más de una vez?. Es decir, ¿qué ocurre si usas if (!MiClase.prototype.toString) {…}?. ¿Cuál sería la solución para poder comprobar si existe una definición propia de toString y no la que tiene por defecto Object? ¿Cómo podrías asegurarte de que no hay otra, aparte de la de Object, ya definida?
Crea una versión adicional de la función ResumeObjeto que creamos en el apartado de “Reflexión” para que tome un parámetro opcional booleano (que puede estar o no) que, en caso de valer true recorra todo el árbol de prototipos hasta llegar a Object (que ya no tiene prototipo).
Pista: La mejor manera de realizar este ejercicio es usando lo que se denomina “recursión”. Este concepto es muy importante en programación ya que permite crear funciones que, llamándose a sí mismas, pueden resolver un problema en un número determinado de llamadas anidadas. Lo que tendrás que hacer es usar una variable compartida entre todas las llamadas (¿te valdrá un closure?) que almacene el resultado, e ir repitiendo el mismo proceso con todos los prototipos hasta que no quede ninguno por procesar.
¿Te devuelve alguna función más de una vez? ¿Por qué? ¿Cómo puedes evitarlo?
¿Cuál es la diferencia entre call y bind aplicados a una función?
La principal diferencia entre bind y call es que en este último caso se llama y se ejecuta la función, pero con bind no ocurre esto. Lo que se obtiene es una referencia a la función lista para ser usada/llamada cuando quieras, lo cual es súper potente.
Un ejemplo sencillo:
function sumar(a, b){ return a+b; } var sumar2 = sumar.bind(this, 2); var cinco = sumar2(3);
En este fragmento definimos una función simple para sumar dos números. Ahora, en una variable llamada sumar2 almacenamos el resultado de llamar a bind sobre esta función. Le pasamos el contexto (que en este caso nos da igual pues no se usa, así que le pasamos el actual con this, pero le podríamos pasar cualquier cosa, incluso null) y un argumento, el primero de los que toma la función, al cual le asignamos el valor 2.
Ahora tenemos una función nueva que se basa en la anterior y tiene prefijados el contexto y el valor del primer argumento. En este caso esta función lo que hará será tomar el parámetro que le falta y sumarle siempre 2, que es el parámetro que tenemos prefijado. Por eso en la variable cinco tendremos un 5 cuando acabe de ejecutarse la función.
Esto es muy potente porque nos permite almacenar funciones pre-cargadas con parámetros y contextos para ser llamadas luego. Se usa mucho para cuestiones avanzadas como gestión de eventos, definición de temporizadores (que no pueden tomar funciones con aragumentos), crear constructores de clases derivadas que tienen algunos parámetros ya fijados y no los podrá variar el usuario…














