This is it! Le contexte en Javascript

« Tous les Crétois sont des menteurs! » C’est par cette phrase que le philosophe Epiménide a livré à l’humanité le plus célèbre des syllogismes. Lui-même étant crétois, il révélait de manière implicite son mensonge et sous-entendait que son affirmation devenait fausse.

J’ai été fort étonné d’apprendre récemment que ce paradoxe logique illustrait une complexité mathématique impliquée dans la théorie des ensembles de Russell, ou encore dans la machine universelle de Turing (mais ne m’en demandez pas plus).

Pour ma part, je garde une préférence pour le paradoxe du fromage à trous, sans doute parce que cet aliment constitue la base de mon déjeuner, si bien que mes collègues se demandent parfois si je supporte toute autre forme de régime.

« Plus il y a de fromage, plus il y a de trous;
or plus il y a de trous, moins il y a de fromage;
donc plus il y a de fromage, moins il y a de fromage. »

Ce syllogisme est intéressant car il montre combien le référent est important dans la compréhension du contexte. Si notre pensée conceptualise facilement le langage (un mot peut avoir un sens radicalement différent suivant son utilisation), il n’en est pas toujours de même en informatique.

Aussi, lorsque l’on vient du monde classique de la programmation orientée objet, le contexte de Javascript peut surprendre. Le mot-clé this, qui désigne habituellement l’objet courant, semble insaisissable, voire même spécieux. Il peut être présent en dehors d’un objet, ou, à l’inverse, ne pas le référencer. Si une fonction l’appelle, il peut contenir tantôt l’espace global, tantôt undefined

Les apparences sont trompeuses et il semble difficile de se fier à ce mot-clé, du moins sans savoir dans quel contexte précis il est utilisé. En somme, this ressemble à un gros fromage à trou: Le référent change en fonction du contexte.

C’est pourquoi cet article propose de réaliser un petit tour des différents types de contexte, d’identifier précisément ce que this nous cache, et de démystifier par la sorte l’orienté objet sous Javascript.

Le contexte par défaut

L’Objet Global (Global Object) est un objet spécifique à Javascript qui ne peut être instancié et qui reçoit, en début d’interprétation du code, une série de propriétés utilisables à travers l’ensemble du code. Il s’agit de valeurs comme undefined, Infinity ou NaN, de fonctions comme isNaN(), parseInt(), ou eval(), ou encore d’objets comme Date, Error, ou Math.

Du côté client, lorsque Javascript est exécuté par un navigateur, cet objet est matérialisé par Window, l’objet racine du BOM (Browser Object Model), lequel est accessible par sa propre propriété window (self-referential property).

Cet objet représente, en quelque sorte, le contexte par défaut.

L’espace global

L’espace global est l’espace de code qui n’est situé à l’intérieur d’aucune fonction. Il est lié à l’Objet Global qui en représente le contexte et auquel this fait référence. Dans le cadre d’une interprétation du code par un navigateur, this et window pointeront vers le même contexte.

Il faut noter qu’il ne s’agit pas d’une vérité universelle, et que pour garantir un portage de Javascript, il est préférable d’éviter autant que possible de faire référence à window.

Les déclarations de fonction

Les fonctions déclaratives sont, par défaut, également liées à l’Objet Global.

Toutefois, une particularité importante différencie le comportement d’une fonction par rapport à l’espace global. Le mode strict de Javascript empêche en effet le référencement de this dans une fonction, alors qu’il est toujours présent dans l’espace global. La fonction ne se mêle ainsi plus à l’Objet Global dont, il est vrai, elle ne constitue normalement pas une méthode.

Les expressions de fonction

Le contexte d’exécution d’une expression de fonction ne diffère guère de la fonction déclarative. A nouveau, l’Objet Global lui est assimilé, uniquement si le mode strict n’est pas activé.

Il en va de même pour une fonction IIFE qui n’est rien d’autre qu’une expression de fonction appelée directement.

Le mode strict annulant également le référencement de this.

Cette caractéristique permet de savoir si le mode strict est supporté par l’environnement d’exécution du code.

Le contexte des objets

Si une fonction est utilisée comme constructeur, c’est-à-dire qu’elle sert à instancier un nouvel objet, son contexte est rattaché à ce dernier, et this représente l’objet courant, tel qu’on en a l’habitude.

Cela semble simple… Nous allons toutefois constater que l’utilisation de ce contexte peut varier dans les fonctions déclarées à l’intérieur de l’objet.

Les fonctions imbriquées

Nous avons vu que le contexte d’une fonction était lié à l’Objet Global. Ce comportement est identique si celle-ci est déclarée à l’intérieur d’un objet.

Ce dernier cas laisse toutefois quelque peu dubitatif. En effet, pour quelle raison la fonction foo n’hériterait pas du contexte de l’objet qui la contient?

