Javascript: Un problema con la palabra reservada this

Un problema con this

Decimos que construir dinámicamente con JavaScript elementos HTML es expresar el código de ese elemento en una cadena (String) y luego insertarla en el documento mediante el uso del método innerHTML que actualmente soportan la mayoría de navegadores. Por ejemplo, con el script var cadena = "<span>xxx</span>" haríamos una declaración literal del código de un elemento HTML y luego con otroElemento.innerHTML = cadena reemplazaríamos todos los elementos que poseyera en su interior ese otroElemento con el anterior. Cuando se ejecuta el script ese reemplazo se hace de forma inmediata sin necesidad de recargar la página.

En un contexto de manejo de objetos la palabra reservada this es una referencia al propio objeto. Por otro lado sabemos que cuando un evento llama a una función, una vez dentro de ésta podemos obtener una referencia al elemento que causó el evento con la palabra reservada this. Esta referencia se pasa como un argumento que va implícito en la función, no siendo necesario declararlo en la lista de argumentos.

Si en la cadena para construcción dinámica HTML quisiéramos expresar un evento que llama a una función no tendríamos ningún problema: var cadena = "<span onclick='miFuncion()' >xxx</span>". Pero el problema surge cuando esa miFuncion() es un método de un objeto creado y dentro de ella necesitamos referirnos con this al propio objeto de tal forma que la función nos devuelve irremediablemente la referencia al elemento que causó el evento.

Para tratar de entender todo esto y buscar una solución, haremos algunos ejemplos con scripts que se ejecutan en esta misma página. En primer lugar creamos una clase muy simple que vamos a usar como modelo en todos los ejemplos:

<div id="div-clase1"></div>
<script>
    function clase1(){
        this.construye = function construye(donde){
            donde.innerHTML = "<input value='click' type='button' 
            onclick='this.hola()' />";
        };
        this.hola = function hola(){
            alert("hola");
        };    
    }
    var objeto1 = new clase1();  
    objeto1.construye(document.getElementById("div-clase1"));   
</script>     
    

El código anterior creará dinámicamente un elemento <input type="button">, un botón con la descripción "click-1", que al pulsarlo en este caso dará un error de script a propósito:

Se trata de un objeto simple con dos métodos. Uno para construir HTML con una cadena que es el literal de un botón con un evento onclick que llama al otro método hola(). La primera tentativa es incluir en el evento la llamada al método this.hola(). Pero si tiene activada la opción del navegador para ver los errores de JavaScript verá que esto no funcionará. Al estar este this encerrado dentro de una cadena, JavaScript no lo reconoce como el this que hace referirse a si mismo. Tampoco funciona si intentamos poner "... onclick='" + this + ".hola()' />" con la esperanza de que, al construir el elemento, convierta la referencia interna. Al fin y al cabo estamos mezclando dos cosas, una referencia a una clase y una cadena que luego se convierte en HTML.

Por lo tanto podríamos pensar en usar funciones anónimas. Puede ver más sobre ellas en nuestro glosario XHTML+CSS. Se trata de, una vez creado el elemento, asignar al evento onclick el nombre de la función, que debe pasarse sin argumentos:

<div id="div-clase2"></div>
<script>
    function clase2(){
        this.construye = function construye(donde){
            donde.innerHTML = "<input value='click-2' id='boton-2' type='button' />";
            document.getElementById("boton-2").onclick = this.hola;                
        };
        this.hola = function hola(){
            alert("hola");
        };
    }
    var objeto2 = new clase2();  
    objeto2.construye(document.getElementById("div-clase2")); 
</script>    
    

Bien, ahora si funciona. Pero ¿Y si necesitáramos pasar argumentos con el evento?. Con esa técnica no hay forma de pasarlos. Si necesitáramos tener acceso al elemento que causó el evento para saber su contenido, por ejemplo, hay un recurso para ello. Los eventos suministran dos argumentos automáticos, uno es el tipo de evento y otro una referencia al elemento que causó el evento. Con el ejemplo anterior quizás podríamos usar esos argumentos automáticos y saber que elemento produjo el evento.

<div id="div-clase3"></div>
<script>
    function clase3(){
        this.construye = function construye(donde){
            donde.innerHTML = "<input value='click-3' id='boton-3' type='button' />";
            document.getElementById("boton-2").onclick = this.hola;     
        };
        this.hola = function hola(eventoFirefox){
            var miEvento = window.event || eventoFirefox;
            alert("El valor del elemento: " +
            this.value +
            "\nEl evento: " + miEvento.type);
        };    
    }
    var objeto3 = new clase3();  
    objeto3.construye(document.getElementById("div-clase3")); 
   
</script>    
    

