El contexto de ejecución de las funciones en Javascript y el identificador «this»

En anteriores entradas, hemos visto cómo las funciones en Javascript son variables de primer orden (con las posibilidades que esto nos ofrece) y cuál es el ciclo de vida de las variables en Javascript (y en qué condiciones las funciones pueden acceder a las variables declaradas).

En esta nueva entrada, vamos a repasar juntos un concepto que es específico de Javascript (al menos, yo no recuerdo haberlo visto en ninguno de los lenguajes «clásicos» con los que me he encontrado) y que nos permite hacer ciertas operaciones propias de las «clases» de los lenguajes tradicionales… pero no exactamente igual: el contexto en que se ejecutan las funciones.

Pero ¿a qué me refiero con esto del «contexto»?

Creación de objetos y propiedades en Javascript

Vamos a crear un objeto en Javascript:

Ya está. Con esta simple expresión, ya hemos creado todo un objeto en Javascript llamado jmj. Por supuesto, es un objeto absolutamente vacío. Pero podemos añadirle nuevas propiedades en cualquier momento:

Pues sí, así de simple. Simplemente usando esta notación podemos añadirle en cualquier momento y a cualquier objeto nuevas propiedades (que pueden ser de cualquier tipo).

No es el tema de esta entrada (espero dedicar una entrada específica a la creación de objetos en Javascript), pero por lo menos mencionaros que si ya sabemos algunas de sus propiedades cuando creamos el objeto, podemos crearlas directamente cuando lo definimos (fijaros que la notación varía):

Los objetos function también pueden ser propiedades de objetos

Si los objetos de tipo function son variables de primer orden y puedo asignar cualquier variable como propiedad de un objeto… puedo crear propiedades de tipo function:

O, usando la otra notación:

¿Puede una propiedad de tipo function acceder a las demás propiedades del objeto?

“Jose, esto es estupendo. Si, además de esto, desde mis propiedades-funciones pudiera acceder a las demás propiedades del objeto, estas propiedades-funciones serían como métodos de los otros lenguajes de programación a los que estoy acostumbrado ¿Sería posible?”

Pues sí, es posible. Fijaros en este pequeño retoque:

Efectivamente, usando el identificador this desde dentro de mi función, puedo acceder al objeto donde se definió… y por tanto al resto de sus propiedades. Así, si uso dentro de mi función this.firstName estoy accediendo a jmj.firstName.

El contexto de ejecución de las funciones

«Jose, esto sí que es cojonudo, ya puedo crear objetos con propiedades y métodos como en los lenguajes a los que estoy acostumbrado. Bueno, gracias por todo y hasta luego»

No corras mucho todavía. Lo que he explicado es cierto… pero no es una explicación completa. Vamos a ver otro trozo de código:

«Jose, no entiendo nada ¿qué es lo que has hecho?»

A ver si lo estudiamos juntos:

  • Primero, he definido mi objeto jmj con sus propiedades y su «método» hello.
  • Después, he comprobado que el «método» hello funciona correctamente y devuelve «hello, world, I am Jose».
  • A continuación, como hello es una variable, puedo hacer con ella lo que hago con cualquier variable. Así que la he asignado a la variable hello. Esta variable hello, al no estar declarada dentro de una función, estará asignada al objeto global window (este detalle, normalmente sin importancia, en este ejemplo es importante).
  • Después, hemos ejecutado la variable hello(). Inicialmente, como apunta a jmj.hello, debería devolver lo mismo («hello, world, I am Jose»)… pero devuelve «hello, world, I am undefined» (¿¿¿WTF???).
  • Para enredar más la cosa, a continuación he definido una nueva variable fistName. Lo mismo que la variable hello, es asignada al objeto global window. Y si ahora vuelvo a ejecutar hello()… de repente, ahora «funciona».

¿Qué es lo que está pasando? Que cuando ejecuto jmj.hello() y hello() sus contextos de ejecución son distintos y por lo tanto el identificador this vale distinto en cada ejecución:

  • Cuando ejecuto jmj.hello() estoy pidiendo “ejecuta el objeto function apuntado por la variable jmj.hello dentro del contexto del objeto jmj”. Dentro de este contexto, el identificador this apunta al objeto jmj.
  • Pero cuando ejecuto hello() estoy pidiendo “ejecuta el objeto function apuntado por la variable hello”. Pero como no le antepongo ningún contexto, lo ejecuto dentro del contexto global. Y el contexto global es el objeto window. En esta ejecución, el identificador this apunta al objeto window.

Por eso, en la primera ejecución de hello() devuelve «hello, world, I am undefined» (porque en el momento de esta ejecución en el contexto window no existe la propieda firstName). Pero en la segunda ejecución (después de haber declarado la variable firstName) la propiedad window.firstName ya existe y la ejecución devuelve «hello, I am Calvin».

