Entendiendo this en javascript, es fácil.

Es el gran enemigo de mucha gente, sobre todo de los que están empezando, hablamos de this en Javascript, y es que en realidad esta palabra reservada de Javascript es un poco peculiar con respecto a la mayoría de los lenguajes de programación que la usan, debido a como Javascript maneja el contexto.

Pero, si te paras a pensarlo y entiendes el contexto en realidad es bastante simple. En este artículo vamos a intentar explicarlo de forma que se entienda fácil para poder utilizarlo sin equivocarte y sin entrar en demasiados tecnicismos sobre el contexto, eso lo dejaremos para otro artículo.

De las principales situaciones en la que la gente suele hacerse un lío es esta:

function foo() {
  return this === window;
}

function foo2() {
  return this === foo2
}

console.log(foo()); // Expected output: true
console.log(foo2()); // Expected output: false

Como podemos ver, en principio puede resultar poco intuitivo, muchos tienden a pensar que en el código anterior this debería hacer referencia a la función, pero si ejecutamos el código, vemos que en realidad en this hace referencia al objeto window. ¿Y esto por qué?

En realidad, es fácil de entender si ejecutamos el siguiente código:

function foo() {
  return this === window;
}

console.log(window.foo()) // Expected output: true

Como podemos ver, la función foo está en el ámbito global de Javascript, es decir, pertenece al objeto window. ¿Y esto que quiere decir? pues que "este" es window. La palabra this siempre hace referencia al objeto al que pertenece (técinamente hablando, el objeto instanciado). Hagamos esto ahora:

function foo() {  
  this.isWindow = function() {
    return this === window;
  }
  
  this.isMyObject = function() {
    return this === MyObject;
  }
  
  return this === window;
}

console.log(window.foo()) // Expected output: true

const MyObject = new foo();
console.log(MyObject.isWindow()); // Expected output: false
console.log(MyObject.isMyObject()); // Expected output: true

¿Qué ha pasado ahora? Bien, foo sigue siendo una función que pertenece al ámbito global, es decir, al objeto window, pero si bien en el primer console.log estamos llamando a foo desde el global (window.foo()), justo después estamos creando un objeto de foo con la palabra new. Por lo tanto si el dueño de foo en el primer caso es objeto window, en el segundo, el dueño es el objeto instanciado MyObject.

Con esto ya es posible hacerse una idea de que es this en Javacript y es que para deducirlo, tan solo hay que preguntarse ¿quién es el objeto dueño de esto?, sabiendo que una función no es un objeto a no ser que sea instanciada con la palabra clave new.

Vínculando el ámbito (bind)

En muchos casos nos encontramos con la necesidad de vincular el ámbito en una función y para eso Javascript nos ofrece la posibilidad de hacerlo con el método bind. Tomemos en cuenta el siguiente ejemplo:

function Car() {
  this.fuelLevel = "40 litters";
  
  setTimeout(function() {
    console.log(`Fuel level: ${this.fuelLevel}`)
  }, 500)
}

const MyCar = new Car(); // Expected output: 'Fuel level: undefined'

¿Qué está ocurriendo? bien en este caso, cuando llamamos a setTimeout que pertenece al ámbito global (window.setTimeout) le estamos pasando una función de callback que se ejecuta pasados 500ms, por tanto la función de callback, está dentro del ámbito del objeto window. Pero de nuevo nos podemos encontrar con algo poco intuitivo si hacemos algo así, veamos dos casos:

function Car() {
  this.fuelLevel = "40 litters";
  
  this.logFuelLevel = function() {
    console.log(`Fuel level: ${this.fuelLevel}`);
  }
  
  this.logFuelLevel();
}

const MyCar = new Car(); // Expected output: 'Fuel level: 40 litters'
function Car() {
  this.fuelLevel = "40 litters";
  
  this.logFuelLevel = function() {
    console.log(`Fuel level: ${this.fuelLevel}`);
  }
  
  setTimeout(this.logFuelLevel, 500)
}

