Contenido

jClass2k, define Class con javascript

21 feb

+ 33

jDeveloper (o Jota) me pasó hace unos días una implementación para conseguir Orientar a Clases nuestros scripts. Me ha encantado por su sencillez y versatilida, nos permite generar Clases con sus respectivos Objetos, extendiendo de Super Clases.

var Class = function(current,previous){
// Comprobamos si tenemos una Class que extender.
      previous = typeof previous == 'undefined' ? {} : previous.prototype;
  // Extendemos con las propiedades de la Class anterior
      for(p in previous){
	// Si no existe la propiedad la añadimos
            if(typeof current[p] == 'undefined') current[p] = previous[p];
	// Si es una función
            else if(typeof previous[p] == 'function'){
	      // añadimos this.parent() a la función de la Class actual.
                  current[p] = (function(tmp){
                                var _parent = function(){
                                    this.parent = _parent.parent;
                                    return tmp.apply(this, arguments);
                                    }
                                    return _parent;
                                })(current[p]);
	// Igualamos this.parent() al método de la Class anterior.
                current[p].parent = previous[p];
            }
        }
	// Construimos el contenedor
        var construct = function(){
          if(this.init) this.init.apply(this,arguments);
        }
	// Le aplicamos los métodos extendidos
        construct.prototype = current;
	// asignamos un constructor
        construct.constructor = Class;
	//Devolvemos el constructor.
        return construct;
    }

Si quitamos todos los comentarios, nos queda un script de 21 líneas que podemos integrar en cualquier código ya que no depende de ningún framework JS.

¿Como lo uso?

Con este script podemos generar Clases, pero ¿como?

var Humano = new Class({
	piernas: 2,
	brazos: 2,
	cabeza: 1,
	init: function(name){
		this.name = name;	
	},
	saludar: function(){
		return "Hola, soy " + this.name;
	}
});

Creamos la Clase Humano que especifica tiene 2 piernas, 2 brazos y una cabeza, además especificamos que el constructor nos solicita un nombre que se le asignará al “Humano“. Como método para testear que funciona he creado saludar() que nos devolverá un saludo cordial personalizado para cada “Humano“.

var Programador = new Class({
	experiencia: 0,
	init: function(name, lenguaje){
		this.parent(name);
		this.lenguaje = lenguaje;
	},
	saludar: function(){
		return this.parent() + " y programo en " + this.lenguaje;
	},
	programar: function(){
		this.experiencia++;
		return "Estoy programando";
	}
},Humano);

La clase Programador extiende de la clase Humano y además le añade experiencia como un valor único de los Programadores. Sobreescribimos los métodos init() y saludar() pero hacemos referencia a los del padre (Class Humano) mediante el uso de this.parent(). Además he añadido un método propio del Programador que obviamente es “programar():D

var Andres = new Programador("Andres", "Javascript");
Andres.saludar();
// --> "Hola, soy Andres y programo en Javascript"

De esta forma creamos un nuevo Objeto llamado Andres y que dispondrá de todos los métodos de Humano y Programador. Incluso podemos hacerlo currar mucho :D

var horas = 24; 
while(horas > 0) {
	console.log(Andres.programar());
	horas--;
}

