5 mai 2023

Méthodes d'objet, "this"

Les objets sont généralement créés pour représenter des entités du monde réel, comme des utilisateurs, des commandes, etc. :

let user = {
  name: "John",
  age: 30
};

Et, dans le monde réel, un utilisateur peut agir : sélectionner un élément du panier, se connecter, se déconnecter, etc.

Les actions sont représentées en JavaScript par des fonctions dans les propriétés.

Exemples de méthodes

Pour commencer, apprenons à user à dire bonjour :

let user = {
  name: "John",
  age: 30
};

user.sayHi = function() {
  alert("Hello!");
};

user.sayHi(); // Hello!

Ici, nous venons d’utiliser une fonction expression pour créer la fonction et l’affecter à la propriété user.sayHi de l’objet.

Ensuite, nous pouvons l’appeler comme user.sayHi(). L’utilisateur peut maintenant parler!

Une fonction qui est la propriété d’un objet s’appelle sa méthode.

Nous avons donc ici une méthode sayHi de l’objet user.

Bien sûr, nous pourrions utiliser une fonction pré-déclarée comme méthode, comme ceci :

let user = {
  // ...
};

// d'abord, déclarer
function sayHi() {
  alert("Hello!");
}

// puis ajouter comme une méthode
user.sayHi = sayHi;

user.sayHi(); // Hello!
Programmation orientée objet

Lorsque nous écrivons notre code en utilisant des objets pour représenter des entités, cela s’appelle une programmation orientée objet, en bref : “POO”.

La programmation orientée objet est un élément important, une science intéressante en soi. Comment choisir les bonnes entités ? Comment organiser l’interaction entre elles ? C’est une architecture, et il existe d’excellents livres sur ce sujet, tels que “Design Patterns: Elements of Reusable Object-Oriented Software” de E. Gamma, R. Helm, R. Johnson, J. Vissides ou “Object-Oriented Analysis and Design with Applications” de G. Booch, et plus.

Méthode abrégée

Il existe une syntaxe plus courte pour les méthodes dans un littéral d’objet :

// ces objets font la même chose

user = {
  sayHi: function() {
    alert("Hello");
  }
};

// la méthode abrégée semble mieux, non ?
user = {
  sayHi() { // identique à "sayHi: function(){...}"
    alert("Hello");
  }
};

Comme démontré, nous pouvons omettre "function" et simplement écrire sayHi().

A vrai dire, les notations ne sont pas totalement identiques. Il existe des différences subtiles liées à l’héritage d’objet (à couvrir plus tard), mais pour le moment, elles importent peu. Dans presque tous les cas, la syntaxe la plus courte est préférable.

“this” dans les méthodes

Il est courant qu’une méthode d’objet ait besoin d’accéder aux informations stockées dans l’objet pour effectuer son travail.

Par exemple, le code à l’intérieur de user.sayHi() peut nécessiter le nom de user.

Pour accéder à l’objet, une méthode peut utiliser le mot-clé this.

La valeur de this est l’objet “avant le point”, celui utilisé pour appeler la méthode.

Par exemple :

let user = {
  name: "John",
  age: 30,

  sayHi() {
    // "this" is the "current object"
    alert(this.name);
  }

};

user.sayHi(); // John

Ici, lors de l’exécution de user.sayHi(), la valeur de this sera user.

Techniquement, il est également possible d’accéder à l’objet sans this, en le référençant via la variable externe :

let user = {
  name: "John",
  age: 30,

  sayHi() {
    alert(user.name); // "user" au lieu de "this"
  }

};

… Mais un tel code n’est pas fiable. Si nous décidons de copier user dans une autre variable, par exemple admin = user et écraser user avec quelque chose d’autre, il accédera au mauvais objet.

Cela est démontré ci-dessous :

let user = {
  name: "John",
  age: 30,

  sayHi() {
    alert( user.name ); // conduit à une erreur
  }

};


let admin = user;
user = null; // écraser pour rendre les choses évidentes

admin.sayHi(); // TypeError: Cannot read property 'name' of null

Si nous utilisions this.name au lieu de user.name dans l’alert, le code fonctionnerait.

