En la anterior entrada sobre el identificador «this», ya creamos nuestros primeros objetos con Javascript:
1 2 3 4 |
var jmj = {}; jmj.firstName = 'José Manuel'; jmj.sayHello = function() { console.debug('hello, my name is ' + this.firstName); }; jmj.sayHello(); // hello, my name is José Manuel |
O también podíamos haber escrito:
1 2 3 4 5 |
var jmj = { firstName : 'José Manuel', sayHello : function() { console.debug('hello, my name is ' + this.firstName); } }; jmj.sayHello(); // hello, my name is José Manuel |
Este método para crear objetos, podríamos llamarlo el método «literal».
También mencionamos una peculiaridad de Javascript: en cualquier momento, podemos añadir a cualquier objeto nuevas propiedades:
1 2 3 4 5 6 7 |
var jmj = { firstName : 'José Manuel', sayHello : function() { console.debug('hello, my name is ' + this.firstName + ' and I live in ' + this.city); } }; jmj.sayHello(); // hello, my name is José Manuel and I live in undefined jmj.city = 'Málaga'; jmj.sayHello(); // hello, my name is José Manuel and I live in Málaga |
Era inevitable crearlos sin dar muchas explicaciones y lo más simples posible, porque necesitábamos crear objetos para explicar los conceptos que tratamos en la entrada. Pero ya advertí que habíamos creado nuestros objetos de la forma más rudimentaria posible y que me hacía falta una entrada específica para explicar en profundidad la creación de objetos en Javascript. Pues, bien, esta entrada ya ha llegado. Me ha salido tan extensa, que he decidido dividirla en dos partes (o incluso es posible que tres). A ver si os gusta esta primera.
Definición de «clases» en Javascript
Fijaros que he titulado esta entrada «Creación de objetos y definición de clases». El motivo de este título es porque, tal como hemos visto podemos crear objetos sobre la marcha, en cualquier momento. Pero en cuanto nuestra aplicación comience a crecer, si no tenemos mucha disciplina con nuestro código, éste se puede volver inmanejable ¿Qué ocurre si necesitamos crear varios objetos con las mismas características? (lo que comunmente se conoce en otros lenguajes de POO como «clases»).
Bueno, Javascript no tiene clases… pero tiene algo por el estilo. Veamos qué podemos hacer en Javascript (y después lo explico):
1 2 3 4 5 6 7 8 9 10 11 |
var comicCharacter = function(name) { this.name = name; this.hello = function() { console.debug('hello, I am ' + this.name); }; } var calvin = new comicCharacter('Calvin'); var hobbes = new comicCharacter('Hobbes'); var dilbert = new comicCharacter('Dilbert'); calvin.hello(); // hello, I am Calvin hobbes.hello(); // hello, I am Hobbes dilbert.hello(); // hello, I am Dilbert |
¿Qué es lo que hemos hecho?:
- Primero, hemos definido una función. Pero, dentro de esta función, hemos creado varias propiedades del objeto
this
(por el momento, conformaros con esta explicación; paciencia y ya insitiré en este punto más adelante). - Segundo, hemos definido varias variables. Pero las hemos inicializado usando un nuevo operador: el operador
new
(pasándole como argumento la función que acababa de definir). - Por último, hemos comprobado que los tres objetos definidos tienen el método
hello
y cuando se invocan responden con su propio valor dename
.
No es como en los lenguajes de POO tradicionales, pero en el fondo podemos decir que hemos definido una clase comicCharacter
y hemos creado tres objetos calvin
, hobbes
y dilbert
de esta clase.
El operador new
Lo que hemos hecho, no ha sido definir una clase (en el sentido de los lenguajes de POO tradicionales). Por eso digo que hemos definido una «clase». Y conviene que comprendamos los mecanismos que operan internamente cada vez que usamos el operador new
. Vamos a estudiar la secuencia de acciones que ocurren cada vez que lo usamos, paso a paso:
- El operador
new
espera como argumento un objetofunction
(que hará el papel de «constructor» para nuestros objetos). - Primero, crea un nuevo objeto, absolutamente vacío.
- A este nuevo objeto, le inicializa su propiedad interna
__proto__
al valor de la propiedadprototype
del objetofunction
que le hemos pasado como parámetro. - Segundo, ejecuta la función que le hemos pasado como parámetro, dentro del contexto de este nuevo objeto ¿Recordáis la anterior entrada sobre el contexto de ejecución? Si la recordáis, entonces ahora comprendéis que las instrucciones que hemos incluido dentro de nuestra función
comicCharacter
lo que están haciendo es dotar al nuevo objeto recién creado de las propiedades que queremos que tenga. - Una vez ejecutada la función, si esta función devuelve un objeto, éste objeto será lo que devuelve el operador
new
. Si la función no devuelve explícitamente un objeto, el operadornew
devolverá el objeto que creó. En nuestro caso, como la funcióncomicCharacter
no devuelve nada, el operadornew
devolverá el objeto que creó (y al que la funcióncomicCharacter
le ha añadido dos propiedadesname
yhello
).
Como veis, el operador new
hace muchas operaciones, algunas de ellas fácilmente comprensibles. Pero otras… realmente todavía no nos cuadran. Parece que nos faltan piezas del puzzle. A ver si las podemos encontrar.
La propiedad function.prototype
Cualquier objeto function
que nos encontremos tiene una propiedad que se llama prototype
(hasta ahora no lo había mencionado en ninguna entrada porque hasta ahora no nos había hecho falta):
1 2 3 4 |
var comicCharacter = function() {}; console.debug(comicCharacter.prototype); // object console.debug(comicCharacter.prototype.constructor); // function console.debug(comicCharacter.prototype.constructor == comicCharacter); // true |
Como vemos, esta propiedad apunta a un objeto que, a su vez tiene una propiedad llamada constructor
que apunta… al mismo objeto function
del que es propiedad.
A ver si se comprende mejor con un pequeño diagrama:
Como cualquier otra propiedad de cualquier otro objeto, podemos ampliarla en cualquier momento:
1 2 |
var comicCharacter = function() {}; comicCharacter.prototype.programmer = 'jmj'; |
O incluso podemos sustituirla por otro valor:
1 2 3 |
var comicCharacter = function() {}; comicCharacter.prototype = {}; console.debug(comicCharacter.prototype.constructor == comicCharacter); // false |
Pero ¡cuidado! Si sustituimos su valor por defecto por otro perdemos la información que ya contenía (como veis, la propiedad constructor
ya no apunta a comicCharacter
como ocurría en el objeto original).
“Vale, Jose. Ya veo que cualquier objeto function
tiene una propiedad que se llama prototype
que a su vez tiene una propiedad que se llama constructor
que a su vez apunta al mismo objeto function
. Y eso ¿para qué me sirve?”
Tranquilo. Paciencia. Estamos repasando las piezas del puzzle antes de montarlo. De momento, me conformo con que hayas comprendido que existe esta propiedad.
La propiedad interna object.__proto__
Cualquier objeto Javascript que nos encontremos tiene una propiedad interna que se llama __proto__
. Hasta donde yo sé, no debería ser asequible desde el código. Pero algunos navegadores permiten acceder a ella.
Como he dicho antes, cuando el operador new
crea el nuevo objeto, hace que esta propiedad __proto__
de este nuevo objeto apunte a la propiedad prototype
de la función que se le pasó como parámetro.
El diagrama cuando creamos el objeto calvin
sería:
“Jose, se me está agotando la paciencia. Primero, te sacas de la manga una propiedad function.prototype
. Ahora, te vuelves a sacar una propiedad object.__proto__
que hasta ahora no habías mencionado. Pero no veo para qué sirven ninguna de ellas”
La cadena de prototipado
Tranquilo, porque ya tenemos todas las piezas. Vamos a montar el puzzle. Veamos un nuevo trozo de código (es el mismo que hemos estudiado antes, pero ligeramente modificado):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var comicCharacter = function(name) { this.name = name; this.hello = function() { console.debug('hello, I am ' + this.name); }; } comicCharacter.prototype.shout = function() { console.debug('grrrr, I am ' + this.name); }; var calvin = new comicCharacter('Calvin'); var hobbes = new comicCharacter('Hobbes'); var dilbert = new comicCharacter('Dilbert'); calvin.hello(); // hello, I am Calvin hobbes.hello(); // hello, I am Hobbes dilbert.hello(); // hello, I am Dilbert calvin.shout(); // grrrr, I am Calvin hobbes.shout(); // grrrr, I am Hobbes dilbert.shout(); // grrrr, I am Dilbert |
Vamos a estudiar lo que hemos hecho:
- Hemos definido, como antes, la función
comicCharacter
y las tres objetoscalvin
,hobbes
ydilbert
usando el operadornew
. - Hemos comprobado que el método
hello
que definimos dentro del constructor sigue funcionando sobre mis tres objetos. - Pero, esta vez, hemos aumentado al objeto
comicCharacter.prototype
y le hemos añadido un nuevo métodoshout
(ya os advertí antes que podíamos hacerlo ¿lo recordáis?). - Y, por fin, viene la magia: si invocamos el método
shout
sobre mis tres objetos… funcionan correctamente.
¿Qué es lo que ha ocurrido? Pues lo que ha ocurrido es que se ha activado la cadena de prototipado («prototype chain», por si no os gusta mi traducción). Y qué es eso de la «cadena de prototipado»?:
- Cuando invocamos una propiedad sobre un objeto (por ejemplo
calvin.hello()
ocalvin.shout()
), el intérprete de Javascript busca dicha propiedad en ese objeto. Y, si la encuentra, la ejecuta (como ocurre concalvin.hello()
). - Sin embargo, si la propiedad no existe en el objeto (como ocurre con
calvin.shout()
) contrariamente a nuestra primera intución no da error sino que decide darle una segunda oportunidad. - Para esta segunda oportunidad, consulta al objeto apuntado por la propiedad
calvin.__proto__
. Y comprueba si encuentra ahí o no a la propiedad invocada. - En nuestro caso,
calvin.__proto__
apunta al objetocomicCharacter.prototype
(esto es así, como he explicado antes, por cortesía de nuestro estimado operadornew
). Y da la casualidad de quecomicCharacter.prototype.shout
sí que existe. Por lo que la invoca (y gracias a eso obtenemos el resultado deseado). - En el caso de que no hubiera encontrado la propiedad invocada en esta segunda oportunidad, no olvidemos que el objeto donde la ha buscado también es un objeto. Esto quiere decir que también tiene su propiedad
__proto__
que apunta a un tercer objeto. Por lo que volvería a buscar la propiedad en el tercer objeto. - Esta búsqueda encadenada terminará cuando encuentre en la cadena un objeto que tenga la propiedad buscada o hasta que
__proto__
valganull
(lo que significa que hemos llegado al fin de la cadena). Esta búsqueda de la propiedad en la cadena de objetos es lo que he llamado «la cadena de prototipado».
En nuestro, caso, el recorrido de la cadena de prototipado que nos interesa llega exclusivamente hasta lo que he llamado «segunda oportunidad» (cuando comprueba calvin.__proto__
). Pero el recorrido recursivo será muy importante cuando diseñemos lo que en POO tradicional se llamaría «herencia de clases».
¿Conclusiones?
En esta entrada, hemos pasado de crear objetos manualmente (usando el método literal) a crear objetos usando el operador new
.
Si creamos objetos usando el operador new
podemos usar objetos function
que nos sirven como constructores para nuestros objetos y que nos dan más versatilidad a la hora de definirlos.
Una de las ventajas que nos da usar funciones constructores es que podemos definir parte del código en su propiedad prototype
. Este código, será accesible desde todos los objetos que construyamos gracias a la cadena de prototipado.
He intentado explicar de la forma más simple posible en qué consiste esa cadena de prototipado y cómo podemos usarla para la creación de «clases». Pero, sinceramente, es uno de los conceptos de Javascript que requiere más esfuerzo. Sobre todo, porque los que estamos acostumbrados a otros lenguajes más convencionales, no nos damos cuenta de las posibilidades de programación que tiene.
La próxima entrada, la dedicaré a insistir en esta cadena de prototipado, proponiendo unos cuantos ejemplos que he ido recopilando. Estos ejemplos, deben servir tanto para comprender sus sutilidades así como para ayudarnos a madurar la compresión de este concepto.
Y cuando por fin comprendamos la cadena de prototipado (simplemente aplicada a la creación de «clases») por fin podremos ampliar estos conocimientos para poder construir una jerarquía de «clases» que hereden entre sí… pero eso tendrá su propia entrada.