JavaScript: ¿Cuál es el significado?

Descubrir el valor de this puede ser complicado en JavaScript. A continuación, te mostramos cómo hacerlo…

El this de JavaScript es el blanco de muchos chistes, y eso se debe a que es bastante complicado. Sin embargo, he visto a desarrolladores hacer tareas mucho más complejas y específicas del dominio para evitar lidiar con este this. Si tienes dudas sobre this, esperamos que esta información te resulte útil. Esta es mi guía de this.

Comenzaré con la situación más específica y terminaré con la menos específica. Este artículo es como un gran if (…) … else if () … else if (…) …, por lo que puedes ir directamente a la primera sección que coincida con el código que estás viendo.

  1. Si la función se define como una función flecha
  2. De lo contrario, si se llama a la función o clase con new
  3. De lo contrario, si la función tiene un valor this "limitado"
  4. De lo contrario, si this se configuró en el momento de la llamada
  5. De lo contrario, si se llama a la función a través de un objeto superior (parent.func())
  6. De lo contrario, si la función o el alcance superior están en modo estricto
  7. De lo contrario

Si la función se define como una función flecha, haz lo siguiente:

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

En este caso, el valor de this siempre es el mismo que this en el alcance superior:

const outerThis = this;

const arrowFunction = () => {
  // Always logs `true`:
  console.log(this === outerThis);
};

Las funciones de flecha son excelentes porque el valor interno de this no se puede cambiar, es siempre el mismo que el this externo.

Otros ejemplos

Con las funciones de flecha, no se puede cambiar el valor de this con bind:

// Logs `true` - bound `this` value is ignored:
arrowFunction.bind({foo: 'bar'})();

Con las funciones de flecha, el valor de this no se puede cambiar con call o apply:

// Logs `true` - called `this` value is ignored:
arrowFunction.call({foo: 'bar'});
// Logs `true` - applied `this` value is ignored:
arrowFunction.apply({foo: 'bar'});

Con las funciones de flecha, el valor de this no se puede cambiar llamando a la función como miembro de otro objeto:

const obj = {arrowFunction};
// Logs `true` - parent object is ignored:
obj.arrowFunction();

Con las funciones de flecha, el valor de this no se puede cambiar llamando a la función como un constructor:

// TypeError: arrowFunction is not a constructor
new arrowFunction();

Métodos de instancia “vinculada”

Con los métodos de instancia, si quieres asegurarte de que this siempre haga referencia a la instancia de la clase, la mejor manera es usar funciones de flecha y campos de clase:

class Whatever {
  someMethod = () => {
    // Always the instance of Whatever:
    console.log(this);
  };
}

Este patrón es muy útil cuando se usan métodos de instancia como objetos de escucha de eventos en componentes (como componentes de React o componentes web).

Lo anterior podría parecer que incumple la regla "this será igual que this en el alcance superior", pero comienza a tener sentido si consideras que los campos de clase son sintaxis enriquecida para configurar elementos en el constructor:

class Whatever {
  someMethod = (() => {
    const outerThis = this;
    return () => {
      // Always logs `true`:
      console.log(this === outerThis);
    };
  })();
}

// …is roughly equivalent to:

class Whatever {
  constructor() {
    const outerThis = this;
    this.someMethod = () => {
      // Always logs `true`:
      console.log(this === outerThis);
    };
  }
}

Los patrones alternativos implican vincular una función existente en el constructor o asignar la función en el constructor. Si, por algún motivo, no puedes usar campos de clase, asignar funciones en el constructor es una alternativa razonable:

class Whatever {
  constructor() {
    this.someMethod = () => {
      // …
    };
  }
}

De lo contrario, si se llama a la función o clase con new:

new Whatever();

Lo anterior llamará a Whatever (o a su función de constructor si es una clase) con this establecido en el resultado de Object.create(Whatever.prototype).

class MyClass {
  constructor() {
    console.log(
      this.constructor === Object.create(MyClass.prototype).constructor,
    );
  }
}

// Logs `true`:
new MyClass();

Lo mismo ocurre con los constructores de estilo antiguo:

function MyClass() {
  console.log(
    this.constructor === Object.create(MyClass.prototype).constructor,
  );
}

// Logs `true`:
new MyClass();

Otros ejemplos

Cuando se llama con new, el valor de this no se puede cambiar con bind:

const BoundMyClass = MyClass.bind({foo: 'bar'});
// Logs `true` - bound `this` value is ignored:
new BoundMyClass();