“this” n’est pas lié

En JavaScript, le mot clé this se comporte différemment de la plupart des autres langages de programmation. Il peut être utilisé dans n’importe quelle fonction, même si ce n’est pas une méthode d’un objet.

Il n’y a pas d’erreur de syntaxe dans le code suivant :

function sayHi() {
  alert( this.name );
}

La valeur de this est évaluée pendant l’exécution, en fonction du contexte.

Par exemple, ici la même fonction est assignée à deux objets différents et a un “this” différent dans les appels :

let user = { name: "John" };
let admin = { name: "Admin" };

function sayHi() {
  alert( this.name );
}

// utiliser la même fonction dans deux objets
user.f = sayHi;
admin.f = sayHi;

// ces appels ont un this différent
// "this" à l'intérieur de la fonction est l'objet "avant le point"
user.f(); // John  (this == user)
admin.f(); // Admin  (this == admin)

admin['f'](); // Admin (le point ou les crochets accèdent à la méthode - peu importe)

La règle est simple : si obj.f() est appelé, alors this est obj pendant l’appel de f. C’est donc l’user ou l’admin dans l’exemple ci-dessus.

Appel sans objet : this == undefined

Nous pouvons même appeler la fonction sans objet du tout :

function sayHi() {
  alert(this);
}

sayHi(); // undefined

Dans ce cas, this est undefined en mode strict. Si nous essayons d’accéder à this.name, il y aura une erreur.

En mode non strict (si on oublie use strict), la valeur de this dans ce cas sera l’objet global (la fenêtre d’un navigateur, nous y reviendrons plus tard). Ceci est un comportement historique que le mode strict corrige.

Ce genre d’appel est généralement une erreur de programmation. Si il y a un this dans une fonction, il s’attend à être appelée dans un contexte d’objet.

Les conséquences d’un this non lié

Si vous venez d’un autre langage de programmation, vous êtes probablement habitué à l’idée d’un “this lié”, où les méthodes définies dans un objet ont toujours this en référence à cet objet.

En JavaScript, this est “libre”, sa valeur est évaluée au moment de l’appel et ne dépend pas de l’endroit où la méthode a été déclarée, mais plutôt de l’objet “avant le point”.

Le concept de temps d’exécution évalué de this présente à la fois des avantages et des inconvénients. D’une part, une fonction peut être réutilisée pour différents objets. D’autre part, une plus grande flexibilité ouvre la place à des erreurs.

Ici, notre position n’est pas de juger si cette décision de conception linguistique est bonne ou mauvaise. Nous comprendrons comment travailler avec elle, comment obtenir des avantages et éviter les problèmes.

Les fonctions fléchées n’ont pas de “this”

Les fonctions fléchées sont spéciales : elles n’ont pas leur “propre” this. Si nous faisons référence à this à partir d’une telle fonction, cela provient de la fonction externe “normale”.

Par exemple, ici arrow() utilise this depuis la méthode externe user.sayHi() :

let user = {
  firstName: "Ilya",
  sayHi() {
    let arrow = () => alert(this.firstName);
    arrow();
  }
};

user.sayHi(); // Ilya

C’est une particularité des fonctions fléchées. C’est utile lorsque nous ne voulons pas réellement avoir un this distinct, mais plutôt le prendre à partir du contexte extérieur. Plus tard dans le chapitre Les fonctions fléchées revisitées nous allons approfondir les fonctions fléchées.

Résumé

  • Les fonctions stockées dans les propriétés de l’objet s’appellent des “méthodes”.
  • Les méthodes permettent aux objets d’agir comme object.doSomething().
  • Les méthodes peuvent référencer l’objet comme this.

La valeur de this est définie au moment de l’exécution.

  • Lorsqu’une fonction est déclarée, elle peut utiliser this, mais ce this n’a aucune valeur jusqu’à ce que la fonction soit appelée.
  • Une fonction peut être copiée entre des objets.
  • Lorsqu’une fonction est appelée dans la syntaxe “méthode” : object.method(), la valeur de this lors de l’appel est objet.

Veuillez noter que les fonctions fléchées sont spéciales : elles n’ont pas this. Lorsque this est accédé dans une fonction fléchée, il est pris de l’extérieur.

