Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

[long] le mot clef this et son contexte d'execution

1 réponse
Avatar
Laurent Vilday
Dans un précédent message, Bertrand B s'interrogeait sur le mot clé this
et comment s'y prendre pour savoir ce qu'il représente (le contexte
d'éxécution), donc comme promis je démarre un nouveau thread avec ce que
j'en sais et je finirais sur l'exemple qu'il avait donné.

Déjà, je pense qu'il est préférable de commencer par regarder ce que la
spécification raconte à ce sujet avant de s'empétrer dans les
différentes implémentations des navigateurs.

http://www.ecma-international.org/publications/standards/Ecma-262.htm

chapitre 10.1.7, je cite :
"There is a *this* value associated with every active execution context.
The *this* value depends on the caller and the type of code being
executed and is determined when control enters the execution context.
The *this* value associated with an execution context is immutable."

Grosso modo, la valeur de *this* dépend de où on se trouve et du type de
traitement en cours. Ce qui je pense, n'aide pas beaucoup à faire la
lumière.

Donc pour faire simple, dès qu'on commence à scripter, un contexte
d'éxecution est créé et *this* représente ce contexte. Donc quand on
commence, le contexte est *window*, donc *this* à ce moment représente
le même objet.

<script type="text/javascript">
alert(this); // [object] pour IE et [Object Window] pour FX
alert(this === window); // true pour tout le monde
</script>

On a créé le premier contexte d'éxécution, c'est lui le père de tous les
autres qui vont se créer. Et c'est grâce au chaînage des différents
contextes pour remonter jusqu'à lui qu'on peut aller chercher des
variables ou des méthodes en dehors du contexte courant (on verra ça
après, le chaînage des contextes est à lui seul un vaste sujet
également). Lorsque on déclare une variable avec var nomvar=''; on
indique au contexte en cours que la variable nomvar est déclarée dans ce
contexte. Si var est omis lors de la déclaration (nomvar='';) c'est
alors dans le contexte global que la variable est déclarée.

Quand une function est créée, le contexte ne varie pas. Par contre,
quand un objet est crée, un nouveau contexte est également créé à ce
moment pour le code que cet objet contient. Ainsi, le mot clé *this*
change quasiment tout le temps, en fonction du type de code exécuté
comme indiqué dans la spécification du langage.

<script type="text/javascript">
function foo()
{
alert(this === window);
}
foo(); // dira true dans la boite d'alerte
var bar = new foo(); // dira false dans la boite d'alerte
</script>

Si on fait *foo()* tout simplement, on ne fait qu'un appel de la
fonction, donc on ne change pas le contexte actif. La boite d'alerte
dira true puisqu'on est toujours dans le cas précédent où this
représente window.
Si on fait *var bar = new foo();*, là on créé un nouvel objet donc un
nouveau contexte et là la boite d'alerte dira false. Et c'est à ce
moment là qu'on peut commencer à s'embrouiller pour savoir ce que
représente *this* au moment de l'éxécution du code comme le soulignait
Bertand.

Il y a de nombreuses façons de créer un nouveau contexte d'éxécution.
Lors de la création des événements par exemple pour commencer par eux,
et évidemment tout change suivant les implémentations des navigateurs et
la façon dont l'event est géré :)

Le cas le plus simple, acceder à this depuis le script placer dans la
balise html :
<div id="foo" onclick="alert(this === window);"></div>

Au moment du clic, la boite d'alerte dit false. Donc on a changé
d'éxécution entre les deux " du onclick, ce qui est normal puisque le
contexte d'éxécution à ce moment précis est l'élément <div> sur lequel
on a cliqué.
Pour s'en assurer, le code suivant nous dit bien *type: 1 tag: DIV*
quand on clique dessus (type 1 qui représente document.ELEMENT_NODE,
donc un tag html en fait) :
<div id="foo" onclick="alert('type: ' + this.nodeType + ' tag: ' +
this.tagName);"></div>

Maintenant, cela change quand on appelle une fonction depuis ce onclick:
<div id="foo" onclick="bar();"></div>
<script type="text/javascript">
function bar()
{
alert(this === window); // true
alert(this.tagName); // undefined
alert(this.nodeType); // undefined
}
</script>

Une fois qu'on est dans bar(), on a perdu le contexte du onclick et on
est revenu sur le contexte principal. Ce qui est plutôt génant parce que
on a perdu l'information précise nous disant qui était cliqué.

