En la anterior entrada, expliqué el funcionamiento interno del operador new
e introduje el concepto de la cadena de prototipado.
Fue una entrada especialmente teórica, demasiado para mi gusto (personalmente, prefiero un buen ejemplo a una buena explicación). Pero era necesario explicar detalladamente los mecanismos que usaba el operador new
para construir un nuevo objeto.
Así, vimos que si creamos el objeto Calvin
usando la función constructora comicCharacter
:
1 2 3 4 5 6 7 |
var comicCharacter = function(name) { this.name = name; this.hello = function() { console.debug('hello, I am ' + this.name); }; } comicCharacter.prototype.shout = function() { console.debug('grrr, I am ' + this.name); }; var calvin = new comicCharacter('Calvin'); |
El diagrama que representa su cadena de prototipado es:

Y también vimos cómo el objeto creado adquiría todos los métodos que estaban definidos no sólo dentro de él, sino también en su cadena de prototipado:

Esta cadena de prototipado, nos permite crear funciones constructoras sofisticadas. Pero personalmente opino que si las llamamos «clases» nos estamos haciendo un flaco favor. El motivo es que nuestra mente intenta encajarlas con los mecanismos de otros lenguajes de POO. Y no es lo mismo. Javascript tiene sus propios mecanismos. Nos gustarán más o menos. Pero son los que son. Y nuestro objetivo es conocerlos lo mejor posible para dominarlos y obtener lo mejor de ellos. Y también para evitar errores por falta de conocimientos.
Para insistir en la peculiaridad de estos mecanismos, en esta entrada vamos a ver unos cuantos trozos de código que fuerzan la cadena de prototipado. De este modo, asentaremos las explicaciones teóricas.
Cómo crear propiedades «privadas»
Veamos un primer ejemplo:
1 2 3 4 5 6 7 8 9 10 |
var counter = function() { var count = 0; this.getCount = function() { count++; return count; }; }; var counter1 = new counter(); var counter2 = new counter(); console.debug(counter1.getCount()); // 1 console.debug(counter1.getCount()); // 2 console.debug(counter2.getCount()); // 1 console.debug(counter1.count); // undefined |
¿Qué es lo que hemos hecho?:
- Hemos creado una función constructor
counter
. Dentro de esta función, hemos definido una variable localcount
y hemos creado una propiedadgetCount
que consulta y modifica a la variable. - Hemos creado dos objetos (
counter1
ycounter2
) con este constructor. Cada uno de estos objetos, cuando invoque a su métodogetCount
, cada uno de los métodos invocará a su propia versión de la variable localcount
(¿recordáis nuestra entrada sobre las funciones closure?). Por eso, cada invocación degetCount
sobre cada objeto actualiza su propia versión de la variable localcount
. - No olvidemos que la variable
count
es local a la función constructoracounter
. Esto quiere decir que si intentamos acceder a ella a través del objeto (intentando usarcounter1.count
) no es posible, porque su visibilidad queda limitada dentro de la funcióncounter
y las funciones que hayamos definida dentro de ella.
Si queréis, podríamos decir que hemos definido una propiedad con acceso privado (pero, insisto, no es lo mismo…).
Para insistir en las peculiaridades de Javscript, vamos a intentar hacer esto mismo, ligeramente diferente:
1 2 3 4 5 6 7 |
var counter = function() { var count = 0; }; counter.prototype.getCount = function() { count++; return count; }; var counter1 = new counter(); var counter2 = new counter(); console.debug(counter1.getCount()); // error: count is not defined |
“Jose, no entiendo nada. Hemos definido nuestro método getCount
en counter.prototype
de modo que counter1.getCounter()
debería funcionar correctamente… Pero está dando un error ¿Por qué?”
Para comprender el error que estamos cometiendo, debemos insistir en el alcance de la variable local count
: está definida dentro de la función counter
y sólo es accesible para las funciones definidas dentro de ella. Y nuestra nueva versión de la función getCount
está definida fuera de counter
. Por eso, no tiene acceso a la variable count
y da un error cuando intenta acceder a ella.
Objetos mutantes
Otro ejemplo de las manipulaciones que podemos hacer a la cadena de prototipado:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var comicCharacter = function(name) { this.name = name; } comicCharacter.prototype.hello = function() { console.debug('hello, I am ' + this.name); }; var calvin = new comicCharacter('Calvin'); var hobbes = new comicCharacter('Hobbes'); calvin.hello(); // hello, I am Calvin hobbes.hello(); // hello, I am Hobbes comicCharacter.prototype.hello = function() { console.debug('grrrr, I am ' + this.name); }; calvin.hello(); // grrrr, I am Calvin hobbes.hello(); // grrrr, I am Hobbes |
¿Qué hemos hecho ahora?:
- Hemos definido nuestra función constructor
comicCharacter
. Esta vez, sólo tiene un métodohello
que hemos definido en su propiedadprototype
. - Los dos objetos que hemos creado
calvin
yhobbes
tienen acceso al métodohello
a través de la cadena de prototipado y lo ejecutan correctamente («hello, I am Calvin» y «hello, I am Hobbes»). - Una vez que hemos comprobado que la ejecución es correcta, hemos redefinido
comicCharacter.prototype.hello
¿Y por qué lo hemos hecho? Porque se puede hacer. - La próxima vez que invoco a
calvin.hello()
ohobbes.hello()
, usarán la nueva definición dehello
(«grrr, I am Calvin» y «grrr, I am Hobbes»).
«¿Qué quieres demostrar, Jose?»
Que Javascript es un lenguaje absolutamente dinámico y que cada vez que se ejecuta el método hello
se busca en la cadena de prototipado. Y que cualquier elemento de esta cadena se puede variar en cualquier momento, afectando a los resultados posteriores.
«Y, esto ¿para qué sirve?»
Eso, ya depende de ti. Mi objetivo es transmitirte lo mejor que pueda lo que yo sé acerca de la cadena de prototipado cuando definas tus constructores y crees objetos basados en ellos. Y evites errores por mala comprensión de los conceptos.
La excepción confirma la regla
Otro ejemplo:
1 2 3 4 5 6 7 8 9 10 11 |
var comicCharacter = function(name) { this.name = name; } comicCharacter.prototype.hello = function() { console.debug('hello, I am ' + this.name); }; var calvin = new comicCharacter('Calvin'); var hobbes = new comicCharacter('Hobbes'); hobbes.hello = function() { console.debug('grrrr, I am ' + this.name); }; calvin.hello(); // hello, I am Calvin hobbes.hello(); // grrrr, I am Hobbes |
Y, ahora ¿qué hemos hecho?:
- Hemos vuelto a definir nuestra función constructor
comicCharacter
con un métodohello
definido en su propiedadprototype
. - Después, hemos creado nuestras dos variables
calvin
yhobbes
usando este constructor. - Pero, ahora, he realizado una nueva variante: hemos definido el método
hobbes.hello
. - Cuando invocamos
calvin.hello()
el intérprete de Javascript busca la propiedadhello
encalvin
. Como no la encuentra, la busca encalvin.__proto__
. Y aquí sí que la encuentra y la ejecuta («hello, I am Calvin»). - Pero cuando invocamos
hobbes.hello()
, el intérprete encuentra la propiedadhobbes.hello
. Ya no busca nada más, sino que directamente ejecuta esta propiedad («grrr, I am Hobbes»).
Como espero que hayáis podido comprobar, tenemos una cadena de prototipado que en cualquier momento podemos modificar cualquiera de sus eslabones. Insisto: la forma de usar esta capacidad, ya depende de vosotros.
Constructores que se saltan la regla
En nuestra anterior entrada, cuando definí los pasos que daba el operador new
cuando construía un nuevo objeto, expliqué:
«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 ese momento, pasé sólo por encima de esta frase y me limité a explicar que el ejemplo práctico que estaba proponiendo seguía el caso más normal (cuando la función constructor no devuelve nada). Pero ¿qué ocurre cuando devuelve un objeto? Ahora, vamos a ver un ejemplo de esta casuística:
1 2 3 4 5 6 7 8 9 10 |
var comicCharacter = function(name) { return { name : name }; }; comicCharacter.prototype.hello = function() { console.debug('hello, I am ' + this.name); }; var calvin = new comicCharacter('Calvin'); console.debug(calvin.name); // Calvin calvin.hello(); // error calvin.hello is not a function console.debug(calvin.constructor == comicCharacter); // false |
¿Qué hemos hecho?:
- Hemos creado la función constructor
comicCharacter
y una funcióncomicCharacter.prototype.hello
en su prototipo. - Hemos creado un objeto
calvin
y hemos intentado usar en ese objeto el métodohello
… pero nos ha dado error.
¿Por qué nos ha dado error? Porque la función constructor, esta vez, ha retornado explícitamente un objeto. Este objeto no tiene nada que ver con el objeto que ha creado el operador new
. Y el objeto devuelto por new
, por tanto, no tiene como constructor a comicCharacter
ni tiene a hello
en su cadena de prototipado. Por eso:
- Si intentamos acceder al método
hello
… no está ni en el objeto devuelto pornew
ni en su cadena de prototipado. - Podemos comprobar explícitamente que este objeto
calvin
no tiene nada que ver con la función constructorcomicCharacter
consultando su propiedadcalvin.constructor
.
Quiero otro ninja
No quiero terminar esta lista de ejemplos sin rendir un homenaje al genial John Resig, citando un ejemplo suyo:
1 2 3 4 5 6 7 |
function Ninja(){} var ninja = new Ninja(); console.debug(ninja instanceof Ninja ? 'The object was instantiated properly.' : ''); // The object was instantiated properly console.debug(ninja.constructor == Ninja ? 'The ninja object was created by the Ninja function.' : '' ); // The ninja object was created by the Ninja function var ninjaB = new ninja.constructor(); console.debug(ninjaB instanceof Ninja ? 'Still a ninja object.' : ''); // Still a ninja object console.debug(ninjaB.constructor == Ninja ? 'The ninjaB object was created by the Ninja function.' : '' ); // The ninjaB object was created by the Ninja funcion |
¿Qué hemos hecho?:
- Hemos creado una función constructor
Ninja
. - Hemos creado un objeto
ninja
usando ese constructor. Comprobamos queninja.constructor
apunta, efectivamente, aNinja
. - Ahora viene lo interesante: para crear el segundo objeto
ninjaB
hemos usado la función apuntada porninja.constructor
(que acabamos de confirmar que apunta a la función constructorNinja
). Y comprobamos que este segundo objeto es un objetoNinja
totalmente válido.
Para crear este segundo objeto, podríamos haber usado directamente la función constructor Ninja
. Pero el ejemplo insiste en el hecho de que objeto.constructor
apunta a la función constructora usada (al menos, como hemos visto, en condiciones normales).
¿Conclusiones?
Como habéis podido comprobar, la operativa construcción de objetos en Javascript usando el operador new
y funciones constructoras es relativamente simple… pero hace falta comprenderla. Por eso, en la primera entrada me concentré en explicar los mecanismos que usa el operador new
mientras que esta segunda entrada por fin he podido concentrarme en ejemplos para comprobar los conceptos explicados.
Tengo previsto dedicar una tercera entrada a los operadores que tiene Javascript para poder explorar las propiedades de cualquier objeto: instanceof
, getPrototypeOf
, isPrototypeOf
. Esta tercera entrada, con estas dos entradas convenientemente digeridas, debe ser mucho más fácil de seguir.
¡Ojo! Los conceptos y la información que he ido exponiendo en estas entradas, son el resultado de mi propia experiencia personal. Y frecuentemente me ha ocurrido que creía dominar un concepto… hasta que me he encontrado una nueva fuente de información que me ha obligado a recapacitar. Cualquier feedback que me deis, será absolutamente bienvenido.
Si hemos comprendido estos conceptos, aparte de que podremos usar el operador new
con conocimiento de causa, estamos más cerca de poder dominar los mecanismos que podemos usar en Javascript para crear herencia de clases… pero eso será ya tema para otra entrada.