ES3, Chap 6. — Les fermetures en JavaScript

Ce billet fait partie de la collection ES3 dans le détail et en constitue le Chapitre 6.

Bien que le contexte de vie soit détruit, les zombis vivent toujours, enfermés, tel des variables dans une fermeture.
Bien que le contexte de vie soit détruit, les zombis vivent toujours, enfermés, tel des variables dans une fermeture.

Dans cet article nous allons parler d'un des sujets le plus souvent questionné en JavaScript : les fermetures (« closures »).

Introduction

Ce sujet n'est pas nouveau et a été abordé maintes fois. Nous allons cependant essayer de l'aborder d'un point de vu théorique dans un premier temps et voir ensuite comment le JavaScript s'en occupe techniquement.

Il serait intéressant d'avoir pris connaissance en amont des deux chapitres précédents dédiés à la chaîne des portées et à l'objet des variables qui aideront à la compréhension du présent chapitre sans pour autant être indispensable à la compréhension globale.

Théorie générale

Avant d'aborder la discussion sur les fermetures en JavaScript, il semble important de définir un certain nombre de concepts de la théorie générale de la programmation fonctionnelle.

Comme vous le savez peut-être, dans les langages de programmation fonctionnelle (et JavaScript en est un), les fonctions sont des données. C.-à-d. qu'elles peuvent être affectées à des variables, passées en tant qu'arguments dans les paramètres des fonctions ou être retournées par les fonctions. Ces fonctions ont des noms et structures spéciales.

Définitions

Un argument fonctionnel (« functionnal argument » ou « funarg ») est un paramètre de fonction dont la valeur est elle-même une fonction.

Code JavaScript

// création et déclaration d'un paramètre fonctionnel `funarg`
function umbrella(funarg) {
    funarg();
}

// appel et définition d'un argument fonctionnel `functionnalArgument`
umbrella(function functionnalArgument() {
    console.log('corporation');
});

L'argument fonctionnel de l'exemple ci-dessus est une expression de fonction passée à la fonction umbrella.

La fonction qui reçoit comme paramètre l'argument fonctionnel est appelé une fonction d'ordre supérieur (« higher-order function »).

Un autre nom donnée aux fonctions d'ordre supérieur est fonction fonctionnelle ou, plus près des mathématiques, un opérateur. Dans l'exemple ci-dessus, umbrella est donc une fonction d'ordre supérieur.

Comme mentionné plus haut, un argument fonctionnel peut-être non seulement passée à travers les paramètres des fonctions appelées, mais également retournée comme valeur par une autre fonction.

Une fonction qui retourne une autre fonction est appelée une fonction avec valeur fonctionnelle (« function valued »).

Code JavaScript

(function functionValued() {
    return function () {
        console.log("appel d'une fonction retournée");
    };
})()();

