jueves, 16 de diciembre de 2010

POO en Javascript (II)

En javascript existen varias formas de crear nuevos tipos personalizados. La primera y más antigua, pero igualmente válida es la siguiente:

        //se declara un constructor sin parámetros para el nuevo tipo Persona

        function Persona() {

            this.nombre = "Sergio"; //declaramos una propiedad “nombre” y le damos un valor por defecto

        }

 

        //se declara una variable del tipo Persona

        var yo = new Persona();

 

        //se declara un constructor con parámetros para el nuevo tipo Persona1

        function Persona1(nombre) {

            //this es el contexto actual

            this.nombre = nombre || "Sergio";  //Asignar valor por defecto a nombre en caso de que no se haya suministrado

        }

 

        //se declara una variable del tipo Persona1

        var otroYo = new Persona1("Sergio");

 

        //investigando el tipo de la variable yo

        alert(yo.nombre); //Sergio

        alert(typeof (yo)); //object

        alert(yo instanceof Persona); //true

 

Como vemos, crear un nuevo tipo es tan simple como declarar una función (que puede o no recibir parámetros), que no retorna ningún valor, y dentro de la función hacer referencia a this (el contexto actual) y finalmente, declarar variables de nuestro nuevo tipo de la forma:
var variable = new <NombreFuncion>.

A propósito del código anterior, la asignación this.nombre = nombre || “Sergio” es una de las opciones que tenemos disponibles para asignar valores por defecto a un parámetro si no se ha suministrado en la llamada a la función. Lo que hace esta instrucción es igual this.nombre, bien a nombre (si está definido) o bien al literal indicado. Puedes ver más información en “Asignar valores por defecto a los parámetros de una función en Javascript”.

Lo primero que podríamos querer es crear métodos para nuestra clase y la verdad es que es también muy sencillo (en este ejemplo, veremos métodos de instancia públicos)

        function Persona() {

            this.nombre = "Sergio"; //propiedad con valor por defecto

            //método que utiliza una función anónima en el contexto actual

            this.saludar = function () {

                alert(this.nombre);

            };

            //método que utilizar una función anónima en el contexto actual

            this.saludarConApellido = function (apellido) {

                alert(this.nombre + " " + apellido);

            };

            //método que utiliza una función con nombre,

            //y además declarada en el contexto global

            this.saludarPersonalizado = saludarPersonalizado;

        }

 

        //función en el contexto global, no en el contexto de Persona

        function saludarPersonalizado(saludo) {

            alert(saludo + " " + this.nombre);

        }

 

        var yo = new Persona();

        yo.saludar();

        yo.saludarConApellido("León");

        yo.saludarPersonalizado("Bienvenido");

 

Si te fijas, el ejemplo de la función saludarConApellido es la mejor opción para añadir un método a una clase. Esto es porque la función saludarPersonalizado está definida en el espacio global, mientras que saludarConApellido está definida dentro de la clase. El problema del espacio global es que más adelante podemos tener algún conflicto de nombres, bien con nuestras propias clases o con clases de terceros. Además, cualquier principio como la encapsulación no los estamos pasando por el forro de los … tú ya me entiendes!

Un método que tienen automáticamente todos los objetos disponibles (se declare o no) es el método toString(). Suele ser una buena práctica declarar explícitamente el método para que un simple alert(variable) no devuelva una cadena estándar no representativa sino el contenido del objeto.

        function Persona() {

            this.nombre = "Sergio";

            //si no se declara el método toString,

            //el valor devuelto predeterminado será “[object Object]”

            this.toString = function () {

                return "nombre: " + this.nombre;

            };

        }

 

        var yo = new Persona();

        alert(yo); //llama implícitamente a toString()

        alert(yo.toString()); //llama explícitamente a toString()

 

