Too Cool for Internet Explorer

Orientación a Objetos en JavaScript: variables y métodos privados/públicos

Hora y Fecha: Marzo 18, 2007 @ 7:04 pm Autor: Moisés Maciá
Categorías:
435 Visitas

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 var dentro 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 al objeto.
  • La porpiedades públicas se declaran con la forma this.variableName y pueden ser leidas y escritas desde fuera del objeto.
  • Lo métodos públicos se definen con la forma Classname.prototype.methodName = function(){...} y pueden ser llamados desde fuera del objeto.
  • 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

  1.  
  2. function Person (n, race)
  3. {
  4.     this.constructor.population++;
  5.  
  6.     // PRIVATE VARIABLES AND FUNCTIONS
  7.     // ONLY PRIVELEGED METHODS MAY VIEW/EDIT/INVOKE
  8.     var alive = true;
  9.     var age = 1;
  10.     var maxAge = 70 + Math.round(Math.random() * 15) + Math.round(Math.random() * 15);
  11.    
  12.     function makeOlder ()
  13.     {
  14.         return alive = (++age < = maxAge)
  15.     }
  16.  
  17.     var myName = n ? n : "John Doe";
  18.     var weight = 1;
  19.  
  20.  
  21.     // PRIVILEGED METHODS
  22.     // MAY BE INVOKED PUBLICLY AND MAY ACCESS PRIVATE ITEMS
  23.     // MAY NOT BE CHANGED; MAY BE REPLACED WITH PUBLIC FLAVORS
  24.     this.toString = this.getName = function () { return myName }
  25.  
  26.     this.eat = function ()
  27.     {
  28.         if (makeOlder())
  29.         {
  30.             this.dirtFactor++;
  31.             return weight *= 3;
  32.         }
  33.         else alert(myName + " can’t eat, he’s dead!");
  34.     }
  35.  
  36.     this.exercise = function()
  37.     {
  38.         if (makeOlder())
  39.         {
  40.             this.dirtFactor++;
  41.             return weight /= 2;
  42.         }
  43.         else alert(myName + " can’t exercise, he’s dead!");
  44.     }
  45.  
  46.     this.weigh = function () { return weight }
  47.     this.getRace = function () { return race }
  48.     this.getAge = function () { return age }
  49.     this.muchTimePasses = function () { age += 50; this.dirtFactor = 10; }
  50.  
  51.  
  52.     // PUBLIC PROPERTIES — ANYONE MAY READ/WRITE
  53.     this.clothing = "nothing/naked";
  54.     this.dirtFactor = 0;
  55. }
  56.  
  57.  
  58. // PUBLIC METHODS — ANYONE MAY READ/WRITE
  59. Person.prototype.beCool = function(){ this.clothing = "khakis and black shirt" }
  60. Person.prototype.shower = function(){ this.dirtFactor = 2 }
  61. Person.prototype.showLegs = function(){ alert(this + " has " + this.legs + " legs") }
  62. Person.prototype.amputate = function(){ this.legs}
  63.  
  64.  
  65. // PROTOTYOPE PROPERTIES — ANYONE MAY READ/WRITE (but may be overridden)
  66. Person.prototype.legs = 2;
  67.  
  68.  
  69. // STATIC PROPERTIES — ANYONE MAY READ/WRITE
  70. Person.population = 0;
  71.  
  72.  