Optimizaciones en los comentarios.

  • Bro una pregunta, no entiendo que significa en javascript hacer algo como esto:

    (function(tmp){... })(current[p]);

    meter una función entre paréntesis, y luego un objeto entre paréntesis, esta en este codigo y en el de JQuery, y no se interpretar eso.

  • @Ashrey: Es simplemente para que la función anónima se “autoejecute”.

  • @Ashrey: Exactamente es una función anónima que permite ejecutar el contenido de su interior sin necesidad de tener que invocarlo posteriormente.

    (function(){
        alert("Hola");
    })()
    

    Si pruebas a ejecutar este código, te mostrará un alert() con la palabra “Hola”.

    Es muy util para generar scripts o variables que se auto programan :D

  • @Ashrey

    En este script es necesaria crear la funcion anonima para que no se sobreescriban zonas de la memoria (punteros), al crear una funcion, los argumentos de la funcion pasan a tener distintas direcciones, asi en el bucle no se machacan las funciones la misma direccion, la explicación es horrible, je, pero espero que se entienda.

    La grafica de rendimiento creado por @anieto2k:
    Rendimiento jClass2k

    eres un maestro andres

  • Hay algo que no entiendo, sobre los objetos.
    Pongamos que creamos un objeto:

    var Currante = new Trabajador("Juan", "PHP")

    ¿Cómo se puede obtener el nombre del trabajador y el lenguaje para usarlos, por ejemplo, en un document.write?

  • @Quitz: Podrías acceder a Currante.name, aunque lo ideal sería que crearas un método .getName() para obtenerlo.

    
    Currante.getName();
    // --> Juan
    
  • @jdeveloper: Tu si que eres un maestro, fijate en los resultados!!!
    Casi obtienes los mismos resultados de la implementación de John Resig.

  • Gracias a todos. De verdad que no sabia para que se hacia eso. Lo que no entendí bien, es xq es necesaria la funcion anonima en este script, pero muy bien de todas formas. XD

  • ¡Qué magnífico trabajo! Creo que podemos afinarla un poco más. En la versión actual para cada método se crean dos lambdas:

    
    current[p] = (function(tmp){ //PRIMERA 
           var _parent = function(){ //SEGUNDA
                    this.parent = _parent.parent;
                    return tmp.apply(this, arguments);
              }
              return _parent;
    })(current[p]);
    

    Podemos hacer que la primera lambda la compartan todos los métodos de la siguiente manera:

    
    var Class = function(current,previous){
       function makePapaCaller(met) {
          return function() {
             this.parent = _parent.parent;
             return met.apply(this, arguments);
          }
       }
       previous = typeof previous == 'undefined' ? {} : previous.prototype;
       for(p in previous){
         if(typeof current[p] == 'undefined') {
    	    current[p] = previous[p];
    	 }
         else if(typeof previous[p] == 'function'){
            current[p] = makePapaCaller(current[p]);
    	    current[p].parent = previous[p];
         }
       }
       var construct = function(){
          if(this.init) this.init.apply(this,arguments);
       }
       construct.prototype = current;
       construct.constructor = Class;
       return construct;
    }
    

    Si n es el número de métodos nos ahorramos n-1 lambdas. Obviamente para n=0 salimos perdiendo, pero para casos como ese (una clase derivada sin métodos), el mecanismo no tendría sentido (¿que método va a llamar a su ‘papá’ si no hay métodos :) ?

    También podriamos sacar ese makePapaCaller() al ámbito público, pero … ¿a qué esa idea ya no nos gusta?

    Por cierto, no he podido someter este código a pruebas. Podría tener algún fallo. Ya nos contaréis.

  • Bug visto:

    Hay que arreglar makePapaCaller. Su código debe ser este:

    
       function makePapaCaller(met) {
          return function() {
             this.parent = arguments.callee.parent;
             return met.apply(this, arguments);
          }
       }
    
  • @joseanpg: El problema que le veo, y es el motivo que nos llevó a usar la segunda Lambda, es que arguments.callee; es una propiedad desaconsejada por los estándares.

    Eso sí, mejoran los resultados, compara la dos modificaciones:

    jClass2k joseanpg

    jClass2k josepg

    Versión inicial

    Versión inicial jClass2k

  • Bueno, despreciemos al amigo arguments.callee. Esto implica que nada nos libra de clausurar de alguna manera el método de la clase base. En el código original se clausura mediante una referencia intermedia:

    El Activation Object de la lambda interior apunta a la propia lambda interior (_parent), y esta tras ser construida apunta al método de la clase base. ¿Por qué dar ese rodeo? Que el Activation Object apunte directamente al método ‘papá’, así:

    
    var Class = function(current,previous){
       function makePapaCaller(met,papa) {
          return function() {
             this.parent = papa;
             return met.apply(this, arguments);
          }
       }
       previous = typeof previous == 'undefined' ? {} : previous.prototype;
       for(p in previous){
         if(typeof current[p] == 'undefined') {
    	    current[p] = previous[p];
    	 }
         else if(typeof previous[p] == 'function'){
            current[p] = makePapaCaller(current[p],previous[p]);
         }
       }
       var construct = function(){
          if(this.init) this.init.apply(this,arguments);
       }
       construct.prototype = current;
       construct.constructor = Class;
       return construct;
    }
    

    ¿Qué tal?

  • @joseanpg: Muy interesante. A ver que os parece, la he probado con el ejemplo de arriba y funciona perfectamente.

    
    var Class = function(current,previous){
       function makeParent(fn, parent) {
          return function() {
             this.parent = parent;
             return fn.apply(this, arguments);
          }
       }
       previous = typeof previous == 'undefined' ? {} : previous.prototype;
       for(p in previous)
    	 current[p] = (typeof previous[p] == 'function')?makeParent(current[p], previous[p]):previous[p];
       var construct = function(){ if(this.init) this.init.apply(this,arguments);}
       construct.prototype = current;
       construct.constructor = Class;
       return construct;
    }
    

    Resultados

    • Class2k1, jClass2k, versión inicial
    • Class2k2,Versión joseanpg

    Comparativa

  • Andrés, esto se pone muy interesante. Probemos a sacar la función auxiliar fuera:

    
    function makePapaCaller(met,papa) {
          return function() {
             this.parent = papa;
             return met.apply(this, arguments);
          }
     }
    
    
    var Class = function(current,previous){
       previous = typeof previous == 'undefined' ? {} : previous.prototype;
       for(p in previous){
         if(typeof current[p] == 'undefined') {
    	    current[p] = previous[p];
    	 }
         else if(typeof previous[p] == 'function'){
            current[p] = makePapaCaller(current[p],previous[p]);
         }
       }
       var construct = function(){
          if(this.init) this.init.apply(this,arguments);
       }
       construct.prototype = current;
       construct.constructor = Class;
       return construct;
    }
    
    

    Veamos que es lo que sale.

  • Da la impresión de que al engine no le gustan las funciones con nombre. Qué curiososo. Con estos experimentos vamos a aprender un montón sobre optimización para Firefox :)

  • @joseanpg: Si lo sacamos obtenemos peores resultados.
    Function fuera

  • Hola aNieto2k, jDeveloper y compañia.

    JotaClass esta muy bien, pero una cosa que hecho de menos es crear clases con una sintaxis más parecida a Java, PHP , etc..

    Tengo implementado desde hace un año una nueva forma de crear clases que no utiliza la sintaxis JSON, que permite entre otras cosas, clase abstractas, herencia simple, herencias multiple, super y algunos métodos más para trabajar con las clases, etc..

    Lo he testado con JSLimus y es más rapida a la hora de crear instancias y a la hora de llamar a los métodos lo resultado son parecidos.

    Bueno lo podeis ver aquí http://www.jsimpleclass.net/.

    Saludos.

    PD:No estoy seguro si lo enviado antes, si es asi mi intención no es hacer spam.

  • Hola Pedro, una duda: para que la cosa funcione, ¿los métodos tienen que estar siempre definidos en el interior del constructor? Si es así ya sabes que cada objeto que crees arrastrará una panoplia de funciones propias (los objetos compartirán el código de los métodos, pero no los métodos) con el consiguiente consumo de memoria.

  • Hola joseanpg

    Con el constructor me imagino que te refieres al método $$constructor, la respuesta es no , ya que $$constructor en realidad debería llamarse $$construct.

    Al testarlo con JSLimus en Firefox en tenido los siguientes resultados.

    Instantiate AdHoc class 796283
    Instantiate Resig class 241294
    Instantiate Base class 104874
    Instantiate Mootools class 115685
    Instantiate jSimpleClass class 43993

    Instantiate AdHoc subclass 744707
    Instantiate Resig subclass 253032
    Instantiate Base subclass 75563
    Instantiate Mootools subclass 70009
    Instantiate jSimpleClass subclass 6586

    Como puedes ver es más rápido incluso cuando instancia una subclase.

    La verdad la página la hice ya hace un año y en todo este tiempo no la he tocado, entre otras cosas porque no ha despertado ningún interés y hace un año que empece a trabajar como programador y prácticamente programo casi todo el tiempo en PHP y no he tenido tiempo de retomar el tema.

    Tengo intención de retomar el tema y actualizar la página en breve para añadirle alguna pequeña mejora.

  • @Pedro Andujar: ¿Pero JSLimus no muestra operaciones/segundo?

    En los comentarios anteriores yo cometí el mismo error. Y según interpreto las imágenes el método más rápido es el llamado AdHoc que se la versión nativa de generar clases.

    Por lo tanto en los resultados que muestras, veo que jSimpleClass es la que menos operaciones por segundo realiza.

    ¿No es así?

  • Lamento decirte que los resultados son operaciones por segundo, es decir, más valor implica más velocidad.

  • Mi ingles es muy pobre, pensaba que Base2 era el más rápido y los resultados están mas cerca de es Base2 que de AdHoc, eso quiere decir que Base2 es el que menos operaciones realiza por segundo, tengo que leer más detenidamente tu anterior articulo Rendimiento de las técnicas de POO en Javascript.

  • Hola de nuevo.
    ¿Creéis que compensa la menor velocidad para crear instancias el hecho de que implemente más soporte OOP?
    Ya me parecía raro que fuera más rápido al instanciar un subclase.. :)

  • Francamente, hace ya tiempo que no echo de menos la POO basada en clases en ECMAScript. Soy feliz con los prototipos y no cambiaría velocidad por clases.

    No obstante, investigar en este asunto es un gran ejercicio que nos ayuda a entender mejor la potencia del lenguaje.

  • Joseanpg, cuando dices “Soy feliz con los prototipos y no cambiaría velocidad por clases”, quieres decir que no programas con ningún framework, a mi me encanta Mootools, solo programaría con prototipos si con Mootools la ejecución del script fuera muy lenta.
    He visto tu script Trekkie , muy guapo, ahí si considero que los prototipos son una ventaja, pero en mi opinión, por lo general para la aplicaciones web no requieren optimizar tanto los script como para no utilizar un framework.

  • Cierto, en las aplicaciones web normales no es necesario optimizar tanto. Normalmente utilizo mis propias funciones e intento no crear ni una clausura de más y reutilizar los objetos al máximo para no darle demasiado trabajo al recolector de basura, vicios que ayudan cuando las aplicaciones corren en ambientes tecnologicamente hostiles.

    Cuando necesito un framework utilizo MooTools, por supuesto, el framework más elegante que hay :) , pero no soy muy dado a utilizar su soporte de clases.

  • Por supuesto Mootols es más elegante sin ninguna duda :)

    Voy a estudiar detenidamente jClass2k, como he dicho ante es un tema que tengo un poco olvidado y me haría ilusión poder aportar alguna mejora si es posible.
    joseanpg y aNieto2k , gracias por sacarme de mi error, tengo en mente una idea conseguir una mayor velocidad al incrementar la velocidad al instanciar una subclase, lo más seguro es que no lo consiga pero por intentarlo que no quede.
    Me gustaría conocer vuestra opinión si lo consigo, soy novato programando y es un placer poder hablar de esto temas con gente con un nivel tan alto.

  • @Pedro Andujar: Ese es el espíritu :D

    Mejorar, mejorar y mejorar :D

  • hmm, genial, aun asi no consigo hacer cierta cosa, a ver si se os ocurre, declaro la clase y tengo la funcion:

    ajax_connect: function(callback){
    this.status = “connecting”;
    ajaxPetition(this.libraryURL,
    “command=connect”,callback);
    },

    al callback que se pasa a ajaxpetition es lo que debe hacer onreadystatechange con readystate = 4, me gustaria que pusiera la propiedad status de la clase de “connecting” a “connected”. Lo ideal seria que se hiciera:

    ajax_connect: function(callback){
    this.status = “connecting”;
    ajaxPetition(this.libraryURL,
    “command=connect”,
    function(){
    this.status = “connected”;
    callback();
    }
    );
    },

    pero claro, ajax al ser asincrono ya no sabe que es this, habria que poner esa propiedad desde el nombre de la instancia de la clase…. :P

    mi pregunta es: ¿hay alguna forma de hacerlo con this?, en caso contrario, ¿hay alguna manera de que la instancia averigüe su nombre desde dentro?

    muchas gracias y un saludo :)

Comentar

#

Me reservo el derecho de eliminar y/o modificar los comentarios que contengan lenguaje inapropiado, spam u otras conductas no apropiadas en una comunidad civilizada. Si tu comentario no aparece, puede ser que akismet lo haya capturado, cada día lo reviso y lo coloco en su lugar. Siento las molestias.