Les closures javascript et la notion de classe

17/05/2013

NodeJS, JQuery, Mootools, HTML5… le javascript s’est imposé. Mais un sujet encore mal compris par beaucoup de développeurs JS me saute aux yeux. Depuis que je développe Knotter et que j’ai l’occasion d’en discuter, la tendance se creuse: beaucoup n’ont pas bien assimilé le concept de closure. Alors faisons un point sur les closures JS.

Les closures ne sont pas nouvelles et Javascript n’est pas le seul à les implémenter. Go par exemple implémente le concept de “full closure”, et PHP entre dans la danse. Mais il y a aussi C#, Perl, Python… tous utilisent le principe de closure.

Si il y a bien un chose qui est mal écrit dans Javascript, c’est ce fichu mot clef “function”. Car en réalité, une fonction Javascript n’est pas réellement une fonction.

Alors c’est quoi une closure ? Et bien simplement, c’est une notion de fonction qui peut garder son contexte actif et faire référence à des variables libres.

Bon, prenons un exemple de “fonction”, elle peut prendre en argument des valeurs (ou pas) et retourner une valeur (ou pas…)

function Add(a, b) {
    var c= a+b;
    return c;
}

C’est simple, c’est basique, c’est une fonction ! Sauf que je peux très bien écrire la même chose de cette manière:

var Add = function (a, b){
    var c = a+b;
    return c;
};

Etonnant ? je peux assigner une fonction à une variable. La variable “Add” contient la fonction. “Contient” ? et oui, car une fonction est un genre d’objet qu’on peut assigner quelque part, ce genre d’objet s’appelle une closure (le terme “fermeture” en français existe)

Alors si je peux l’assigner, au même titre que la variable “c” dans mon exemple, je peux faire une fonction qui retourne une fonction ! On tente:

function createAddFunction(){
    var add = function (a, b){
        var c = a+b;
        return c;
    };

    return add;
}

var myadd = createAddFunction();

console.log( myadd(4, 5) ); //=> retourne 9

Pour le moment l’utilité n’est pas probante, mais vous comprennez aisément ce qu’il se passe. La variable “myadd” se voit assignée de la fonction retournée par createAddFunction. Par conséquent elle contient la fonction “add” qui va être créée seulement au moment de l’appel “createAddFunction”.

Si c’est pas génial ça ?

Mais là où la closure prend tout son sens et son intérêt, c’est qu’elle capture le contexte dans lequelle elle est créée.

Je vais être plus clair: la fonction “add” retournée est créée dans une fonction nommé “createAddFunction”. Mais cette seconde fonction, c’est aussi une closure ! Si je créee une closure à l’intérieur d’un autre, les variables de la closure parente sont accessibles dans la descendante tant qu’elle s’en sert.

Voici un exemple simple:

function getCounter() {
    var c = 0;
    return function (){
        return c++;
    }
}

var counter1 = getCounter();
var counter2 = getCounter();

// on utilise le premier compteur
console.log( counter1() ); // 1
console.log( counter1() ); // 2

// on passe au seconde compteur
console.log( counter2() ); // 1
console.log( counter2() ); // 2

// on revient au premier compteur
console.log( counter1() ); // 3
console.log( counter1() ); // 4

Voilà ce qu’est une closure que l’on appelle “full closure”. Le contexte parent est assigné à ses descendants. Ainsi, “counter1” contient la fonction qui retourne “c++”. Mais comme cette variable “c” est initialisée dans la closure “getCounter” elle a accès à la variable “c” qui a été créée au moment de l’appel à getCounter. Je vous rappelle au passage que chaque appel à getCounter est séparé, on parle de contexte, et par conséquent la variable “c” n’est pas partagé à tous les compteurs. Ce ne sont pas des singletons et c’est tant mieux.

Ce concept est très utile car il permet de ne pas garder globalement des variables, et surtout il permet de controller la mémoire aisément.

Mais ce qui est fabuleux en JS, c’est qu’en plus d’être “full closure”, les fonctions peuvent avoir un prototype. Du coup, on peut instancier une fonction dont on aura accès à des méthodes, des propriétés… ha mais oui ! on dirait bien une classe ! Sauf qu’il faut garder à l’esprit que ce ne sont pas des classes !

Tout comme en Go qui utilise un principe de structures et d’interfaces permettant de “simuler” des classes, gardez à l’esprit que JS n’a pas de classe réelle mais des closures avec un prototype.

Voici une classe simple:

function Animal() {
    console.log("Naissance de l'animal");
}
Animal.prototype.name = null ;

Animal.prototype.getName = function (){
    return this.name;
}

Ce mot clef “prototype” est une quelque sorte un “objet” qui contient des propriétés et fonctions.

Vous avez remarqué le mot “this” dans la méthode “getName”. Elle fera référence à l’animal en cours. Sauf que voilà… pour que “this” soit une référence à un animal (et pas tous les animaux qu’on créera) il faut “instancier” la closure. C’est le rôle du mot clef “new”.

var tom = new Animal()
tom.name = "Tom le chat"
console.log( tom.getName() ); //=> Tom le chat

Et voilà, on a l’impression d’utiliser une classe qui s’instancie dans un objet nommé “tom”. En réalité, on a instancié le prototpye initialisé. En passant, l’appel à “new Animal()” a appelé la fonction “Animal” que l’ont peut voir comme un “constructeur”.

Et donc, si un prototype est un objet… rien ne nous empêche de l’initialiser avec un autre objet. Par exemple, un chat :

function Chat() {

}

Chat.prototype = new Animal();

Chat.prototype.classification = "chat";

