JavaScript: ¿Qué significa esto (this)?

Averiguar el valor de "this" puede ser complicado en JavaScript, aquí explicamos cómo hacerlo…

Jake Archibald
Jake Archibald

La palabra clave this es el blanco de muchas bromas, y eso es porque, bueno, es bastante complicado. Sin embargo, he visto a los desarrolladores hacer cosas mucho más complicadas y específicas del dominio para evitar lidiar con this. Si no se siente seguro usando this, esperamos que esto le ayude. Esta es mi guía para 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 (…) … , así que puedes ir directamente a la primera sección que coincide con el código que estás viendo.

  1. Si la función se define como una función de flecha
  2. De otro modo, si se llama a la función/clase con new
  3. De otro modo, si la función tiene un 'límite' use this con este valor
  4. De otro modo, si this se establece en el momento de la llamada
  5. De otro modo, si la función se llama a través de un objeto principal ( parent.func() )
  6. De otro modo, si la función o el ámbito principal está en modo estricto
  7. De otro modo

Si la función se define como una función de flecha:

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

En este caso, el valor de this es siempre el mismo que this en el ámbito primario:

const outerThis = this;

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

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

Otros ejemplos

Con las funciones de flecha, el valor de this no se puede cambiar 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 con 'bound'

Con los métodos de instancia, si desea asegurarse de que this siempre se refiera 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 realmente útil cuando se utilizan métodos de instancia como detectores de eventos en componentes (como componentes React o componentes web).

Lo anterior puede parecer que está quebrando la regla "this será igual que this en el ámbito principal", pero comienza a tener sentido si piensa en los campos de clase como azúcar sintáctico para configurar cosas 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 o asignar la función en el constructor. Si no puede usar campos de clase por alguna razón, asignar funciones en el constructor es una alternativa razonable:

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

De otro modo, si se llama la función/clase con new:

new Whatever();

Lo anterior llamará a Whatever (o su función constructora si es una clase) con this configurado con 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 con new, el valor de this no se puede cambiar llamando a la función como miembro de otro objeto:

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

De otro modo, si la función tiene un 'bound', el valor de this:

function someFunction() {
  return this;
}

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

Siempre que se llama boundFunction, el valor de this será el objeto pasado a bind (boundObject).

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

Otros ejemplos

Al llamar 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 se llama 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 otro modo, si this se ha establecido en call-time:

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.

Desafortunadamente, se establece algún otro valor para this por elementos como los detectores de eventos DOM, cuyo uso puede dar como resultado un código difícil de entender:

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

Yo evito usar this en casos como el anterior, y en vez de eso utilizo:

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 otro modo, si la función se llama a través de un objeto principal (parent.func()):

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

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

En este caso, la función se llama como miembro de obj, por lo que this será un obj. Esto sucede en el momento de la llamada, por lo que el enlace se corta si se llama a la función sin su objeto principal o con un objeto principal 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 se haya encontrado con este problema al intentar algo como esto:

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

Esto da un error como resultado porque la implementación de querySelector ve su proprio valor de this y espera que sea una especie de nodo DOM, y lo anterior corta esa conexión. Para conseguir el resultado deseado con el código anterior correctamente use:

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

Dato curioso: no todas las API utilizan this internamente. Los métodos de consola como console.log se cambiaron para evitar las referencias a this, por lo que no es necesario que el log esté vinculado a console.

De otro modo, si la función o el ámbito principal está 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. 'use strict' no es necesario en la función si el ámbito principal está en modo estricto (y todos los módulos están en modo estricto).

De otro modo:

function someFunction() {
  return this;
}

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

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

¡Uf!

¡Y eso es! Eso es todo lo que sé sobre this. ¿Alguna pregunta? ¿Se me ha escapado algo? Envíame un tuit.

Gracias a Mathias Bynens, Ingvar Stepanyan y Thomas Steiner por la revisión.