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:
1 |
var jmj = {}; |
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:
1 2 3 4 5 6 |
var jmj = {}; jmj.firstName = 'Jose'; jmj.lastName = 'Jimenez'; jmj.dateOfBirth = new Date(1967, 1, 9); console.debug(jmj.firstName); // Jose console.debug(jmj.dateOfBirth.getFullYear()); // 1967 |
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):
1 2 3 4 5 6 7 |
var jmj = { firstName : 'Jose', lastName : 'Jimenez', dateOfBirth : new Date(1967, 1, 9) }; console.debug(jmj.firstName); // Jose console.debug(jmj.dateOfBirth.getFullYear()); // 1967 |
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
:
1 2 3 4 5 6 7 8 |
var jmj = {}; jmj.firstName = 'Jose'; jmj.lastName = 'Jimenez'; jmj.dateOfBirth = new Date(1967, 1, 9); jmj.hello = function() { return 'hello, world'; }; console.debug(jmj.firstName); // Jose console.debug(jmj.dateOfBirth.getFullYear()); // 1967 console.debug(jmj.hello()); // hello, world |
O, usando la otra notación:
1 2 3 4 5 6 7 8 9 |
var jmj = { firstName : 'Jose', lastName : 'Jimenez', dateOfBirth : new Date(1967, 1, 9), hello : function() { return 'hello, world'; } }; console.debug(jmj.firstName); // Jose console.debug(jmj.dateOfBirth.getFullYear()); // 1967 console.debug(jmj.hello()); // hello, world |
¿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:
1 2 3 4 5 6 7 8 |
var jmj = {}; jmj.firstName = 'Jose'; jmj.lastName = 'Jimenez'; jmj.dateOfBirth = new Date(1967, 1, 9); jmj.hello = function() { return 'hello, world, I am ' + this.firstName; }; console.debug(jmj.firstName); // Jose console.debug(jmj.dateOfBirth.getFullYear()); // 1967 console.debug(jmj.hello()); // hello, world, I am Jose |
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:
1 2 3 4 5 6 7 8 |
var jmj = {}; jmj.firstName = 'Jose'; jmj.hello = function() { return 'hello, world, I am ' + this.firstName; }; var hello = jmj.hello; console.debug(jmj.hello()); // hello, world, I am Jose console.debug(hello()); // hello, world, I am undefined var firstName = 'Calvin'; console.debug(hello()); // hello, world, I am Calvin |
«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 variablehello
. Esta variablehello
, al no estar declarada dentro de una función, estará asignada al objeto globalwindow
(este detalle, normalmente sin importancia, en este ejemplo es importante). - Después, hemos ejecutado la variable
hello()
. Inicialmente, como apunta ajmj.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 variablehello
, es asignada al objeto globalwindow
. Y si ahora vuelvo a ejecutarhello()
… 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 objetofunction
apuntado por la variablejmj.hello
dentro del contexto del objetojmj
”. Dentro de este contexto, el identificadorthis
apunta al objetojmj
. - Pero cuando ejecuto
hello()
estoy pidiendo “ejecuta el objetofunction
apuntado por la variablehello
”. Pero como no le antepongo ningún contexto, lo ejecuto dentro del contexto global. Y el contexto global es el objetowindow
. En esta ejecución, el identificadorthis
apunta al objetowindow
.
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:
1 2 3 4 5 6 7 8 |
var jmj = {}; jmj.firstName = 'Jose'; jmj.hello = function() { return 'hello, world, I am ' + this.firstName; }; function sayHello(helloFunc) { return helloFunc(); }; console.debug(jmj.hello()); // hello, world, I am Jose console.debug(sayHello(jmj.hello)); // hello, world, I am undefined var firstName = 'Calvin'; console.debug(sayHello(jmj.hello)); // hello, world, I am Calvin |
- He definido una función
sayHello
que espera como parámetro un objetofunction
. Ejecutará dicho objeto y devolverá el resultado de su ejecución. - Como
jmj.hello
es un objetofunction
, 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 globalfirstName
.
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 sí funciona:
1 2 3 4 5 6 |
var jmj = {}; jmj.firstName = 'Jose'; jmj.hello = function() { return 'hello, world, I am ' + this.firstName; }; function sayHello(helloObj) { return helloObj.hello(); }; console.debug(jmj.hello()); // hello, world, I am Jose console.debug(sayHello(jmj)); // hello, world, I am Calvin |
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:
1 2 3 4 5 6 7 8 |
var myAlarm = {}; myAlarm.message = 'Wake up, Jose!!!'; myAlarm.timeout = 1000; myAlarm.activate = function() { setTimeout(function() { console.debug('New alarm: ' + this.message); }, this.timeout); }; myAlarm.activate(); // New alarm: undefined |
Creo que la intención es bastante directa:
- Defino un objeto
myAlarm
con dos propiedadesmessage
ytime
. - También le defino una función
activate
que invoca a la funciónsetTimeout
para que en el tiempo definido pormyAlarm.timeout
muestre el mensaje guardado enmyAlarm.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:
1 2 3 4 5 6 7 8 9 |
var myAlarm = {}; myAlarm.message = 'Wake up, Jose!!!'; myAlarm.timeout = 1000; myAlarm.activate = function() { var self=this; setTimeout(function() { console.debug('New alarm: ' + self.message); }, self.timeout); }; myAlarm.activate(); // New alarm: Wake up, Jose!!! |
¿Qué es lo que he hecho?:
- Dentro de mi función
myAlarm.activate
, he declarado una variableself
que apunta al valor dethis
en este momento de la ejecución. Cuando ejecutomyAlarm.activate()
,this
apunta al objetomyAlarm
. Así pues, la variableself
apuntará amyAlarm
. - Cuando declaro mi
setTimeout
, uso dentro la variableself
. - Cuando se ejecute la función definida dentro de
setTimeout
, se ejecuta dentro del contexto del objetowindow
. Así pues, como ya he dicho,this
apunta awindow
(por lo que no me sirve para lo que quiero hacer). Pero ya no me importa, porque yo estoy usando la variableself
… que sigue apuntando amyAlarm
. 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 eventokeyup
del elementotext
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var myTextInput = {}; myTextInput.subscribers = []; myTextInput.subscribe = function(action) { this.subscribers.push(action); }; myTextInput.id = 'input#text1'; myTextInput.activate = function() { jQuery(this.id).keyup(function() { for (var i=0; i<this.subscribers.length; i++) { this.subscribers[i](jQuery(this.id).val()); } }); jQuery(this.id).trigger('keyup'); }; myTextInput.subscribe(function(value) { jQuery('label#label1').text(value); }); myTextInput.subscribe(function(value) { jQuery('label#label2').text(value !== '' ? 'correct' : 'incorrect'); }); myTextInput.subscribe(function(value) { if (value === '') jQuery('input#button1').attr('disabled', 'disabled'); else jQuery('input#button1').removeAttr('disabled'); }); myTextInput.activate(); |
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var myTextInput = {}; myTextInput.subscribers = []; myTextInput.subscribe = function(action) { this.subscribers.push(action); }; myTextInput.id = 'input#text1'; myTextInput.activate = function() { console.debug(this); // object jQuery(this.id).keyup(function() { console.debug(this); // input type text for (var i=0; i<this.subscribers.length; i++) { this.subscribers[i](jQuery(this.id).val()); } }); jQuery(this.id).trigger('keyup'); }; myTextInput.subscribe(function(value) { jQuery('label#label1').text(value); }); myTextInput.subscribe(function(value) { jQuery('label#label2').text(value !== '' ? 'correct' : 'incorrect'); }); myTextInput.subscribe(function(value) { if (value === '') jQuery('input#button1').attr('disabled', 'disabled'); else jQuery('input#button1').removeAttr('disabled'); }); myTextInput.activate(); |
- La primera traza de
this
nos devuelve el objetomyTextInput
(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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
var myTextInput = {}; myTextInput.subscribers = []; myTextInput.subscribe = function(action) { this.subscribers.push(action); }; myTextInput.id = 'input#text1'; myTextInput.activate = function() { var me = this; jQuery(me.id).keyup(function() { for (var i=0; i<me.subscribers.length; i++) { me.subscribers[i](jQuery(me.id).val()); } }); jQuery(this.id).trigger('keyup'); }; myTextInput.subscribe(function(value) { jQuery('label#label1').text(value); }); myTextInput.subscribe(function(value) { jQuery('label#label2').text(value !== '' ? 'correct' : 'incorrect'); }); myTextInput.subscribe(function(value) { if (value === '') jQuery('input#button1').attr('disabled', 'disabled'); else jQuery('input#button1').removeAttr('disabled'); }); myTextInput.activate(); |
- Dentro de la función, creamos una variable
me
que apunta athis
(o sea, a nuestro objetomyTextInput
). - Dentro la llamada a
jQuery.keyup
en lugar de usar el identificadorthis
(que no apunta a nuestro objetomyTextInput
) usamos nuestra variableme
(que sí 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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var calvin = { name : 'Calvin', hello : function() { console.debug('Hello, I am ' + this.name); } }; var hobbes = { name : 'Hobbes', hello : function() { console.debug('Grrrr, I am ' + this.name); } }; var dad = {}; calvin.hello(); // Hello, I am Calvin hobbes.hello(); // Grrrr, I am Hobbes calvin.hello.call(hobbes); // Hello, I am Hobbes calvin.hello.call(dad); // Hello, I am undefined hobbes.hello.call(calvin); // Grrrr, I am Calvin |
¿Qué hemos hecho?:
- Primero, hemos declarado dos objetos (
calvin
yhobbes
) cada uno con una propiedadname
y un métodohello
. - 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? Nuestrocalvin.hello
es una propiedad de tipofunction
. Pues, por ser un objeto de tipofunction
este objeto tiene sus propias propiedades y métodos (entre ellas, el métodocall
). - 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óncalvin.hello
usando como contexto el objetohobbes
. Como este objeto tiene una propiedadname
que vale «Hobbes», por eso devuelve «Hello, I am Hobbes». - Pero cuando ejecutamos
calvin.hello.call(dad)
el nuevo contexto es el objetodad
que no tiene la propiedadname
. Por eso, devuelve «Hello, I am undefined». - Por último, ejecutamos
hobbes.hello
dentro del contexto del objetocalvin
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.