JavaScript – ¿Qué es exactamente this?

Si vienes de un lenguaje orientado a objetos “clásico” como C# o Java, tendrás claro el concepto de this: Se refiere al propio objeto. Dado que todos los objetos son instancias de una clase en concreto y el código se define a nivel de clase el valor de this está claramente definido. Sólo leyendo el código puedes saber a que se refiere this en cada momento.

JavaScript también tiene la palabra this pero su significado es un poco más “complejo” que el de C#.

Antes que nada mencionar que this en JavaScript NO es opcional. Siempre que quieras acceder a una propiedad de this, debes usarla. Si no usas this, JavaScript asume siempre que estás accediendo a una variable local (o global).

El valor de this en JavaScript es parecido al de C#, pero a la vez es tan distinto que lo normal es que alguien que venga de C# termine sin entender nada.

Empecemos diciendo que el valor de this dentro de una función es el valor del objeto en el que se define esta función. Veamos un ejemplo:

  1. var persona = {
  2.     name: 'edu',
  3.     twitter: 'eiximenis',
  4.     twitterUrl: 'http://twitter.com/' + this.twitter
  5. };
  6.  
  7. console.log(persona.twitterUrl);

Este código lo que imprime por pantalla es… http://twitter.com/undefined

Pero… a ver: No valía this el valor del objeto sobre el que estaba definido el código? No está twitterUrl definido dentro del objeto persona? Qué narices ocurre?

Sencillo: Si relees mi definición de this, verás que empezaba diciendo “el valor de this dentro de una función…”. La propiedad twitterUrl no es una función, así que eso no se aplica. En este caso el valor de this es el contexto en el cual se esté ejecutando este código, que por defecto es el contexto global (window en los navegadores).

Para solucionarlo, debemos convertir twitterUrl a una función:

  1. var persona = {
  2.     name: 'edu',
  3.     twitter: 'eiximenis',
  4.     twitterUrl: function () {
  5.         return 'http://twitter.com/' + this.twitter;
  6.     }
  7. };
  8.  
  9. console.log(persona.twitterUrl());

Con este cambio, ahora si que el código imprime la respuesta esperada.

Funciones constructoras

Si leíste mi post sobre si JavaScript era orientado a objetos, ya sabrás que llamo función constructora a aquella función que se usa para crear objetos que compartan un mismo prototipo. Las funciones constructoras se invocan con new y es precisamente el uso de new lo que convierte a una función en constructora, no la función en sí.

El siguiente código también muestra el valor esperado:

Code Snippet
  1. var Persona = function (n, tw) {
  2.     this.name= n;
  3.     this.twitter = tw;
  4.     this.twitterUrl =  function () {
  5.         return 'http://twitter.com/' + this.twitter;
  6.     }
  7. };
  8.  
  9. var persona = new Persona('edu','eiximenis');
  10. console.log(persona.twitterUrl());

Insisto: Es el uso de new el que hace que this sea el objeto que se está creando. ¿Que pasaría si me olvidase el new al llamar a la función Persona?

  1. var foo = Persona('edu', 'eiximenis');
  2. console.log(foo.name);
  3. console.log(name);

Ahora llamo a la función Persona pero sin usar new. Ahora el valor de this dentro de Persona ya NO es el objeto que se está creando. Ahora el valor de this dentro de persona es el valor del objeto dentro del cual la función Persona está definido… Como Persona es una función global, pues ahora this es el contexto global.

De hecho el primer console.log dará error porque foo ahora vale undefined (¡la función Persona no devuelve nada!) y el segundo console.log mostrará “edu” porque la llamada a Persona ha creado la variable “name” en el contexto global.

Recuerda: No hay clases en JavaScript. Si quieres crear objetos a través de una función constructora asegúrate de usar siempre new. Si no obtendrás resultados imprevistos. Es el uso de new el que convierte una función en constructora.

Call / Apply

Vamos a ponernos un poco más serios. Este ejemplo anterior muestra una característica fundamental de this: Su valor depende de como se llame a una función. Hemos visto que dentro de Persona el valor de this dependía de si llamábamos a la función con new o sin new.