«Pero, Jose, este ejemplo es muy enrevesado, esta casuística no me la voy a encontrar en mis programas Javascript, así que este tipo de errores deben ser muy raros»

Efectivamente, el ejemplo que he puesto es muy artificial. Pero lo he construido a propósito (lo más pequeño y claro posible) para evidenciar el error. Pero este mismo error nos lo podemos encontrar en situaciones más comunes de las que inicialmente esperamos. Veamos el próximo código.

Pasar un «método» como parámetro a una función

Vamos a estudiar una ligera variante del código anterior:

  • He definido una función sayHello que espera como parámetro un objeto function. Ejecutará dicho objeto y devolverá el resultado de su ejecución.
  • Como jmj.hello es un objeto function, puedo pasárselo como parámetro a mi nueva función… pero el resultado vuelve a devolver «hello, world, I am undefined»… hasta que definimos la variable global firstName.

Nos encontramos con un caso exactamente igual al anterior: cuando pasamos un «método» como parámetro a una función y ejecutamos dicho «método» dentro de esa función (una situación más común de lo que creéis cuando comenzamos a estructurar nuestro código en objetos y tenemos que responder a muchísimos eventos y operaciones asíncronas pasando objetos function como parámetros)… ha perdido su conexión con el objeto donde se definió y el identificador this no funciona como esperábamos. Por eso no escribo método sino «método» (porque es como un método… pero no es igual).

Para insistir cómo el contexto varía según la ejecución, veamos una variante del código anterior que funciona:

En este caso, nuestra función espera un parámetro de tipo function y pasamos como parámetro el objeto jmj. El parámetro helloObj apunta a jmj. Y cuando ejecuto helloObj.hello(), la referencia a this dentro de la ejecución apunta helloObj (y, por tanto, a jmj) ya funciona correctamente.

Vamos a ver un par más de ejemplos. Son ejemplos pequeños, pero que espero que reproduzcan situaciones comunes que nos encontramos cuando programamos en Javascript. Y en los que tenemos que tener cuidado con el valor del identificador this.

Ejecución de «métodos» dentro de eventos

Ahora, vamos a definir una alarma:

Creo que la intención es bastante directa:

  • Defino un objeto myAlarm con dos propiedades message y time.
  • También le defino una función activate que invoca a la función setTimeout para que en el tiempo definido por myAlarm.timeout muestre el mensaje guardado en myAlarm.message.

Sin embargo, cuando ejecuto myAlarm.activate() a los 1000ms (correcto) muestra el mensaje «New alarm: undefined» (¿¿¿WTF???).

¿Qué es lo que ha ocurrido? Lo que ocurre es que la función anónima que le paso como parámetro a setTimeout se ejecuta dentro del contexto del objeto window. Y, dentro de este contexto, la propiedad message no existe (insisto: en este contexto, this.message equivale a window.message y no myAlarm.message).

¿Cómo puedo solucionar este error? Si recordamos la entrada sobre alcance de las variables y closures, la solución la tenemos cerca:

¿Qué es lo que he hecho?:

  • Dentro de mi función myAlarm.activate, he declarado una variable self que apunta al valor de this en este momento de la ejecución. Cuando ejecuto myAlarm.activate(), this apunta al objeto myAlarm. Así pues, la variable self apuntará a myAlarm.
  • Cuando declaro mi setTimeout, uso dentro la variable self.
  • Cuando se ejecute la función definida dentro de setTimeout, se ejecuta dentro del contexto del objeto window. Así pues, como ya he dicho, this apunta a window (por lo que no me sirve para lo que quiero hacer). Pero ya no me importa, porque yo estoy usando la variable self… que sigue apuntando a myAlarm. Por eso, en esta segunda versión, mi objeto alarma funciona correctamente.

Manejo de eventos con jQuery

“Bueno, Jose, pues ya sé que debo tener cuidado cuando use la función setTimeOut. Pero tampoco es para tanto”

El problema es que la situación anterior puede ocurrirnos en más ocasiones de las que parece a simple vista. Veamos este ejemplo:

  • Tenemos un elemento text y queremos realizar una serie indeterminada de acciones cada vez que su contenido cambie.
  • Para gestionar esta serie de acciones, hemos decidido crear un objeto myTextInput que las gestione.
  • Para ellos, este objeto tiene una propiedad subscribers que es un array con todas las acciones a ejecutar.
  • Para añadir nuevas acciones, tiene el método subscribe a la que se le pasa como parámetro una función callback con la acción a ejecutar cuando ocurra el evento.
  • Por último, tiene un método activate que usará jQuery parar capturar el evento keyup del elemento text y ejecutará la lista de acciones suscritas (pasándole a cada una como parámetro el nuevo contenido).

El código javascript es el siguiente:

En este ejemplo, hemos registrado tres acciones distintas: actualizar el elemento label con id label1, actualizar el elemento label con id label2 y habilitar / inhabilitar el elemento button con id button.

