Páginas

2009/07/21

Objetos y clases en javascript: prototype, constructor y closures


Creando Objetos


Un objeto en javascript es una coleccion no ordenada de propiedades, que puede incluir primitivas, otros objetos o funciones. Es decir, es un array de valores no ordenados.

Las formas de crear objetos en javascript son varias:

 Las clases en javascript no utilizan la palabra Class como en otros lenguajes. Para crear clases en javascript utilizaremos la palabra function.

Esta tercera forma (la c) se utiliza desde la versión 1.1 de Javascript y se le denomina función constructora o constructor de la clase, (no confundir con la propiedad constructor de una función que veremos más adelante). Con esta forma creamos una función que nos servirá en el futuro para que un objeto se instancie a partir de esta ,es decir, necesitaremos la palabra clave new para crear el objeto basado en esta función.

 [Instanciar quiere decir crear un nuevo objeto con la palabra new a partir de una clase]


Por ejemplo, si queremos hacer un objeto de cocheDeModa de tipo Coche haremos lo siguiente:




El objeto cocheDeModa ahora contiene la función 'Coche' dentro su propiedad llamada constructor. Veámoslo:



Constructores

Todo objeto tiene una propiedad intrínseca llamada constructor que hace referencia a la función constructora que inicializa el objeto. Los constructores son funciones, que a su vez, son objetos. Es decir, todo 'constructor' de un objeto es una función, que se hereda de Function, y que es también una instancia de Object, por lo que diremos que en Javascript, todo es un objeto (todo se extiende de Object).

El resultado es:

Veamos ahora el tipo de objeto que se crea en cada caso (para saber el tipo de un objeto podemos utilizar tanto typeof, constructor o instanceof):


