JavaScript: Qu'est-ce que cela signifie ?

Il peut être difficile de déterminer la valeur de this en JavaScript. Voici comment procéder...

Jake Archibal
Jake Archibal

this de JavaScript est le but de nombreuses blagues, et c'est parce que c'est assez compliqué. Cependant, j'ai vu des développeurs effectuer des tâches beaucoup plus complexes et spécifiques à un domaine pour éviter de gérer cette this. Nous espérons que ces informations vous seront utiles si vous n'êtes pas sûr de this. Ceci est mon guide this.

Je vais commencer par la situation la plus spécifique et terminer par la moins spécifique. Cet article ressemble à un gros if (…) … else if () … else if (…) …. Vous pouvez donc accéder directement à la première section qui correspond au code que vous examinez.

  1. Si la fonction est définie comme une fonction fléchée
  2. Sinon, si la fonction/classe est appelée avec new
  3. Sinon, si la fonction a une valeur this "limitée"
  4. Sinon, si this est défini au moment de l'appel
  5. Sinon, si la fonction est appelée via un objet parent (parent.func())
  6. Sinon, si la fonction ou le champ d'application parent est en mode strict
  7. Sinon

Si la fonction est définie comme une fonction fléchée:

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

Dans ce cas, la valeur de this est toujours la même que celle de this dans le champ d'application parent:

const outerThis = this;

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

Les fonctions fléchées sont utiles, car la valeur interne de this ne peut pas être modifiée. Elle est toujours identique à la valeur this externe.

Autres exemples

Avec les fonctions fléchées, la valeur de this ne peut pas être modifiée avec bind:

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

Avec les fonctions fléchées, la valeur de this ne peut pas être modifiée avec call ou apply:

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

Avec les fonctions fléchées, la valeur de this ne peut pas être modifiée en appelant la fonction en tant que membre d'un autre objet:

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

Avec les fonctions fléchées, la valeur de this ne peut pas être modifiée en appelant la fonction en tant que constructeur:

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

Méthodes d'instance "liées"

Avec les méthodes d'instance, si vous souhaitez vous assurer que this fait toujours référence à l'instance de classe, le meilleur moyen consiste à utiliser des fonctions fléchées et des champs de classe:

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

Ce modèle est très utile lorsque vous utilisez des méthodes d'instance en tant qu'écouteurs d'événements dans des composants (tels que les composants React ou Web).

Ce qui précède peut sembler entraver la règle "this sera identique à this dans le champ d'application parent", mais cela commence à être logique si vous considérez les champs de classe comme du sucre syntaxique pour définir des éléments dans le constructeur:

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);
    };
  }
}

D'autres modèles impliquent de lier une fonction existante dans le constructeur ou d'attribuer la fonction dans le constructeur. Si vous ne pouvez pas utiliser de champs de classe pour une raison quelconque, l'attribution de fonctions dans le constructeur est une alternative raisonnable:

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

Sinon, si la fonction/classe est appelée avec new:

new Whatever();

Ce qui précède appelle Whatever (ou sa fonction constructeur s'il s'agit d'une classe) avec this défini sur le résultat de Object.create(Whatever.prototype).

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

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

Il en va de même pour les constructeurs de style plus ancien:

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

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

Autres exemples

Lorsqu'elle est appelée avec new, la valeur de this ne peut pas être modifiée avec bind:

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

Lorsqu'elle est appelée avec new, la valeur de this ne peut pas être modifiée en appelant la fonction en tant que membre d'un autre objet:

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

Sinon, si la fonction a une valeur this "limitée" :

function someFunction() {
  return this;
}

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

Chaque fois que boundFunction est appelé, sa valeur this correspond à l'objet transmis à bind (boundObject).

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

Autres exemples

Lors de l'appel d'une fonction liée, la valeur de this ne peut pas être modifiée avec call ou 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);

Lorsque vous appelez une fonction liée, la valeur de this ne peut pas être modifiée en appelant la fonction en tant que membre d'un autre objet:

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

Sinon, si this est défini au moment de l'appel:

function someFunction() {
  return this;
}

const someObject = {hello: 'world'};

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

La valeur de this correspond à l'objet transmis à call/apply.

Malheureusement, this est défini sur une autre valeur par des éléments tels que les écouteurs d'événements DOM. Son utilisation peut entraîner un code difficile à comprendre:

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

Dans les cas ci-dessus, j'évite d'utiliser this. À la place:

À faire
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);
});

Sinon, si la fonction est appelée via un objet parent (parent.func()):

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

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

Dans ce cas, la fonction est appelée en tant que membre de obj. this sera donc obj. Cela se produit au moment de l'appel. Le lien est donc rompu si la fonction est appelée sans son objet parent ou avec un objet parent différent:

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 est "false", car someMethod n'est pas appelé en tant que membre de obj. Vous avez peut-être rencontré ce piège en essayant quelque chose comme ceci:

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

Cela ne fonctionne pas, car l'implémentation de querySelector examine sa propre valeur this et s'attend à ce qu'il s'agisse d'un nœud DOM. Ce qui précède rompt cette connexion. Pour effectuer correctement ce qui précède:

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

Fait intéressant: toutes les API n'utilisent pas this en interne. Les méthodes de la console telles que console.log ont été modifiées pour éviter les références this. log n'a donc pas besoin d'être lié à console.

Sinon, si la fonction ou le champ d'application parent est en mode strict:

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

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

Dans ce cas, la valeur de this n'est pas définie. 'use strict' n'est pas nécessaire dans la fonction si le champ d'application parent est en mode strict (et si tous les modules sont en mode strict).

Sinon :

function someFunction() {
  return this;
}

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

Dans ce cas, la valeur de this est identique à celle de globalThis.

Ouf !

Voilà, c'est terminé ! C'est tout ce que je sais sur this. Des questions ? Quelque chose que j'ai manqué ? N'hésitez pas à me tweeter.

Merci à Mathias Bynens, Ingvar Stepanyan et Thomas Steiner pour leurs commentaires.