En este artículo, se discutirá la creación de objetos en JavaScript utilizando la herencia de prototipos como una alternativa al operador new.
Un aspecto importante de JavaScript es que raramente hay una sola forma correcta de realizar cualquier tarea. JavaScript es un lenguaje libremente escrito, dinámico y expresivo, lo cual significa que usualmente hay muchas formas de ejecutar la misma tarea. No estoy diciendo que los métodos descritos aquí para crear objetos son la forma correcta de hacerlo o incluso la mejor manera, pero yo siento que están muy cerca de la verdadera naturaleza del lenguaje y te ayudaran a comprender que está pasando bajo las capas si eliges utilizar otros métodos.
Para ayudarte a comprender mejor estos conceptos, este artículo describe la creación del sistema de partículas básica con procesamiento de múltiples objetivos. Esta es una tarea bastante compleja para representar en una prueba del mundo real de los conceptos que demostraré, en lugar de un sencillo Hello World.
Conceptos básicos de la creación de un objeto
El objetivo de este artículo es la creación de objetos JavaScript. Muchos tutoriales te dirán que crees una función constructora, agregues métodos a la propiedad prototype de la función, y entonces uses el operador new de esta forma:
1 2 3 4 5 6 7 8 |
function Foo() { this.name = "foo"; } Foo.prototype.sayHello = function() { alert("hello from " + this.name); }; var myFoo = new Foo(); myFoo.sayHello(); |
El objeto recientemente creado ahora tiene todas las propiedades que fueron definidas en la función constructora del prototipo. Esto crea algo que luce mucho más a una clase basada en lenguaje. Para hacer nuevas “subclases” que heredan esa “clase”, deberías establecer la propiedad prototype de las “subclases” a una nueva instancia de la “clase” original. (estoy usando comillas debido a que las entidades no son clases o subclases reales).
1 2 3 4 5 6 7 8 9 10 |
function Bar() { } Bar.prototype = new Foo(); Bar.prototype.sayGoodbye = function() { alert("goodbye from " + this.name); } var myBar = new Bar(); myBar.sayHello(); myBar.sayGoodbye(); |
El problema es que debido a que esta estructura luce similar a las clases reales en otros lenguajes, la gente espera que se comporten exactamente como las clases reales se comportan en otros lenguajes. Pero mientras más trabajes con estos tipos de “clases” verás más que no se comportan de esa forma. Así que la gente se molesta con JavaScript, y comienza a pensar que es un mal lenguaje que no puede ser utilizado para algo serio. Otros intentarán arreglar estas estructuras parecidas a las clases, al construir frameworks muy complejos para obtener funciones constructoras y prototipos que luzcan y se comporten más como las clases.
Personalmente, veo esto como un esfuerzo equivocado. No es necesariamente incorrecto, pero la energía que será gastada probablemente produciría mejores resultados si fuera en otra dirección.
Alcanzando la herencia de prototipos
JavaScript no es un lenguaje basado en clases, pero es uno basado en prototipos. El reusó de código no es hecho al crear plantillas de clases que son utilizadas para instanciar objetos, es hecho al crear nuevos objetos directamente, y entonces crear otros objetos basados en estos ya existentes. El objeto existente es asignado como el prototipo del nuevo objeto y entonces se le puede agregar nuevo comportamiento al nuevo objeto. Es un sistema muy elegante y está muy bien implementado en lenguaje io, el cual te invito a revisar.
Antes de ir más lejos, quiero aclarar el término prototipo. Primero, está la propiedad prototype de una función constructora como se mostró en la última sección de ejemplo. Hay otra propiedad oculta que es el prototipo real de un objeto. Esto puede ser muy confuso. La propuesta de ECMAScript se refiere a esta propiedad oculta como [[Prototype]]. Esto es expuesto en algunos entornos de JavaScript como la propiedad __proto__, pero esta no es una parte estándar del lenguaje y no debería ser contada como tal. Cuando creas un nuevo objeto utilizando new con una función constructora, se establece el [[Prototype]] de ese nuevo objeto con una referencia al prototipo de la función constructora.
Además de esta confusión de nombramiento, en el lenguaje se tuvieron que tomar dos decisiones de diseño que agregan confusión desde entonces. Primero, debido a la preocupación de que algunos desarrolladores no están conformes con la herencia de prototipos, se introdujeron las funciones constructoras y el operador new. Segundo, no había forma nativa directa para crear un nuevo objeto con otro objeto como su [[Prototype]], excepto a través del nuevo operador new con una función constructora.
Afortunadamente, muchos navegadores soportan el método Object.create. Este método toma un objeto existente como parámetro. Retorna un nuevo objeto que tiene asignado al objeto existente como su [[Prototype]]. Aún más, este método es muy fácil de crear para aquellos entornos que no lo soportan.
1 2 3 4 5 6 7 |
if(typeof Object.create !== "function") { Object.create = function (o) { function F() {} F.prototype = o; return new F(); }; } |
Entonces, ¿cómo rescribirías el ejemplo anterior usando Object.create? Primero crea un objeto foo que tiene una propiedad name y una función sayHello.
1 2 3 4 5 6 7 |
var foo = { name: "foo", sayHello: function() { alert("hello from " + this.name); } }; foo.sayHello(); |
Entonces, se usa Object.create para hacer un objeto bar que tiene a foo como su prototipo, y se le agrega una función sayGoodBye:
1 2 3 4 5 6 |
var bar = Object.create(foo); bar.sayGoodbye = function() { alert("goodbye from " + this.name); } bar.sayHello(); bar.sayGoodbye(); |
También es muy común el crear y extender una función que simplifica el agregar métodos y propiedades al objeto nuevo. El siguiente método simplemente copia cualquier propiedad desde props a obj:
1 2 3 4 5 6 7 |
function extend(obj, props) { for(prop in props) { if(props.hasOwnProperty(prop)) { obj[prop] = props[prop]; } } } |
Esto te permite crear bar de esta forma:
1 2 3 4 5 6 |
var bar = Object.create(foo); extend(bar, { sayGoodbye: function() { alert("goodbye from " + this.name); } }); |
No hay gran cosa aquí, pero simplifica las cosas grandemente cuando se están agregando muchas más propiedades o métodos.
Muy bien, ahora que ya revisamos los fundamentos, podemos comenzar a ponerlos juntos en un escenario del mundo real.
Creando partículas usando Object.create
Las partículas utilizadas en el proyecto de ejemplo serán muy básicos: puntos negros que se mueven alrededor en un espacio bidimensional y rebotan en las paredes. Estos además soportan gravedad y fricción según sea necesario. Se definirá un objeto particular que tiene todas las propiedades y métodos necesarios, y se colocara en un objeto adc para evitar contaminar el namespace global. Para más información de namespaces, revisa el artículo de Wikipedia sobre JavaScript discreto.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
var adc = adc || {}; adc.particle = { x: 0, y: 0, vx: 0, vy: 0, gravity: 0.0, bounce: -0.9, friction: 1.0, bounds: null, color: "#000000", context: null, update: function() { this.vy += this.gravity; this.x += this.vx; this.y += this.vy; this.vx *= this.friction; this.vy *= this.friction; if(this.x < this.bounds.x1) { this.x = this.bounds.x1; this.vx *= this.bounce; } else if(this.x > this.bounds.x2) { this.x = this.bounds.x2; this.vx *= this.bounce; } if(this.y < this.bounds.y1) { this.y = this.bounds.y1; this.vy *= this.bounce; } else if(this.y > this.bounds.y2) { this.y = this.bounds.y2; this.vy *= this.bounce; } }, render: function() { if(this.context === null) { throw new Error("context needs to be set on particle"); } this.context.fillStyle = this.color; this.context.fillRect(this.x - 1.5, this.y - 1.5, 3, 3); } }; |
Después, se necesitará un sistema de partículas para realizar el seguimiento de todas las partículas y controlar su actualización y procesamiento.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
var adc = adc || {}; adc.particleSystem = { particles: [], addParticle: function(particle) { this.particles.push(particle); }, update: function() { var i, numParticles = this.particles.length; for(i = 0; i < numParticles; i += 1) { this.particles[i].update(); } }, render: function() { var i, numParticles = this.particles.length; for(i = 0; i < numParticles; i += 1) { this.particles[i].render(); } } }; |
Y finalmente, se necesitará un archivo principal que cree el sistema, cree y agregue todas las partículas, y configure el bucle de animación.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
(function() { if (typeof Object.create !== "function") { Object.create = function (o) { function F() {} F.prototype = o; return new F(); }; } var system, numParticles, canvas, context, bounds; function initSystem() { system = Object.create(adc.particleSystem); numParticles = 200; canvas = document.getElementById("canvas"); context = canvas.getContext("2d"); canvas.width = window.innerWidth; canvas.height = window.innerHeight; bounds = { x1: 0, y1: 0, x2: canvas.width, y2: canvas.height }; } function initParticles() { var i, particle; for(i = 0; i < numParticles; i += 1) { particle = Object.create(adc.particle); particle.bounds = bounds; particle.context = context; particle.x = Math.random() * bounds.x2; particle.y = Math.random() * bounds.y2; particle.vx = Math.random() * 10 - 5; particle.vy = Math.random() * 10 - 5; system.addParticle(particle); } } function animate() { context.clearRect(bounds.x1, bounds.y1, bounds.x2, bounds.y2); system.update(); system.render(); } initSystem(); initParticles(); setInterval(animate, 1000 / 60); }()); |
El código en este archivo es contenido en una expresión de función que se invoca inmediatamente, de nuevo para evitar la contaminación del namespace. (Para más información de expresiones de función invocadas inmediatamente, revisa el blog de Ben Alman donde habla del tema. Incluye el suplemento de Object.create para los navegadores que pudieran necesitarlo. Todo esto está reunido en el siguiente archivo HTML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<!DOCTYPE html> <html> <head> <title>Particles v1</title> <style type="text/css"> .html, body { margin: 0; padding: 0; } </style> </head> <body> <div> <canvas id="canvas"/> </div> <script type="text/javascript" src="v1/particle.js"></script> <script type="text/javascript" src="v1/particleSystem.js"></script> <script type="text/javascript" src="v1/main.js"></script> </body> </html> |
Las líneas importantes, para el propósito de este artículo, son aquellas que crean el sistema de partículas:
1 |
system = Object.create(adc.particleSystem); |
Y las que crean a las mismas partículas:
1 2 3 4 5 6 7 |
particle = Object.create(adc.particle); particle.bounds = bounds; particle.context = context; particle.x = Math.random() * bounds.x2; particle.y = Math.random() * bounds.y2; particle.vx = Math.random() * 10 - 5; particle.vy = Math.random() * 10 - 5; |
Todavía no se ha implementado ningún tipo de función extendida, pero aquí se puede ver donde sería útil -llamando a extender una sola vez, en lugar de hacerlo después de cada línea en que se asignan propiedades. En la siguiente interacción, se agregara eso y algo más.
Agregando extend e init
Para la segunda versión del sistema de partículas, en vez de tener el archivo principal, crear y extender cada partícula por sí misma, sería mejor tener partículas que sepan cómo crear, extender e inicializarse a sí mismas. Para soportar eso, se pueden usar dos funciones nuevas, extend e init, las cuales son agregadas a adc.particle:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
var adc = adc || {}; adc.particle = { x: 0, y: 0, vx: 0, vy: 0, gravity: 0.0, bounce: -0.9, friction: 1.0, bounds: null, color: "#000000", context: null, extend: function(props) { var prop, obj; obj = Object.create(this); for(prop in props) { if(props.hasOwnProperty(prop)) { obj[prop] = props[prop]; } } return obj; }, init: function() { this.x = Math.random() * this.bounds.x2; this.y = Math.random() * this.bounds.y2; this.vx = Math.random() * 10 - 5; this.vy = Math.random() * 10 - 5; }, // … // rest of methods are the same as version 1 }; |
El método extend se encarga de crear nuevos objetos, pasarle this como parámetro a Object.create. Así, hace una copia de sí mismo. Entonces toma las propiedades que hayan sido enviadas dentro de extend, las copia dentro del nuevo objeto que recién creó, y finalmente retorna el nuevo objeto.
Ahora, en lugar de llamar Object.create(adc.particle) y establecer y ajustar propiedad por propiedad, se puede llamar adc.particle.extend, pasándole un objeto con las propiedades que se le quieran establecer, y entonces llamar init en la partícula creada recientemente.
Cuando se agrega el método extend al sistema de partículas, el archivo principal llega a ser más simple. En initSystem, se llama a adc.particleSystem.extend() para crear el nuevo sistema. No se necesita agregar cualquier propiedad al sistema, así se llama a extend sin parámetros. No hay muchos cambios aquí:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function initSystem() { system = adc.particleSystem.extend(); numParticles = 200; canvas = document.getElementById("canvas"); context = canvas.getContext("2d"); canvas.width = window.innerWidth; canvas.height = window.innerHeight; bounds = { x1: 0, y1: 0, x2: canvas.width, y2: canvas.height }; } |
En el método initParticles, hay una mejora:
1 2 3 4 5 6 7 8 9 10 11 |
function initParticles() { var i, particle; for(i = 0; i < numParticles; i += 1) { particle = adc.particle.extend({ bounds: bounds, context: context }); particle.init(); system.addParticle(particle); } } |
Ahora se puede llamar a adc.particle.extend para crear cada partícula, pasándole un objeto que contiene los límites y el contexto, los cuales son copiados para cada partícula. Finalmente, se puede llamar a init en la nueva partícula, lo cual se encarga de establecer su posición y velocidad aleatoriamente. Esta versión trabaja exactamente igual que la última vez, pero la creación individual de partículas ha sido simplificado.
Agregando herencia
La tercera versión del sistema de partículas soporta herencia. Esto es clave para reusar código. Se tiene un tipo de objeto y se quiere hacer otro tipo de objeto que sea ligeramente diferente. No se quiere recrear completamente el primer objeto con un par de cambios.
La reutilización de código tiene dos beneficios importantes. Primero, hay menos código que escribir. Ciertamente no se querrá escribir el mismo código dos veces. Tampoco se quiere copiar y pegar código, ya que esto puede hacer que las cosas se salgan de sincronía, con una función implementada de una forma aquí y la misma función implementada un poco diferente por allá. El segundo beneficio es mejor rendimiento. Cuando se tiene el mismo código duplicado en la aplicación, toma más tiempo descargarla, consume más memoria, y puede causar que la ejecución del código sea lenta, particularmente en la inicialización de objetos (debido a que inicializa el mismo código una y otra vez).
Actualmente el sistema de partículas procesa en un canvas de HTML5. Ahora, se puede querer un tipo diferente de partículas que se procesen a sí mismas como un objeto DOM. Idealmente, casi todo el código de las partículas sería reutilizado, solo con un método de procesamiento diferente. Entonces, con gran confianza, se puede tomar particle.js y removerle el método render.
Después, hacer dos archivos nuevos, canvasParticle.js y comParticle.js. La versión de canvas será similar a lo que acabamos de hacer:
1 2 3 4 5 6 7 8 9 10 11 |
var adc = adc || {}; adc.canvasParticle = adc.particle.extend({ render: function() { if(this.context === null) { throw new Error("context needs to be set on particle"); } this.context.fillStyle = this.color; this.context.fillRect(this.x - 1.5, this.y - 1.5, 3, 3); } }); |
Este código es muy sencillo. Solo se llama a adc.particle.extend, pasándole un objeto que contenga el método render antiguo. Eso creará un nuevo objeto que tiene particle como su [[Prorotype]], y render como un nuevo método directamente en el objeto.
Después se tendrá que cambiar un poco a main.js para que permita los nuevos tipos de objetos. Crea un archivo mainCanvas.js para configurar las partículas basadas en canvas. Esto solo difería en una línea, donde utiliza el tipo adc.canvasParticle para inicializar las partículas, en lugar de solo adc.particle:
1 2 3 4 5 6 7 8 9 10 11 |
function initParticles() { var i, particle; for(i = 0; i < numParticles; i += 1) { particle = adc.canvasParticle.extend({ bounds: bounds, context: context }); particle.init(); system.addParticle(particle); } } |
El archivo particleSystem.js puede permanecer sin cambios, pero por supuesto el archivo HTML tendrá que reflejar los nuevos recursos que se han creado. Este ejemplo debería funcionar idénticamente a las primeras dos versiones.
Ahora estás listo para crear la versión DOM.
El archivo domParticle.js será casi tan sencillo como canvasParticle.js. Este asume que hay un elemento que puede posicionar, y lo hace utilizando propiedades de estilo:
1 2 3 4 5 6 7 8 9 |
adc.domParticle = adc.particle.extend({ render: function() { if(this.element === null) { throw new Error("element needs to be set on particle"); } this.element.style.left = this.x; this.element.style.top = this.y; } }); |
Pero en este ejemplo, el archivo HTML y main.js necesitarán cambios importantes. Además de referenciar diferentes archivos fuente, el HTML puede eliminar el elemento canvas y agregar un contenedor div en el cual poner todas las partículas:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<html> <head> <title>Particles v3</title> <style type="text/css"> .html, body { margin: 0; padding: 0; overflow: hidden; } </style> </head> <body> <div id="container"> </div> <script type="text/javascript" src="v3/particle.js"></script> <script type="text/javascript" src="v3/domParticle.js"></script> <script type="text/javascript" src="v3/particleSystem.js"></script> <script type="text/javascript" src="v3/mainDom.js"></script> </body> </html> |
El archivo main.js se convertirá en mainDom.js y obviamente necesitará cambiar un poco para crear domParticles y darles elementos individuales en vez de referencias al contexto del canvas:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
(function() { var system, numParticles, container, bounds; function createElement() { var el = document.createElement("div"); el.style.position = "absolute"; el.style.width = 3; el.style.height = 3; el.style.backgroundColor = "#000000"; container.appendChild(el); return el; } function initSystem() { system = adc.particleSystem.extend(); numParticles = 200; container = document.getElementById("container"); bounds = { x1: 0, y1: 0, x2: window.innerWidth, y2: window.innerHeight }; } function initParticles() { var i, particle; for(i = 0; i < numParticles; i += 1) { particle = adc.domParticle.extend({ bounds: bounds, element: createElement() }); particle.init(); system.addParticle(particle); } } function animate() { system.update(); system.render(); } initSystem(); initParticles(); setInterval(animate, 1000 / 60); }()); |
Esto hace uso de una nueva función, createElement, que solamente crea un div, lo estiliza, y lo agrega al contenedor div. Esto es lo que posicionara las partículas cuando se llame al método render.
Este último ejemplo debería ser casi idéntico al de todas las otras versiones. Por supuesto, hay muchas formas de optimización y mejoras que se pueden hacer para mejorar todos estos ejemplos. A propósito lo hice sencillo para ilustrar mejor el aspecto de la herencia.
A dónde ir desde aquí
Puede que aún prefieras funciones constructoras y el operador new. Personalmente encuentro este método de creación de objetos muy limpio y de acuerdo a la naturaleza básica de prototipos de JavaScript.
Te invito a explorar el código fuente proporcionado en los archivos de ejemplo de este tutorial y probar el sistema de partículas en un navegador que soporte HTML5.
A medida que tu necesidad de aplicaciones más complejas crezca, puedes agregar características dentro de esta configuración básica. Para más información, revisa los siguientes recursos:
Herencia de Prototipos en JavaScript, de Douglas Crockford
Herencia Clásica en JavaScript, de Douglas Crockford
Objetos JavaScript 005 JSJ, de WOODY2SHOES
La versión original de este artículo está publicada en Adobe Devnet bajo licencia Creative Commons, fue traducido y adaptado en nuestro blog por Jesús Macedo.
Muy interesante informacion, ahora, quisiera saber si ademas de propiedades y metodos publicos y privados, existiran alguno relativo al protected
me parece muy intyeresante pero no consigo montar los trozos de codigo;puedes enviamerme el codigo completo funcionando?
gracias.
No en realidad el tema de la herencia en JavaScript esta basado en prototipos y como tal no hay un protected
Hola Salvador!
Puedes descargar el ejemplo de
http://download.macromedia.com/pub/developer/html5/object_creation.zip
Ya solo pruebalo y si tienes alguna duda o falla avisame 😉
Saludos
@yacaFx