Pero oye… ya que el valor de this dentro de una función depende de como esta sea llamada, porque no explotar al máximo esta capacidad y permitir que el valor de this sea el que el desarrollador quiera? Hasta ahora hemos visto que el valor de this podía ser:

  1. El objeto dentro del cual estaba definida la función
  2. Un objeto nuevo que se crea en aquel momento (al usar new).

Pues bien call y apply añaden un tercer escenario: el valor de this puede ser cualquier objeto que el desarrollador desee… Veamos un ejemplo:

  1. var Persona = function (n, tw) {
  2.     this.name= n;
  3.     this.twitter = tw;
  4.     this.twitterUrl =  function () {
  5.         return 'http://twitter.com/' + this.twitter;
  6.     }
  7. };
  8.  
  9. var p = {
  10.     blog:'http://geeks.ms/blogs/etomas'
  11. };
  12.  
  13. Persona.call(p, 'edu','eiximenis');
  14. console.log(p.twitterUrl());
  15. console.log(p.blog);

La función Persona es exactamente la misma que teníamos antes. Luego definimos un objeto p con una propiedad blog. Y en la línea 13 tenemos la clave: Usamos call para llamar a la función Persona. La función call permite especificar el valor de this dentro de una función. El primer parámetro de call es el valor que tomará this, mientras que el resto de parámetros son los que se pasan a la función.

Por lo tanto, como el primer parámetro de Persona.call es el objeto p, el valor de this dentro de Persona será el objeto p… por lo tanto las propiedades name, twitter y twitterUrl serán añadidas al objeto p.

Apply hace exactamente lo mismo que call, salvo que los parámetros que se mandan a la función se especifican en un array:

  1. Persona.apply(p, ['edu','eiximenis']);

¿P: ej. no te has preguntado nunca porque si te suscribes usando jQuery a un evento, puedes usar “this” dentro de la función gestora del evento para acceder al elemento del DOM que ha lanzado el evento? Pues ahí tienes la respuesta: Los que crearon jQuery conocen call y apply 😉

El problema de la pérdida de this

Este es un problema bastante molesto y difícil de detectar si no lo conoces y se entiende mejor con un ejemplo:

  1. var Persona = function (n, tw) {
  2.     this.name= n;
  3.     this.twitter = tw;
  4.     this.twitterUrl =  function () {
  5.         return (function () {
  6.             return 'http://twitter.com/' + this.twitter;
  7.         }());
  8.     }
  9. };
  10.  
  11. var p = new Persona('edu', 'eiximenis');
  12. console.log(p.twitterUrl());

El ejemplo es un pelín forzado pero ilustra la problemática de la pérdida de this.

Primero, que sepas que este código imprime http://twitter.com/undefined en lugar de http://twitter.com/eiximenis como sería de esperar. Si entiendes porque entenderás la problemática de pérdida de this.

La propiedad twitterUrl sigue siendo una función, así que… ¿porque ahora this.twitter no tiene valor? Pues porque ahora estamos accediendo a this dentro de una función que está dentro de una función. Fíjate que ahora twitterUrl es una función que define internamente a otra función anónima, la invoca y devuelve su resultado. La función interna NO está definida dentro del mismo objeto en el cual se define la propiedad twitterUrl y por lo tanto el valor de this dentro de la función interna NO es el propio objeto…  De hecho en este caso this es el contexto global. Y claro el contexto global no tiene definida ninguna propiedad twitter.

Como digo el ejemplo está muy forzado porque nadie haría eso… pero tener una función dentro de otra función es muy habitual en el caso de callbacks, así que te terminarás encontrando con este caso. Y cuando te encuentres con él… te acordarás de mi! 😛

Hay dos soluciones para esto:

  1. Hacer que el valor de this dentro de la función interna sea el correcto (a través de call/apply). Algunas librerías de javascript se encargan de ello con las funciones de callback que gestionan ellos.
  2. Guardar el valor de this en una variable.

Veamos como quedaría el código para el primer caso:

  1. var Persona = function (n, tw) {
  2.     this.name= n;
  3.     this.twitter = tw;
  4.     this.twitterUrl =  function () {
  5.         return (function () {
  6.             return 'http://twitter.com/' + this.twitter;
  7.         }.apply(this));
  8.     }
  9. };
  10.  
  11. var p = new Persona('edu', 'eiximenis');
  12. console.log(p.twitterUrl());

