Una de las características de JavaScript que más confusión genera es la palabra clave this. Más concretamente a qué o quién hace referencia y en qué circunstancias.

Para dejarlo claro desde el principio: this, de ninguna manera, es una referencia a una función. this tampoco permite acceder o hacer de puente entre ámbitos de diferentes funciones. No, nada de eso es this.

Pero entonces, ¿qué es?.

¿Que es this?

this es una referencia que se crea cuando una función es invocada, no declarada. El valor de esa referencia depende al 100% del lugar en la que esa invocación se realice, llamado call-site.

Ese lugar de llamada es la invocación en sí a la función. Es decir, el momento justo en que es llamada (no declarada, no referenciada) esa función.

Como estamos tratando con JavaScript, vamos a examinar los call-site a funciones en los escenarios más comunes.

this en el contexto global

Fuera de cualquier función, es decir, en el ámbito global, this siempre hace referencia al objeto global window:

// Estamos en el contexto de ejecución global
console.log(this === window); // true

this en invocaciones de funciones

Dentro de una función, el valor de this está determinado por el lugar en el que esa función es invocada.

En una sencilla función declarada, this hace referencia al objeto global window:

function funcion() {
    console.log(this);
}

funcion(); // window

Sin embargo, ese escenario cambia de forma drástica si utilizamos el modo estricto de JavaScript:

"use strict";

function funcion() {
    console.log(this);
}

funcion(); // undefined

Esta distinción es vital si no queremos ensuciar el ámbito global accidentalmente. Por ejemplo, utilizando de forma errónea una llamada de construcción a una función.

function Gato(raza, color) {
    this.raza = raza;
    this.color = color;
}

const nino = Gato('europeo', 'negro');
console.log(nino); // undefined

Como puedes comprobar, con sólo olvidar el operador new ya no estarás creando una nueva instancia de Gato bajo el objeto nino.

Lo que es peor: sin darte cuenta estarás añadiendo propiedades al objeto global window, ya que dentro de una función this hace siempre referencia al objeto global.

/* Código anterior */
console.log(window.raza, window.color); // europeo negro

Basta con usar el modo estricto para evitar estos accidentes:

"use strict"
/* Código anterior */
console.log(window.raza); // Uncaught TypeError: Cannot set property 'raza' of undefined

Este ejemplo me lleva al siguiente escenario de invocación o call-site: los constructores.

this en invocaciones de construcción

Como sabes, a diferencia de otros lenguajes, JavaScript no dispone de constructores como tal, sino de llamadas de construcción ”construction calls” a una función.

A través del operador new se crea un nuevo objeto, se asigna su prototipo a la función constructora y lo que es más importante: dentro de la función que hace de constructor, el valor de this hace referencia a ese nuevo objeto que se está creando.

Por eso, cuando una función es invocada como un constructor, gracias a ese valor específico de this podemos establecer los parámetros que recibe la función como propiedades del nuevo objeto:

function Gato(raza, color) {
    this.raza = raza;
    this.color = color;
    console.log(this);
    // Gato {raza: "europeo", color: "negro"}
}

const guizmo = new Gato('europeo', 'negro');

Sigamos hablando de objetos. En concreto en lo que ocurre con el valor de this cuando invocamos una función que pertenece a un objeto.

this en llamadas a métodos

Cuando una función es llamada como un método de un objeto, su contexto de this se asocia al objeto que contiene el método.

const rouco = {
    nombre: 'Rouco',
    especie: 'gato',
    saludar() {
        console.log('Miauuuuu (Hola me llamo ${this.nombre})');
        console.log(this === rouco);
    }
};

rouco.saludar(); 
// Miauuuuu (Hola me llamo Rouco) 
// true

Sin embargo es relativamente sencillo perder el contexto de this si por ejemplo guardamos el método en una función declarada para ejecutarla como tal y no como un método (con su referencia al objeto):

const rouco = {
    /* Código anterior */
};

let saludar = rouco.saludar;
saludar(); // <— Este es el "call-site"
// Miauuuuu (Hola me llamo undefined) 
// false

Fíjate que aunque nos estamos refiriendo al método de un objeto, la llamada (call-site) ocurre bajo la forma de una función declarada en el ámbito global.

Otra forma muy común de perder la referencia de this al objeto que contiene el método es al pasarlo —el método— como un callback a otra función:

function funcion() {
    console.log(this.lugar);
}

function ejecutar(funcion) {
    funcion(); // <— Este es el "call-site"
}

var objeto = {
    lugar: 'objeto',
    funcion: funcion,
};

var lugar = 'global';

ejecutar(objeto.funcion); // 'global'

El lugar donde se invoca la función es lo único importante. En los dos últimos casos se trata de una función declarada. Como ya sabes this, en ambos casos, hará referencia a window o undefined, dependiendo si estamos en modo estricto o no.

this en arrow functions

Las funciones flecha o «arrow functions» se comportan de una forma muy particular en lo que se refiere a this.

Cuando creamos una arrow function, su valor de this queda asociado permanentemente a al valor de this de su ámbito externo inmediato. window en el caso del ámbito global:

const funcion = () => {
    console.log(this === window);
};

funcion(); // true

O, si se encuentra dentro un método, el valor de this del método que la contiene. Esto hace que utilizar arrow functions como callbacks sea mucho más sencillo. Considera este ejemplo:

const contador = {
    cantidad: 0,
    incremetar() {
        setInterval(function() {
            console.log(++this.cantidad);
        }, 1000);
    }
};

contador.incremetar(); // NaN NaN NaN …

Aunque not a number (NaN) no es el resultado esperado, tiene sentido encontrarlo ya que dentro de setInterval() el valor de this, que dentro del método incrementar() hacía referencia al propio objeto contador, se ha perdido.

Estamos buscando la propiedad cantidad en el objeto global, window.cantidad en lugar de contador.cantidad.

Una solución muy común hasta la aparición de las funciones flecha es hacer una copia del valor de this para utilizarla en otros ámbitos:

const contador = {
    cantidad: 0,
    incremetar() {
            const that = this; // <— Hacemos una copia de this
        setInterval(function() {
            console.log(++that.cantidad); // La usamos…
        }, 1000);
    }
};

contador.incremetar(); // 1 2 3 …

Funciona, pero es una solución sucia y requiere crear una variable que sólo se utiliza para no perder el contexto de this. Afortunadamente, como las arrow functions tienen su valor de this asociado al ámbito exterior, resolver el problema del contador es sencillo:

const contador = {
    cantidad: 0,
    incremetar() {
        setInterval(() => {
            console.log(++this.cantidad);
        }, 1000);
    }
};

contador.incremetar(); // 1 2 3 …

Dentro de la función flecha el valor de this es el mismo que en el método incrementar, permitiendo entonces acceder a la propiedad cantidad sin problemas.

¿Qué hacer con this entonces?

Habiendo examinado las situaciones más comunes en las que invocar una función y que ocurre con el valor de this en cada una de ellas, en el próximo artículo veremos qué herramientas pone JavaScript a nuestra disposición para controlar e incluso forzar (hard binding) un valor de this determinado en cada una de nuestras llamadas a funciones.

Como teaser te diré que se trata de los métodos pertenecientes a todas las funciones: call(), apply() y bind().

Referencias