En el método anterior hola() se recibe de forma automática una referencia al elemento que causó el evento al cual se puede acceder también con la palabra reservada this. Entonces en este contexto de ese método this se refiere a ese elemento y no al objeto donde está insertado dicho método. Por otro lado hemos incluido también el otro argumento automático event aunque con Explorer podría funcionar simplemente hola() y luego usar event.type, (realmente se trata del objeto window.event), para Firefox es necesario poner hola(event), pero entonces Explorer no lo soporta. Para que sirva a ambos, usamos una variable intermedia miEvento donde toma uno de los dos valores, o bien window.event de Explorer o bien el argumento eventoFirefox (con cualquier nombre de argumento) para ese navegador, pues Explorer no hará uso de ese argumento.

NOTA: El objeto window.event con Explorer puede también darnos el elemento con window.event.srcElement aparte del tipo con window.event.type, lo que incluso podríamos abreviar simplemente con event.srcElement y event.type si estamos dentro de una función que viene desde un evento, pero no va bien con Firefox que prefiere usar this y un argumento pasado en la función para el evento.

Bueno parece que lo hemos conseguido. Pero ¿y si necesitamos acceder a una propiedad interna del objeto?. Veámos el siguiente ejemplo donde queremos extraer el valor de la propiedad con nombre propiedad y cuyo valor es "UN VALOR":

<div id="div-clase4"></div>
<script>
    function clase4(){
        this.propiedad = "UN VALOR";
        this.construye = function construye(donde){
            donde.innerHTML = "<input value='click-4' id='boton-4' type='button' />";
            document.getElementById("boton-2").onclick = this.hola;   
        };
        this.hola = function hola(eventoFirefox){
            var miEvento = window.event || eventoFirefox;
            alert("¿Qué es este 'this'?: " + this +
                  "\n¿TAGNAME?: " + this.tagName +                          
                  "\n¿ID?: " + this.id +                       
                  "\n¿VALUE?: " + this.value +
                  "\n¿Evento invocado?: " + miEvento.type +
                  "\n" +
                  "\nPropiedad del objeto 'clase-4': " + this.propiedad);
        };
    }
    var objeto4 = new clase4();  
    objeto4.construye(document.getElementById("div-clase4")); 
  
</script>    
    

Ahora vemos que no podemos acceder a this.propiedad porque este this no hace referencia al objeto, sino al elemento que causó el evento. Vea como podemos acceder a los atributos del elemento <input> pero cuando vamos a buscar la propiedad del objeto nos da undefined porque no la encuentra como atributo del elemento HTML.

Puede que hayan otra forma para resolver a este problema que desconozca, pero mi solución es pasar el nombre del objeto instanciado y luego construir el HTML con onclick = '" + nombreObjeto + ".hola()' .

<div id="div-clase5"></div>
<script>
    function clase5(){
        this.propiedad = "UN VALOR";
        this.construye = function construye(donde, nombreObjeto){
            donde.innerHTML = "<input value='click-5' id='boton-5' type='button' " +
            " onclick = '" + nombreObjeto + ".hola()' />";
        };
        this.hola = function hola(){
            alert("¿Qué es este 'this'?: " + this +
                  "\n¿TAGNAME?: " + this.tagName +                          
                  "\n¿ID?: " + this.id +                       
                  "\n¿VALUE?: " + this.value +
                  "\nPropiedad del objeto 'clase-5': " + this.propiedad);
        };
    }
    var objeto5 = new clase5();  
    objeto5.construye(document.getElementById("div-clase5"), "objeto5"); 
</script>    
    

En este ejemplo anterior, cuando la cadena HTML onclick = '" + nombreObjeto + ".hola()' se construye con innerHTML quedará onclick = 'objeto5.hola()' , de tal forma que el this del método hola() hace referencia al objeto y no al elemento. El acceso a id, tagName o value no es posible pues no existen en nuestro objeto como propiedades o métodos (si hubieran algunas con ese nombre las sacaría). Para evitar el problema de definición del this, lo que hacemos es pasar expresamente la referencia al elemento cuando llamemos al método onclick = '" + nombreObjeto + ".hola(this)', donde este this SI es la referencia al propio elemento que causa el evento:

<div id="div-clase6"></div>
<script>
    function clase6(){
        this.propiedad = "UN VALOR";
        this.construye = function construye(donde, nombreObjeto){
            donde.innerHTML = "<input value='click-6' id='boton-6' type='button' " +
            " onclick = '" + nombreObjeto + ".hola(this)' />";
        };
        this.hola = function hola(elemento){
            alert("¿Qué es este 'elemento'?: " + elemento +
                  "\n¿TAGNAME?: " + elemento.tagName +                          
                  "\n¿ID?: " + elemento.id +                       
                  "\n¿VALUE?: " + elemento.value +
                  "\nPropiedad del objeto 'clase-6': " + this.propiedad);
        };    
    }
    var objeto6 = new clase6();  
    objeto6.construye(document.getElementById("div-clase6"), "objeto6"); 
  
</script>   
   

Así ahora queda claro que this dentro del método será la referencia al objeto mientras que la referencia al elemento la traemos expresamente como un argumento usando el otro this en la declaración del evento.