On peut réussir a retrouver le contexte qui nous intéresse (l'élément
DIV), en le forçant avec la méthode .call() (chapitre 13.2.1 de la
spécification )

<div id="foo" onclick="bar.call(this);"></div>
<script type="text/javascript">
function bar()
{
alert(this === window); // false
alert(this.tagName); // 1
alert(this.nodeType); // DIV
}
</script>

C'est une des raisons je pense qui fait que ce modèle de gestion des
événements (dit modèle traditionnel) a disparu ou tend à disparaitre au
profit des listeners. Je parle ici de quasiment tous les navigateurs
sauf IE évidemment qui continue à se tromper de contexte d'éxécution
avec son système d'attachement d'événement. Mais il ne faut pas
s'inquiéter, il y a énormément de littérature sur internet qui explique
quels sont les problèmes du système d'event de IE et comment les
corriger. Je ne m'étendrais pas plus sur la gestion des events ici car à
lui seul encore, on pourrait en faire des pages.
Au pire, si la recherche est infructueuse (même si je peux pas y
croire), il reste l'excellente librairie de Yahoo qui même si elle peut
paraitre compliqué à comprendre est d'une simplicité d'utilisation
ahurissante.
http://developer.yahoo.net/yui/event/index.html

On a vu que quand on créait un objet on créait un contexte et cela va
beaucoup plus loin puisqu'il faut se rappeller que tout ce qui est
manipulé en javascript est un objet. Même les chiffres ou encore
pourquoi pas les booleens.

Imaginons ceci, on créé un nouveau membre (méthode) à l'objet Number,
que peut bien représenter *this* à l'intérieur au moment de l'éxécution?
<script type="text/javascript">
Number.prototype.bar = function()
{
alert(this === window); // false
alert(this); // affiche la valeur, 432 ici en l'occurence
};
var foo = 432;
foo.bar();
</script>

La première alerte dit false, heureusement le contexte c'est pas la fenêtre.
La deuxième alerte affiche la valeur du chiffre, ce qui est encore une
fois bien heureux puisque le contexte c'est effectivement le chiffre en
question.

Ce genre de manipulation des objets est constant en javascript puisque
c'est ce qui s'appelle le prototypage. Javascript est un langage objet
prototypé. C'est grâce à ce mécanisme qu'on peut créer de nouveaux
objets avec leurs propriétés et leurs méthodes et que le mot clef *this*
gagne encore en puissance.

<script type="text/javascript">
function circle(rayon)
{
this.setRayon(rayon);
return this;
}
circle.prototype.getRayon = function()
{
return this.rayon;
};
circle.prototype.setRayon = function(rayon)
{
this.rayon = rayon;
};
circle.prototype.getDiametre = function()
{
return this.rayon * 2;
};
circle.prototype.getCirconference = function()
{
return this.getDiametre() * Math.PI;
};
</script>

On pourrait objecter que les méthodes getDiametre et getCirconference
ferait certainement mieux d'être en dehors du prototype, mais bon c'est
pas le problème du moment je pense.

On aurait pu écrire cet objet de plein d'autres façons, mais c'est à mon
avis celle qui est la plus lisible. Peut être pas la plus compacte je
l'accorde volontier, mais au moins je pense qu'on ne s'y perd pas.
Contrairement à la façon suivante par exemple qui certes a le mérite
d'être concise mais qui à mon avis peut très vite devenir un sac de
noeud incompréhensible :
<script type="text/javascript">
function circle(r)
{
this.rayon = r;
this.getRayon = function() { return this.rayon; };
this.setRayon = function(r) { this.rayon = r; };
this.getDiametre = function() { return this.rayon * 2; };
this.getCirconference = function() { return this.getDiametre() *
Math.PI; };
return this;
}
</script>

D'autres préfèrerons une notation JSON, c'est pareil mais je ne trouve
pas qu'elle éclaircisse la lecture du code. JSON est parfait pour
transmettre des infos lors des requêtes xmlhttp par exemple, mais d'un
ennui à lire et surtout à écrire.
http://www.json.org/

Enfin bref, que représente *this* au fur et à mesure de l'éxécution du
script ? Ca dépend de la façon de l'appeler évidemment.

Si on appelle la function circle() comme ce qu'elle est, c'est à dire
comme une fonction, on se retrouve à manipuler l'objet window, ce qui
est plutot mal puisqu'on va créer (ou écraser) des propriétés dans le
contexte global. Par exemple :

<script type="text/javascript">
function circle(r) { ... }
...
var rayon = 100;
circle(25);
alert(rayon); // 25 :(
</script>