Exercices

importance: 5

Ici, la fonction makeUser renvoie un objet.

Quel est le résultat de l’accès à sa ref ? Pourquoi ?

function makeUser() {
  return {
    name: "John",
    ref: this
  };
}

let user = makeUser();

alert( user.ref.name ); // Quel est le résultat ?

Réponse : une erreur.

Essayez le :

function makeUser() {
  return {
    name: "John",
    ref: this
  };
}

let user = makeUser();

alert( user.ref.name ); // Erreur: Impossible de lire la propriété 'nom' de undefined

C’est parce que les règles qui définissent this ne prennent pas en compte la définition d’objet. Seul le moment de l’appel compte.

Ici, la valeur de this à l’intérieur de makeUser() est undefined, car elle est appelée en tant que fonction et non en tant que méthode avec la syntaxe au “point”.

La valeur de this est la même pour toute la fonction, les blocs de code et les littéraux d’objet ne l’affectent pas.

Donc ref: this prend actuellement le this courant de la fonction.

Nous pouvons réécrire la fonction et renvoyer le même this avec la valeur undefined :

function makeUser(){
  return this; // cette fois il n'y a pas d'objet littéral
}

alert( makeUser().name ); // Error: Cannot read property 'name' of undefined

Comme vous pouvez le constater, le résultat de alert( makeUser().name ) est identique à celui de alert( user.ref.name ) de l’exemple précédent.

Voici le cas contraire :

function makeUser() {
  return {
    name: "John",
    ref() {
      return this;
    }
  };
}

let user = makeUser();

alert( user.ref().name ); // John

Maintenant cela fonctionne parce que user.ref() est une méthode. Et la valeur de this est définie pour l’objet avant le point ..

importance: 5

Créez un objet calculator avec trois méthodes :

  • read() demande deux valeurs et les enregistre en tant que propriétés d’objet avec les noms a et b respectivement.
  • sum() renvoie la somme des valeurs sauvegardées.
  • mul() multiplie les valeurs sauvegardées et renvoie le résultat.
let calculator = {
  // ... votre code ...
};

calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );

Exécuter la démo

Open a sandbox with tests.

let calculator = {
  sum() {
    return this.a + this.b;
  },

  mul() {
    return this.a * this.b;
  },

  read() {
    this.a = +prompt('a?', 0);
    this.b = +prompt('b?', 0);
  }
};

calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );

Ouvrez la solution avec des tests dans une sandbox.

importance: 2

Il y a un objet ladder qui permet de monter et descendre :

let ladder = {
  step: 0,
  up() {
    this.step++;
  },
  down() {
    this.step--;
  },
  showStep: function() { // affiche l'étape en cours
    alert( this.step );
  }
};

Maintenant, si nous devons faire plusieurs appels en séquence, nous pouvons le faire comme ceci :

ladder.up();
ladder.up();
ladder.down();
ladder.showStep(); // 1
ladder.down();
ladder.showStep(); // 0

Modifiez le code de up et down pour rendre les appels chaînables, comme ceci :

ladder.up().up().down().showStep().down().showStep(); // shows 1 then 0

Cette approche est largement utilisée dans les bibliothèques JavaScript.

Open a sandbox with tests.

La solution consiste à renvoyer l’objet lui-même à partir de chaque appel.

let ladder = {
  step: 0,
  up() {
    this.step++;
    return this;
  },
  down() {
    this.step--;
    return this;
  },
  showStep() {
    alert( this.step );
    return this;
  }
};

ladder.up().up().down().showStep().down().showStep(); // shows 1 then 0

Nous pouvons également écrire un seul appel par ligne. Pour les longues chaînes, c’est plus lisible :

ladder
  .up()
  .up()
  .down()
  .showStep() // 1
  .down()
  .showStep(); // 0
let ladder = {
  step: 0,
  up: function() {
    this.step++;
    return this;
  },
  down: function() {
    this.step--;
    return this;
  },
  showStep: function() {
    alert(this.step);
    return this;
  }
};

Ouvrez la solution avec des tests dans une sandbox.

Carte du tutoriel