Cuando se llama a new, no se puede cambiar el valor de this llamando a la función como miembro de otro objeto:

const obj = {MyClass};
// Logs `true` - parent object is ignored:
new obj.MyClass();

De lo contrario, si la función tiene un valor this "limitado", sucede lo siguiente:

function someFunction() {
  return this;
}

const boundObject = {hello: 'world'};
const boundFunction = someFunction.bind(boundObject);

Cada vez que se llame a boundFunction, su valor this será el objeto que se pasa a bind (boundObject).

// Logs `false`:
console.log(someFunction() === boundObject);
// Logs `true`:
console.log(boundFunction() === boundObject);

Otros ejemplos

Cuando llamas a una función vinculada, el valor de this no se puede cambiar con call o apply:

// Logs `true` - called `this` value is ignored:
console.log(boundFunction.call({foo: 'bar'}) === boundObject);
// Logs `true` - applied `this` value is ignored:
console.log(boundFunction.apply({foo: 'bar'}) === boundObject);

Cuando llamas a una función vinculada, el valor de this no se puede cambiar llamando a la función como miembro de otro objeto:

const obj = {boundFunction};
// Logs `true` - parent object is ignored:
console.log(obj.boundFunction() === boundObject);

De lo contrario, si this se configura en el momento de la llamada:

function someFunction() {
  return this;
}

const someObject = {hello: 'world'};

// Logs `true`:
console.log(someFunction.call(someObject) === someObject);
// Logs `true`:
console.log(someFunction.apply(someObject) === someObject);

El valor de this es el objeto que se pasa a call/apply.

Lamentablemente, this se establece en algún otro valor por elementos como los objetos de escucha de eventos del DOM, y su uso puede generar un código difícil de entender:

Qué no debes hacer
element.addEventListener('click', function (event) {
  // Logs `element`, since the DOM spec sets `this` to
  // the element the handler is attached to.
  console.log(this);
});

Evito usar this en casos como el anterior y, en su lugar, hago lo siguiente:

Qué debes hacer
element.addEventListener('click', (event) => {
  // Ideally, grab it from a parent scope:
  console.log(element);
  // But if you can't do that, get it from the event object:
  console.log(event.currentTarget);
});

De lo contrario, si se llama a la función mediante un objeto superior (parent.func()):

const obj = {
  someMethod() {
    return this;
  },
};

// Logs `true`:
console.log(obj.someMethod() === obj);

En este caso, se llama a la función como miembro de obj, por lo que this será obj. Esto sucede en el momento de la llamada, por lo que el vínculo se rompe si se llama a la función sin su objeto superior o con un objeto superior diferente:

const {someMethod} = obj;
// Logs `false`:
console.log(someMethod() === obj);

const anotherObj = {someMethod};
// Logs `false`:
console.log(anotherObj.someMethod() === obj);
// Logs `true`:
console.log(anotherObj.someMethod() === anotherObj);

someMethod() === obj es falso porque someMethod no se llama como miembro de obj. Es posible que hayas encontrado este problema cuando intentaste algo como lo siguiente:

const $ = document.querySelector;
// TypeError: Illegal invocation
const el = $('.some-element');

Esto se produce porque la implementación de querySelector observa su propio valor this y espera que sea un tipo de nodo DOM, y lo anterior interrumpe esa conexión. Para lograr lo anterior correctamente:

const $ = document.querySelector.bind(document);
// Or:
const $ = (...args) => document.querySelector(...args);

Dato curioso: No todas las APIs usan this de forma interna. Se cambiaron los métodos de consola, como console.log, para evitar referencias de this, por lo que log no necesita estar vinculado a console.

De lo contrario, si la función o el alcance superior están en modo estricto:

function someFunction() {
  'use strict';
  return this;
}

// Logs `true`:
console.log(someFunction() === undefined);

En este caso, el valor de this no está definido. No se necesita 'use strict' en la función si el alcance superior está en modo estricto (y todos los módulos están en modo estricto).

En caso contrario:

function someFunction() {
  return this;
}

// Logs `true`:
console.log(someFunction() === globalThis);

En este caso, el valor de this es el mismo que globalThis.

¡Vaya!

Eso es todo. Eso es todo lo que sé sobre this. ¿Alguna pregunta? ¿Me perdí de algo? No dudes en tuitearme.

Agradecemos a Mathias Bynens, Ingvar Stepanyan y Thomas Steiner por su opinión.