Aquí hay un pequeño programa que utiliza la clase persona:

  1.  
  2. function RunGavinsLife()
  3. {
  4.     // Instanciamos el objeto perrsona
  5.     var gk = new Person("Gavin","caucasian");
  6.     var lk = new Person("Lisa","caucasian");
  7.     alert("There are now " + Person.population + " people");
  8.  
  9.     // ambos comparten la misma variable ‘Person.prototype.legs’ cuando accedemos a su ‘this.legs’
  10.     gk.showLegs(); lk.showLegs();
  11.  
  12.     // crea una variable publica, pero no sobreeescribe la variable privada ‘race’.
  13.     gk.race = "hispanic";
  14.     // devuelve ‘caucasian’ desde la variable privada ‘race’ especificada en la creacion.
  15.     alert(gk + "’s real race is " + gk.getRace());
  16.     // engorda 3… 9… y 27
  17.     gk.eat(); gk.eat(); gk.eat();
  18.     alert(gk + " weighs " + gk.weigh() + " pounds and has a dirt factor of " + gk.dirtFactor);
  19.  
  20.     // adelgaza hasta 13.5
  21.     gk.exercise();
  22.     // la ropa se actualiza para estar a la moda
  23.     gk.beCool();
  24.     // la ropa es una variable publica que puede ser actualizada a cualquier valor guay
  25.     gk.clothing="Pimp Outfit";
  26.     gk.shower();
  27.     alert("Existing shower technology has gotten " + gk + " to a dirt factor of " + gk.dirtFactor);
  28.  
  29.     // han pasado 50 años
  30.     gk.muchTimePasses();
  31.     // sobreescribe la ducha original para todo el mundo
  32.     Person.prototype.shower = function ()
  33.     {
  34.         this.dirtFactor = 0;
  35.     }
  36.     // Gavin tiene su propia idea de la moda
  37.     gk.beCool = function ()
  38.     {
  39.         this.clothing = "tinfoil";
  40.     };
  41.  
  42.     gk.beCool(); gk.shower();
  43.     alert("Fashionable " + gk + " at "
  44.         +gk.getAge()+" years old is now wearing "
  45.         +gk.clothing+" with dirt factor "
  46.         +gk.dirtFactor);
  47.  
  48.    
  49.     // utiliza la propiedad de prototipo
  50.     gk.amputate();
  51.     // lisa continua teniendo su propiedad de prototipo
  52.     gk.showLegs(); lk.showLegs();
  53.  
  54.     // han pasado 50 años, Gavin tiene mas de 100 años
  55.     gk.muchTimePasses();
  56.     // la muerte muestra una incapacidad para comer
  57.     gk.eat();
  58. }
  59.  

Notas

  • maxAge es 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.
  • race es 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 objeto gk, no a la clase Persona entera. Otras personas creadas y que uticen el método beCool() 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ódigo alert(gk + ' is so cool.') poner la palabra ‘Gavin’, y es equivalente a hacer alert(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 prototype de manera externa, como se ve en los métodos beCool() y shower().
  • Como he intentado mostrar en la propiedad Person.prototype.legs y en la función amputate(), las propiedades de prototipo estan compartidads por todas las instancias del objeto. Preguntando por lk.legs nos devuelve ‘2′ despues de mirar la propiedad de prototipo. Sin embargo, si intentamos cambiar este valor utilizando gk.legs=1 o (en el objeto Persona) this.legs=1 acaba creando una nueva propiedad pública del objeto específico de dicha instancia. (Esta es la razón por la que llamando a gk.amputate() sólo se le quia una pierna a Gavn, pero no a Lisa.) Para modificar una propiedad de prototipo, debes utilizar Person.prototype.legs=1 o algo parecido a this.constructor.prototype.legs=1. (Digo algo parecido a porque he descubierto que this.constructor no esta disponible dentro de las funciones privadas de un objeto, sino que apunta a el objeto window de su ambito.)
  • Si una función anonima se declara inline con
    1. foo = function (p1,p2) { some code }

    el constructor new Function() no es equivalente, por ejemplo

    1. 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.race no sobreescribe la variable privada race. 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étodo yell() en la siguiente clase devuelve diferentes valores para foo y this.foo:
    1. function StupidClass()
    2. {
    3.     var foo = "internal";
    4.     this.foo = "external";
    5.     this.yell=function(){ alert("Internal foo is "+foo+"\nExternal foo is "+this.foo) }
    6. }
  • 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 de makeOlder(), toString(), getName(), eat(), exercise(), weigh(), getRace(), getAge(), y muchTimePasses() son creadas. Para cada persona, cada vez.

    Si comparamos esto con los métodos públicos (sólo existe una copia de beCool() y shower() 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 age y maxAge son variables privadas; age sólo puede ser accedida externamente a trevés de getAge() (y no puede cambiar su valor) y maxAge no puede ser leida ni modificada externamente. Cambiandolas para que sean propiedades públicas permite que cualquier código haga cosas como

    1. 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 alive no 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.





« Anterior post: No tienes ni puta idea | Próximo post: SVN Hook: Cómo gestionar tickets automáticamente en Trac »

2 Comentarios para “Orientación a Objetos en JavaScript: variables y métodos privados/públicos”

penyaskito
18 de Marzo de 2007 a las 10:37 pm    

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…

Deja un comentario si te atreves

Al publicar de manera voluntaria un comentario mediante la herramientas que brinda esta página web, el autor del comentario autoriza expresamente al dueño de la página a eliminar, mantener, reproducir, almacenar y disponer del contenido del comentario como se considere apropiado. Los comentarios publicados expresan las opniones individuales de los individuos que contribuyen con ellos. Se entiende que al publicar un comenario, el autor del mismo acepta de buen grado estas condiciones.



Escribe estas palabras corréctamente, estarás ayudando a traducir libros y a dejar limpio de spam este blog.



Bad Behavior has blocked 458 access attempts in the last 7 days.