La première chose à bien comprendre, c’est que nous n’avons pas affaire ici à une méthode de l’objet. On parle souvent, à tort, de « méthode privée » lorsqu’une fonction est déclarée de la sorte, parce qu’elle ne sera pas accessible depuis l’extérieur de l’objet. Il s’agit plus exactement d’une fonction imbriquée (nested function), c’est-à-dire d’une fonction autonome déclarée tout simplement à l’intérieur d’une autre fonction, et accessible depuis cette dernière uniquement. Cette écriture est également permise par PHP, bien qu’elle n’y soit, par contre, absolument pas recommandée.

Deuxièmement, il ne faut pas perdre de vue que this n’est pas une variable. Il s’agit d’un mot-clé qui possède une valeur contextuelle. Concrètement, cela signifie que this n’a pas de portée, contrairement à une variable dont la portée est, en Javascript, toujours descendante. Ainsi, les fonctions imbriquées, qui bénéficient normalement d’un accès aux variables de leur fonction conteneur, n’héritent pas du même contexte.

Les expressions de fonctions initialisées à l’aide du mot-clé var, tout comme les déclarations de fonction, possèdent un contexte indépendant de l’objet. La preuve en est que this n’est plus, ici aussi, référencé sous le mode strict, ce qui permet d’éviter toute confusion. Une raison supplémentaire de ne pas hésiter à utiliser ce mode.

Les méthodes d’objet

Les expressions de fonction n’ont toutefois pas dit leur dernier mot, ce serait trop simple. Car si la variable contenant l’expression est liée à un objet comme attribut, son contexte va, cette fois-ci, être rattaché à ce dernier. La fonction partagera le même contexte que l’objet, en devenant ainsi une de ses méthodes.

Cela fonctionne au sein d’une instance d’Object, par exemple sous la forme d’un littéral.

Cela fonctionne lorsqu’une fonction sert de constructeur. La variable est directement liée au contexte de MyClass, ce qui permet à la fonction d’en hériter.

Cela fonctionne si on passe par l’attribut prototype.

Et cela fonctionne également lorsqu’on utilise la classe Array, qui n’est rien d’autre qu’un objet, certes un peu particulier, mais pouvant tout aussi bien accueillir une fonction.

On constate un comportement différent des fonctions imbriquées qui tient dans la façon dont chacune est appelée. En étant rattachée à l’attribut d’un objet, les méthodes appartiennent à celui-ci, à l’inverse des fonctions imbriquées contenues dans une variable indépendante de l’objet.

On pourrait croire à un particularisme de Javascript. Pourtant, tout comme dans les autres langages orientés objet, seules les fonctions utilisées comme méthodes héritent de manière implicite du contexte de leur objet. De même, les fonctions internes n’héritent jamais de ce contexte, simplement car il ne s’agit pas de méthodes, ce qu’on a tendance à oublier.

La différence de Javascript tient ainsi plus en l’utilisation particulière de ses objets, où méthodes et fonctions sont déclarées à un même niveau (ce qui n’est pas le cas en PHP où toutes les fonctions d’un objet sont ses méthodes, et où les fonctions indépendantes ne peuvent être déclarées de manière imbriquée qu’à l’intérieur d’une de ses méthodes), qu’en une interprétation saugrenue de son contexte.

Détournement de contexte

Vous l’aurez compris, le contexte d’une fonction varie selon son utilisation, laquelle peut s’avérer multiple en Javascript. Nous avons étudié un usage classique d’objet et de ses méthodes. Mais déclarer un objet demeure très proche d’une simple fonction qui peut tout aussi bien être utilisée comme constructeur, sans que sa syntaxe interne ne soit modifiée. Les seules différences résident dans l’utilisation du mot-clé new lors de l’appel de la fonction, et de la modification du contexte à l’intérieur de celle-ci.

Il en va bien sûr de même pour les instances d’Object dont chaque méthode peut retourner un nouvel objet.

Ce comportement déplaisant peut se révéler dangereux. Il engendrera d’ailleurs une erreur si, this valant undefined en mode strict, cette valeur est utilisée tel un objet existant.

Enfin, puisqu’une méthode n’est jamais que la simple valeur d’un attribut, il est également possible de l’assigner à un autre objet. Le contexte de la fonction référence alors l’objet receveur, et plus celui d’origine, ce qui peut entraîner une certaine confusion par rapport au résultat attendu.

Systématiser l’accès à this

Il est assez frustrant que this demeure inaccessible dans les fonctions imbriquées, alors qu’il est bien reconnu dans les méthodes de l’objet. Comment se dépêtrer d’une telle architecture si le contexte est soumis de la sorte à la déclaration des fonctions?

Une astuce existe pour récupérer le contexte de l’objet et accéder à ses attributs dans une fonction imbriquée. Elle consiste à assigner this à une variable dont la portée sera plus simple à manier.

