Patrice Ferlet
Patrice Ferlet
Créateur de ce blog.
Publié le mars 28, 2016 Temps de lecture: 6 min

Me suivre sur Mastodon

Utiliser les closures en Javascript

J’en ai parlé il y peu de temps en Go mais je sais que beaucoup d’entre vous utilise Javascript. Et justement, l’utilisation de closure en JS est très intéressante. On va voir dans ce billet que bien souvent on devrait réfléchir avec ce pattern que ce soit avec du JS pur ou avec Angular.

Petit rappel: une closure est une fonction. Cette fonction sait garder en mémoire l’état de variables. Ainsi, on peut créer des générateurs et/ou retourner des fonctions. Car en JS, comme en Go ou en Python (pour ne citer qu’eux), une fonction est un type et donc peut être assignée à une variable.

Douleur aux yeux

Combien de fois j’ai vu ça:


function foo(){
    function bar(){
        console.log("coucou");
    }

    document.querySelector('a').addEventListener('click', onClick);
    
    
    var self = this;
    function onClick(){
        self.bar()
    });
}

Vous le repérez ce foutu souci qu’on a en JS (ES5) ? Et oui, le fameux “this” perdu dans des fonctions appelantes qui nous force à garder une référence dans une variable. Ici, c’est “self” qui fait office de gardien.

Tous les développeurs un peu expérimentés et ayant bossé un temps soit peu dans les profondeurs de JS ont eut ce souci au moins une fois. C’est agaçant, on perd en lisibilité et franchement c’est moche.

Oui on peut utiliser “bind()” dans pas mal de cas, l’idée n’est pas d’outrepasser cette méthode, mais souvent on ne peut pas l’utiliser.

Une autre façon de faire est d’utiliser les closures.

Les closures, pensez-y !

Il suffit de générer la fonction “onClick”. C’est tout bête mais c’est franchement plus logique. Surtout si vous avez dans l’idée que “self” puisse représenter d’autres classes.

Voici donc un premier exemple plus “propre”:


function foo(){
    function bar(){
        console.log("coucou");
    }

    document.querySelector('a').addEventListener('click', getOnClick(this));
    
    // génère une fonction qui utilise "obj"
    function getOnClick(obj) {
        return function (){
            obj.bar()
        });
    }
}

Ça parait con comme ça, mais ça règle le souci. D’une part nous n’avons plus besoin de “self”, mais en plus la fonction retournée peut utiliser n’importe quel objet qui contient la méthode “bar”.

Et c’est pas fini !

Et en plus…

Je me suis retrouvé à devoir utiliser du “jsonp”. Il n’y a pas de faute de frappe, le “p” à la fin de “jsonp” est bien là pour une raison. JsonP est une manière détournée de pouvoir utiliser un appel “asynchrone” d’une ressource externe sans se prendre une exception si la ressource est en dehors du domaine appelant, et que ce dernier ne gère pas les CORS (Cross Origin Resource Sharing).

Prenons un exemple.

J’ai plusieurs articles qui affichent des “tags”.

Je veux appeler une API qui récupère des images à insérer dans chaque article à partir de leurs tags. Sauf que voilà, cette API ne me permet pas d’utiliser XHTTPRequest (faussement appelée AJAX), et je dois donc fournir le nom d’une fonction en “callback”. Callback qui prend en argument les données que l’appel doit me retourner.

En gros, je crée une balise “script” qui pointe sur “api.to.call/?&callback=TOTO” => le contenu de cette appel va me retourner quelque chose du genre:

TOTO({
    // ... du contenu
})

Et donc, comme c’est une balise “script” que j’ai utilisé, la fonction “TOTO” sera appelée.

Je simplifie mais c’est grosso-modo ceci que je vais faire et qui ne marchera pas.

Il va falloir:

  • stocker une fonction par article sinon on ne saura pas où injecter l’image - on va les appeler “addImage0”, puis “addImage1” etc. On les gardera dans “window”.
  • appeler l’api en question en fournissant le nom de la fonction à appeler quand on a un résultat