Llamadme friki, pero a mi esto me encanta… Como puedes ver la única diferencia que hay respecto al código anterior es que ahora invocamos a la función anónima interna usando apply. Da igual que la función sea anónima y la defina justo en este lugar: puedo usar apply igualmente… Una maravilla. Si vienes de C# igual te cuesta entender esto, pero ten presente que en JavaScript las funciones son ciudadanas de primer orden. No necesitamos artificios raros como los delegates para poder pasar funciones como parámetro. Y las funciones tienen un prototipo común (de hecho apply está definido en el prototipo de Function).

El código para la segunda opción (guardar el valor de this) sería el siguiente:

  1. var Persona = function (n, tw) {
  2.     this.name= n;
  3.     this.twitter = tw;
  4.     this.twitterUrl = function () {
  5.         var self = this;
  6.         return (function () {
  7.             return 'http://twitter.com/' + self.twitter;
  8.         }());
  9.     }
  10. };
  11.  
  12. var p = new Persona('edu', 'eiximenis');
  13. console.log(p.twitterUrl());

El código sigue siendo muy parecido con la salvedad de que ahora en la línea 5 guardamos el valor de this en una variable llamada self. En la línea 5 el valor de this es el objeto que queremos (pues estamos dentro de la funcion twitterUrl y no dentro de la función interna). Y dentro de la función interna (línea 7) no usamos this si no la variable anterior (self). Es una convención usar los nombres that o self para esta variable que guarda el valor de this.

Bind

Bueno… vamos a ponernos ya en plan superlativo. Si creías que ya lo habíamos visto todo sobre JavaScript y this, todavía nos queda una sorpresa más: La función bind.

Bind es muy similar a call o apply, pero en lugar de devolver el resultado de la función que se llama, devuelve… otra función. ¿Y qué es esta otra función? Pues es la función original pero con el valor this por defecto establecido siempre al valor del parámetro pasado a bind.

Veamos un ejemplo:

  1. var Persona = function (n, tw) {
  2.     this.name= n;
  3.     this.twitter = tw;
  4. };
  5.  
  6. function getTwitterUri() {
  7.     return 'http://twitter.com/' + this.twitter;
  8. }
  9.  
  10. var p = new Persona('edu', 'eiximenis');
  11. console.log(getTwitterUri.apply(p));
  12.  
  13. var ftw = getTwitterUri.bind(p);
  14. console.log(ftw());

¿Cual es la salida de este código? ¡Exacto! Se imprime dos veces ‘http://twitter.com/eiximenis’:

  1. La primera vez es porque usamos apply para invocar a la función getTwitterUri con el objeto p, por lo que el valor de this dentro de getTwitterUri es el objeto p.
  2. La segunda es porque invocamos la funcion ftw. La función ftw es devuelta por la llamada a getTwitterUri.bind por lo que al invocar la función ftw el valor de this es el propio objeto p.

Ahora añadamos las siguientes dos líneas más de código:

  1. console.log(getTwitterUri.apply({ twitter: 'msdev_es' }));
  2. console.log(ftw.apply({ twitter: 'msdev_es' }));

¿Qué crees que imprimen esas dos líneas de código adicionales?

Si has respondido que ambas van a imprimir ‘http://twitter.com/msdev_es’ déjame darte una buena noticia y una mala:

  • La buena es que has entendido como funcionan call y apply! 🙂
  • La mala es que la respuesta no es correcta 😛

La primera línea SI que imprime ‘http://twitter.com/msdev_es’, ya que llamamos a getTwitterUri usando apply y pasándole un objeto cuya propiedad twitter es ‘msdev_es’…

… Pero la segunda línea NO imprime ‘http://twitter.com/msdev_es’. Debería hacerlo si ftw fuese una funcion “normal”. Pero ftw es una función obtenida a través de bind, por lo que sigue conservando su valor de this con independencia de call y apply.

Así que ya ves… Para saber el valor de this que usa una función ya ni te basta con mirar si es invocada usando call y apply: debes además saber si esta función ha sido generada a través de bind o no 😛

En fin… Creo que más o menos con esto cubrimos todos los aspectos de this en JavaScript. Como puedes ver, aunque su significado es parecido al de C# realmente es tan distinto que lo más normal es que si asumes que el this de JavaScript funciona como el de C# termines por no entender nada y maldecir a JavaScript… 😉

Un saludo! 😉

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *