JavaScript: ¿Cuál es el significado?

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

this de JavaScript es el tema central de muchas bromas, y eso se debe a que, bueno, es bastante complicado. Sin embargo, noté que los desarrolladores hacen tareas mucho más complicadas y específicas del dominio para evitar lidiar con este this. Si no estás seguro sobre this, esperamos que esta información te resulte útil. Esta es mi guía de this.

Voy a empezar con la situación más específica y terminaré con la menos específica. Este artículo es como una if (…) … else if () … else if (…) … grande, 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 de 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 "vinculado"
  4. De lo contrario, si this se configura al 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. En caso contrario

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

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

En este caso, el valor de this es siempre 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 y siempre es el mismo que el this externo.

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 si se llama 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 constructor:

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

Métodos de instancia “vinculados”

Con los métodos de instancia, si deseas asegurarte de que this siempre haga referencia a la instancia de 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 puede parecer que rompe la regla "this será lo mismo que this en el alcance superior", pero comienza a tener sentido si piensas en los campos de clase como azúcar sintáctica 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);
    };
  }
}

Las plantillas alternativas 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 la función o clase se llama con new, sucederá lo siguiente:

new Whatever();

El comando anterior llamará a Whatever (o a su función de constructor si es una clase) con this configurado como resultado de Object.create(Whatever.prototype).

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

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

Lo mismo sucede 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 lo contrario, si la función tiene un valor this "vinculado":

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 pasará a bind (boundObject).

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

Otros ejemplos

Cuando se llama a una función vinculada, el valor de this no se puede cambiar con call ni 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 si se llama 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 al 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, elementos como los objetos de escucha de eventos del DOM establecen this con otro valor, 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 los anteriores y, en su lugar:

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 a través de un objeto superior (parent.func()), ejecuta el siguiente comando:

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 no se llama a someMethod como miembro de obj. Es posible que te hayas encontrado con este problema al intentar algo como lo siguiente:

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

Este error se interrumpe porque la implementación de querySelector analiza su propio valor de this y espera que sea un tipo de nodo del DOM, por lo que lo anterior interrumpe esa conexión. Para lograr lo anterior correctamente, haz lo siguiente:

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 la consola, como console.log, para evitar referencias this, por lo que log no necesita estar vinculado a console.

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

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 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 el de globalThis.

¡Vaya!

Eso es todo. Eso es todo lo que sé sobre this. ¿Alguna pregunta? ¿Hay algo que me perdí? No dudes en enviarme un tweet.

Gracias a Mathias Bynens, Ingvar Stepanyan y Thomas Steiner por sus comentarios.