La boite d'alerte, au lieu de nous dire 100 nous dit 25, on a changé une
variable globale sans le vouloir (enfin, si, on l'a voulu puisque on a
appelé notre fonction comme une bête fonction)
Pire, les fonctions *getRayon*, *setRayon*, *getDiametre* et
*getCirconference* on été définies dans l'objet window, ce qui peut
entrainer des écrasements de fonctions dans tous les sens et ce serait
ingérable. Donc attention à *this* et à la façon dont on le manipule :)

Si par contre on appelle notre function comme il se doit, c'est à dire
en instanciant l'objet, à ce moment *this* représente l'instance de
l'objet en cours. Et on a donc accès à toutes les propriétés et méthodes
qui nous sont propres sans risque d'écrasement du contexte global.

<script type="text/javascript">
function circle(r) { ... }
...
var rayon = 100;
var cercle = new circle(25);
alert(rayon); // 100 :)
</script>

Il existe également un cas où la variable *this* peut être délicate à
identifier dans son contexte, sur un setTimeout ou un setInterval

Tout d'abord, on reprend notre objet circle puis on lui ajoute les
propriétés x et y et quelques méthodes de manipulation (setX, setY,
getX, getY, moveTo)
<script type="text/javascript">
function circle(rayon)
{
this.setRayon(rayon);
this.setX(0);
this.setY(0);
return this;
}
...
circle.prototype.setX = function(x) { this.x = x; };
circle.prototype.setY = function(y) { this.y = y; };
circle.prototype.getX = function() { return this.x; };
circle.prototype.getY = function() { return this.y; };
circle.prototype.moveTo = function(x, y)
{
this.setX(x);
this.setY(y);
};
</script>

Maintenant, imaginons qu'on veuille déplacer notre cercle de la position
0,0 à 100,100 par pas de 10 (humm, rappel avant qu'on me le dise, le
code de déplacement d'un cercle est juste à but didactique, il est
évident que c'est un peu plus complexe que ça de déplacer des éléments).
Pour le faire, il faut utiliser un timeout ou une intervalle, mais le
principe reste le même quelque soit la méthode retenue. Mais que peut
bien représenter *this* quand on déclare et éxécute un timeout ? Encore
une fois, tout dépend de la façon de déclarer les choses, donc
commençons par identifier *this* sans se préoccuper réellement du
déplacement du cercle.

La façon la plus répandue est de faire ceci : setTimeout("....", ms);

<script type="text/javascript">
/**
* déclaration de la fonction d'animation
* @param {Number} toX La position X finale
* @param {Number} toY La position Y finale
* @param {Number} step Le décalage à utiliser
*/
circle.prototype.anime = function(toX, toY, step)
{
var x = Math.min(this.getX() + step, toX);
var y = Math.min(this.getY() + step, toY);
this.moveTo(x, y);
if (x !== toX || y !== toY)
{
setTimeout("alert(this === window);", 500);
}
}
// création de l'instance avec un rayon de 50
var cercle = new circle(50);
// lance l'animation jusqu'à la position 100,100 par pas de 10
cercle.anime(100, 100, 10);
</script>

Au moment ou le timeout va se lancer, la boite d'alerte va nous dire
*true* parce que le contexte d'éxécution du timeout n'a aucune notion de
l'instance de cercle qui essaye de se déplacer, le contexte d'éxécution
se trouve être le global.

Pour pallier cette perte du this, on voit souvent une pratique sur le
web (très mauvaise à mon goût) qui consiste à déclarer une variable
*dans le contexte global* qui réprésente this au moment de l'éxécution.
Ca donne ça :

if (x !== toX || y !== toY)
{
moi = this;
setTimeout("moi.anime(" + toX + "," + toY + "," + step + ")", 500);
}

Une horreur sans nom imo, mais d'accord ça a le mérite de faire ce qu'on
lui demande à la condition extrème qu'une seule instance à la fois
essaye de s'animer. Je laisse imaginer l'écrasement de la variable moi
quand 100 instances de cercles se déplacent en même temps.

Mais alors, comment faire ? :)

moi (ehehe var moi je dirais) je ferais ça :

if (x !== toX || y !== toY)
{
// on créé une référence sur this
var moi = this;
// on ajoute un membre sur l'instance qui est
// le handler anonyme du timeout
this._animeHandler = function()
{
// anime l'objet déclaré (closure)
moi.anime(toX, toY, step);
// détruit ce handler
moi._animeHandler = null;
// détruit la variable moi
moi = null;
};
// on associe notre handler anonyme au timeout
setTimeout(this._animeHandler, 500);
}

