23 octobre 2022

Type référence

Sujet avancé

Cet article couvre un sujet avancé pour mieux comprendre certains cas limites.

Ce n’est pas important. De nombreux développeurs expérimentés vivent bien sans le savoir. Continuez à lire si vous voulez savoir comment les choses fonctionnent sous le capot.

Un appel de méthode évalué dynamiquement peut perdre this.

Par exemple:

let user = {
  name: "John",
  hi() { alert(this.name); },
  bye() { alert("Bye"); }
};

user.hi(); // fonctionne

// essayons maintenant d'appeler user.hi ou user.by selon name
(user.name == "John" ? user.hi : user.bye)(); // Error !

Sur la dernière ligne il y a un opérateur conditionnel qui choisit entre user.hi ou user.bye. Ici le résultat est user.hi.

Ensuite la méthode est immédiatement appelée avec les parenthèses (). Mais cela ne fonctionne pas !

Comme vous pouvez le voir, l’appel se résout avec une erreur car la valeur de "this" dans l’appel devient undefined.

Cet appel fonctionne (syntaxe de notation par points):

user.hi();

Celui-là non (méthode évaluée):

(user.name == "John" ? user.hi : user.bye)(); // Error !

Pourquoi ? Si nous voulons comprendre pourquoi cela arrive, regardons comment l’appel de obj.method() fonctionne sous le capot.

Le type référence expliqué

En y regardant plus précisement, on peut remarquer 2 opérations dans la déclaration de obj.method():

  1. En premier, le point '.' récupère la propriété obj.method.
  2. Puis les parenthèses () l’éxécute.

Mais comment l’information du this est passée de la première opération à la deuxième ?

Si on sépare ces opération sur 2 lignes, alors this sera perdu :

let user = {
  name: "John",
  hi() { alert(this.name); }
};

// On sépare l'accès à la méthode et son appel en deux lignes
let hi = user.hi;
hi(); // Error, car this n'est pas définit

Ici hi = user.hi assigne la fonction à la variable, ensuite sur la dernière ligne this est complétement autonome et donc il n’y a pas de this.

Pour faire que user.hi() fonctionne, JavaScript utilise une astuce – le point '.' ne retourne pas une fonction, mais une valeur de type référence.

Le type référence n’est pas un “type de spécifiation”. On ne peut l’utiliser explicitement, mais il est utilisé en interne par le langage.

La valeur de Type Référence est une combinaison de 3 valeurs (base, name, strict), où :

  • base est l’objet.
  • name est le nom de la propriété.
  • strict est vrai si use strict est en vigueur.

Le résultat de l’accès à la propriété user.hi n’est pas une fonction, mais une valeur de Type Référence. Pour user.hi en mode strict cela est :

// Valeur de type référence
(user, "hi", true)

Lorsque les parenthèses () sont appelées sur le type de référence, elles reçoivent les informations complètes sur l’objet et sa méthode, et peuvent définir le bon this (user dans ce cas).

Le type référence est un type interne “intermédiaire”, avec comme but de passer l’information du point . aux parenthèses ().

N’importe quelle autre opération d’assignement comme hi = user.hi rejette le type référence, prends la valeur de user.hi (une fonction) et la passe. Ainsi n’importe quelle opération suivante “perd” this.

Il en résulte que la valeur de this n’est passée correctement seulement lorsque la fonction est appelée directement en utilisant la notation par points obj.method() ou la notation par crochet obj['method']() (c’est la même chose). Il existe différentes manières de résoudre ce problème comme func.bind().

Résumé

Le type référence est un type interne au langage.

En lisant une propriété, comme avec le point . dans obj.method(), qui ne retourne pas la valeur de la propriété mais la valeur spéciale de “type référence”, qui garde le nom de la propriété et l’objet relié à la propriété.

That’s for the subsequent method call () to get the object and set this to it. Cela est fait pour que l’éxécution suivante, l’appel à la méthode (), reçoive l’objet et lui assigne this.

Pour toutes les autres opérations, le type référence sera automatiquement la valeur de la propriété (une fonction dans notre cas).

Le fonctionnement est caché de notre vision. Cela n’a d’importance que dans certains cas, comme lorsqu’une méthode est obtenue dynamiquement de l’object en utilisant une expression.

Exercices

importance: 2

Quel est le résultat de ce code ?

let user = {
  name: "John",
  go: function() { alert(this.name) }
}

(user.go)()

P.S. Il y a un piège :)

Erreur!

Essayez le :

let user = {
  name: "John",
  go: function() { alert(this.name) }
}

(user.go)() // error!

Le message d’erreur dans la plupart des navigateurs ne permet pas de comprendre ce qui s’est mal passé.

L’erreur apparaît parce qu’un point-virgule est manquant après user = {...}.

JavaScript n’insert pas automatiquement un point-virgule avant une accolade (user.go)(), il lit donc le code comme ceci :

let user = { go:... }(user.go)()

Ensuite, nous pouvons également voir qu’une telle expression conjointe est syntaxiquement un appel de l’objet { go: ... } en tant que fonction avec l’argument (user.go). Et cela se produit également sur la même ligne avec let user, alors que l’objet user n’a même pas encore été défini, d’où l’erreur.

Si nous insérons le point-virgule, tout va bien :

let user = {
  name: "John",
  go: function() { alert(this.name) }
};

(user.go)() // John

Veuillez noter que les parenthèses autour de (user.go) ne font rien ici. Habituellement, elles configurent l’ordre des opérations, mais ici le point . fonctionne toujours en premier de toute façon, donc aucun effet. Seul le point-virgule compte.

importance: 3

Dans le code ci-dessous, nous avons l’intention d’appeler la méthode obj.go() 4 fois de suite.

Mais les appels (1) et (2) fonctionnent différemment de (3) et (4). Pourquoi ?

let obj, method;

obj = {
  go: function() { alert(this); }
};

obj.go();               // (1) [object Object]

(obj.go)();             // (2) [object Object]

(method = obj.go)();    // (3) undefined

(obj.go || obj.stop)(); // (4) undefined

Voici les explications.

  1. C’est un appel de méthode d’objet standard.

  2. De même, les parenthèses ne changent pas l’ordre des opérations ici, le point est le premier quand même.

  3. Nous avons ici un appel plus complexe (expression)(). L’appel fonctionne comme s’il était divisé en deux lignes :

    f = obj.go; // calculer l'expression
    f();        // appeler ce que nous avons

    Ici, f() est exécuté en tant que fonction, sans this.

  4. La chose similaire à (3), à gauche des parenthèses (), nous avons une expression.

Pour expliquer le comportement de (3) et (4), nous devons rappeler que les accesseurs de propriétés (points ou crochets) renvoient une valeur du type de référence.

Toute opération sur celle-ci, à l’exception d’un appel de méthode (comme affectation = ou ||), la convertit en une valeur ordinaire, qui ne porte pas les informations permettant de définir this.

Carte du tutoriel

Commentaires

lire ceci avant de commenter…
  • Si vous avez des améliorations à suggérer, merci de soumettre une issue GitHub ou une pull request au lieu de commenter.
  • Si vous ne comprenez pas quelque chose dans l'article, merci de préciser.
  • Pour insérer quelques bouts de code, utilisez la balise <code>, pour plusieurs lignes – enveloppez-les avec la balise <pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepen…)