const MyCar = new Car(); // Expected output: 'Fuel level: undefined'

Como podemos ver, en el primer ejemplo, this.logFuelLevel se comporta como nos dice la intuición, y nos devuelve el nivel de combustible del coche. Sin embargo, en el segundo caso nos dice que el nivel de combustible no está definido. Y es que para el segundo caso this.logFuelLevel se está llamando desde el setTimeout que pertenece al ámbito global, y es por eso que no está definido. El dueño de this no es Car, es window que es donde está definido el método setTimeout. Probemos esto:

var fuelLevel = "20 litters";

function Car() {
  this.fuelLevel = "40 litters";
  
  this.logFuelLevel = function() {
    console.log(`Fuel level: ${this.fuelLevel}`);
  }
  
  setTimeout(this.logFuelLevel, 500)
}

const MyCar = new Car(); // Expected output: 'Fuel level: 20 litters'

Ahora al declarar la variable fuelLevel con var (const y let se comportan diferente, pero eso quizás para otro artículo) en el ámbito global es como si estuviéramos haciendo esto:

window.fuelLevel = "20 litters";

por lo tanto, dentro del ámbito global (window) fuelLevel ahora si está definido y como setTimeout pertenece al ámbito global esta vez nos dice que son 20 litters. Como hemos dicho parece poco intuitivo, pero de nuevo la pregunta a hacerse sería ¿quién es el dueño de setTimeout que es el método al que estamos llamando? y la respuesta te dice quien es this.

Pero como hemos dicho Javascript nos da una opción de vincular las funciones con el métod bind. Vamos a vincularlo en los dos suiguientes ejemplos:

function Car() {
  this.fuelLevel = "40 litters";
  
  setTimeout(function() {
    console.log(`Fuel level: ${this.fuelLevel}`)
  }.bind(this), 500)
}

const MyCar = new Car(); // Expected output: 'Fuel level: 40 litters'
function Car() {
  this.fuelLevel = "40 litters";
  
  this.logFuelLevel = function() {
    console.log(`Fuel level: ${this.fuelLevel}`);
  }
  
  setTimeout(this.logFuelLevel.bind(this), 500)
}

const MyCar = new Car(); // Expected output: 'Fuel level: 40 litters'

En estos dos casos, le estamos pidiendo a Javascript que nos vincule la función con el this actual, es decir con el objeto Car, por tanto, this dentro del setTimeout ahora queda vinculado a Car.

Otro ejemplo de vinculación a la inversa podría ser este:

var fuelLevel = "20 litters";

function logFuelLevel() {
  console.log(`Fuel level: ${this.fuelLevel}`);
}

const foo = logFuelLevel;
foo();  // Expected output: 'Fuel level: 20 litters' 
var fuelLevel = "20 litters";

function logFuelLevel() {
  console.log(`Fuel level: ${this.fuelLevel}`);
}

function Car() {
  this.fuelLevel = "40 litters";
}

const MyCar = new Car();

const foo = logFuelLevel.bind(MyCar);
foo();  // Expected output: 'Fuel level: 40 litters' 

En este caso, tenemos una función que imprime un log de this.fuelLevel esta vez fuera de la clase. Como vemos en el primer ejemplo, obviamente, nos dice que el nivel de combustible son 20 litros ya que el this dentro de la función logFuelLevel pertence al ámbito global, como veíamos al principio del artículo. Pero en el segundo ejemplo, hemos vinculado la función logFuelLevel al ámbito del objeto MyCar, por lo que la salida son 40 litros.

Es decir, en el primer ejemplo el dueño de logFuelLevel es el objeto window, mientras que en el segundo, tras haberla vinculado el dueño es el objeto MyCar

"this" y las funciones de flecha

Parece que estos de Javascript quieren ponerlo complicado, pero para eso han introducido las funciones flecha, con la intención de simplificar un poco el ámbito de this, probemos esto:

var fuelLevel = "20 litters";

function Car() {
  this.fuelLevel = "40 litters";
  
  setTimeout(() => {
    console.log(`Fuel level: ${this.fuelLevel}`)
  }, 500)
}

const MyCar = new Car(); // Expected output: 'Fuel level: 40 litters'
var fuelLevel = "20 litters";

function Car() {
  this.fuelLevel = "40 litters";
  
  setTimeout(function() {
    console.log(`Fuel level: ${this.fuelLevel}`)
  }, 500)
}

const MyCar = new Car(); // Expected output: 'Fuel level: 20 litters'

Como vemos ahora esto se comporta como esperamos sin necesidad de haer ningún tipo de vinculación ya que las funciones flecha se ejecutan en el ámbito donde se crean por lo que se podría decir que "heredan el this".

En el primero de los dos ejemplos anteriores, utilizamos una función flecha, que se crea en la clase Car por lo que el ámbito es "heredado" de donde se ha creado, mientras que en el segundo ejemplo, utilizamos una función clásica por lo tanto respeta el ámbito donde se ejecuta aunque se haya creado en la clase Car.

Esto simplifica la escritura de código y nos ahorra el tener que estar vinculando o pensando el ámbito donde se ejecuta la función ya que siempre es el ámbito donde ha sido creada.

El viejo truco de self

Si visitas repositorios antiguos te darás cuenta que en muchas ocasiones ver que this es asignado a una variable llamada self, esto se convirtió en una especie de estandar popular para evitar el conflicto con el ámbito al usar this, veamos un ejemplo:

var fuelLevel = "20 litters";

function Car() {
  var self = this;
  this.fuelLevel = "40 litters";
  
  setTimeout(function() {
    console.log(`Fuel level: ${self.fuelLevel}`)
  }, 500)
}

const MyCar = new Car(); // Expected output: 'Fuel level: 40 litters'

Como se puede observer, self siempre es el this del ámbito Car y de este modo se evitaba la confusión, pero como hemos visto anteriormente, con las funciones flecha, este problema desaparece y el uso de self carece de sentido.

Clases, this, addEventListener y removeEventListener

Terminemos el artículo con un ejemplo dentro de una clase que suelo ver que crea confusión en gente que está empezando.

Supongamos que tenemos una clase Car para seguir con el ejemplo y tenemos un input[name=refuel] que al introducir una cantidad la añade al nivel de combustible del coche con un addEventListener.

class Car {
  constructor(manufacturer, model, color) {
    this.manufacturer = manufacturer;
    this.model = model;
    this.color = color;
    this.fuelLevel = 10;
    
    document.querySelector("input[name=refuel]")
      .addEventListener("change", this.handleRefuel);
  }
  
  handleRefuel(ev) {
    const fuelToAdd = parseFloat(ev.target.value);
    this.addFuel(fuelToAdd);
  }
  
  addFuel(fuelToAdd) {
    this.fuelLevel += fuelToAdd;
  }
}

const MyCar = new Car();

Esto no va a funcionar, nos dará un error diciendo que addFuel no está definido, ¿por qué?, por lo que explicamos al principio del artículo, el método addEventListener pertenece al ámbito global, por lo que this es window y en el ámbito global no se encuentra el método addFuel.

Para solucionarlo tenemos que hacer una vinculación del objeto tal que así:

class Car {
  constructor(manufacturer, model, color) {
    this.manufacturer = manufacturer;
    this.model = model;
    this.color = color;
    this.fuelLevel = 10;
    
    document.querySelector("input[name=refuel]")
      .addEventListener("change", this.handleRefuel.bind(this));
  }
  
  handleRefuel(ev) {
    const fuelToAdd = parseFloat(ev.target.value);
    this.addFuel(fuelToAdd);
  }
  
  addFuel(fuelToAdd) {
    this.fuelLevel += fuelToAdd;
  }
}