Hay un problema con el operador typeof en el propio lenguaje Javascript al evaluar un objeto tipo Array y al evaluar los valores null, como nos cuenta Douglas Crockford (http://javascript.crockford.com/remedial.html)
Para ello utilizaremos su función para tratar correctamente con typeof:



Chequeamos tipos a través de constructor:

cocheDeModa es un objeto como nos dice typeof/typeOf(), pero también es una instanciación del objeto tipo Coche. Para saber de quién instancia podemos utilizar el operador instanceof:

Creando clases

Antes de nada decir que Javascript no crea clases de igual forma a Java, C++ o C#. Javascript no es un lenguaje orientado a objetos puro, aunque sí podemos decir que todo dentro del lenguaje es un objeto, y podemos emular ciertas partes de la programación orientada a objetos.

Vamos a crear dos 'clases' genéricas por medio de funciones constructoras que nos servirán como molde para hacer coches. Para establecer las propiedades y métodos utilizaremos el operador this.


Prototype y la herencia

Los objetos pueden crear nuevos objetos y también pueden 'heredar' unos de otros, bajo un nuevo concepto denominado herencia prototipal. Los programadores aconstumbrados a la herencia clásica de los objetos verán una forma novedosa de herencia basada en el prototipo. Es aquí donde entra en juego la propiedad prototype.

Cuando creamos una función constructora (una 'clase'), Javascript crea una propiedad llamada 'objeto prototipo' o 'prototype object'. Cuando creamos un objeto en javascript se heredan/extienden las propiedades de su prototipo.

Cuando utilizamos la siguiente expresión:

function F()

se crea automáticamente una nueva propiedad (que es, a su vez, un objeto)

F.prototype

y este, a su vez se enlaza con el genérico Object.prototype.

A partir de ahora, si añadimos nuevas funciones y propiedades a F.prototype, todos los objetos creados a partir de F(), heredaran las nuevas funcionalidades.


En Javascript, todo se hereda de Object.prototype, esta es la verdadera fuente de herencia. Si queremos cambiar cualquier variable u objeto del programa, solamente extenderemos desde aquí.
Como ejemplo Number.prototype hereda de Object.prototype. Si modificamos o añadimos algún método a Number, al invocar a este método, primero se busca en el prototipo de Number y si no se encuentra, subirá hasta Object.prototype para encontrarlo. Lo mismo ocurre con los demás objetos de javascript.
De aquí podemos deducir que:

alert(oJose.constructor === Persona.prototype.constructor); // true


El constructor de un objeto es equivalente al constructor del prototipo de la clase de la cual instancia. O dicho de otro modo, tanto la función constructora como el constructor de su prototipo, son del mismo tipo (menuda frase).


En Javascript, cada vez que instanciamos un objeto, se hace una copia literal de todo el objeto en la memoria, es decir, se copian todas las propiedades y métodos por cada objeto creado, con el gasto de memoria que ello implica.
Siguiendo el ejemplo anterior, tanto unCocheDeModa como otroCocheDeModa existen en memoria con las mismas propiedades y métodos.
Siendo las propiedades las que más varían su valor, se entiende que cada objeto tenga las suyas propias, pero ¿y los métodos? acelera() siempre devolverá 'Bruumm' y getColor siempre nos dará el color del coche. Así pues podemos extraer estos métodos comunes y ponerlos bajo la propiedad prototype, esto ahorrará memoria.
Se puede decir que así extendemos también los métodos de la superclase.

Los nuevos objetos que instancien de Coche serán mucho más ligeros en memoria y enlazarán directamente con Coche.prototype cuando se llame a los métodos acelera() y getColor().


Heredando/extendiendo

Veamos un ejemplo ahora que ya sabemos cómo funciona prototype, veamos un ejemplo de herencia clásica:

Resultando:

Viendo lo anterior, podemos hacer una función que nos facilite la extensión:

El problema aquí es que si superClase es muy grande y la instanciamos gastaremos mucha memoria. Así pues una solución es aplicar una función 'puente' en la que asignemos a su propiedad prototype el prototipo de la superclase:


Aplicándolo al ejemplo anterior:



Herencia prototypal

Lo visto anteriormente es la denominada herencia clásica en javascript que intenta simular la herencia de la programación orientada a objetos. Ya que Javascript usar los prototipos, vamos a hacer uso de ellos para crear extensiones.

La diferencia entre los dos tipos de herencia es que mientras la clásica necesita de funciones constructoras, la prototypal se fija solamente en el objeto. La creación de un objeto será el prototipo para todos los demás, y no es más que una copia de su propiedad prototype.


Cuándo utilizar el tipo de herencia más adecuado dependerá del programa. Los pros de la herencia clásica es que es más fácil de leer y permite utilizar funciones constructoras. Los contras, el consumo de memoria. En cambio la herencia prototypal es más ligera, pero se limita a la creación de objetos. La decisión dependerá del resultado final y del propio programador.




Para profundizar más, no dejar de leer el artículo escrito por Shelby H. Moore 'Correct OOP for Javascript'
(http://www.coolpage.com/developer/javascript/Correct%20OOP%20for%20Javascript.html)
o 'Javascript Prototypal Inheritance, My 5 Cents From Web Reflection'
(http://www.3site.eu/doc/)



Un paso más allá de la herencia

Uno de los fundamentos de la extensión es que se pueda llamar a los métodos del objeto que extiende (o comúnmente 'padre').

Desde Javascript 1.3 disponemos de dos funciones llamadas call y apply que permite ejecutar un método de otro objeto. Su sintaxis es:

call (objeto, argumentos);

objeto será aquel objeto en el cual recaerá la ejecución de la función

apply (objeto, [argumentos]) ,igual que call exceptuando que los argumentos se pasan como un Array.

Ejecutar esto

Area.call(obj, base, altura);


es lo mismo que:

obj.metodo = Area;
obj.metodo(base,altura);
delete obj.metodo;


Bien, pues para extender los métodos de la clase 'padre' debemos hacer lo siguiente:


Ámbito de las variables

Seguramente sepas que el objeto que engloba todas las variables globales es window, que es a su vez global también.

Las dos sentencias siguientes son equivalentes:
Es lógico que si una variable se declara fuera de una función se haga global, hasta aquí todo normal, pero miremos el siguiente caso:
Curiosamente, propiedad pasa a ser global aun estando dentro de una función. Esto pasa porque el operador this hace 'públicas' las variables y los métodos (muy importante recordar esto).

No ocurre lo mismo si declaramos una variable dentro de una función (ámbito local):
En el primer ejemplo, al llamar a la función Prueba() lo hemos hecho dentro de un ámbito global y por tanto su propiedad también lo era. Si a esta función constructora / 'clase' la instanciamos dentro de una variable 'p', el objeto global window no tendrá acceso a él, no de una forma directa, con lo que nos ahorramos un poco de memoria:

Ámbito de los métodos: públicos, privados y privilegiados

Siguiendo a Douglas Crockford (http://javascript.crockford.com/private.html), el ámbito de los métodos serán los siguientes:

* Métodos Públicos, los métodos públicos son completamente accesibles dentro del contexto del objeto.

- Desde el constructor. Con el operador this, hacemos públicos los métodos como ya hemos visto.

- Desde el objeto prototype. También visto anteriormente, prototype nos permite hacer públicos los métodos:

Utilizando la funcion prueba anterior...

* Métodos Privados. Métodos que solamente son accesibles desde el propio objeto, ya sea desde otros métodos privados dentro de él mismo, otras variables privadas que contenga o otros métodos privilegiados (como veremos más adelante). Los métodos privados son muy importantes ya que permiten una correcta gestión del código sin que se produzcan coincidencias con otros objetos.

* Métodos Privilegiados. Es una combinación de los dos anteriores. Ya que los métodos privados no son accesibles desde el exterior, si queremos obtener sus valores debemos hacer un puente entre lo público y lo privado. Como ya hemos visto en el ejemplo anterior, this.capitaliza es un método que se hace público devolviendo una operación privada (unionLetras), éste es un método privilegiado.


La mayor diferencia entre los tres es que únicamente nuevos métodos públicos pueden añadirse una vez creado el objeto. Los privados y privilegiados deben establecerse en la función constructora.


Closures, Cierres o Clausuras

Desde que Javascript en su creación se vió influenciado por otros lenguajes como Scheme o Self, este adoptó los denominados closures o clausuras (aunque a mi me gusta más el termino cierre).

Indagando por Internet en busca de una definición que explicara los cierres de una forma menos académica, he encontrado la mejor a mi parecer en la Wikipedia que dice así:

"En Informática, una clausura es una función que es evaluada en un entorno conteniendo una o más variables dependientes de otro entorno. Cuando es llamada, la función puede acceder a estas variables... En algunos lenguajes, una clausura puede aparecer cuando una función está definida dentro de otra función, y la función más interna refiere a las variables locales de la función externa. En tiempo de ejecución, cuando la función externa se ejecuta, una clausura se forma, consistiendo en el código de la función interna y referencias a todas las variables de la función externa que son requeridas por la clausura... Una clausura puede ser usada para asociar una función con un conjunto de variables 'privadas'..."

Esta sentencia puede explicarse visualmente así:
incremento(x) es una función que devuelve otra función. El cierre es function(y) y opera con la variable x declarada exteriormente.

Otro ejemplo:
La manera más usual de crear closures es devolviendo funciones anidadas. Una funcion hija puede tener acceso al entorno de la función padre siempre que este haya completado su ejecución.

¿Por qué son importantes las clausuras? Porque aparte de que ofrecen flexibilidad, permiten crear variables que solamente pueden ser accesibles internamente.


Funciones anónimas

Una función anónima es aquella que no tiene nombre. Por ejemplo:
Normalmente, siguen este patrón:

(function () { ... }) ();


Pero también puede valer:

var foo = function(x) {return (x*x);};

Otro ejemplo dado por Dustin Diaz (http://www.dustindiaz.com/scoping-anonymous-functions/)
Las funciones anónimas suelen ser utilizadas:
- para ejecutar código una sola vez y se ejecutan de inmediato.
- para tratar mejor su contenido, haciéndolo privativo: esconden código que solamente es accesible dentro del objeto. Los objetos dejan de ser globales.
- ahorrar memoria


Quitando las referencias a los objetos

var obj=new Object();
obj=null;


Asignando a null rompemos la referencia y destruimos el objeto para que más tarde el Garbage Collector de javascript lo limpie de la memoria.


JSON

Como hemos dicho antes, un objeto puede declararse así:
Pero también se puede declarar así en formato JSON:
Que es muy similar al resultado dado por la función toSource() del objeto String.

alert(oCocheDeModa.toSource());

Con esto creamos el objeto inmediatamente, ya está instanciado. Su constructor será Object. OJO!: estaremos creando un objeto no una 'clase' / función


Extendiendo los objetos propios de Javascript

Después de todo lo visto, vamos a hacer un ejemplo extendiendo algunas funcionalidades extra del objeto String de Javascript:

El objeto String no dispone de las funciones 'trim' que existen en otros lenguajes. Trim, RTrim (right trim) y LTrim (left trim) quitan los espacios del inicio y final de una cadena, del inicio solamente o del final, respectivamente.
Para añadir estas funciones de una forma pública y que todos los objetos tipo String puedan utilizarlas, debemos modificar la propiedad prototype.

(Actualizado 29-ago-2013)
Seguramente te preguntes viendo el código anterior, ¿no puedo reescribirlo en formato JSON de la forma?:
Siento decir que la respuesta es que no, porque la propiedad prototype de String es 'readonly' (de sólo lectura) y no se le puede asignar código directamente. (http://stackoverflow.com/questions/8613907/why-string-prototype-wont-work)

Últimos apuntes


* Sentencias equivalentes

- Declarar una variable en un ámbito global como

 var texto="texto";

equivale a

  window.texto="texto";

- Una función cuando se declara, se le asigna una variable por defecto.

function f(){
   return 'una función';
};


equivale a

var f=function(){
   return 'una función';
};



* == y ===

Tanto == como === comparan los valores a ambos extremos del operador, pero con una gran y sutil diferencia. Mientras == sólo compara valores, === además compara los tipos. Es sorprendente como Javascript puede hacer las siguientes cosas:

'1' == 1 //true
null == undefined //true


En cambio, de una forma mucho más lógica:

'1' === 1 //false, un String no es un Number
null === undefined //false
'1' === '1' //true

Se recomienda utilizar === en casi todos los casos de comparación por ser más riguroso.



Referencias

http://javascript.crockford.com/survey.html
http://javascript.crockford.com/remedial.html
http://javascript.crockford.com/private.html (ambito de los métodos)
http://webreflection.blogspot.com/2007/03/javascript-constructor-did-you-know.html (una función Constructor muy interesante)
http://www.coolpage.com/developer/javascript/Correct%20OOP%20for%20Javascript.html ('Correct OOP for Javascript)
http://www.3site.eu/doc/ ('Javascript Prototypal Inheritance, My 5 Cents From Web Reflection')
https://work.dustindiaz.com/
http://www.javascriptkata.com/

4 comentarios:

  1. Muy buen post, aunque mis conocimientos en javascript son limitados se reconocer cuando algo esta bien hecho.

    Solo un par de preguntas.

    En la siguente funcion porque dices "...superClase es muy grande y la instanciamos gastaremos mucha memoria."
    function extiende (subClase, superClase){
    subClase.prototype = new superClase;
    subClase.prototype.constructor=subClase;
    }

    Y como contribuye "fPuente(){};" a que la funcion "extiende" sea mas eficiente.

    function extiende (subClase, superClase){
    function fPuente(){};
    fPuente.prototype=superClase.prototype;
    subClase.prototype = new fPuente();
    subClase.prototype.constructor=subClase;
    }

    Saludos.
    Gracias.
    P.D. Soy un pesado, pero me he quedado intrigado con estas funciones.

    ResponderEliminar
  2. (Perdona mi tardanza)

    Respecto a superClase, digo que si es muy grande, gastaremos mucha memoria al asignarla directamente al prototipo de la subClase. Es por esto que se utiliza una función puente (vacía) a la que asignamos el prototipo de la superClase a su prototipo solamente, no la clase entera. Haciendo lo siguiente: fPuente.prototype=superClase.prototype; estamos asignando solamente el prototipo y para luego jugar con el constructor de la subClase.

    Espero no haberte liado más. Un saludo.

    ResponderEliminar
  3. muy buen articulo, muchas gracias por tu tiempo.

    ResponderEliminar

MsiInv o cómo obtener información del software instalado en tu ordenador (en Windows)

Pues como dice el título, si quieres saber realmente qué software tienes instalado en tu computadora con el sistema operativo Windows, recom...