Pork Center

JavaScript

De Hack-it.org.

(this)
m
 
Ligne 55 : Ligne 55 :
<javascript>document.body.addEventListener('click', function(evt){ alert(this.nodeName); };
<javascript>document.body.addEventListener('click', function(evt){ alert(this.nodeName); };
// un clic sur la page affichera "body"</javascript>
// un clic sur la page affichera "body"</javascript>
 +
 +
=== S'affranchir de '''this''' ===
 +
 +
Etant donné la différence entre le '''this''' JavaScript et le '''this''' tel qu'il est admis dans d'autres langages orientés objet, beaucoup de programmeurs utilisent ''self'' comme référence à l'objet courant.
 +
 +
Il suffit de procéder ainsi dans le constructeur de l'objet :
 +
<javascript>function Truc(){ // constructeur
 +
    var self = this;
 +
    this.param = 5;
 +
    this.getParam = function(){ return self.param; }
 +
}
 +
var mon_truc = new Truc();
 +
mon_truc.getParam(); // renvoie bien sur 5
 +
fnp = mon_truc.getParam;
 +
fnp(); // renvoie aussi 5 !</javascript>
 +
 +
En effet, dans la fonction ''getParam'', ''self'' fait référence au ''this'' en vigueur lors de la construction de l'objet, soit l'objet lui même.
 +
 +
Mais la vraie question est : comment ''getParam'' peut-il faire référence à une variable qui n'est pas dans sa portée, et qui plus est dans un bloc qui a terminé de s'exécuter depuis ?!
 +
 +
Ceci est possible grâce (ou à cause, selon votre humeur) aux ''closures'', que nous verrons dans le chapitre suivant.
 +
 +
= Les ''closures'' =
 +
 +
On lit par ci par là sur le net des articles techniques expliquant les ''closures'' en JavaScript, que certains trouvent imbitables, et d'autres qui se veulent des articles de vulgarisation, que je trouve peu rigoureux voire faux.
 +
 +
Une ''closure'' est, pour faire simple, un contexte d'exécution (Scope) contenant l'ensemble des variables "locales" de cette fonction, dont ses paramètres.
 +
Une ''closure'' est '''créée''' à chaque '''appel''' de fonction (et non définition) :
 +
 +
<javascript>f = function(a) { var b = 3; }
 +
f(2); // une closure est créée et contient {a:2, b:3}</javascript>
 +
 +
Une des spécificités d'une ''closure'' est qu'elle englobe à la fois les variables définies dans la fonction, mais aussi celles du contexte dans lequel a été défini cette fonction. C'est ce qu'on appelle la ''Scope Chain'' : quand JavaScript ne trouve pas la variable à laquelle on fait référence dans le contexte courant, il regarde dans le contexte appelant, et ainsi de suite, jusqu'à arriver aux bornes de la ''closure'', au bout de la ''Scope Chain''.
 +
 +
<javascript>f = function(a) {
 +
    this.f2 = function() { var b=2; return a+b; }
 +
}
 +
var tmp = f(3); // une closure est créée, contenant {a:3}
 +
  // pendant la création, dans f2, la scope chain est : {a:3}<={b:2}
 +
tmp.f2(); // retourne 5</javascript>
 +
''f2'' a accès à la variable ''a'' définie dans ''f''.
 +
 +
== Problèmes courants ==
 +
 +
Les ''closures'' peuvent être un merveilleux outil, mais le plus souvent elles nous jouent des tours.
 +
 +
Un problème récurrent à cause des ''closures'' est celui de la définition de fonctions dans une boucle :
 +
<javascript>function go() {
 +
    for(var i=0; i<3; i++) {
 +
        var a = document.createElement('a');
 +
        a.href = "javascript:";
 +
        a.innerHTML = "Je dis "+i+". ";
 +
        a.addEventListener('click', function() { alert("Je dis "+i); }, false);
 +
        document.body.appendChild(a);
 +
    }
 +
}
 +
go(); // ici une closure est créée</javascript>
 +
 +
On a une page contenant trois liens cliquables "Je dis 0. Je dis 1. Je dis 2.", mais quelque soit le lien sur lequel on clique, une alerte apparait nous disant "Je dis 3" !
 +
 +
C'est très simple. On a défini trois fonctions anonymes (les trois gestionnaires d'évènements) qui partagent toutes la même (la seule) ''closure'' : celle de go !
 +
 +
Dans cette closure, il existe une variable ''i''. A la fin de l'appel de go(), ''i'' vaut 3. La closure contient donc {i:3}.
 +
 +
Lors de l'appel des gestionnaires d'évènements, une des trois fonctions anonymes est appelée, et évalue ''i'', qui vaut 3. Voilà pourquoi quelque soit le lien que l'on clique, il affiche "Je dis 3".
 +
 +
 +
Comment corriger ce problème ? Il faut que la variable ''i'' dans les fonctions anonymes soit unique, qu'elle ne fasse pas référence à une variable partagée par toutes les fonctions !
 +
On peut le faire en créant de nouvelles ''closures'', contenant chacune une telle variable.
 +
Rappel : une ''closure'' est créée lors d'un '''appel''' de fonction.
 +
 +
<javascript>function gestionnaire(elem, num) {
 +
    elem.addEventListener('click', function() { alert("Je dis "+num); }, false);
 +
}
 +
 +
function go() {
 +
    for(var i=0; i<3; i++) {
 +
        var a = document.createElement('a');
 +
        a.href = "javascript:";
 +
        a.innerHTML = "Je dis "+i+" ";
 +
        gestionnaire(a, i); // un appel de fonction => création d'une closure
 +
        document.body.appendChild(a);
 +
    }
 +
}
 +
go(); // ici une closure est créée</javascript>
 +
 +
Comme avant, une ''closure'' contenant ''{i}'' est créée à l'appel de go. Mais au premier tour de boucle, on appelle la fonction gestionnaire qui crée une nouvelle ''closure'' contenant ''{elem:[DOMRef], num:0}'', puis qui définit une fonction anonyme (le gestionnaire d'évènements) qui fait référence à ''num''.
 +
Au second tour, une ''closure {elem:[DOMRef], num:1}'' et une autre fonction anonyme qui fait référence à ''num'' (mais le ''num'' de la 2ème ''closure'', qui vaut 1).
 +
Et de même au 3e tour.
 +
 +
Ainsi quand on clique sur un lien, il appelle la fonction anonyme gérant l'évènement, qui affiche la bonne valeur.
 +
 +
=== Autres écritures ===
 +
 +
Il existe plusieurs moyens plus ou moins élégants de réaliser cet appel de fonction nécessaire à la création d'une ''closure''. Ici on a défini une fonction externe qu'on appelle depuis la boucle. On peut aussi définir la fonction dans ''go'', avant la boucle :
 +
 +
<javascript>function go() {
 +
    var gestionnaire = function(elem, num) {
 +
        elem.addEventListener('click', function() { alert("Je dis "+num); }, false);
 +
    };
 +
 +
    for(var i=0; i<3; i++) {
 +
        var a = document.createElement('a');
 +
        a.href = "javascript:";
 +
        a.innerHTML = "Je dis "+i+" ";
 +
        gestionnaire(a, i); // un appel de fonction => création d'une closure
 +
        document.body.appendChild(a);
 +
    }
 +
}</javascript>
 +
 +
Ou bien dans la boucle :
 +
 +
<javascript>function go() {
 +
    for(var i=0; i<3; i++) {
 +
        var a = document.createElement('a');
 +
        a.href = "javascript:";
 +
        a.innerHTML = "Je dis "+i+" ";
 +
        var gestionnaire = function(elem, num) {
 +
            elem.addEventListener('click', function() { alert("Je dis "+num); }, false);
 +
        };
 +
        gestionnaire(a, i); // un appel de fonction => création d'une closure
 +
        document.body.appendChild(a);
 +
    }
 +
}</javascript>
 +
 +
Note : définir une fonction dans la boucle amène à la définir autant de fois que de tours de boucle, ce qui est un peu plus couteux que de ne la définir qu'une fois. Il faut choisir entre élégance et performance.
 +
 +
On peut combiner définition et appel :
 +
 +
<javascript>function go() {
 +
    for(var i=0; i<3; i++) {
 +
        var a = document.createElement('a');
 +
        a.href = "javascript:";
 +
        a.innerHTML = "Je dis "+i+" ";
 +
        (function(elem, num) {
 +
            elem.addEventListener('click', function() { alert("Je dis "+num); }, false);
 +
        })(a, i);
 +
        document.body.appendChild(a);
 +
    }
 +
}</javascript>
 +
 +
=== Les références aux autres ''closures'' ===
 +
 +
Au lieu de passer un simple nombre, on veut maintenant passer un objet plus complexe dans notre nouvelle ''closure'' (ici un tableau à deux éléments : la chaîne "Je dis" et le nombre, que l'on fusionnera pour l'affichage) :
 +
 +
<javascript>function go() {
 +
    var truc = new Array();
 +
    truc[0] = "Je dis";
 +
    for(var i=0; i<3; i++) {
 +
        var a = document.createElement('a');
 +
        a.href = "javascript:";
 +
        a.innerHTML = "Je dis "+i+" ";
 +
        truc[1] = i;
 +
        (function(tablo) {
 +
            a.addEventListener('click', function() { alert(tablo.join(" ")); }, false);
 +
        })(truc);
 +
        document.body.appendChild(a);
 +
    }
 +
}
 +
go();</javascript>
 +
 +
Incroyable mais vrai, ils se mettent tous à dire "Je dis 2" ! Ca s'explique assez facilement :
 +
 +
Lors de l'appel d'un des gestionnaires d'évènements, il fusionne bien son ''tablo'' personnel (enfermé dans sa ''closure''), mais que contient ce ''tablo'' ?
 +
 +
En fait ''tablo'' ne contient rien : c'est une référence vers un '''Array''' de deux éléments, tout comme ''truc''.
 +
 +
On a donc trois ''closures'' contenant chacune une référence ''tablo'' vers le même '''Array'''. Et cet '''Array''' a été modifié pour la dernière fois lors du 2ème tour de boucle, il contient donc ["Je dis", 2].
 +
 +
 +
Pourquoi n'avions-nous pas le problème avec num ? Car quand on a passé ''i'' à la fonction, ''i'' étant un type primitif (i.e. un nombre, un booléen, une chaîne ou null), JavaScript le passe par valeur, c'est à dire qu'il '''copie''' ''i'' dans ''num''. Si ''i'' valait 1, ''num'' était défini à 1. Les entiers ne sont pas des '''références''', mais ils contiennent réellement le scalaire en eux. Alors que les tableaux sont des '''références''' vers un objet '''Array'''. Pour preuve :
 +
 +
<javascript>var a = [1, 2]; // a est une référence vers un Array contenant [1, 2]
 +
var b = a; // b fait référence à la même chose que a
 +
b[0] = 666;
 +
alert(a[0]); // 666 !</javascript>
 +
 +
Donc lorsqu'on passe ''truc'' à la fonction, ''tablo'' fait référence au même objet '''Array''' que ''truc''.
 +
 +
Pour garantir d'avoir un type complexe enfermé dans une ''closure'', il faut faire attention qu'il ne réfère pas à un autre objet, mais qu'il réfère à une copie unique.
 +
 +
A chaque tour de notre boucle, il faut que l'on crée un '''Array''' unique. Deux possibilités :
 +
 +
- en créer un nouveau à chaque tour :
 +
 +
<javascript>function go() {
 +
    for(var i=0; i<3; i++) {
 +
        var a = document.createElement('a');
 +
        a.href = "javascript:";
 +
        a.innerHTML = "Je dis "+i+" ";
 +
        var truc = new Array();
 +
        truc[0] = "Je dis";
 +
        truc[1] = i;
 +
        (function(tablo) {
 +
            a.addEventListener('click', function() { alert(tablo.join(" ")); }, false);
 +
        })(truc);
 +
        document.body.appendChild(a);
 +
    }
 +
}
 +
go();</javascript>
 +
 +
- ou bien copier l'objet :
 +
 +
<javascript>function go() {
 +
    var truc = new Array();
 +
    truc[0] = "Je dis";
 +
    for(var i=0; i<3; i++) {
 +
        var a = document.createElement('a');
 +
        a.href = "javascript:";
 +
        a.innerHTML = "Je dis "+i+" ";
 +
        truc[1] = i;
 +
        (function() {
 +
            var tablo = copy(truc);
 +
            a.addEventListener('click', function() { alert(tablo.join(" ")); }, false);
 +
        })();
 +
        document.body.appendChild(a);
 +
    }
 +
}
 +
go();</javascript>
 +
 +
La fonction ''copy'' n'existe pas en JavaScript, mais de telles fonctions ont été codées par de nombreux frameworks. C'est ce qu'ils appellent la '''deep copy''', copie profonde d'un objet.
 +
 +
Attention cependant, ces fonctions échouent parfois à copier certains objets de type natif. Par exemple, dans ''JQuery'', copier une expression régulière ne fonctionnera pas, et de manière générale, copier un objet contenant dans ses membres ou sous-membres une ''RegExp'', ''Date'' ou ''Function'' ne fonctionne pas.
 +
 +
Firefox (et surement d'autres) peuvent réaliser une copie assez facilement :
 +
 +
<javascript>var dolly2 = eval(dolly.toSource());</javascript>
 +
 +
L'intérêt est qu'elle supporte les types natifs. En revanche, impossible de copier des références vers des objets DOM.
 +
 +
= Conclusion =
 +
 +
En résumé, une ''closure'' est '''créée''' à chaque '''appel''' de fonction. Elle contient les variables et paramètres de cette fonction et de la fonction qui l'a '''définie''' (et ainsi de suite, si on imbrique les définitions). Si on veut '''créer''' une ''closure'', il faut définir puis '''appeler''' une fonction, et faire attention qu'elle n'ait pas de références vers des objets situés dans une autre ''closure'', si ce n'est pas ce qu'on attend.

Version actuelle en date du 2 février 2011 à 06:00