const MyCar = new Car();

Esto si funcionaría. Pero ahora supongamos que en algún momento queremos dejar de escuchar el evento, por ejemplo, si el input desaparece del DOM. Para el ejemplo, vamos a querer dejar de escuchar el cambio en el input pasado un minuto y podríamos querer hacer algo así.

class Car {
  constructor(manufacturer, model, color) {
    this.manufacturer = manufacturer;
    this.model = model;
    this.color = color;
    this.fuelLevel = 10;
    
    document.querySelector("input[name=refuel]")
      .addEventListener("change", this.handleRefuel.bind(this));

    setTimeout(() => {
      document.querySelector("input[name=refuel]")
        .removeEventListener("change", this.handleRefuel.bind(this));
    }, 60000);
  }
  
  handleRefuel(ev) {
    const fuelToAdd = parseFloat(ev.target.value);
    this.addFuel(fuelToAdd);
  }
  
  addFuel(fuelToAdd) {
    this.fuelLevel += fuelToAdd;
  }
}

const MyCar = new Car();

Puede parecernos que está bien, pero en realidad esto no eliminaría el evento, ya que el método removeEventListener requiere que pasemos el mismo método (referencia) para que tenga efecto. Algo así:

function handleRefuel() {
  const fuelToAdd = parseFloat(ev.target.value);
  console.log(`Added ${fuelToAdd}`)
}
    
document.querySelector("input[name=refuel]")
  .addEventListener("change", handleRefuel);

setTimeout(() => {
  document.querySelector("input[name=refuel]")
    .removeEventListener("change", handleRefuel);
}, 60000);

En este caso si funcionaría el removeEventListener ya que la referencia de handleRefuel es exactamente la misma. Pero en el caso de nuestra clase no, porque el método bind no retornaría la referencia de método de nuestra clase this.handleRefuel si no, sin entrar en mucho detalle una copia del mismo. Ya hablaremos de las referencias en otro artículo. Por lo tanto en realidad el método pasado al addEventListener aunque lo pueda parecer no es el mismo que el pasado al removeEventListener. Para solcionar esto podemos recurrir al truco antiguo de usar el self:

class Car {
  constructor(manufacturer, model, color) {
    this.manufacturer = manufacturer;
    this.model = model;
    this.color = color;
    this.fuelLevel = 10;
    
    const self = this;
    
    document.querySelector("input[name=refuel]")
      .addEventListener("change", self.handleRefuel);

    setTimeout(() => {
      document.querySelector("input[name=refuel]")
        .removeEventListener("change", self.handleRefuel);
    }, 60000);
  }
  
  handleRefuel(ev) {
    const fuelToAdd = parseFloat(ev.target.value);
    this.addFuel(fuelToAdd);
  }
  
  addFuel(fuelToAdd) {
    this.fuelLevel += fuelToAdd;
  }
}

const MyCar = new Car();

O bien podemos usar una forma más moderna, hacer uso de la función flecha, que como hemos mencionado mantiene el ámbito de donde se crea y devolver el método handleRefuel de nuestra clase:

class Car {
  constructor(manufacturer, model, color) {
    this.manufacturer = manufacturer;
    this.model = model;
    this.color = color;
    this.fuelLevel = 10;
    
    document.querySelector("input[name=refuel]")
      .addEventListener("change", () => this.handleRefuel);

    setTimeout(() => {
      document.querySelector("input[name=refuel]")
        .removeEventListener("change", () => this.handleRefuel);
    }, 60000);
  }
  
  handleRefuel(ev) {
    const fuelToAdd = parseFloat(ev.target.value);
    this.addFuel(fuelToAdd);
  }
  
  addFuel(fuelToAdd) {
    this.fuelLevel += fuelToAdd;
  }
}

const MyCar = new Car();

Ambas soluciones sirven ya que en ambas estamos respetando el ámbito y por tanto this es nuestro objeto MyCar