Orientación a Objetos en JavaScript: variables y métodos privados/públicos
Vamos a ver si retomamos el pulso del blog que últimamente no tengo apenas tiempo para escribir y esta la cosa de capa caida.
En las últimas semanas he escrito varios miles de lineas en JavaScript, un lenguaje por el que no daba ni un duro y que me ha sorprendido gratamente. Creo que ese desprecio por parte de los desarrolladores es una inmerecida lacra que tiene JavaScript desde el principio de su existencia, siendo considerado como un lenguaje para hacer chorradillas de no más de 5 lineas en las webs. Nada más lejos de la realidad: JavaScript es increíblemente potente pero para apreciar su potencia hay que aprender a jugar en su campo ya que es muy diferente de otros lenguajes de programación.
Voy a dedicar bastantes artículos en el blog a JavaScript. El primero de la serie está dedicado a la peculiar orientación a objetos que propone el lenguaje, muy diferente de la orientación clásica de lenguajes como C++ o Java.
En JavaScript no hay una palabra reservada Class con la que definir estáticamente un objeto —recordemos que es un lenguaje de script por lo que estático no es un atributo adecuado que podamos utilizar para definirlo— por el contrario tenemos varias formas de simular la orientación a ojetos (encapsulación, herencia, etc.), yo voy a emplear las clausuras y los prototipos para declarar una clase con métodos y variables privadas.
Resumen
- Hay una función base que toma el rol de constructor de clase a la vieja usanza de Java o C++. A esta función se le pueden
enchufar
métodos en tiempo de ejecución por medio de los prototipos. - Las variables privadas se declaran con la palabra reservada
vardentro de la función base, y sólo pueden ser accedidas por funciones privadas y métodos privilegiados. - Las funciones privadas se declaran dentro de la función constructora (o de manera alternativa a través de
var functionName = function () {...}) y sólo pueden ser llamadas por los métodos privilegiados (incluyendo la función constructora). - Los métodos privilegiados se declaran con la forma
this.methodName = function () {...}y pueden ser invocados desde el código externo alobjeto
. - La porpiedades públicas se declaran con la forma
this.variableNamey pueden ser leidas y escritas desde fuera delobjeto
. - Lo métodos públicos se definen con la forma
Classname.prototype.methodName = function(){...}y pueden ser llamados desde fuera delobjeto
. - Las propiedades de prototipo se definen con la forma
Classname.prototype.propertyName = someValue. - Las propiedades estáticas se definen con la forma
Classname.propertyName = someValue.
Ejemplo
En este ejemplo, una persona obtiene su nombre y raza al nacer y estos parámetros no pueden ser cambiados nunca. Cuando nace, empieza teniendo un año y se determina un valor de edad máxima para esta persona. El individuo tiene un peso que se modifica al comer (triplicando su peso) o haciendo ejercicio (reduciéndolo a la mitad). Cada vez que la persona come o hace ejercicio, crece un año. El objeto persona tiene una propiedad de acceso público ‘clothing‘ que cualquiera puede modificar, asi como un factor de suciedad que puede ser modificado manualmente (ensuciándose o limpiándose), pero que se incrementa cada vez que la persona come o hace ejercicio, y se reduce cada vez que se utiliza el método shower().
Código de ejemplo
-
-
function Person (n, race)
-
{
-
this.constructor.population++;
-
-
// PRIVATE VARIABLES AND FUNCTIONS
-
// ONLY PRIVELEGED METHODS MAY VIEW/EDIT/INVOKE
-
var alive = true;
-
var age = 1;
-
var maxAge = 70 + Math.round(Math.random() * 15) + Math.round(Math.random() * 15);
-
-
function makeOlder ()
-
{
-
return alive = (++age < = maxAge)
-
}
-
-
var myName = n ? n : "John Doe";
-
var weight = 1;
-
-
-
// PRIVILEGED METHODS
-
// MAY BE INVOKED PUBLICLY AND MAY ACCESS PRIVATE ITEMS
-
// MAY NOT BE CHANGED; MAY BE REPLACED WITH PUBLIC FLAVORS
-
this.toString = this.getName = function () { return myName }
-
-
this.eat = function ()
-
{
-
if (makeOlder())
-
{
-
this.dirtFactor++;
-
return weight *= 3;
-
}
-
else alert(myName + " can’t eat, he’s dead!");
-
}
-
-
this.exercise = function()
-
{
-
if (makeOlder())
-
{
-
this.dirtFactor++;
-
return weight /= 2;
-
}
-
else alert(myName + " can’t exercise, he’s dead!");
-
}
-
-
this.weigh = function () { return weight }
-
this.getRace = function () { return race }
-
this.getAge = function () { return age }
-
this.muchTimePasses = function () { age += 50; this.dirtFactor = 10; }
-
-
-
// PUBLIC PROPERTIES — ANYONE MAY READ/WRITE
-
this.clothing = "nothing/naked";
-
this.dirtFactor = 0;
-
}
-
-
-
// PUBLIC METHODS — ANYONE MAY READ/WRITE
-
Person.prototype.beCool = function(){ this.clothing = "khakis and black shirt" }
-
Person.prototype.shower = function(){ this.dirtFactor = 2 }
-
Person.prototype.showLegs = function(){ alert(this + " has " + this.legs + " legs") }
-
Person.prototype.amputate = function(){ this.legs– }
-
-
-
// PROTOTYOPE PROPERTIES — ANYONE MAY READ/WRITE (but may be overridden)
-
Person.prototype.legs = 2;
-
-
-
// STATIC PROPERTIES — ANYONE MAY READ/WRITE
-
Person.population = 0;
-
-
Aquí hay un pequeño programa que utiliza la clase persona:
-
-
function RunGavinsLife()
-
{
-
// Instanciamos el objeto perrsona
-
var gk = new Person("Gavin","caucasian");
-
var lk = new Person("Lisa","caucasian");
-
alert("There are now " + Person.population + " people");
-
-
// ambos comparten la misma variable ‘Person.prototype.legs’ cuando accedemos a su ‘this.legs’
-
gk.showLegs(); lk.showLegs();
-
-
// crea una variable publica, pero no sobreeescribe la variable privada ‘race’.
-
gk.race = "hispanic";
-
// devuelve ‘caucasian’ desde la variable privada ‘race’ especificada en la creacion.
-
alert(gk + "’s real race is " + gk.getRace());
-
// engorda 3… 9… y 27
-
gk.eat(); gk.eat(); gk.eat();
-
alert(gk + " weighs " + gk.weigh() + " pounds and has a dirt factor of " + gk.dirtFactor);
-
-
// adelgaza hasta 13.5
-
gk.exercise();
-
// la ropa se actualiza para estar a la moda
-
gk.beCool();
-
// la ropa es una variable publica que puede ser actualizada a cualquier valor guay
-
gk.clothing="Pimp Outfit";
-
gk.shower();
-
alert("Existing shower technology has gotten " + gk + " to a dirt factor of " + gk.dirtFactor);
-
-
// han pasado 50 años
-
gk.muchTimePasses();
-
// sobreescribe la ducha original para todo el mundo
-
Person.prototype.shower = function ()
-
{
-
this.dirtFactor = 0;
-
}
-
// Gavin tiene su propia idea de la moda
-
gk.beCool = function ()
-
{
-
this.clothing = "tinfoil";
-
};
-
-
gk.beCool(); gk.shower();
-
alert("Fashionable " + gk + " at "
-
+gk.getAge()+" years old is now wearing "
-
+gk.clothing+" with dirt factor "
-
+gk.dirtFactor);
-
-
-
// utiliza la propiedad de prototipo
-
gk.amputate();
-
// lisa continua teniendo su propiedad de prototipo
-
gk.showLegs(); lk.showLegs();
-
-
// han pasado 50 años, Gavin tiene mas de 100 años
-
gk.muchTimePasses();
-
// la muerte muestra una incapacidad para comer
-
gk.eat();
-
}
-
Notas
maxAgees una variable privada sin un método accesor privilegiado; como tal, no hay manera pública de hacer un get o un set sobre ella.racees una variable privada definida sólo como una rgumento del constuctor. Las variables pasadas al constructor están disponibles para el objeto como variables privadas.- El método
'tinfoil' beCool()fue aplicado sólamente al objetogk, no a la clase Persona entera. Otras personas creadas y que uticen el métodobeCool()pueden continuar usando el original ‘khakis and black shirt’ que Gavin deshechó en vida. - Observa la llamada implicita al método
gk.toString() cada vez que hay una concatenación de cadenas. Eso es lo que permite al códigoalert(gk + ' is so cool.')poner la palabra ‘Gavin’, y es equivalente a haceralert(gk.toString() + ' is so cool.'). Cada objeto de cada tipo en JavaScript tiene un método .toString(), pero lo puedes sobeescribir con uno propio. - No puedes asignar métodos públicos a una clase dentro del objeto constructor. Debes uilizar la propiedad
prototypede manera externa, como se ve en los métodosbeCool()yshower(). - Como he intentado mostrar en la propiedad
Person.prototype.legsy en la funciónamputate(), las propiedades de prototipo estan compartidads por todas las instancias del objeto. Preguntando porlk.legsnos devuelve ‘2′ despues de mirar la propiedad de prototipo. Sin embargo, si intentamos cambiar este valor utilizandogk.legs=1o (en el objeto Persona)this.legs=1acaba creando una nueva propiedad pública del objeto específico de dicha instancia. (Esta es la razón por la que llamando agk.amputate()sólo se le quia una pierna a Gavn, pero no a Lisa.) Para modificar una propiedad de prototipo, debes utilizarPerson.prototype.legs=1o algo parecido athis.constructor.prototype.legs=1. (Digo algo parecido a porque he descubierto quethis.constructorno esta disponible dentro de las funciones privadas de un objeto, sino que apunta a el objetowindowde su ambito.) - Si una función anonima se declara inline con
-
foo = function (p1,p2) { some code }
el constructor
new Function()no es equivalente, por ejemplo-
foo = new Function(‘p1′,‘p2′,‘code’);
La razón es que la última función se ejecuta en el ámbito global —en lugar de heredar el ámbito de su función constructora— previniendola de acceder a las variables privadas.
-
- Como habrás notado en los comentarios del código, el echo de asigna un valor a
gk.raceno sobreescribe la variable privadarace. Aunque si bien pede parecer una idea estúpida, puedes tenerr dos variables (una pública y otra privada) con el mismo nobre. Por ejemplo, el métodoyell()en la siguiente clase devuelve diferentes valores parafooythis.foo:-
function StupidClass()
-
{
-
var foo = "internal";
-
this.foo = "external";
-
this.yell=function(){ alert("Internal foo is "+foo+"\nExternal foo is "+this.foo) }
-
}
-
- Las funciones privadas y los métodos privilegiados, como las variables privadas y las propiedades públicas, son instanciadas con cada nuevo objeto creado. Asi que cada vez que se llama a un nuevo
Person(), nuevas copias demakeOlder(),toString(),getName(),eat(),exercise(),weigh(),getRace(),getAge(), ymuchTimePasses()son creadas. Para cada persona, cada vez.Si comparamos esto con los métodos públicos (sólo existe una copia de
beCool()yshower()sin importar cuantas personas hayamos creados) puedes ver que por razones de rendimiento y memoria es preferible degradar la protección de un objeto y utilizar métodos públicos.Hacer esto requiere que transformemos variables privadas en públicas (porque sin accesores privilegiados no hay manera de utilizarlas) así los método públicos podrán acceder a ellas… y también código externo podrá ver y destruir dichas variables. La optimización de utilizar únicamente propiedades y métodos públicos hace que tu código sea menos robusto.
Por ejemplo, en el código de arriba
ageymaxAgeson variables privadas;agesólo puede ser accedida externamente a trevés degetAge()(y no puede cambiar su valor) ymaxAgeno puede ser leida ni modificada externamente. Cambiandolas para que sean propiedades públicas permite que cualquier código haga cosas como-
gk.maxAge=1; gk.age=200;
cosa que no sólo no tiene sentido (no deberías poder manipular directamente la esperanza de vida de alguien), sino que modificando esas variables directamente la variable
aliveno se actualiza correctamente, dejando a la persona en un estado incongruente. -
Recursos
Más información sobre clausuras, funciones anónimas y prototipos en le libro JavaScript, The Definitive Guide
de David Flanagan editado por O’Reilly.
El código original de la clase Persona fue creado por Gavin Kistner.
2 Comentarios para “Orientación a Objetos en JavaScript: variables y métodos privados/públicos”
Muy interesante…
Yo aprendí a aplicar la OOP en JavaScript cuando empecé a mirar el código de OpenLayers.
No es exactamente igual a como lo haces, y sin embargo el comportamiento es similar.
Un saludo, nos leemos!
www.programame.net
20 de Marzo de 2007 a las 9:45 pm
Orientación a Objetos en JavaScript: variables y métodos privados/públicos…
En JavaScript no hay una palabra reservada Class con la que definir estáticamente un objeto —recordemos que es un lenguaje de script por lo que estático no es un atributo adecuado que podamos utilizar para definirlo— por el contrario tenemos vari…
















menéame
penyaskito
18 de Marzo de 2007 a las 10:37 pm