JavaScript: Qu'est-ce que cela signifie ?

Déterminer la valeur de this peut s'avérer délicat en JavaScript. Voici comment procéder...

Jake Archibald
Jake Archibald

La this de JavaScript est l'objet de nombreuses blagues, car elle est assez complexe. Toutefois, j'ai vu des développeurs faire des choses beaucoup plus complexes et propres au domaine pour éviter de gérer cette this. Nous espérons que cela vous aidera si vous n'êtes pas sûr de this. Voici mon guide this.

Je vais commencer par la situation la plus spécifique, et finir par la moins spécifique. Cet article est un peu comme un grand if (…) … else if () … else if (…) …. Vous pouvez donc accéder directement à la première section correspondant au code que vous consultez.

  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 possède une valeur this "lié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 identique à 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 très utiles, car la valeur interne de this ne peut pas être modifiée. Elle est toujours la même que l'this externe.

Autres exemples

Avec les fonctions flèche, 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 est d'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 des composants React ou des composants Web).

Il peut sembler que le code ci-dessus ne respecte pas la règle "this sera identique à this dans le champ parent", mais cela commence à avoir du sens 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);
    };
  }
}

Les 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, attribuer des fonctions dans le constructeur est une alternative raisonnable :

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

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

new Whatever();

La commande ci-dessus appelle Whatever (ou sa fonction de 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 plus anciens :

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

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

Autres exemples

Lorsqu'il est appelé 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

Lorsque vous appelez une fonction liée, la valeur de this ne peut pas être remplacée par 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 est 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, et 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);
});

J'évite d'utiliser this dans les cas comme ci-dessus, et à 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 faux, 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'elle soit un nœud DOM, et ce qui précède interrompt cette connexion. Pour obtenir les résultats ci-dessus correctement:

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

Fait amusant : toutes les API n'utilisent pas this en interne. Les méthodes de 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 que 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 !

Et voilà ! C'est tout ce que je sais sur this. Des questions ? Ai-je manqué quelque chose ? N'hésitez pas à nous envoyer un tweet.

Merci à Mathias Bynens, Ingvar Stepanyan et Thomas Steiner pour leur examen.