Une fonction pouvant être utilisée comme une donnée standard (c.-à-d. être passée en tant qu'argument ou recevoir des arguments fonctionnels ou encore être retournée en tant que valeur fonctionnelle) est appelée une fonction de première classe. En JavaScript, toutes les fonctions sont des fonctions de première classe.

Une fonction se recevant elle-même via un paramètre, est une fonction auto-applicative (« self-applicative ») :

Code JavaScript

(function selfApplicative(funarg) {

    if (funarg && funarg === selfApplicative) {
        console.log('auto-applicative');
        return;
    }

    selfApplicative(selfApplicative);
})();

et une fonction qui se retourne elle-même est, quand à elle, appelée une fonction auto-réplicative (« self-replicative ») :

Code JavaScript

(function selfReplicative() {
    console.log('auto-réplicative');
    return selfReplicative;
})();

L'un des motifs les plus intéressant des fonctions auto-réplicatives est leurs formes déclaratives fonctionnant avec un seul argument de collection au lieu d'accepter la collection elle-même :

Code JavaScript

// fonction impérative qui
// accepte une collection

function starsList(modes) {
     modes.forEach(function (mode) {
         console.log(mode);
     });
}

// et s'utilisant ainsi
starsList(['jill', 'chris', 'barry']);
// `jill`
// `chris`
// `barry`

// vs

// forme déclarative des
// fonction auto-replicative

function stars(mode) {
     console.log(mode);
     return stars; // on retourne la fonction elle-même
}

// s'utilisant en *déclarant* les S.T.A.R.S

stars
    ('jill')
    ('chris')
    ('barry')

Cependant, dans la pratique, travailler avec des collections sera plus efficient et intuitif.

Les variables locales qui sont définies dans les arguments fonctionnels passés sont bien sur accessibles lors de l'activation (appel) de la fonction. Et cela grâce à un objet des variables qui stocke les données du contexte est créé à chaque fois en entrant dans le contexte :

Code JavaScript

function umbrella(funarg) {

    // activation local de funarg
    // la variable `localVar` est disponible
    // à l'intérieur de `funarg`

    funarg(10); // `20`
    funarg(20); // `30`

}

umbrella(function (localParam) {

    var localVar = 10;
    console.log(localParam + localVar);

});

Cependant, comme nous l'avons vu dans le chapitre 4, les fonctions en JavaScript peuvent être encapsulées dans des fonctions parentes et utiliser des variables depuis des contextes parents. Quand une variable utilisée dans une fonction provient d'une fonction parente, cela conduit à une particularité connue sous le nom de problème de l'argument fonctionnel (« Funarg problem »).

Problème de l'argument fonctionnel

Dans un langage de programmation avec pile d'exécution, les variables locales des fonctions sont stockées dans la pile (« stack »). Cette pile est augmentée chaque fois qu'une fonction est appelée et ses variables et arguments sont stockés dans le niveau ajouté.

Après retour d'une fonction, les variables et arguments de cette fonction sont supprimés de la pile en même temps que le niveau associé. Ce modèle est une grosse restriction pour l'utilisation de fonctions en tant que valeurs fonctionnelles puisque la fonction qui stockait la valeur retournée n'est plus dans la pile.

Ce problème apparaît le plus souvent quand une fonction utilise des variables libres.

Une variable libre est une variable qui est utilisée par une fonction mais qui n'est, ni un paramètre de la fonction, ni une variable locale de la fonction.

Code JavaScript

function biohazard() {

    var freeVar = 10;

    function stars(localParam) {
        console.log(localParam + freeVar);
    }

    return stars;
}

var residentEvil = biohazard();
residentEvil(20); // `30`

Dans cet exemple, la variable freeVar est libre pour la fonction stars.

Si ce système utilisait un modèle avec pile d'exécution pour stocker les variables locales, cela signifierait qu'au retour de la fonction biohazard toutes les variables auraient été supprimées de la pile. Et cela aurait causé une erreur lors de l'activation de stars depuis l'extérieur.

Cependant pour ce cas particulier, dans une implémentation orientée pile, retourner la fonction stars n'aurait pas été possible du tout, puisque stars est aussi locale à biohazard et aurait donc également été supprimée au retour de la fonction biohazard.

Un autre problème avec les objets fonctionnels est lié au passage de fonctions en tant qu'arguments dans un système avec une implémentation de portée dynamique.

Pseudo-code

var z = 10

function zombi() {
    console.log(z)
}

zombi() // `10` avec une portée statique ou une portée dynamique

(function () {

    var z = 20
    zombi() // `10` avec une portée statique, `20` avec une portée dynamique

})()

// et la même chose en passant `zombi`
// en tant qu'argument du premier paramètre `funarg`

(function (funarg) {

    var z = 30
    funarg() // `10` avec une portée statique, `30` avec une portée dynamique

})(zombi)

Nous voyons que dans un système avec une portée dynamique, la résolution des variables est gérée grâce à une pile dynamique de variables. Donc, les variables libres sont cherchées dans la chaîne dynamique de l'activation courante mise en place lors de la phase d'appel de la fonction, et non dans une portée (lexicale) statique qui est créée lors de la phase de création de la fonction.

Donc même si z existe (contrairement à l'exemple précédent ou la variable locale aurait été supprimée de la pile), une question se pose : quel valeur de z (c.-à-d. z depuis quel contexte, depuis quel portée_) devrait être utilisé dans les différents appels de la fonction zombi ?

Ces deux cas de figure lèvent deux types de problème :

  • comment faire fonctionner les valeurs fonctionnelles retournées depuis des fonctions (« upward funarg ») et
  • comment faire fonctionner les arguments fonctionnelles passés à des fonctions (« downward funarg »).

Pour résoudre ces problèmes (et leurs variantes), le concept de fermeture a été proposé.

Une fermeture

Une fermeture (« closure ») est la combinaison d'un pend de code et des données du contexte dans lequel ce pend de code est créé. Imageons cela avec le code suivant :

*Pseudo-code

var t = 20

function mansion() {
    console.log(t) // variable libre `t` valant `20`
}

// Fermeture pour le pend de code `mansion`
mansionClosure = {
    call: mansion // référence à la fonction
    lexicalEnvironment: { t: 20 } // contexte pour la recherche de variables libres
}

Même si le mot « lexical » est souvent omis quand on parle de portée lexicale le fait est qu'une fermeture sauve les variables parentes dans un champ lexical dédié au pend de code. Là où il est définie. Lors des prochaines activations de ce code, les variables libres seront cherchées à travers ce contexte lexical fermé (et ainsi comme nous l'avons vu dans l'exemple plus haut, la variable z sera toujours résolue à 10 pour une portée statique).

Dans la définition nous avons utilisé le terme générique de « pend de code » mais celui-ci peut être plus précis en fonction du langage. En JavaScript par exemple, le terme est complètement remplaçable par « fonction ». Mais cela n'est pas nécessairement le cas de tous les langages comme par exemple, en Ruby, ou les fermetures peuvent être appliquées à autre chose que des fonctions.

Comme type d'implémentation pour le stockage de variables après la destruction d'un contexte, l'implémentation basée sur une pile ne convient pas du tout (car cela contredit la définition d'une structure basée sur une pile). Dans ce cas, les données gérées par la fermeture d'un contexte parent peuvent être sauvée dynamiquement en mémoire comme dans un tas (« heap »), c.-à-d. dans une implémentation basée sur un tas. Une telle implémentation utilise un collecteurs de miettes (« garbage collector ») et des références par comptage. Ces systèmes sont moins rapides que les systèmes basés sur une pile. Cependant, les implémentations peuvent toujours optimiser cela en vérifiant lors de la phase d'analyse d'une fonction quels variables libres sont utilisées et décider en fonction de cela de les placer dans une pile ou dans un tas.

Les fermetures en JavaScript

Maintenant que nous avons discuté de la théorie, nous allons parler des fermetures (« closures ») spécifiquement dans le contexte du JavaScript. Il est nécessaire de noter ici que le JavaScript utilise uniquement une portée (lexicale) statique) (alors que dans certains langages, comme en Perl, les variables peuvent être déclarées en utilisant des portées dynamiques ou statiques).

