Javascript. La variable “this”
Una introducción a la variable “this” y la forma en que adopta su valor
English version: https://medium.com/@ger86/javascript-the-this-variable-da6c38765cb
La variable this
representa uno de los principales quebraderos de cabeza para los desarrolladores que comienzan a aprender Javascript. A diferencia de otros lenguajes donde su valor es muy intuitivo, en Javascript es necesario conocer una serie de reglas de cara a entender el valor que finalmente adoptará esta variable.
Con el fin de ayudar a que el proceso de familiarizarnos con esta variable sea mucho más fácil he escrito este artículo donde analizaré las principales normas que rigen el uso de this
en Javascript.
¡Espero que os sirva!
El entorno global de ejecución
Para comenzar a entender la variable this
lo primero de que deberemos hacer es referirnos al entorno de ejecución de nuestra aplicación.
Por ejemplo, si estamos ejecutando Javascript en un navegador, este entorno estará representado por el propio objeto window
, de modo que por defecto this
tendrá dicho valor:
window === this; // true
Por otra parte, si estamos trabajando con NodeJS el entorno estará representado por la variable global
por lo que:
global === this; // true
Es decir, this
está mucho más relacionado con el entorno o contexto de ejecución que con el concepto de esta misma variable en la programación orientada a objetos, donde this
representa al objeto en sí.
Funciones
A diferencia de la programación orientada a objetos donde sólo los objetos tienen disponible la variable this
, en Javascript podemos acceder a esta variable dentro de las funciones.
Como norma general el valor en estos casos estará asociado al objeto que invocó la función. ¿Qué quiere decir esto? Veámoslo con algunos ejemplos.
Supongamos que definimos la siguiente función en nuestro navegador:
function foo() {
console.log(this);
}
Y la invocamos desde la propia consola:
foo(); // Window
Lo que obtendremos al imprimir la variable this
será el objeto window
. ¿Por qué? Porque el objeto que invoca la función foo()
es, por defecto, el objeto global window
.
Lo mismo sucederá si por ejemplo escribimos la siguiente función:
function Person(name, email) {
this.name = name;
this.email = email;
}const person = Person('Gerardo', 'gerardo@latteandcode.com');
console.log(person);
En este caso, lo que obtendremos por pantalla será undefined
. ¿Por qué? Por la misma razón de antes. Person
es una función que está siendo invocada por el objeto global window
, de modo que al hacer la asignación this.name
lo que realmente estamos haciendo es window.name = name
.
Para lograr el efecto deseado deberemos recurrir al operador new
, de modo que, ahora sí, tengamos un objeto person
con sus propiedades name
y email
setteadas:
function Person(name, email) {
this.name = name;
this.email = email;
}const person = new Person('Gerardo', 'gerardo@latteandcode.com');
console.log(person);
New
En Javascript podemos añadir el operador new
delante de la invocación de una función para convertir dicha invocación en un constructor. Fue lo que hicimos en el ejemplo anterior cuando invocamos la función Person
:
const person = new Person('Gerardo', 'gerardo@latteandcode.com');
console.log(person);
Es decir, el operador new
permite la creación de un nuevo objeto que pasa a ser el this
de esa invocación y que es devuelvo implícitamente por la función invocada (salvo que explícitamente devolvamos otra cosa dentro de la función empleando return
).
Métodos
Bien, y ¿qué sucede cuando invocamos una función que pertenece a un objeto? Es decir, ¿qué valor adopta this
cuando invocamos el método de un objeto?
En general, es lo que esperamos:
const person = {
name: 'Gerardo',
surname: 'Fernández',
fullname: function() {
console.log(`${this.name} ${this.surname}`);
}
};person.fullname(); // Gerardo Fernández
Es decir, cuando invocamos la función fullname()
la variable this
es el objeto person
y por eso podemos acceder a sus propiedades name
y surname
. Esto se debe a que es el objeto person
quien está invocando el método fullname
.
Pero si por ejemplo lo cambiamos a lo siguiente:
const person = {
name: 'Gerardo',
surname: 'Fernández',
fullname: function() {
return function() {
console.log(`${this.name} ${this.surname}`);
};
}
};person.fullname()(); // undefined
Lo que obtenemos es undefined
en la consola. ¿Por qué? Veámoslo de otro modo:
name = 'Chrome';
surname = 'Window';const person = {
name: 'Gerardo',
surname: 'Fernández',
fullname: function() {
return function() {
console.log(`${this.name} ${this.surname}`);
};
}
};const printFullname = person.fullname();
printFullname(); // Chrome Window
Es decir, el objeto que está invocando la función devuelta por el método fullname
del objeto person
es el objeto global como sucedía al principio del artículo y, por tanto, this
tiene el valor del objeto global ( window
si estamos trabajando con la consola del navegador).
Bind, call y apply
Por tanto, this
puede perder la referencia según el objeto que invoque a la función. Para prevenir esto existe la posibilidad de emplear las funciones bind
, call
y apply
de cara a forzar el valor de this
en la invocación.
Por ejemplo, en el ejemplo anterior podemos emplear bind
para devolver una función cuyo valor para this
sea el propio objeto:
const person = {
name: 'Gerardo',
surname: 'Fernández',
fullname: function() {
const printFullname = function() {
console.log(`${this.name} ${this.surname}`);
};
return printFullname.bind(this);
}
};const printFullname = person.fullname(); // bound
printFullname(); // `Gerardo Fernández`
En este caso la variable this
dentro de la función printFullname
estará ya “siempre” ligada al objeto person
.
Otra forma es recurrir a las funciones call
y apply
que poseen un funcionamiento similar permitiéndonos especificar explícitamente el valor de this
.
const book = {
currentPage: 1,
gotoPage: function(page) {
this.currentPage = page;
console.log(`Current page: ${this.currentPage}`);
}
}const gotoPage = book.gotoPage;gotoPage.call(book, 10); // Current page: 10
gotoPage.apply(book, [10]); // Current page: 10
Nota. La diferencia como podéis ver entre call
y apply
es la forma de especificar los argumentos con los que será invocada la función: bien de forma individual o bien contenidos dentro de un array.
Arrow functions
Con la llegada de ES6 se introdujo una nueva forma de declarar funciones que afecta directamente al valor tomado por this
: las “arrow functions”.
Este tipo de funciones toman el valor de this
del contexto de ejecución que las rodea sin importar la forma en que sea invocada la función y previniendo que apply
, call
o bind
lo modifiquen.
En Javascript cada vez que se ejecuta una función se crea un contexto de ejecución. Sin embargo, las “arrow functions” no definen por sí mismas un contexto de ejecución por lo que lo toman del inmediatamente superior.
Veámoslo con un ejemplo:
const book = {
currentPage: 1,
readPage: function() {
setInterval(function() {
this.currentPage += 1;
console.log(this.currentPage);
}, 1000);
}
}book.readPage();
❗❗¿Qué creéis que imprime el código anterior por pantalla? Por “desgracia” una sucesión de Nan
ya que el “callback” de la función setInterval
es ejecutado en el contexto global, es decir, allí donde el valor de this
es window
. Para que lo visualicéis es como si internamente Javascript estuviese haciendo lo siguiente:
var callback = function() {
this.currentPage += 1;
console.log(this.currentPage);
}callback(); // cada 1 segundo
Sin embargo, las “arrow functions” nos permiten esquivar este problema:
const book = {
currentPage: 1,
readPage: function() {
setInterval(() => {
this.currentPage += 1;
console.log(this.currentPage);
}, 1000);
}
}book.readPage();
Puesto que las “arrow functions” toman su contexto del inmediatamente superior, el valor de this
será el que tenga la función readPage
. Puesto que estamos invocando el método readPage
del siguiente modo: book.readPage
, el valor de this
dentro del método será el del objeto book
y también así el de this
dentro de la “arrow function”.
Pero ojo, si hiciéramos lo siguiente:
const book = {
currentPage: 1,
readPage: () => {
setInterval(() => {
this.currentPage += 1;
console.log(this.currentPage);
}, 1000);
}
}const readPage = book.readPage;
readPage();
Volveríamos a recibir solo Nan
por pantalla, ya que el contexto de readPage
sería el global.
Clases
Las clases en Javascript también fueron una adición de ES6 con el objetivo de hacer más fácil la transición desde los lenguajes de programación orientada a objetos. No, antes de 2015 no había clases en Javascript. 😅
Una clase en Javascript generalmente contiene un método constructor
donde el valor de this
refiere al objeto que está siendo creado. Sin embargo, dentro de los métodos del objeto this
sigue las mismas reglas que hemos visto anteriormente:
class Person {
constructor(name, surname) {
this.name = name;
this.surname = surname;
} printFullname() {
console.log(`${this.name} ${this.surname}`);
}
}const person = new Person('Gerardo', 'Fernández');
person.printFullname(); // Gerardo Fernández ✅const foo = person.printFullname;
foo(); // Uncaught Type Error cannot read 'name' of undefined ❌
Como veis, al trabajar con clases no obtenemos undefined
cuando almacenamos una referencia al método de la función. Esto es debido a que las clases se ejecutan en strict mode
, de modo que el valor para this
en esa invocación para el cual no se ha definido ningún valor para this
es undefined
.
Esta es la razón por la que antiguamente en React se solía hacer el bind
de los “callbacks” asociados a eventos en el constructor:
Conclusiones
Como habéis podido ver, la variable this
puede ser a veces un quebradero de cabeza, especialmente en aquellos momentos en lo que perdemos la referencia a su valor como cuando trabajamos con “event listeners” o funciones de temporización.
Por eso me ha parecido interesante escribir este artículo resumiendo los casos principales para que así la próxima vez os resulte más fácil identificar su valor.
Referencias
¿Quieres recibir más artículos como este?
Si te ha gustado este artículo te animo a que te suscribas a la newsletter que envío cada domingo con publicaciones similares a esta y más contenido recomendado: 👇👇👇