Ayer tuve el placer de participar en un hangout de #JsIO junto a dos bestias pardas como Erick Ruiz y Tomás Corral discutiendo si JavaScript es o no es un lenguaje orientado a objetos.
Así que aprovecho para escribir este post y hacer algunas reflexiones más al respecto sin las prisas ni la improvisación del “directo”.
¿Qué es la orientación a objetos?
Es complicado definir que significa exactamente “orientación a objetos”. Seguro que si le preguntas a varia gente distinta obtendrás diferentes respuestas. Con puntos coincidentes, pero también puntos divergentes. Esto se debe a que el concepto es lo suficientemente vago como para admitir varias interpretaciones y además de que la gran mayoría de lenguajes no son 100% orientados a objetos.
Si yo hubiese de definir orientación a objetos diría básicamente que se trata de modelar el problema, y por lo tanto de diseñar un programa, basándose en la interacción entre distintos objetos que tienen determinadas propiedades y con los cuales se pueden realizar determinadas acciones. Los objetos tienen tres propiedades fundamentales:
- Comportamiento: Se define como “lo que es posible hacer con un objeto”. En terminología más purista, el conjunto de mensajes a los que este objeto responde (en OOP purista se usa la terminología mensaje para referirse al paso de información entre objetos).
- Estado: Los objetos tienen un estado en todo momento, definido por el conjunto de las variables o campos que contienen. El estado es pues la información contenida en un objeto.
- Identidad: Cada objeto existe independientemente del resto. Pueden haber dos objetos iguales pero no tienen porque ser el mismo (de igual forma que dos mellizos pueden ser idénticos, pero no por ello dejan de ser dos personas).
No iría mucho más allá, porque para mi esta definición contiene la clave que diferencia de la POO de la programación procedural: Los datos (estado) y funciones relacionadas (comportamiento) están agrupados en una sola entidad (objeto), en lugar de estar dispersos en el código. Es decir, el objeto encapsula los datos y el comportamiento. Como desarrollador debemos pensar en modelar nuestro sistema como un conjunto de objetos, en lugar de como un conjunto de funciones (procedimientos) invocadas una tras otra y pasándose datos más o menos arbitrarios para solucionar el problema.
El resto son detalles: herencia, polimorfismo, visibilidad… son detalles. No son menores, ciertamente, pero desde un punto de visto teórico, son meros detalles.
Y fíjate: he sido capaz de definir POO sin usar la palabra clase una sola vez. Y es que las clases son un mecanismo para implementar la POO, pero nada más. Y por supuesto, no son el único.
JavaScript tiene objetos, y estos objetos tienen comportamiento, estado e identidad. Así que desde punto de vista… Es orientado a objetos.
Pero como entiendo que esta respuesta puede no colmar a todos, especialmente a aquellos que vengan de Java, C# o algún otro lenguaje orientado a objetos “tradicional” (debería decir basado en clases) vayamos a ver como en JavaScript también tenemos esos detalles que mencionaba antes disponibles… 😉
¿Herencia en JavaScript?
Vale… Antes que nada: Si has aprendido POO con Java, C#, C++ o algún otro lenguaje basado en clases recuerda el concepto fundamental: No hay clases en JavaScript. Por lo tanto, hazte un favor y deja de pensar en clases cuando desarrolles en JavaScript. Te ahorrarás muchos dolores de cabeza.
La herencia en JavaScript es herencia entre objetos.
-
En un lenguaje basado en clases, la herencia es una relación definida entre clases. Si una clase Rectángulo deriva de la clase Figura, todos los objetos Rectángulo heredarán todo el comportamiento y estado definidos en la clase Figura.
-
En JavaScript la herencia es una relación definida entre objetos. Si un objeto deriva de otro, heredará todo el comportamiento y estado definido en el objeto base.
El mecanismo de herencia que se usa en JavaScript se conoce como herencia por prototipo y para resumirlo diré básicamente que cada objeto tiene asociado un prototipo. Si el desarrollador invoca un método sobre un objeto y este método NO está definido en el propio objeto, el motor de JavaScript buscará el método en el prototipo de este objeto. Por supuesto el prototipo es un objeto y puede tener otro prototipo y así sucesivamente, creando lo que se conoce como cadena de prototipado. La pregunta es pues como crear y asignar el prototipo de un objeto. Hay varias maneras de hacerlo, pero veamos una rápida y sencilla:
- var figura = {
- draw: function() {
- console.log("figura::draw");
- }
- };
- var rect = Object.create(figura);
- rect.draw();
Si ejecutas este código, la salida por la consola es “figura::draw”. La clave es el método Object.create. Este método crea un objeto vacío cuyo prototipo es el objeto pasado por parámetro. Esta es una forma sencilla de tener herencia en JavaScript.
Igual te preguntas si el objeto rect puede sobreescribir la implementación de draw (si vienes de Java sabrás que una clase hija puede redefinir los métodos de su clase base; si vienes de C# sabrás eso mismo siempre que los métodos sean virtuales). La respuesta es claro que sí:
- var figura = {
- draw: function() {
- console.log("figura::draw");
- }
- };
- var rect = Object.create(figura);
- rect.draw = function() {
- console.log("rect::draw");
- };
- rect.draw();
Ahora la salida por pantalla es “rect::draw”. ¿Qué ha sucedido? Pues como hemos llamado al método draw del objeto rect y este ya lo tiene definido, JavaScript ya lo invoca y no lo busca por la cadena de prototipos.
¿Lo ves? Herencia sin clases. ¡Es posible! 😉
¿Polimorfismo en JavaScript?
Una de las preguntas que salieron en el hangout fue si había polimorfismo en JavaScript. Respondí diciendo que esta pregunta implica una forma de pensar “basada en clases” pero que sí que era posible simular el polimorfismo en JavaScript.
Pero es que una de las claves es que no es necesario el concepto de polimorfismo en JavaScript. Antes que nada aclaremos que es el polimorfismo: La posibilidad de enviar un mismo mensaje (es decir, de invocar a un método) a varios objetos de naturaleza homogénea (wikipedia). Me gusta especialmente esta definición porque define polimorfismo sin usar la palabra “clase”. Si habéis aprendido POO con un lenguaje basado en clases quizá tenéis una definición de polimorfismo redactada de forma ligeramente distinta, algo como: Que los objetos de una clase derivada pueden tratarse como objetos de la clase base, pero manteniendo su comportamiento distintivo.
Supón que estás en un lenguaje basado en clases, como C#, y tienes la clase Figura y su derivada la clase Rect. La clase Figura define un método virtual llamado Draw() que es redefinido en la clase Rect. El polimorfismo implica que puedo pasar un objeto de la clase Rect a un método que acepte como parámetro un objeto de la clase Figura. Y que si este método llama al método Draw del parámetro Figura que ha recibido… se ejecutará el método Draw de la clase Rect, porque aunque el parámetro es de tipo Figura, el objeto real es de tipo Rect.
¿Y en JavaScript? Veamos:
- var figura = {
- draw: function() {
- console.log("figura::draw");
- }
- };
- var rect = Object.create(figura);
- rect.draw = function() {
- console.log("rect::draw");
- };
- function Polimorfismo(figura) {
- figura.draw();
- }
- Polimorfismo(rect);
La función Polimorfismo llama al método draw() del objeto que recibe. Como lo paso el objeto rect lo que se ejecuta es el método draw del objeto rect. Por lo tanto… tenemos polimorfismo automático en JavaScript. De hecho JavaScript tiene una característica conocida como Duck Typing, que bueno… es dificil de definir, pero una manera de hacerlo es: “Si camina como un pato, grazna como un pato y luce como un pato… es un pato”. O dicho de otro modo: Para el método Polimorfismo lo único que importa del parámetro figura es que tenga un método draw(). Nada más. Por lo tanto cualquier objeto que tenga un método draw es válido para ser usado como parámetro del método Polimorfismo.
El Duck Typing se suele asociar a los lenguajes dinámicos… pero no es exclusivo de ellos. Java y C# (sin usar dynamic) no tienen Duck Typing, pero p. ej. C++ lo ofrece a través de los templates.
¿Clases en JavaScript?
A esta altura del post ya debes tener claro que no. JavaScript no tiene clases, solo objetos y relaciones entre objetos. Pero existe un método de crear objetos a través de una función constructora y la palabra clave new que se parece mucho a como se hace en un lenguaje basado en clases. Eso que a priori es buena idea, ha ayudado mucho a la confusión de JavaScript… ¿Uso new para crear objetos pero no hay clases? ¿Y eso?
Déjame mostrarte un trozo de código de como crear un objeto basado en función constructora y el uso de new:
- var Figura = function() {
- this.draw = function() {
- console.log("Figura::draw");
- };
- };
- var figura = new Figura();
- var rect = Object.create(figura);
- rect.draw();
Ahora la variable Figura no es nada más que una función. Pero un tipo especial de función que llamamos función constructora. Pero ¡ojo! lo que convierte Figura en una función constructora no es nada que defina la propia función. Es como se invoca. Es el hecho de usar new lo que convierte Figura en una función constructora. El uso de new implica varias cosillas, cuyo ámbito se escapa de este post, pero la norma básica es que es la forma para invocar funciones cuya funcionalidad es crear objetos. En este ejemplo pues tengo:
-
Figura: Función que sirve para construir objetos que tienen un método draw.
-
figura (con la f minúscula): Objeto creado a partir de Figura
-
rect: Objeto cuyo prototipo es el objeto figura.
Lo interesante de usar funciones constructoras es que todos los objetos creados a través de ellas comparten el mismo prototipo. Si tenemos:
- var figura = new Figura();
- var figura2 = new Figura();
Ambos objetos comparten el mismo prototipo. Si ahora quiero añadir un método, p.ej. clear() que esté disponible para todos los objetos creados a partir de la función constructora Figura puedo añadirlo al prototipo. Y como hago esto? Pues así:
- var Figura = function() {
- this.draw = function() {
- console.log("Figura::draw");
- };
- };
- var figura = new Figura();
- var figura2 = new Figura();
- Figura.prototype.clear = function() {
- console.log("Figura::clear");
- };
- var rect = Object.create(figura);
- figura.clear();
- figura2.clear();
- rect.clear();
Figura.prototype es el nombre del prototipo de todos los objetos creados a través de new Figura. Este codigo imprime tres veces “Figura::clear”. Fíjate que funciona incluso cuando el método clear ha sido añadido al prototipo después de crear figura y figura2. Y es interesante el caso del objeto rect. Que ocurre cuando hacemos rect.clear()?
-
JavaScript busca el método clear() en el objeto rect. No lo encuentra y
-
Busca el método en el prototipo de rect que es figura. No lo encuentra y
-
Busca el método en el prototipo de figura que es Figura.prototype. Lo encuentra y lo ejecuta.
Aquí tienes a la cadena de prototipado en acción.
Fíjate que seguimos teniendo tan solo objetos. Figura no es una clase. Figura es una función. Una función para crear objetos que comparten un prototipo. Nada más.
¿Variables privadas en JavaScript?
JavaScript no incorpora de serie ningún mecanismo de visibilidad para los miembros de un objeto. Todo es público por defecto. Pero por supuesto no hay nada que no podamos conseguir… 🙂
- var Figura = function (c, stroke) {
- // Variable privada
- var _color = c;
- // Variable pblica
- this.stroke = null;
- // Necesario para poder acceder
- // a this desde los mtodos privados
- var self = this;
- // Mtodo privado
- var _setup = function(s) {
- // Ah podemos acceder a mtodos pblicos
- // a travs de self. Y a los privados directamente
- self.stroke = s;
- };
- _setup(stroke);
- // Mtodos pblicos
- this.draw = function() {
- console.log("Figura::draw in color " + _color + " and stroke " + this.stroke );
- };
- this.getColor = function() {
- return _color;
- };
- };
- var f = new Figura("red", "thin");
- console.log(f._color); // undefined
- console.log(f.getColor()); // red
- console.log(f.stroke); // thin
- //f._setup("thick"); // Error: Object has no method _setup
- var f2 = new Figura("blue","thick");
- console.log(f2._color); // undefined
- console.log(f2.getColor()); // blue
- console.log(f2.stroke); // thick
- //f._setup("thick"); // Error: Object has no method _setup
- f.draw();
- f2.draw();
Este ejemplo muestra como definir métodos y variables privadas. ¡Hay otras técnicas y alternativas!
Básicamente la regla es:
-
Usar la técnica de función constructora
-
Los métodos/variables privados son funciones o variables declaradas dentro de la función constructora.
-
Los métodos/variables públicas se asignan a this.
-
Guardar el valor de this en una variable privada (usualmente se usa that o self).
-
Desde las funciones privadas debes usar self para acceder a las variables públicas.
-
Desde las funciones públicas puedes usar self o this para acceder a las variables públicas.
-
En ambos casos puedes acceder a las variables privadas directamente con su nombre.
¿Herencia múltiple en JavaScript?
Si vienes de C# o Java igual alzas una ceja ahora… ¿No era peligrosa la herencia múltiple? C# y Java no incorporan este concepto debido a su posible peligrosidad y porque muchas veces da más problemas de los que puede solucionar. El peligro de la herencia múltiple es conocido como el problema de la herencia en diamante. Todos los lenguajes que soportan herencia múltiple se deben enfrentar a este problema, así que para evitarlo algunos lenguajes como Java o C# han decidido prescindir de ella.
JavaScript no soporta por defecto herencia múltiple pero si que soporta un tipo especial de herencia múltiple llamada mixin. Con los mixins ya entramos en un terreno pantanoso si vienes de C# o Java puesto que estos dos lenguajes no soportan este concepto.
Resumiendo, un Mixin es una clase que está pensada para ser incorporada dentro de otra clase ofreciendo funcionalidad adicional. Es como si tuvieras 3 clases A, B y C y las “combinases” todas ellas en una clase D (que además añadiría su propia funcionalidad). Algunos lenguajes como Lisp o Python soportan Mixins nativamente. Otros como C++ no, pero pueden imitarlos fácilmente debido al soporte de herencia múltiple (del que el uso de mixins es un tipo específico). Usar mixins en Java o C# es realmente complicado (aunque en Java8 el uso de default methods en interfaces lo hace posible). Si estás interesado en Mixins y C# echa un vistazo a heredar de José F. Romaniello.
¿Y como usar Mixins en C#? Aquí tienes un ejemplo:
- var asCircle = function() {
- this.area = function() {
- return Math.PI * this.radius * this.radius;
- };
- return this;
- };
- var asButton = function() {
- this.click = function() {
- console.log("Button has been clicked");
- };
- return this;
- };
- var a = { radius: 10 };
- asCircle.call(a);
- asButton.call(a);
- console.log(a.area());
- console.log(a.click());
En este cas asCircle y asButton son los dos Mixins. El primero añade una funcionalidad area a todos los objetos que tengan una propiedad llamada radius. El segundo añade un método click.
Para aplicar los Mixins sobre un objeto usamos la función call. No entraré en detalles de call ahora porque excede el objetivo de este post. Pero la clave está en que después de aplicar los Mixins, el objeto a tiene los métodos area y click.
Y más o menos… ¿con esto podemos dar por terminado el post no? Espero, que esto os haya ayudado a entender un poco mejor la POO bajo JavaScript y que bueno… os hayáis convencido de que JavaScript no tendrá clases pero orientado a objetos es 🙂
Saludos!
PD: Os paso el enlace del video de youtube donde podeis ver el hangout: http://www.desarrolloweb.com/en-directo/particularidades-programacion-orientada-objetos-poo-devio-8452.html
Muy bueno. Me ha gustado mucho.
Lo que me gustaria ver en otro/s post serian ejemplos de patrones/modelos/diseños con javascrip; que es la mejor manera para aprender a «cambiar el chip» a la hora de ponerse a pensar.
Hola Eduard,
En primer lugar, enhorabuena por este articulo tan revelador que a más de uno nos sirve para aclarar y abrir los ojos frente a la POO en JavaScript.
Por otro lado hay un par de dudas que me surgen al leer el articulo:
1- En que casos o que ventajas tenemos al crear los objetos mediante «Create.object» vs «uso del new»? Tiene algo que ver con el patrón que utilicemos en nuestra aplicación? intuyo que la principal diferencia podría venir de que con Create.object creamos objetos «puros», quizás para representar el Modelo de la aplicación, y el uso de clases pega más con aplicaciones que utilicen MVVM. Pero me gustaría que me lo aclarases.
2- Nuevos métodos para objetos que comparten prototipos: Si tengo una clase, y varios objetos que son de ese tipo de clase, se me ocurre que si quiero añadir un método nuevo a todos, pues en la definición de la clase pongo el método de manera pública. Sin embargo aquí lo haces con «Object.prototype.[nombre_del_metodo]»… ¿Qué diferencia existe entre una forma u otra?¿Afecta esto a la cadena de prototipado? (entiendo que en tu ejemplo si afecta ya que encuentra el metodo al mirar en figura(con f minúscula)).
Sin más, espero me puedas aclarar estas pequeñas dudas.
Gracias y un saludo.
@Crowley
Muchas gracias! Tengo intención de ir publicando posts sobre patrones y «buenas prácticas» en JavaScript para intentar ayudar a hacer este «cambio de chip» que comentas! 😉
@luisxkimo
Intento responder a tus dos preguntas…
1.
Desde el punto de vista de JavaScript o del objeto resultante no hay diferencia alguna.
Ambos mecanismos permiten lo mismo: asignar el prototipo a un objeto.
Así si haces:
var f1 = new Foo();
El prototipo de f1 es Foo.prototype.
Mientras que si haces
var f2 = Object.create(f);
El prototipo de f2 es f.
Pero desde el punto de vista de JavaScript tanto f1 como f2 son dos objetos totalmente normales. 🙂
Recuerda: en JavaScript SOLO tenemos objetos. Y todo objeto tiene un prototipo (que es otro objeto). new y Object.create son dos herramientas para asignar un prototipo a un objeto.
2.
Cuando usas new Figura() para crear un objeto, el prototipo de este objeto NO es Figura (que es una función) si no Figura.prototype (que es un objeto). De ahí que añada el método a Figura.prototype, porque este objeto es el prototipo de TODOS los objetos creados con new Figura.
Si tuviese var figura = new Figura(); y añado el método a figura, entonces solo este objeto tendrá este método, pero NO el resto de objetos creados con new Figura();
Por supuesto, este objeto «figura» puede ser el prototipo de otros objetos (creados usando Object.create).
No sé si he solucionado tus dudas… 🙂
Muchas gracias a todos por comentar!