Code JavaScript

var g = 10;

function virus() {
    console.log(g);
}

(function (funarg) {

    var g = 20;

    // la variable `g` pour le `funarg` est sauvée de manière statique
    // dans le contexte lexical dans lequel elle est créée

  funarg(); // `10`, mais pas `20`

})(virus);

Techniquement, les variables d'un contexte parent sont sauvées dans la propriété interne [[Scope]] de la fonction. Aussi si vous souhaitez complètement comprendre [[Scope]] et la chaîne des portées, cela a été discuté en détail dans le chapitre 4. Ainsi les problématiques de fermetures devraient disparaître d'elles-même.

En s'appuyant sur l'algorithme de création des fonctions, nous voyons que toutes les fonctions en JavaScript sont des fermetures, puisque toutes créent une chaîne des portées du contexte parent. Ce qu'il faut retenir étant qu'au moment ou une fonction est activée (appelée), la portée parente est déjà sauvée depuis le moment de sa création.

Pseudo-code

var g = 10;

function virus() {
    console.log(g);
}

// `virus` est une fermeture
virus = virusFunctionObject = {
    [[Call]]: <pend de code de `virus`>,
    [[Scope]]: [{
        g: 10
    } // === `GO`],
    <...> // autres propriétés
};

Pour des questions d'optimisations, quand une fonction n'utilise pas de variables libres, l'implémentation peut ne pas sauver de chaîne des portées. Cependant rien n'est mentionné à ce propos dans la spécification ECMA-262-3. C'est donc une liberté prise au niveau de l'implémentation (et par l'algorithme technique) car « toutes les fonctions sauvent une chaîne des portées dans leur propriété interne [[Scope]] lors de la création ».

Plusieurs implémentations permettent un accès direct à cette portée fermée. Par exemple dans Rhino, une propriété non standard __parent__ correspond à la propriété interne [[Scope]][0] dont nous avons discuté dans le chapitre sur l'objet des variables :

Code JavaScript

var GO = this;
var t = 10;

var virus = (function () {

    var g = 20;

    return function () {
        console.log(g);
    };

})();

virus(); // `20`
console.log(virus.__parent__.g); // `20`

virus.__parent__.g = 30;
virus(); // `30`

// Nous pouvons nous déplacer à travers la chaîne des portées jusqu'au bout
console.log(virus.__parent__.__parent__ === GO); // `true`
console.log(virus.__parent__.__parent__.t); // `10`

Une valeur [[Scope]] pour tous

Il est nécessaire de noter que la propriété fermée [[Scope]] en JavaScript est le même objet pour plusieurs des fonctions internes créées dans le contexte parent. Cela signifie que la modification d'une variable dans une fermeture va se refléter lors de la lecture de cette variable depuis une autre fermeture.

En clair : toutes les fonctions internes partagent la même chaîne des portées parente.

Code JavaScript

var tyran;
var nemesis;

function weapons() {

    var x = 1;

    tyran = function () { return ++x; };
    nemesis = function () { return --x; };

    x = 2; // affectation de AO(<weapons>)[`x`], qui est dans la propriété `[[Scope]]` de chaque fermeture

    console.log(tyran()); // `3`, via tyran.[[Scope]]
}

weapons();

console.log(tyran()); // `4`, via tyran.[[Scope]]
console.log(nemesis()); // `3`, via nemesis.[[Scope]]

Il y a une erreur très rependue liée à cette fonctionnalité. Souvent les développeurs obtiennent un résultat qu'ils n'attendaient pas quand ils créent des fonctions dans des boucles, essayant d'associer à chaque fonction de la boucle une variable de comptage, s'attendant à ce que chaque fonction garde sa « propre » valeur.

Code JavaScript

var data = [];

for (var k = 0; k < 3; k++) {
    data[k] = function () {
        var x = true;
        console.log(k);
    };
}

data[0](); // `3`, et pas `0`
data[1](); // `3`, et pas `1`
data[2](); // `3`, et pas `2`

Ce comportement est expliqué par l'exemple précédent. Une même portée liée au contexte est créée pour ces trois fonctions. Chaque fonction la référence dans sa propriété interne [[Scope]], et la variable k depuis cette portée parente peut-être facilement changée.

Regardez plutôt ce qu'il se passe étape par étape pour la chaîne des portées :

Phase d'entrée dans le contexte global :

Pseudo-code

// activeGlobalContext.Scope = activeGlobalContext.AO
activeGlobalContext.Scope = [
    { data: <référence à `data`>, k: undefined }
]

Phase d'exécution du contexte global :

Pseudo-code

// activeGlobalContext.Scope = activeGlobalContext.AO
activeGlobalContext.Scope = [
    { data: [<...>], k: 3 }
]

Phase d'entrée de la fonction data[1] (par exemple) :

Pseudo-code

// activeFunctionContext.Scope = activeFunctionContext.AO + data[1].[[Scope]]
activeFunctionContext.Scope = [
    { x: undefined }, // objet d'activation courant `AO`
    { data: [<...>], k: 3 } // l'objet global (`activeFunctionContext.AO`)
]

Phase d'exécution de la fonction data[1] (par exemple) :

Pseudo-code

// activeFunctionContext.Scope = activeFunctionContext.AO + data[1].[[Scope]]
activeFunctionContext.Scope = [
    { x: true },
    { data: [<...>], k: 3 }
]

// c.-à-d

data[1].[[Scope]].k === globalContext.Scope.k === `3`

Au moment de l'activation de la fonction, c'est la dernière valeur de k affectée qui s'affiche, c.-à-d. 3.

Cela est dû au fait que toutes les variables sont créées avant l'exécution du code, c.-à-d. lors de la phase d'entrée dans le contexte. Ce comportement est aussi connu sous le nom de hissage (« hoisting ») de variable.

La création d'un contexte fermé additionnel peut résoudre ce problème :

Code JavaScript

var data = [];

for (var k = 0; k < 3; k++) {
    data[k] = (function helper(x) {
        return function () {
            var x = true;
            console.log(k);
        };
    })(k); // passage de la valeur `k` pour chaque objet des variables
}

// maintenant c'est le résultat souhaité
data[0](); // `0`
data[1](); // `1`
data[2](); // `2`

Regardons ce qu'il se passe dans ce cas.

En premier lieux, la fonction helper est créée et immédiatement activée avec comme premier argument k.

Puis, la valeur retournée par la fonction helper est aussi une fonction, contenant les éléments du tableau data original.

Cette technique produit les effets suivants : à l'activation, helper crée à chaque fois un nouvel objet d'activation qui contient le paramètre x, et la valeur de ce paramètre est la valeur de l'argument k passé.

Donc, les propriétés [[Scope]] de chaque fonction retournée sont les suivantes lors de la phase d'exécution :

Pseudo-code

data[0].[[Scope]] === [
    { x: 0 },
    { data: [<...>], k: 3 }
]

data[1].[[Scope]] === [
    { x: 1 },
    { data: [...], k: 3 }
]

data[2].[[Scope]] === [
    { x: 2 },
    { data: [...], k: 3 }
]

Nous voyons maintenant que la propriété [[Scope]] des fonctions a une référence sur la valeur souhaitée via la variable x qui est capturée par la portée additionnelle créée.

Notons que les fonctions retournées gardent toujours leurs références à la variable k. La variable k garde toujours à travers toutes les fonctions la valeur 3.

Parfois les fermetures JavaScript incomplètes sont réduites au motif montré plus haut, avec la création de fonctions additionnelles de capture des valeurs désirées. D'un point de vu pratique, ce motif se doit d'exister, mais d'un point de vue théorique comme mentionné : toutes les fonctions en JavaScript sont des fermetures, et pas seulement ce cas de figure.

Le motif décrit plus haut n'est pas le seul utilisable. Pour conserver la valeur souhaité de la variable k il est aussi possible, par exemple, d'utiliser cette approche :

Code JavaScript

var data = [];

for (var k = 0; k < 3; k++) {
    (data[k] = function () {
        console.log(arguments.callee.x);
    }).x = k; // sauver `k` en tant que propriété de la fonction
}

// et tout est encore correcte
data[0](); // `0`
data[1](); // `1`
data[2](); // `2`

Notons que ES6 standardise la portée de structure, qui peut être mise en place en utilisant les mots clés let ou const en tant que déclaration de variable. L'exemple du dessus peut alors simplement être ré-écrit ainsi :

Code JavaScript

let data = [];

for (let k = 0; k < 3; k++) {
    data[k] = function () {
        console.log(k);
    };
}

// toujours des sorties correctes
data[0](); // `0`
data[1](); // `1`
data[2](); // `2`

Argument fonctionnel et return

Une autre fonctionnalité est le retour des fermetures. En JavaScript, une instruction avec le mot-clé return dans une fermeture rend le contrôle de flux depuis un contexte appelant.

Voici un exemple pour comprendre le comportement standard de return en JavaScript :

Code JavaScript

function getElement() {

    [1, 2, 3].forEach(function (element) {

        if (element % 2 == 0) {
            // retour de la fonction « forEach »,
            // mais pas retour de la fonction `getElement`
            console.log('trouvé: ' + element); // trouvé: 2
            return element;
        }

    });

    return null;
}

console.log(getElement()); // `null`, et non `2`

ainsi en JavaScript, lancer et attraper certaines exceptions peut aider :

Code JavaScript

function getElement() {

    var $break = {};

    try {

        [1, 2, 3].forEach(function (element) {

            if (element % 2 === 0) {
                // « return » pour getElement depuis cette fermeture
                console.log('trouvé: ' + element); // trouvé: 2
                $break.data = element;
                throw $break;
            }

        });

    } catch (e) {
        if (e === $break) {
            return $break.data;
        }
    }

    return null;
}

console.log(getElement()); // `2`

Théorie et exception

Comme nous l'avons noté, souvent les développeurs réduisent les fermetures seulement à des fonctions internes retournées par leurs contextes parents. Ceci réduit les fermetures seulement à être exploitables dans les fonctions anonymes.

Laissez-moi vous le dire à nouveau, toutes les fonctions ; indépendamment de leurs types (expressions nomées et anonymes ou déclarations), parce qu'elles possèdent une chaîne des portées, sont des fermetures.

Une exception à cette règle subsiste pour les fonctions créées via le constructeur Function dont la propriété [[Scope]] ne contient que l'objet global.

Usage pratique des fermetures

Dans la pratique les fermetures permettent la création de structures élégantes, permettant la personnalisation de différents calculs définis par un argument fonctionnel. En voici des exemples non exhaustifs.

Argument fonctionnel

Voici un exemple avec la méthode sort des tableaux qui accepte en tant que premier paramètre un argument fonctionnel de « condition de tri » :

Code JavaScript

[1, 2, 3].sort(function (a, b) {
    // ... conditions de tri de votre choix
});

Voici un autre exemple avec la méthode find. Il est parfois intéressant d'utiliser des fonctions de recherche en utilisant des arguments fonctionnels définissant les conditions de recherche :

Code JavaScript

someCollection.find(function (element) {
    return element.someProperty === 'condition de recherche';
});

Association fonctionnelle

Voici un exemple de ce que l'on appel de l'association fonctionnelle (« mapping functionnals ») avec la méthode map d'un tableau. Celle-ci va associer à un nouveau tableau une valeur calculée à chaque élément :

Code JavaScript

[1, 2, 3].map(function (element) {
    return element * 2;
}); // `[2, 4, 6]`

Boucle de fonction

Il est aussi intéressant d'autres fois d'appliquer les fonctions fonctionnelles, par exemple, dans une méthode forEach qui applique des instructions pour chaque élément d'un tableau :

Code JavaScript

[1, 2, 3].forEach(function (element) {
    if (element % 2 !== 0) {
        console.log(element);
    }
});
// affiche `1`
// affiche `3`

apply et call

Au passage, les méthodes de fonction apply et call utilisent également des arguments fonctionnels. Nous avons déjà discuté de ces méthodes dans une note à propos de la valeur de this mais ici, nous allons voir leurs rôles avec les arguments fonctionnels ou fonctions appliquées en tant qu'argument (à une liste d'arguments avec apply et des positions d'argument avec call) :

Code JavaScript

(function () {
    console.log([].join.call(arguments, ';'));
}).apply(this, [1, 2, 3]);
// affiche `1;2;3`

Appel différés

Un autre point important des fermetures sont la possibilité des appels différés (« deferred calls ») :

Code JavaScript

var a = 10;
setTimeout(function () {
    console.log(a);
}, 1000);
// une seconde d'attente...
// ...puis affichaqe de `10`

Fonction de rappel

Plus simplement, un cas d'usage rependu des fermetures est celui des fonctions de rappel (« callback functions ») :

Code JavaScript

// ...
var x = 10;

xmlHttpRequestObject.onreadystatechange = function () {
  // la fonction de rappel est appelée en différé,
  // quand les données sont prêtes.
  // La variable `x` est ici disponible indépendamment
  // du fait que lorsque le contexte interne existe,
  // l'exécution du code externe est déjà fini.
  console.log(x); // `10`
};
// ...

Encapsulations privées

Les fermetures servent également à « masquer » des variables dans une portée encapsulante lors de l'exécution d'instructions :

Code JavaScript

var resitentEvil = {};

// initialisation
(function (object) {

    var veronica = 10;

    object.getVirus = function _getVirus() {
        return veronica;
    };

})(resitentEvil);

console.log(resitentEvil.getVirus()); // retourne la valeur `veronica` enfermée : `10`
console.log(veronica); // « erreur : `veronica` n'est pas défini(e) »

Conclusion

Cet article vous en a dit plus à propos de la théorie générale des fermetures afin de mieux aborder son application en JavaScript même si le fait d'avoir étudié la chaîne des portées en amont nous a bien facilité la tâche. Nous entrerons prochainement dans le détail sur la programmation orientée objet dans le domaine du JavaScript !

Références

Lectures additionnelles :

Ce texte est une libre adaptation française de l’excellent billet Тонкости ECMA-262-3. Часть 6. Замыкания. de Dmitry Soshnikov.