Qu’on soit bien clair sur la question des fonctions “addImageN”, je dois les enregistrer dans “window” parce que la balise d’appel en jsonp va appeler la fonction globale “addImageN”; or je génère tout ça depuis une fonction (addEventListener), donc je force l’insertion de ces fonctions de manière globale. C’est tordu mais c’est quasiement inévitable.

Et je suis obligé de créer une fonction par article parce que l’API ne me permet pas de définir un argument personnel qui me permet de savoir dans quel article je dois injecter l’image.

Bref, c’est le boxon. Mais c’est gérable. Sauf que ce code ne va pas marcher:


/*CODE FOIREUX, NE MARCHERA PAS !!!*/

document.addEventListener('load', function(){

    var head = document.querySelector('head');
    var articles = document.querySelectorAll('.article')
    var i, j, script, article, tags, fncName;
    
    for (i=0; i<articles.length; i++) {
        article=articles[i];
        tags = article.querySelector('.tag');
        
        // le nom de la fonction à appeler
        fncName = 'addImage' + i;
        
        // on crée la fonction globale qui ajoute l'image dans "article"
        window[fncName] = function(data){
            var img = document.createElemen('img');
            img.src = data;
            article.appendChild(img);
        };
        
        // pour chaque tag
        for (k=0; k<tags.length; k++) {
            script = document.createElement('script');
            script.setAttribute('src', 'http://api.to.call/?&callback'+fncName+'&q='+tags[k]);
            head.appendChild(script); // on inject le script
        }
    }
});

Et bien rien ne marche comme on le veut. Dans mon cas (si j’ai pas merdé mon exemple), toutes les images apparaissent dans le dernier article. Et c’est très logique.

La variable “article” change à chaque itération. Quand la fonction “fncName” sera appelée, par exemple “addImage1”, et bien la variable “article” est positionné sur l’article “4” par exemple. Donc, c’est pas bon.

La solution la plus simple, qui n’ajoute pas de “classe css”, qui ne cherche pas à manipuler des variables rémanentes dans la fonction, etc, est de générer la fonction qui ajoute l’image.

Voici comment je m’y prends:


/*CODE QUI FONCTIONNE !!!*/

document.addEventListener('load', function(){

    var head = document.querySelector('head');
    var articles = document.querySelectorAll('.article')
    var i, j, script, article, tags, fncName;

    // cette fonction génère une fonction.
    function generateAddImageFnc(elem) {
        // on retourne la fonction qui 
        // manipule un element passé en argument.
        return function(data){
            var img = document.createElemen('img');
            img.src = data;
            elem.appendChild(img);
        }
    }
    
    for (i=0; i<articles.length; i++) {
        article=articles[i];
        tags = article.querySelector('.tag');
        
        // le nom de la fonction à appeler
        fncName = 'addImage' + i;
        
        // on appelle le générateur
        window[fncName] = generateAddImageFnc(article);
        
        // pour chaque tag
        for (k=0; k<tags.length; k++) {
            script = document.createElement('script');
            script.setAttribute('src', 'http://api.to.call/?&callback'+fncName+'&q='+tags[k]);
            head.appendChild(script); // on inject le script
        }
    }
});

Et cette fois ça fonctionne. La raison est simple: à chaque appel du générateur nous passons l’élement (article) qui est gardé en mémoire dans la closure (generateAddImageFnc). Chaque appel à cette fonction alloue de la mémoire qui reside pour la fonction retournée.

Nous n’avons pas utilisé de “bind”, nous avons simplement éliminé le souci en utilisant un générateur ou plutôt une closure. Un générateur ayant plutôt pour vocation de rester actif (par exemple un compteur ou un itérateur infini comme je l’ai présenté dans mon article pour Go)

Ainsi, quand nous appellerons “addImage2” nous serons sûrs que l’article est celui qui a été référencé dans la closure.

Ça parait obscure pour certains mais c’est pourtant l’un des avantages des closures, et surtout c’est certainement la meilleure raison de les utiliser.

comments powered by Disqus