En extrapolant un peu, on peut imaginer prévenir tout problème lié à l’utilisation de la fonction de manière directe plutôt que comme constructeur, en assignant un objet par défaut à cette variable si this n’est pas défini. Tout attribut rattaché à la variable est alors assigné à un objet vide, ce qui ne déclenchera pas d’erreur.

Mais je ne sais pas si cette astuce constitue une bonne pratique, et s’il ne vaut tout simplement pas mieux lancer une erreur en testant la valeur de this.

Call, apply, et bind

Nous avons tous appris qu’en Javascript, les fonctions étaient des objets, et que, comme tout bon objet qui se respecte, elles possédaient elles-mêmes des propriétés et des méthodes natives.

L’une de ces méthodes nous intéresse particulièrement car elle permet de spécifier le contexte. Il s’agit de la méthode call() (et de sa soeur apply() dont le comportement est similaire si ce n’est que cette dernière accepte les arguments à faire passer à la fonction sous la forme d’un tableau).

Cette méthode sert à invoquer une fonction et prend, comme premier argument, la valeur que this aura dans celle-ci. Si aucun argument n’est passé (ou que l’on passe null ou undefined) et que la fonction n’est pas exécutée en mode strict, this continuera à valoir l’Objet Global.

Le contexte d’une méthode peut ainsi être associé à n’importe quel objet.

En EcmaScript 5, la méthode bind() repose sur un principe similaire, mais permet une écriture plus élégante ainsi qu’un appel transparent.

Le contexte des évènements

Le contexte des évènements liés au DOM évolue également suivant la manière dont il est appelé. Mais nous allons constater que l’utilisation de fonctions dans ce cadre se conforme aux règles précédemment étudiées.

D’une manière générale, le contexte d’un attribut pointe vers l’objet HTML associé à l’évènement.

Cependant, le contexte de l’attribut onload de la balise body ne pointe pas vers cette dernière, mais vers l’objet Window. Il s’agit d’une exception un peu déroutante.

En réalité, ce code est l’équivalent HTML d’un appel à la méthode onload de l’objet Window.

Il ne faut par ailleurs pas confondre le contexte de l’attribut avec le contexte d’une fonction que celui-ci peut appeler. En effet, toute fonction conserve son comportement habituel en associant l’Objet Global à this (ou undefined selon le mode).

Si l’évènement est déclaré dans une méthode associée à l’objet HTML, this référence normalement son objet. Le contexte change, car l’écriture de la fonction change.

Enfin, en jQuery, n’oublions pas que this ne référence pas un objet jQuery, mais bien un objet HTML.

Conclusion

En définitive, le contexte semble plus simple qu’imaginé de prime abord, et peut se borner à quelques grandes règles:

  • Au niveau de l’espace global, this référence l’Objet Global.
  • A l’intérieur de l’attribut d’une balise HTML, this référence l’objet HTML de cette balise (à l’exception de l’attribut onload de la balise body qui référence Window).
  • Dans une fonction, qu’il s’agisse d’une déclaration ou d’une expression, qu’elle soit imbriquée ou non, qu’elle ait été appelée depuis l’espace global ou un attribut HTML, this vaut l’Objet Global, ou undefined si le mode strict est activé.
  • Dans une méthode, this vaut l’objet qui contient la méthode.
  • Call, apply et bind sont des méthodes de la fonction qui permettent de redéfinir this en une valeur arbitraire.

9 réflexions au sujet de « This is it! Le contexte en Javascript »

  1. Ping : Javascript | Pearltrees

  2. Ping : This is it! Le contexte en Javascript | The Dark Side Of The Web | Bonnes Pratiques Web | Scoop.it

    • Tu n’as pas d’instance : tu n’as pas initialisé ton objet ie. lancé son constructeur.
      La fonction « foo » n’es pas une méthode d’instance mais juste une propriété de l’objet unique que tu as créé plutôt.

      var b = new Bar();// ton objet est initialisé
      console.log(‘b’, b.arg);//arg existe , est une propriété
      b.foo();// foo n’existe pas

      • Tout à fait, mais c’était justement le propos de mon commentaire: je parle d’une fonction de classe et pas d’une fonction d’instance. Le this n’est pas le même. Et dans une fonction de classe, le this n’est pas l’instance. Il n’est donc pas possible d’accéder aux propriétés de l’instance, comme depuis l’instance il n’est pas possible d’accéder directement à la fonction de classe (ou alors il faut faire bar.constructor.foo();).

        merci pour ta remarque!

  3. Ping : Déclaration et définition de fonctions en Javascript | The Dark Side Of The Web

  4. Ping : Appel et retour de fonctions en Javascript | The Dark Side Of The Web

  5. Ping : Les closure en PHP | The Dark Side Of The Web

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Vous pouvez utiliser ces balises et attributs HTML : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>