var hercule = new Chat();
hercule.name = "Hercule le copain de Pif";
console.log( hercule.getName() ); // appelle getName de "Animal"
console.log( hercule.classification ); // => chat

Cela fonctionne pour une simple et bonne raison. Le prototype de “Chat” est un objet, auquel on a assigné une instance de “Animal”. Ainsi, Chat.prototype.getName existe tout comme Chat.prototype.name

Ensuite on a ajouté au prototype une nouvelle propriété, nommée “classification”.

Notez au passage, il fait bien construire le prorotype après avoir assigné le prototype parent. Sinon vous allez écraser ce que vous avez assigné.

C’est typiquement une façon de faire de l’héritage. Même si au final, c’est de la composition…

Par contre, vous avez remarqué que “new Chat()” n’a pas appelé le constructeur parent. Encore une fois, comprennez bien que nous ne faisons pas ici de classes, mais des closures !

La méthode pour forcer l’appel au parent est la suivante, appeler une fonction qui existe dans toutes les closures: “call()”. Cette méthode prend en argument un objet et va appeler la méthode en prenant un contexte donné. En clar, modifiez la closure:

function Chat() {
    Animal.call(this); // on envoit le chat dans Animal
}

Cela va appeler le constructeur “Animal” avec le contexte du “Chat”. Pour aller plus loin, regardez cet exemple:

function Animal(name) {
    this.name = name;
    console.log("Nom assigné !");
}

Animal.prototype.name = null;
Animal.prorotype.getName = function (){
    return this.name;
}

function Chat(name){
    Animal.call(this, name); // on appelle le constructeur
}

// on hérite... mais le souci c'est que là on 
// ne donne pas "name" en paramètre...
// Dans tous les cas, Chat() sera appelé, donc on aura
// quand même un appel à Animal() avec le nom...
Chat.prototype = new Animal();
Chat.prototype.classification = "chat";

var grosminet = new Chat("Sylvestre");

Là on a bien appelé la construction parente… 2 fois ??? et oui… le prorotype est initialisé avec “new Animal()” puis on appelle “Animal.call(this, name)” depuis le constructeur de Chat… Ralalala c’est gênant !

Il existe heureusement d’autres méthodes pour ça. Mais avant de lire la suite, assurez vous d’avoir tout compris à ce que je raconte au dessus.

C’est bon ? on continue. Le but du jeu c’est d’initialiser le prorotype de la closure “Chat” avec celui de “Animal”, c’est tout simple. Alors pourquoi ne pas copier le prototype de “Animal” ?

function Chat(name) {
    console.log("Constructeur de chat");
    Animal.call(this, name);
}

Chat.prototype = Animal.proyotype;
// etc...

var c = new Chat("Choupette");

Et là… désespoir… vous n’avez pas vu passer “Constructeur de chat”. Car en réalité, vous avez assigné tout le prototype de Animal à Chat. Y compris le constructeur !

La solution est de remettre le constructeur à sa place:

//...
Chat.prototype = Animal.prototype;
Chat.prorotype.constructor = Chat;

//...

Et là, ça marche !

Bon on va récapituler:

  • une closure c’est une fonction qui a un prototype, on peut l’assigner à une variable et par conséquent la retourner depuis une fonction
  • une classe en JS n’existe pas, c’est une closure (fonction) dont on a initialisé un prototype
  • on peut copier le prototype d’une closure dans une autre closure, du coup on compose la closure avec une autre => simili d’héritage de classe

Du coup c’est relativement simple de créer des factories. Voici un exemple simplifié de ce que j’utilise dans mes helpers de Knotter :

function createClass(proto, parentClass) {

    var f = null;

    if (parentClass) {
        // le constructeur appellera le constructeur parent
        f = function (){
            parentClass.call(this);
        };

        // on copie le prototype parent
        f.prototype = parentClass.prototype;

        //on remet le constructeur enfant
        f.prototype.constructor = f;

    }
    else {
        // pas d'héritage, donc une simple closure
        // suffit
        f = function (){};
    }

    //assigne le prototype sans écraser
    //le prototype parent (on garde le constructeur, etc...)
    for (var p in proto) {
        f.prototype[p] = proto[p];
    }

    return f;
}

Ce qui permet de faire joliement:

var MaClass = createClass({
    name : "",
    //fonction parente, retourne le nom tel quel
    getName : function (){
        return this.name;
    }
});

var ChildClass = createClass({
    // on fait pareil que le le parent
    // mais en lowercase
    getName : function () {
        return this.name.toLowerCase()
    }
}, MaClass);


var c = new ChildClass();
c.name = "Foo Bar";

//affiche "foo bar"
console.log(c.getName());

C’est assez propre, joli, facile à utiliser…

Bref, j’espère voir avoir éclairé un peu plus sur ce qu’est une closure JS, et sur le principe même des classes Javascript.

Ça peut vous intéresser aussi


Sortie de Knotter

Et bien on y est. Je me suis lancé un ...


JQuery toujours pas pour moi

Suite à un vieux post qui s’est terminé par ...


Raycaster en Javascript

À l’époque où les cartes graphiques n’existaient pas,...

Merci de m'aider à financer mes services

Si vous avez apprécié cet article, je vous serai reconnaissant de m'aider à me payer une petite bière :)

Si vous voulez en savoir plus sur l'utilisation de flattr sur mon blog, lisez cette page: Ayez pitié de moi

Commentaires

Ajouter un commentaire

Ajouter un commentaire

(*) Votre e-mail ne sera ni revendu, ni rendu public, ni utilisé pour vous proposer des mails commerciaux. Il n'est utilisé que pour vous contacter en cas de souci avec le contenu du commentaire, ou pour vous prévenir d'un nouveau commentaire si vous avez coché la case prévue à cet effet.