Ahora toca hablar de la palabra prototype (no confundir con el framework prototype http://prototypejs.org/ , que en mi opinión podían haber elegido otro nombre para eviar la confusión, pero bueno…). prototype es una propiedad que tienen todos los objetos en javascript, y que se utiliza para buscar referencias a propiedades o métodos, cuando estas propiedades o métodos no se encuentran en el objeto. Para entender mejor el uso de prototype, antes veremos algunas operaciones que podrían resultarnos algo “extrañas”.

Por ejemplo, se puede agregar a cualquier instancia de cualquier objeto una propiedad o método, simplemente estableciendo un valor (es por esto que se dice que javascript es un lenguaje dinámico). No es necesario que existe en la definición de su clase, simplemente asignamos un valor y de inmediato, “esa instancia” (no todas las instancias de esa clase, sino sólo esa) pasa a tener esa propiedad. Esto significa que la nueva propiedad “no” forma parte del prototipo, es decir, no se ha agregado al prototipo sino a la instancia. Cuando preguntemos por ella, el orden de búsqueda es: primero buscar en el objeto y si no lo encuentra buscar en el prototipo.

        function Persona() {

            this.nombre = "Sergio";

        }

 

        var yo = new Persona();

        yo.apellidos = "León"; //la propiedad no existe pero se crea dinámicamente

        alert(yo.apellidos); //ahora se puede utilizar la propiedad con toda normalidad

 

Si en cambio quiero agregar una propiedad a todas las instancias de una clase (y también a las instancias venideras), tengo que utilizar prototype. De este modo, cuando se pregunte por la propiedad, primero se buscará en el objeto (y no la encontrará) y luego se buscará en el prototipo y “sí” la encontrará (es por eso que todas las instancias pasan a tener automáticamente la nueva propiedad).

        function Persona() {

            this.nombre = "Sergio";

        }

 

        var yo = new Persona();

 

        //todas las instancias existentes de Persona tienen ahora una propiedad 'apellidos' con el valor 'León'.

        //además, nuevas instancias de Persona también tendrán esta propiedad

        Persona.prototype.apellidos = "León";

 

        alert(yo.apellidos);

 

        var otroYo = new Persona();

        alert(otroYo.apellidos);

 

También cabe mencionar que si queremos eliminar una propiedad o método de un objeto o clase, podemos utilizar el operador delete (da igual que la propiedad o método haya sido creada dinámicamente o en la definición de su clase o en su prototipo).

        function Persona() {

            this.nombre = "Sergio";

        }

       

        Persona.prototype.apellidos = "León";

       

        var yo = new Persona();

        yo.edad = 35; //propiedad dinámica

 

        delete yo.nombre; //eliminar propiedad definida en la clase

        delete yo.edad; //eliminar propiedad dinámica para la instancia

        delete Persona.prototype.apellidos; //eliminar propiedad definida en el prototipo

 

En lo relativo a tipos de usuario, yo personalmente prefiero incluir las propiedades y métodos en la definición de clase, en vez de que con prototype. Es decir prefiero

        function Persona() {

            this.nombre = "Sergio";

            this.toString = function () {

                return "nombre: " + this.nombre;

            };

        }

 

Que esto otro (que también es válido pero no me gusta):

        function Persona() {

        }

 

        Persona.prototype.nombre = "Sergio";

        Persona.prototype.toString = function () {

            return "nombre: " + this.nombre;

        };

 

Aunque prefiero no usar prototype, parece ser que desde el punto de vista del uso de memoria es más óptimo prototype que incluir las funciones en el constructor de la clase. El motivo es que cuando se declaran las funciones en el constructor, cada instancia de la clase copia la definición de todo el método constructor y ocupa más en memoria.

Otro uso muy extendido de propotype es la extensión de los tipos de datos nativos de javascript. Por ejemplo, la clase String no tiene un método trim, así que implementarlo sería algo como esto:

        String.prototype.trim = function () {

            //quitar espacios de this...

            return this;

        };

 

        var nombre = "Sergio y Carmen";

        alert(nombre.trim());

 

De hecho, una característica muy común de cualquier framework de javascript (jQuery, mootools, prototype, ASP.NET AJAX, etc.) es dotar a los tipos básicos de operaciones muy demandadas y no implementadas de base (por ejemplo, el famoso trim() para el tipo String).

Volviendo a la definición de una clase, otra característica útil podría ser la de declarar propiedades y métodos privados a la clase. Esta operación puede implementarse de la siguiente forma:

        function Persona() {

            var that = this; //guardar referencia al contexto actual en una variable privada

            this.nombre = "panicoenlaxbox";

            this.saludar = function () {

                alert("Hola " + this.nombre + " en público.");

                saludarPrivado();

            };

 

            //variable privada

            var nombrePrivado = "Sergio";

 

            //métodos privados

            function saludarPrivado() {

    //utilizar that, porque this no está disponible

                alert("Hola " + that.nombre + ", en realidad eres " + nombrePrivado);

            };

        }

 

        var yo = new Persona();

        yo.saludar();

 

Por convención, se suele declarar una variable privada llamada “that”, “_this” o similar, para guardar una referencia al contexto actual, esto es a this. De este modo, las funciones privadas (que no se han agregado al contexto actual a través de this.funcionPrivada = …) pueden acceder igualmente al contexto actual a través de la variable privada “that” (ver que diferenciamos entre contexto de ejecución y contexto de definición: se puede ver “that” por el contexto de definición, pero no “this” por el contexto de ejecución, es decir, cuando hablamos de this, siempre estamos refiriéndonos al contexto de ejecución).

Realmente, saludarPrivado puede acceder a “that” porque el contexto de definición de la función incluye a esta variable. Si recordamos, el alcance de una variable puede ser global (si está definida fuera de cualquier función o no se utilizado var) o local a una función. Pues bien, cuando declaramos una variable en una función (así como también los parámetros que recibe la función), el alcance de la variable es la propia función y cualquiera otra que esté anidada dentro de ella. Es por ello que saludarPrivado puede acceder a “that”´, estamos hablando del contexto de definición. Por otro lado, nada impide que aunque veamos una variable desde una función interna, podamos definir nuestra propia variable local a la función interna, con el mismo nombre. Esto es posible porque las referencias a la variable se buscan desde dentro hacia fuera (hasta llegar incluso al ámbito global). Para ilustrar el primero de los casos (la visibilidad), este sería el ejemplo:

        function funcA() {

            var a = 1;

            funcB();

            function funcB() {

                a += 1; //trabaja sobre la variable de funcA

                alert(a); //2

            }

            alert(a); //2, refleja los cambios desde funcB

        }

 

        funcA();

 

Para el caso de las referencias de dentro a fuera, este otro ejemplo podría ser válido:

        function funcA(nombre) {

            var a = 1;

            alert(nombre); //pedro

            funcB("sergio");

            function funcB(nombre) {

                alert(nombre); //sergio

                var a = 2; //crea la variable “a” local a la función

                a += 1; //trabaja sobre la variable local

                alert(a); //3

            }

            alert(a); //1, no se ha hecho ningún cambio desde funcB

        }

 

        funcA("pedro");

 

Íntimamente ligado a este concepto, aparece otro que es el de Closure. Este tema es muy amplio y a la vez muy importante. Pues ampliar información aquí. En cualquier caso, y si lo he entendido bien, se crea una Closure cuando hay referencias a variables desde una función interna a una función ascendente (no necesariamente padre) y es necesario que estas referencias perduren en tiempo de ejecución (cuando las referencias ya no existan, los Closures serán eliminados por el recolector de basura). Veamos este otro ejemplo, muy similar al existente en el post de Venkman.

        function funcA() {

            var a = 1;           

            function funcB() {

                a += 1;

                alert(a);

            }

            return funcB;

            //al devolver la función, ahora "perdura" más allá de su ámbito de definición y por eso se crea la Closure           

        }

 

        var a = funcA();

        a(); //alert(2); fijarse que la variable "a", aunque fuera de ámbito, ha perdurado en tiempo de ejecución

 

También podemos declarar métodos y propiedades estáticas de la siguiente forma:

        function Persona() {

            this.nombre = "Sergio";

            this.saludar = function () {

                alert("Hola " + this.nombre);

                Persona.numero += 1; //acceso a la propiedad estática

                Persona.mostrarNumero(); //acceso al método estático

            };

        }

 

        Persona.numero = 5;

        Persona.mostrarNumero = function () {

            alert(this.numero);

        };

 

        Persona.mostrarNumero();

       

        var yo = new Persona();

        yo.saludar();

 

Uno de los puntos clave que también hay que entender es como se implementa la herencia en javascript porque, aunque parezca lo contrario, es posible llevarla a cabo.

Una explicación detallada se puede encontrar en http://javis.wordpress.com/2006/11/22/herencia-en-javascript/ pero si no quieres o no tienes tiempo te resumo la forma más habitual de simular la herencia en javascript.

El método más utilizado para simular la herencia en javascript, es el denominado “Prototype chaining” (encadenación de prototipos).

Si recordamos, Prototype es una propiedad “vacía” (en principio), del objeto Function (en realidad cualquier tipo complejo de javascript tiene esta propiedad, pero nos interesa especialmente el objeto Function) , que será utilizada como modelo o plantilla inicial cuando se creen objetos del tipo de la función. Para seguir con nuestro ejemplo lo que se logra con Prototype es que un objeto de la clase B herede todas las propiedades y métodos de la clase A. Y lo más importante, que un objeto de tipo B “sea” también un objeto de tipo A (es decir, que instanceof nos devuelve true si le preguntamos si el objeto B es un tipo de la clase A).

        function Persona() {

            this.nombre = "";

        }

 

        function Programador() {

            this.lenguajePreferido = "";

        }

 

        //esta sentencia hace posible la herencia

        Programador.prototype = new Persona();

 

        var sergio = new Programador();      

        sergio.nombre = "Sergio"; //heredada de Persona

        sergio.lenguajePreferido = "VB.NET";

       

        if (sergio instanceof Programador) {

            alert("Sergio es programador."); //OK

        }

        if (sergio instanceof Persona) {

            alert("Sergio es una persona también."); //OK

        }

 

Al principio del documento dijimos que new <Function> era la primera de las formas en la que podíamos declarar nuevos objetos en javascript. La segunda forma (y de hecho muy de moda en estos momentos) es a través de JSON.

Por ejemplo var yo = {} crea un nuevo objeto vacío que no ha utilizado ninguna de las técnicas expuestas en este post (prototype, función constructora, etc.), pero que funciona igualmente y de hecho, en su versión más simple, var yo = new Object() y var yo = {} son casi equivalentes.

Esta segunda forma es muy útil para alguna de las siguientes tareas:

·           Devolver como retorno de una función un objeto simple en vez de un valor sencillo.

·           Simular espacios de nombres.

·           Crear objetos ad-hoc no reutilizables.

Lo principal que tenemos que tener en cuenta, utilizando esta notación, es que no hay ninguna “plantilla” que reutilizar en nuevos objetos. Cada objeto que creamos es una clase única y una instancia única. Salvando las distancias sería como un “tipo anónimo” de .NET.

        function validarFormulario() {

            //objeto ad-hoc, sin clase ni plantilla ni molde...

            var validacion = {};

            validacion.correcto = false;

            validacion.mensaje = "El email no es válido.";

            return validacion;

        }

 

        var resultado = validarFormulario();

        if (!resultado.correcto) {

            alert(resultado.mensaje);

        }

 

Este código es equivalente a este otro:

        function validarFormulario() {

            //objeto ad-hoc que utiliza JSON

            //JavaScript Object Notation

            var validacion =

            {

                correcto: false,

                mensaje: "El email no es válido."

            }

            return validacion;

        }

 

        var resultado = validarFormulario();

        if (!resultado.correcto) {

            alert(resultado.mensaje);

        }

 

Por último y hablando de espacios de nombres, me encantaría poder emular el comportamiento de los espacios de nombres de lenguajes como VB.NET o C#, en javascript. Esto es porque quiero mantener organizado mi código, y está claro que los espacios de nombres o paquetes, son la solución ideal.

        var tab =

            {

                "ERP": {}

            };

        tab.ERP.Persona = function () {

            this.nombre = "";

        }

        var sergio = new tab.ERP.Persona();

 

El anterior código crea un espacio de nombres tab.ERP con una clase Persona. Bueno, en realidad hemos creado una variable llamada tab, con una propiedad llamada ERP, y dentro una propiedad Persona que define un constructor.

Aunque esto parezca definitivo, hay que tener cuidado porque cualquier otro var tab = … machacaría cualquier otra definición de tab que hubiera previamente definida, por ello, o bien definimos todo el espacio tab es un solo fichero .js o bien detectamos si existe tab antes de macharlo.

        window.tab = { "ERP": {} };

        tab.ERP.Persona = function () {

            this.nombre = "";

        }

        if (typeof (window.tab) == "undefined") {

            //sólo si no existe tab, crea de nuevo tab.ERP

            window.tab = { "ERP": {} };

        }

        //resto de código...

 

Aunque esta solución es muy válida, mira este post donde se crea la función registerNS que funciona de maravilla y resuelve la creación de espacios de nombres, de forma muy sencilla y directa.

Como verás, javascript es un lenguaje de scripting, orientado a objetos basado en prototipos, dinámico y de tipado débil.

Es un lenguaje muy flexible pero que también puede ser un dolor de cabeza cuando tus ficheros .js son algo más que una colección aislada de funciones. Cada vez más, la parte cliente requiere de ingentes cantidades de código bien estructurado y con una clara orientación a objetos. Todo ello para que nuestro experiencia 2.0 sea agradable y satisfactoria. Por ello y aunque hemos dado un repaso a cómo hacer de casi todo con javascript “a pelo”: al igual que utilizo jQuery para manipular el DOM y dotar de animaciones a la UI, quiero utilizar las librerías de ASP.NET AJAX para refinar la parte de orientación a objetos.

Dicho esto, un saludo y hasta más ver!!

3 comentarios:

  1. Vaya tela, estamos ante el Gurú de la Programación Extrema.
    Enhorabuena por tu blog.

    Ciao!

    ResponderEliminar