Ce genre de manipulation, c'est ce qu'on appelle une closure, la
variable moi est déclarée dans le contexte de la méthode anime() mais on
y accède depuis un autre contexte, celui de la function _animeHandler.
J'ai pris pour habitude de détruire le handler et la variable utilisée
pour le handler, parce que il est très facile pour les navigateurs (et
IE en première ligne) de laisser fuir leur mémoire suivant le type
d'objet utilisé tout au long des processus. Donc dans le doute, je les
détruis toujours à la main sans compter sur leur garbage collector tout
cassé :)

Pourquoi se serait mieux ? Là, je ne saurais quoi répondre, si ce n'est
que à force d'expérience je me suis rendu compte que c'était la façon de
faire qui était la plus facile à gérer pour moi tout en conservant une
clarté de code suffisante. Il est fort possible que je me trompe soit
dit en passant.

Maintenant, je reprends le code d'origine fourni par Bertrand B,
histoire de lui faire quand même une réponse sur le code qu'il avait
fourni, même si je pense m'être un peu trop étalé dans tous les sens :)

> function Requeteur(Nom){
> this.fifo = new Array();

this.fifo = [];
C'est plus concis et c'est tout aussi bien imo

> this.nom=Nom
> this.actived = false;
> this.stop=function(){
> //alert("stop");
> this.actived=false;
> this.fifo=new Array();
> }

Là, comme je l'ai souligné plus haut, je trouve que tu devrais éviter de
déclarer les méthodes de ton objet au sein du constructeur. Ca
permettrais d'y voir beaucoup plus clair dans l'objet Requeteur en lui même.

> this.donext=function(){
> if (this.fifo.length >0 && this.actived) {
> job=this.fifo.shift();

attention là, la variable job est créée dans le contexte global (window)
puisque il n'y a pas de déclaration *var*. Ca parait rien, mais même si
ce n'est pas une fuite mémoire puisque le navigateur arrivera quand même
à la supprimer lorsqu'il quittera la page, il n'empêche que il y a un
risque d'écrasement. A moins que ce ne soit voulu peut être, mais ça me
parait louche de manipuler des variables globales quand on est au sein
de l'éxécution d'une méthode d'un objet.

> if(cache[job.methode][job.URL] && this.actived)
> {job.Apres(cache[job.methode][job.URL]);this.donext();}
> else job.xhr.send(null);
> //Probleme si le serveur ne retourne pas d'ereur 404
> }
> else this.actived=false;
> }
> this.send= function (Methode,URL,apres){
> var me=this;

ehehe, et c'est là qu'on retrouve le fameux var moi = this; dont je
parlais plus haut. Ils sont vicieux à manipuler, là pour le coup je
pense qu'il faudrait restructurer un peu l'ensemble de l'objet. Mais mis
à part le *var* qui manque à un moment, si le script fait bien ce qu'il
est supposé faire, je ne vois pas trop de raison de le modifier. Si ce
n'est par plaisir personnel :) Pas la peine de toujours tout casser pour
refaire quand le besoin n'existe pas.

Voila dans les grandes lignes ce que je pourrais raconter sur le mot clé
*this*. J'espère ne pas avoir fait trop d'erreurs dans ce que j'ai pu
raconter et surtout que ce sera profitable à qqun. Si c'est le cas,
qu'un erreur s'est glissée ce dont je ne doutes pas malheureusement,
j'encourage évidemment tout à chacun de compléter ou de rectifier toutes
mes erreurs, approximations, inexactitudes et oublis.

--
laurent

1 réponse

Avatar
Bertrand B

Grosso modo, la valeur de *this* dépend de où on se trouve et du ty pe de
traitement en cours. Ce qui je pense, n'aide pas beaucoup à faire la
lumière.


Exactement ce que je déteste dans un langage que ça dépende de l'en droit
où l'on se trouve OK mais le type de traitement en cours alors là ...


Donc pour faire simple, dès qu'on commence à scripter, un contexte
d'éxecution est créé et *this* représente ce contexte. Donc qua nd on
commence, le contexte est *window*, donc *this* à ce moment représe nte
le même objet.



Contexte d'exécution dans ce cas cela semble un peu différent de la
notion de contexte d'exécution des closures. Mais bon je suis

contexte. Si var est omis lors de la déclaration (nomvar='';) c'est
alors dans le contexte global que la variable est déclarée.


Paf le doigt sur quelque chose que je n'avais pas compris, pour moi
habtiué à Python, une déclaration même implicite dans une fonctio n était
forcément locale. Ce fonctionnement atypique je ne l'avais pas vu venir .


Quand une function est créée, le contexte ne varie pas. Par contre,
quand un objet est crée, un nouveau contexte est également créé à ce
moment pour le code que cet objet contient. Ainsi, le mot clé *this*
change quasiment tout le temps, en fonction du type de code exécuté
comme indiqué dans la spécification du langage.
D'autres préfèrerons une notation JSON, c'est pareil mais je ne tro uve
pas qu'elle éclaircisse la lecture du code. JSON est parfait pour
transmettre des infos lors des requêtes xmlhttp par exemple, mais d'u n
ennui à lire et surtout à écrire.
http://www.json.org/


Moi je trouve le à la json clair tant que l'objet n'a pas de méthode
après c'est un problème d'indentation



Pour pallier cette perte du this, on voit souvent une pratique sur le
web (très mauvaise à mon goût) qui consiste à déclarer une va riable
*dans le contexte global* qui réprésente this au moment de l'éxé cution.
Ca donne ça :

if (x !== toX || y !== toY)
{
moi = this;
setTimeout("moi.anime(" + toX + "," + toY + "," + step + ")", 500);
}

moi (ehehe var moi je dirais) je ferais ça :

if (x !== toX || y !== toY)
{
// on créé une référence sur this
var moi = this;
// on ajoute un membre sur l'instance qui est
// le handler anonyme du timeout
this._animeHandler = function()
{
// anime l'objet déclaré (closure)
moi.anime(toX, toY, step);
// détruit ce handler
moi._animeHandler = null;
// détruit la variable moi
moi = null;
};
// on associe notre handler anonyme au timeout
setTimeout(this._animeHandler, 500);
}

C'est ce que j'ai utilisé mais j'aurais pu me planter et ne pas mettre

le var n'ayant pas compris la subtilité.

Là, comme je l'ai souligné plus haut, je trouve que tu devrais év iter de
déclarer les méthodes de ton objet au sein du constructeur. Ca
permettrais d'y voir beaucoup plus clair dans l'objet Requeteur en lui
même.


Ca ne ferait pas de mal .... il faudrait aussi que je reprenne le cde
complet de manière plus modulaire.

this.donext=function(){
if (this.fifo.length >0 && this.actived) {
job=this.fifo.shift();


attention là, la variable job est créée dans le contexte global ( window)
puisque il n'y a pas de déclaration *var*. Ca parait rien, mais mêm e si
ce n'est pas une fuite mémoire puisque le navigateur arrivera quand m ême
à la supprimer lorsqu'il quittera la page, il n'empêche que il y a un
risque d'écrasement. A moins que ce ne soit voulu peut être, mais ç a me
parait louche de manipuler des variables globales quand on est au sein
de l'éxécution d'une méthode d'un objet.
Surtout que j'utilise l'objet en pseudo traitement parallèle (puisque t u

es allé sur le site il y a 7 instances qui bossent en parallèle) pas
encore rencontré de problème mais à corriger.



ehehe, et c'est là qu'on retrouve le fameux var moi = this; dont je
parlais plus haut. Ils sont vicieux à manipuler, là pour le coup je
pense qu'il faudrait restructurer un peu l'ensemble de l'objet. Mais mi s
à part le *var* qui manque à un moment, si le script fait bien ce q u'il
est supposé faire, je ne vois pas trop de raison de le modifier. Si c e
n'est par plaisir personnel :) Pas la peine de toujours tout casser pou r
refaire quand le besoin n'existe pas.


Oui il fait ce que j'attends avec les perfs que j'en attendais mais il
faudra forcément que je remette un jour les mains dedans pour supprimer
les impasse que j'ai laissé. Et honnêtement rajouter un traitement
d'exception dedans me fait peur.


Voila dans les grandes lignes ce que je pourrais raconter sur le mot cl é
*this*. J'espère ne pas avoir fait trop d'erreurs dans ce que j'ai pu
raconter et surtout que ce sera profitable à qqun. Si c'est le cas,
qu'un erreur s'est glissée ce dont je ne doutes pas malheureusement,
j'encourage évidemment tout à chacun de compléter ou de rectifier toutes
mes erreurs, approximations, inexactitudes et oublis.

--
laurent


Chapeau bas. Et merci de ces éclaircissement que je ne manquerai pas de
relire une troisième fois.