Podéis verlo en funcionamiento aquí:

Como veis, no funciona (el elemento button, por ejemplo, debería estar inhabilitado cuando el elemento text está vacío) ¿Qué estamos haciendo mal? Afortunadamente, la veteranía es un grado y decidimos comprobar el valor del identificador this:

  • La primera traza de this nos devuelve el objeto myTextInput (correcto).
  • Pero la segunda traza, nos devuelve el elemento input (¿¿¿WTF???).

¿Por qué la segunda traza no devuelve nuestro objeto myTextInput, como queremos? El problema, es que esta segunda traza está dentro de la llamada a jQuery.keyup. Y dentro de esta llamada, jQuery hace que el identificador this apunte al elemento que está procesando (o, sea el elemento input). Por eso, deja de apuntar a nuestro objeto deseado myTextInput.

La solución para corregir este cambio de contexto no deseado, es el mismo que en el ejemplo anterior:

  • Dentro de la función, creamos una variable me que apunta a this (o sea, a nuestro objeto myTextInput).
  • Dentro la llamada a jQuery.keyup en lugar de usar el identificador this (que no apunta a nuestro objeto myTextInput) usamos nuestra variable me (que que apunta a nuestro objeto).

El ejemplo operativo es éste:

Como podéis comprobar, dada la frecuencia que empleamos «métodos» de objetos como funciones callback para atender a eventos asíncronos, las posibilidades de tener errores por cambios de contexto son mayores de lo que parece a simple vista.

Los métodos apply y call de las funciones

Si queremos asignar de forma inequívoca sobre qué contexto queremos ejecutar una función (o queremos cambiar el contexto sobre el que se ejecuta una función) debemos usar los métodos apply y call de dicho objeto function:

¿Qué hemos hecho?:

  • Primero, hemos declarado dos objetos (calvin y hobbes) cada uno con una propiedad name y un método hello.
  • Hemos comprobado que los dos métodos funcionan según lo previsto.
  • Ahora viene lo interesante: hemos invocado al método calvin.hello.call ¿Qué hemos hecho? Nuestro calvin.hello es una propiedad de tipo function. Pues, por ser un objeto de tipo function este objeto tiene sus propias propiedades y métodos (entre ellas, el método call).
  • Este método calvin.hello.call admite como argumento un objeto. Cuando lo ejecutamos, se ejecuta a sí misma pero dentro del contexto del objeto que le pasamos como parámetro.
    • Cuando ejecutamos calvin.hello.call(hobbes) estamos ejecutando la función calvin.hello usando como contexto el objeto hobbes. Como este objeto tiene una propiedad name que vale «Hobbes», por eso devuelve «Hello, I am Hobbes».
    • Pero cuando ejecutamos calvin.hello.call(dad) el nuevo contexto es el objeto dad que no tiene la propiedad name. Por eso, devuelve «Hello, I am undefined».
  • Por último, ejecutamos hobbes.hello dentro del contexto del objeto calvin por lo que devuelve «Grrr, I am Calvin».

Además de este método, está el método function.apply. Es lo mismo que function.call pero además admite un segundo argumento: un array que serán los parámetros que se le pasen a function cuando se invoque (o sea, que se le pasan el contexto en que se ejecuta y los parámetros con los que debe ejecutarse).

¿Muy lioso? Espero que no. Pero estos dos métodos de los objetos function los puedes llegar a necesitar cuando definas librerías de objetos (con propiedades y «métodos») y necesites ejecutar un «método» dentro del contexto de otro objeto distinto en el que se definió.

¿Conclusiones?

El contexto en el que se ejecuta una función (y el valor de this) es uno de los conceptos que, personalmente, considero pueden ser más problemáticos para dominar en Javascript. Y una fuente potencial de errores. Desgraciadamente, conforme comiences a declarar objetos con propiedades y «métodos» necesitas dominar este concepto.

No me gusta usar la palabra método porque sugiere que estamos hablando de métodos similares a otros lenguajes de programación. Pero ya hemos visto que son propiedades de tipo function que podemos ejecutar tanto dentro del contexto del objeto donde se definió… pero también pueden ejecutarse en otros contextos (pasarla como parámetro, como respuesta a un evento asíncrono, etc). Son métodos… pero no lo son. Por eso, veréis que he preferido usar «método».

Para nuestras prácticas, hemos tenido que crear nuestros propios objetos. Son objetos muy simples y creados de la forma más rudimentaria que nos permite Javascript. Pero el objetivo de esta entrada no es la creación de objetos, sino ejecutar sus «métodos» y dominar su contexto de ejecución. Espero en un futuro cercano dedicar una entrada específica a la creación de objetos siguiendo las mejores prácticas que me he ido encontrando en mis aventuras.

Deja una respuesta

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

*
*
Sitio Web