ES3 en détail

Tutoriel présentant les fonctions en JavaScript

Nous allons nous intéresser à différents types de fonctions, et définir comment chacun de ces types influence l'objet des variables d'un contexte et ce qu'il y a à l'intérieur de chaque chaîne des portées de chaque fonction.

14 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Nous répondrons à la question fréquemment posée qui est : « qu'elle est la différence entre une fonction créée comme cela :

 
Sélectionnez
1.
2.
3.
var downBelow = function () {
    /* ... */
};

et une fonction créée de la manière habituelle ? »

 
Sélectionnez
1.
2.
3.
function upTop() {
    /* ... */
}

ou encore, « pourquoi dans l'appel ci-dessous, la fonction est entourée de parenthèses ? » :

 
Sélectionnez
1.
2.
3.
(function () {
    /* ... */
})();

Parce que ce tutoriel va mentionner des éléments vus dans les précédents tutoriels, il est préférable, pour une meilleure compréhension, d'en lire plus sur l'objet des variables et la chaîne des portées dont nous utiliserons les terminologies dans le présent tutoriel.

C'est parti pour les types de fonctions.

II. Les types de fonctions

Il y a trois types de fonctions en JavaScript et chacun d'entre eux offre ses propres fonctionnalités.

II-A. Déclaration de fonction

Une déclaration de fonction (dont la forme abrégée sera FD pour « function declaration ») est une fonction :

  • dont le nom est obligatoire ;
  • dont le code source se trouve au niveau Programme ou directement dans le corps d'une autre fonction en dehors d'une position attendant une expression ;
  • qui est créée lors de la phase d'entrée dans le contexte ;
  • qui influence l'objet des variables ;
  • et qui se déclare de la façon suivante :
 
Sélectionnez
1.
2.
3.
function upTop() {
    /* ... */
}

La fonctionnalité principale de ce type de fonctions est qu'il est le seul à influencer l'objet des variables (car les variables sont stockées dans l'objet des variables des contextes). Cette fonctionnalité définit le second point important : la fonction est déjà disponible lors de la phase d'exécution du code (car les déclarations de fonction sont stockées dans l'objet des variables lors de la phase d'entrée dans le contexte, avant que l'exécution ne commence).

Une fonction peut donc être appelée avant sa déclaration :

 
Sélectionnez
1.
2.
3.
4.
5.
upTop(); // `"Monde d'en haut"`

function upTop() {
    console.log("Monde d'en haut");
}

Ce qu'il est également important de noter, c'est que ces fonctions ne peuvent être définies dans une instruction qu'en dehors de toutes expressions (comme nous allons le décrire ci-dessous) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
// une déclaration de fonction peut-être faite :

// 1) directement dans le contexte global
function upTop() {

    // 2) ou à l'intérieur du corps
    // d'une autre fonction
    function eden() {}
}

Elles ne peuvent être définies qu'à ces deux types d'endroits et nulle part ailleurs (c'est-à-dire. qu'il est impossible de déclarer une fonction à une position attendant une expression ou dans une structure de contrôle if, for, etc.).

Il existe une alternative à la déclaration de fonction qui est appelée l'expression de fonction, c'est ce dont nous allons parler à présent.

II-B. Expression de fonction

Une expression de fonction (dont la forme abrégée sera FE pour « function expression ») est une fonction :

  • qui peut seulement être définie à une position attendant une expression :
  • dont le nom est facultatif ;
  • dont la définition n'a aucun effet sur l'objet des variables ;
  • qui est créée lors de la phase d'exécution du code.

La fonctionnalité principale de ce type de fonctions est qu'elle se trouvera toujours dans une expression. Voici ici un exemple avec expression d'affectation :

 
Sélectionnez
1.
2.
3.
var downBelow = function () {
    /* ... */
};

Cet exemple montre comment une expression de fonction anonyme est affectée à la variable downBelow. Après cela, la fonction sera disponible via l'identifiant downBelow et pourra être appelée (activée) avec downBelow().

Il est possible également pour les expressions de fonction de posséder un nom, mais celui-ci est facultatif :

 
Sélectionnez
1.
2.
3.
var downBelow = function lowerWorld() {
    /* ... */
};

Ce qu'il est important de noter ici c'est que depuis l'extérieur, l'expression de fonction de l'exemple ci-dessus peut être accédé via la variable downBelow (et appelée avec downBelow()) alors que depuis l'intérieur de la fonction (dans le cas d'un appel récursif par exemple), il est aussi possible d'utiliser le nom lowerWorld (et d'appeler lowerWorld()).

Quand un nom est assigné à une expression de fonction, il devient difficile de la distinguer d'une déclaration de fonction pour un œil non averti. Cependant, si vous connaissez bien la définition, il est simple de les distinguer : les expressions de fonction se trouvent toujours dans des expressions. Dans les exemples suivants, nous pouvons voir que toutes ces fonctions sont des expressions de fonction, car elles se trouvent dans des expressions :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
// entre des parenthèses (opérateur de groupement) on ne peut placer que des expressions
(function downBelow() {});

// dans des initialiseurs de tableau il ne peut aussi y avoir que des expressions
[function lowerWorld() {}];

// l'opérateur virgule ne se trouve que dans des expressions  ne pas confondre avec le séparateur d'instructions `;`)
1, function poorSide() {};

Une expression de fonction n'est créée que pendant la phase d'exécution du code et n'est donc pas stockée dans l'objet des variables. Voyons un exemple induit par ce comportement :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
// L'expression de fonction n'est pas disponible avant sa déclaration
// (car elle est créée lors de la phase d'exécution du code),

console.log(downBelow); // « erreur : `downBelow` n'est pas défini(e) »

(function downBelow() {});

// ni après (car elle n'est pas dans l'objet des variables)

console.log(downBelow);  // « erreur : `downBelow` n'est pas défini(e) »

La question logique est maintenant : « pourquoi avons-nous besoin de ce type de fonctions ? »

La réponse la plus simple est que c'est lui qui permet au JavaScript de passer des fonctions à travers les paramètres des fonctions et qui permet l'utilisation des fonctions de rappel (« callback »). Sans elles, on aurait bien du mal à gérer le caractère asynchrone du JavaScript. Voyez plutôt :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
// Déclaration de fonction ajoutée à l'objet des variables
function downBelow(callback) {
    callback();
}

// Expression de fonction...
downBelow(function adam() {
    console.log('downBelow.adam');
});
// ...et exécution du downBelow avec cette `FE` en paramètre,
// aussitôt « oubliée » après l'exécution.

// Nouvelle expression de fonction
downBelow(function bob() {
    console.log('downBelow.bob');
});

De plus ainsi, adam et bob ne polluent pas inutilement l'objet des variables.

Dans le cas où une expression de fonction est affectée à une variable, la fonction reste accessible en mémoire et peut être utilisée ultérieurement via ce nom de variable (car les variables influencent l'objet des variables) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
// Cette expression de fonction est affectée à `downBelow`
// qui elle est ajoutée à l'objet des variables.
var downBelow = function () {
    console.log("Monde d'en bas");
};

// Ce qui permet plus tard d'appeler la fonction anonyme ci-dessus.
downBelow();

Un autre exemple est la création de portée encapsulée pour masquer aux contextes extérieurs les données utilisées pour la création d'une fonction (dans cet exemple, l'expression de fonction utilisée est appelée aussitôt qu'elle est créée) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
var downBelow = {};

(function prepare() {

    var upMaterial = 10;

    downBelow.adam = function () {
        console.log(upMaterial);
    };

})();

downBelow.adam(); // `10`;

console.log(upMaterial); // « erreur : `upMaterial` n'est pas défini(e) »

Nous voyons que la fonction downBelow.adam (via sa propriété [[Scope]]) a accès à la variable upMaterial de l'expression de fonction prepare. D'un autre côté, cette variable upMaterial n'est pas accessible directement depuis l'extérieur. Cette stratégie est utilisée dans beaucoup de bibliothèques pour créer des données « privées » et masquer les éléments de construction. Souvent dans ce motif, le nom facultatif de la fonction est omis :

 
Sélectionnez
1.
2.
3.
4.
5.
(function () {

    // Portée d'initialisation

})();

Voici encore un autre exemple d'expression de fonction créée conditionnellement à l'exécution et qui ne pollue pas l'objet des variables :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
var upMaterial = 10;

var goOrStay = (upMaterial > 5
    ? function () { console.log("Aller dans le monde d'en haut !"); }
    : function () { console.log("Rester dans le monde d'en bas..."); }
);

goOrStay(); // `"Aller dans le monde d'en haut !"`

Notons que ES5 standardise les fonctions liées (« bound function »). Ce type de fonctions permet de définir la valeur de this lors de la phase de création, bloquant sa valeur lors des futurs appels de la fonction.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
var eternalLove = function () {
    return this.love;
}.bind({ love: true });

eternalLove(); // `true`
eternalLove.call({ love: false }); // toujours `true`

L'usage le plus courant des fonctions liées est celui d'être attachées à des écouteurs d'évènements, ou des fonctions retardées (comme setTimeout) qui ont besoin de continuer des opérations sur l'élément this du contexte appelant.

II-C. À propos des parenthèses encadrantes ou l'opérateur de groupement

Revenons donc à notre question du début d'article qui était « pourquoi dans l'appel ci-dessous, la fonction est entourée avec des parenthèses ? » :

 
Sélectionnez
1.
2.
3.
(function () {
    /* ... */
})();

ou encore « pourquoi avons-nous besoin de ces parenthèses autour d'une fonction si nous voulons l'exécuter juste après sa définition. Voici la réponse : c'est pour permettre à l'instruction d'être à « une position attendant une expression » qui est la condition requise pour créer une expression de fonction.

D'après le standard, une instruction interprétée en tant qu'expression ne peut pas commencer par l'accolade ouvrante {, car on ne pourrait la distinguer d'une structure de contrôle et de la même manière, elle ne peut commencer par le mot-clé function, car on ne pourrait la distinguer d'une déclaration de fonction. c'est-à-dire que si nous essayons de définir une fonction immédiatement appelée (qui ne peut l'être que si elle est de type expression de fonction) de cette manière (en commençant par le mot-clé function) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
function () {
    /* ... */
}();

// ou même avec un nom

function dualGravity() {
    /* ... */
}();

cela va entrer en conflit avec la manière dont les déclarations de fonction sont créées. Les raisons de ce conflit varient.

Si nous prenons le cas d'un code global (c'est-à-dire au niveau Programme), l'analyseur va traiter la fonction comme une déclaration, car l'instruction commence par le mot-clé function.

Dans le premier cas, nous obtiendrons une SyntaxError, car la fonction n'a pas de nom (une déclaration de fonction doit obligatoirement avoir un nom qui représente son identifiant dans l'objet des variables).

Dans le second cas, puisque nous avons un nom (dualGravity) la déclaration de fonction est créée normalement. Mais nous avons de nouveau une erreur de syntaxe, car nous avons un opérateur de groupement sans expression à l'intérieur. Notez bien que dans ce cas nous avons un opérateur de groupement qui suit la déclaration de fonction, mais pas des parenthèses d'appel de fonction ! Aussi si nous avions la source suivante :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
// `dualGravity` est une déclaration de fonction
// et est créée à l'entrée dans le contexte

console.log(dualGravity); // `function dualGravity(world) { console.log(world); }`

function dualGravity(world) {
    console.log(world);
}('Upside Down'); // et ceci est juste un opérateur de groupement, pas un appel !

dualGravity('Upside Down'); // et ceci est un appel qui retourne `'Upside Down'`

Tout se déroule bien ici, car nous avons deux syntaxes valides — une déclaration de fonction et un opérateur de groupement contenant une expression composée de l'opérande 'Upside Down' de type chaîne de caractères. L'exemple ci-dessous est donc identique !

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
// déclaration de fonction
function dualGravity(world) {
    console.log(world);
}

// un opérateur de groupement
// avec une expression
('Upside Down');

// un autre opérateur de groupement
// avec une expression (de fonction)
(function () {});

// ceci est aussi un opérateur de groupement
// avec une expression composée d'un opérande
// de type nombre
(1);

// etc.

Dans le cas d'une définition à l'intérieur d'une structure de contrôle à cause d'une ambiguïté nous aurons également une erreur de syntaxe :

 
Sélectionnez
1.
if (true) function dualGravity() { console.log('Upside Down'); }

La construction ci-dessus en accord avec les spécifications est syntaxiquement incorrecte (une expression ne peut commencer avec un mot-clé function), mais comme nous le verrons plus bas, chaque implémentation gérera ce cas de figure de sa propre manière (au lieu de renvoyer une erreur comme le voudrait la spécification).

Sachant tout cela, le meilleur moyen d'explicitement faire comprendre au moteur que nous souhaitons une expression de fonction et non une déclaration de fonction est d'utiliser un opérateur de groupement (car à l'intérieur de celui-ci, il y a obligatoirement une expression). Ainsi, l'analyseur distingue le code comme une expression de fonction et il n'y a aucune ambiguïté. La fonction va être créée pendant la phase d'exécution du code, puis exécutée, puis retirée (car il n'y a plus de référence sur elle).

 
Sélectionnez
1.
2.
3.
(function dualGravity(world) {
    console.log(world);
})('Upside Down'); // OK, c'est un appel avec en premier paramètre la valeur `'Upside Down'` et pas un opérateur de groupement.

Dans l'exemple ci-dessus, les parenthèses de fin exécutent le retour de l'expression (à savoir une fonction) en lui passant un paramètre.

Notons que dans l'exemple suivant, l'exécution immédiate de la fonction associée comme valeur de adam ne requiert pas de parenthèses, car le code de la fonction est déjà dans un endroit réservé pour une expression et l'analyseur sait qu'à ce niveau il ne peut s'agir que d'une expression de fonction qui sera créée lors de la phase d'exécution du code :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
var downBelow = {

    adam: function (love) {
        return love ? 'oui' : 'non';
    }(true)

};

console.log(downBelow.adam); // `'oui'`

downBelow.adam est donc une chaîne de caractères et non une fonction comme nous pourrions nous y attendre à première vue. La fonction est utilisée ici uniquement comme initialiseur de propriété (dépendant d'un paramètre conditionnel) et est créée et exécutée juste après cela (puis perdue).

Donc la réponse complète à propos des parenthèses est la suivante :

les parenthèses de l'opérateur de groupement sont requises quand une fonction ne se trouve pas à une position attendant une expression si vous souhaitez appeler immédiatement la fonction après sa création. Dans ce cas-là nous transformons juste manuellement une déclaration de fonction en expression de fonction.

Dans le cas où l'analyseur sait déjà résoudre cette fonction comme une expression de fonction c'est-à-dire que la fonction est déjà à une position attendant une expression, les parenthèses ne sont pas obligatoires.

Comme l'opérateur de groupement n'est qu'un moyen d'indiquer à l'analyseur que le code en cours doit être analysé comme une expression, il est possible d'utiliser tous les autres moyens pour transformer une instruction en une position nécessitant une expression pour créer une expression de fonction. Par exemple :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
// Ceci indique que nous manipulons une expression de fonction
(function () {
    console.log("La gravité partagée");
}());

//, mais ceci aussi
1, function () {
    console.log("Le monde d'en haut");
}();

// ainsi que ceci
!function () {
     console.log("Le monde d'en bas");
}();

// et n'importe quelles autres
// transformations manuelles

/* ... */

C'est juste que l'opérateur de groupement est la méthode la plus élégante et répandue.

Au passage vous aurez peut-être remarqué que les parenthèses de groupement peuvent être placées autour de la déclaration de la fonction (sans inclure ses parenthèses appelantes) ou bien autour de la totalité, dans tous les cas l'instruction sera reconnue comme une expression de fonction que l'on souhaite appeler immédiatement.

 
Sélectionnez
1.
2.
3.
// Deux expressions de fonction tout aussi valides
(function () {})();
(function () {}());

Dans le premier cas, nous utilisons un opérateur de groupement pour indiquer que nous créons une expression de fonction (function () {}). Cette opération retourne à la syntaxe suivante sa référence. Puis nous utilisons les parenthèses d'appel () qui appliquées à la référence retournée exécutent le code (car une paire de parenthèses suivant une référence demandent un appel).

Dans le second cas, nous utilisons un opérateur de groupement pour indiquer que nous créons une expression de fonction que l'on exécute immédiatement (function () {}()).

II-C-1. Implémentation : déclaration de fonction conditionnelle

Les exemples suivants montrent qu'aucune des implémentations ne respecte les spécifications en matière de déclaration de fonction.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
if (true) {

    function dualGravity() {
        console.log(0);
    }

} else {

    function dualGravity() {
        console.log(1);
    }

}

dualGravity(); // `1` ou `0` ?

Il est important de noter que d'après le standard, cette construction est syntaxiquement incorrecte, car comme nous l'avons vu, une déclaration de fonction ne doit pas être faite à l'intérieur d'une structure de contrôle (alors qu'ici les structures de contrôle if et else les contiennent). Comme nous l'avons dit, les déclarations de fonction ne peuvent apparaître qu'à deux endroits : au niveau Programme ou directement à l'intérieur du corps d'une autre fonction.

Le code ci-dessus est incorrect, car les structures de contrôle ne doivent contenir que des expressions ou d'autres structures de contrôle, mais pas des déclarations. Et le seul endroit ou une fonction peut apparaître dans une structure de contrôle est dans une expression. Mais par définition, elle ne peut pas commencer par le mot-clé function (sinon on ne peut la distinguer d'une déclaration de fonction).

Une section consacrée aux erreurs d'analyse du standard couvre ce cas de figure (apparition d'une fonction dans une structure de contrôle). Mais aucune implémentation à ce jour ne lance d'exception. Elles y répondent chacune de leur propre manière.

Pourquoi cela est gênant d'avoir une déclaration de fonction dans une structure de contrôle ?

La présence des structures if et else signifie qu'un choix va être fait entre deux fonctions qui vont être déclarées. Puisque cette décision ne se ferra que lors de la phase d'exécution, cela implique qu'une expression de fonction doit être utilisée. Cependant la majorité des implémentations vont plutôt créer une déclaration de fonction dont la valeur sera la dernière fonction déclarée. Dans ce cas, notre exemple de fonction dualGravity va retourner 1 même si la structure else n'est jamais exécutée.

Voyons par exemple comment Mozilla Firefox (SpiderMonkey) traite ce cas. D'un côté il ne considère pas ces fonctions comme des déclarations (c'est-à-dire que la fonction est créée lors de la phase d'exécution du code), mais d'un autre côté ce ne sont pas de vraies expressions de fonction, car elles ne pourraient pas être appelées ultérieurement (ou alors immédiatement avec des parenthèses) et sont donc stockées dans l'objet des variables.

Cette extension syntaxique est nommée déclaration de fonction conditionnelle (dont la forme abrégée sera FS pour « function statement ») et est proposée par l'inventeur du JavaScript Brendan Eich comme un type de fonctions supplémentaire (qui n'est pas l'un des trois types de fonctions officiels).

II-C-2. Fonctionnalité : l'expression de fonction nommée

Dans le cas où une expression de fonction a un nom, elle est appelée une expression de fonction nommée (dont la forme abrégée sera NFE pour « named function expression »). Comme nous l'avons vu dans la définition (et vu dans des exemples plus haut) l'expression de fonction n'influence pas l'objet des variables d'un contexte (c'est-à-dire qu'il est impossible de l'activer en utilisant son nom avant et après sa définition). Cependant, une expression de fonction peut être appelée elle-même par son nom dans un appel récursif interne :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
(function edenAndAdam(love) {

    if (love) {
        return;
    }

    edenAndAdam(true); // le nom `edenAndAdam` est disponible

})();

// mais depuis l'extérieur ce n'est pas possible

edenAndAdam(); // « erreur : `edenAndAdam` n'est pas défini(e) »

Mais où est stocké le nom edenAndAdam ? Dans l'objet d'activation de edenAndAdam ? Non, car personne n'a déclaré aucune fonction avec le nom edenAndAdam. Dans l'objet des variables du parent depuis lequel la fonction edenAndAdam a été créée ? Toujours pas, souvenez-vous de la définition d'une expression de fonction : elle n'influence pas l'objet des variables et c'est pour cela qu'on ne peut pas appeler edenAndAdam depuis l'extérieur. Où alors ?

Voici comment cela marche : quand l'analyseur rencontre l'expression de fonction nommée lors de l'exécution du code (avant sa création), il crée un objet supplémentaire spécial et l'ajoute en amont de la chaîne des portées courante. Puis l'expression de fonction nommée est créée par la fonction parente elle-même et obtient sa propriété interne [[Scope]] (comme nous l'avons vu dans le tutoriel sur la chaîne des portées) qui contient la chaîne des portées de ce contexte parent. Après cela, le nom de l'expression de fonction nommée est ajouté à cet objet spécial en tant qu'unique propriété et sa valeur est une valeur de type Reference vers l'expression de fonction. Enfin, la dernière chose qui est faite est de retirer cet objet spécial de la chaîne des portées parentes. Voyons cet algorithme :

Pseudocode
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
__specialObject = {};

Scope.unshift(__specialObject) // ajout en amont

foo = new FunctionExpression()

foo.[[Scope]] = [].concat(Scope) // copie

foo.[[Scope]][0].foo = foo // {DontDelete}, {ReadOnly}

Scope.shift() // retrait en amont

Ainsi depuis l'extérieur, cette fonction n'est pas accessible par son nom (car il n'est pas présent dans la chaîne des portées parente), mais comme l'objet spécial a été sauvé dans la propriété interne [[Scope]] de la fonction, ce nom sera accessible via sa propre chaîne des portées de son objet d'activation.

Il est nécessaire de noter cependant que dans beaucoup d'implémentations, par exemple dans Mozilla Firefox (Rhino), cette valeur est enregistrée dans l'objet d'activation de l'expression de fonction. Où encore une implémentation dans Internet Explorer (JScript) brise complètement les règles des expressions de fonction et rend ce nom accessible dans l'objet des variables parent et la fonction devient disponible à l'extérieur.

II-C-2-a. Firefox et expression de fonction nommée

Jetons un œil sur différentes implémentations dédiées à cette problématique. Plusieurs versions de SpiderMonkey ont une fonctionnalité de l'objet spécial s'apparentant à un bogue. Cela est lié au mécanisme de résolution d'identifiants : l'analyse de la chaîne est bidirectionnelle et lors de la résolution, la chaîne des prototypes est également mise à contribution pour chaque objet dans la chaîne des portées.

Nous pouvons voir ce mécanisme en action si nous définissons une propriété dans Object.prototype et que nous utilisons une variable « non existante » dans le code. Dans l'exemple suivant, en résolvant le nom de side l'objet global est atteint sans que la variable side ne soit trouvée dans l'objet d'activation de la fonction. Cela est dû au fait que dans SpiderMonkey l'objet global hérite de Object.prototype et le nom side est résolu :

 
Sélectionnez
1.
2.
3.
4.
5.
Object.prototype.side = 2;

(function () {
    console.log(side); // `2`
})();

L'objet d'activation n'a pas de prototype. Avec les mêmes conditions de départ, il est possible de voir le même comportement dans les fonctions internes. Si nous déclarons une variable locale side depuis une fonction interne (une déclaration de fonction ou une expression de fonction anonyme) et que nous faisons référence à side depuis cette fonction interne, cette variable devrait normalement être résolue dans le contexte de la fonction parente, au lieu de la résoudre dans Object.prototype :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
Object.prototype.side = 3;

function upSideDown() {

    var side = 2;

    // déclaration de fonction

    function upTop() {
        console.log(side);
    }

    upTop(); // `2`, depuis `AO(<upSideDown>)`

    // la même chose avec une expression de fonction anonyme

    (function () {
        console.log(side); // `2`, aussi de `AO(<upSideDown>)`
    })();

}

upSideDown();

Cependant certaines implémentations attachent un prototype aux objets d'activation comme dans l'implémentation de Blackberry. La valeur side de l'exemple précédent est alors résolue à 3. c'est-à-dire n'atteint jamais l'objet d'activation de upSideDown, car la valeur dans Object.prototype est trouvée avant :

Pseudocode
Sélectionnez
1.
2.
AO(<bar>) // rien, puis
AO(<bar>).[[Prototype]] // trouvée, et vaut `3`. On s'arrête.

ou

 
Sélectionnez
1.
2.
AO(anonymous) // rien, puis
AO(anonymous).[[Prototype]] // trouvée, et vaut `3`. On s'arrête.

Et nous pouvons voir exactement le même comportement dans les versions de SpiderMonkey (avant ES5) dans le cas de l'objet spécial des fonctions d'expressions nommées. Cet objet spécial (selon le standard) est un objet normal « comme celui de l'expression new Object() », et donc qui devrait hériter de Object.prototype, c'est exactement ce que nous constatons dans l'implémentation de SpiderMonkey (mais uniquement jusqu'à la version 1.7) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
Object.prototype.side = 3;

function upTop() {

    var side = 2;

    (function downBelow() {

        console.log(side); // `3`, et non `2`, car AO(<upTop>) n'est jamais atteint.

        // `side` est résolue ainsi :
        // __specialObject(<downBelow>) // rien puis
        // __specialObject(<downBelow>).[[Prototype]] // trouvée, et vaut 3. On s'arrête.

    })();
}

upTop();

Les autres implémentations (et également les nouvelles versions de SpiderMonkey) n'attachent pas de prototype à cet objet spécial.

Notons qu'en ES5 ce comportement a changé et les environnements des moteurs n'héritent plus de Object.prototype.

II-C-2-b. JScript et expression de fonction nommée

L'implémentation de ECMAScript par Microsoft, JScript (implémenté dans les versions de Internet Explorer jusqu'à IE8), a un certain nombre de bogues en rapport avec les expressions de fonction nommée. Beaucoup de ces bogues contredisent complètement le standard ECMA-262-3 ; et certains d'entre eux causent de sérieuses erreurs.

Premièrement, JScript brise la règle principale des expressions de fonction nommée à savoir qu'elles ne doivent pas stocker le nom dans l'objet des variables. Le nom optionnel des expressions de fonction nommée doit être stocké dans un objet spécial et accessible seulement à l'intérieur de la fonction nommée (et nulle part ailleurs). Dans notre cas, elle se retrouve donc accessible directement dans l'objet des variables parent. Cependant, les expressions de fonction sont traitées en JScript comme des déclarations de fonction, c'est-à-dire qu'elles sont créées pendant la phase d'entrée dans le contexte et sont disponibles avant qu'elles ne soient définies dans le code source :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
// Une expression de fonction est disponible dans l'objet des variables
// via un nom optionnel
// c'est une définition comme dans une déclaration de fonction
upSideDown(); // `"Le monde inversé"`

(function upSideDown() {
    console.log("Le monde inversé");
});

// elle est aussi disponible après sa définition
// comme pour les déclarations de fonction ; le nom optionnel
// se trouve dans l'objet des variables
upSideDown(); // `"Le monde inversé"`

Comme nous l'avons vu, c'est une violation des règles.

Deuxièmement, dans le cas où l'on affecte une expression de fonction à une déclaration de variable, JScript crée deux différents objets de fonction.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
var upTop = function downBelow() {
    console.log('upSideDown');
};

// L'expression de fonction est toujours dans l'objet des variables - première erreur.

console.log(typeof downBelow); // `function downBelow() { console.log('upSideDown'); }`

//, mais, ceci est encore plus intéressant
console.log(upTop === downBelow); // `false` !

upTop.side = 1;
console.log(downBelow.side); // `undefined`

//, mais les deux fonctions
// font la même chose

upTop(); // `'upSideDown'`
downBelow(); // `'upSideDown'`

Il est amusant de noter cependant que si la définition d'une expression de fonction nommée est faite séparément de son affectation à une variable (par exemple avec l'utilisation de l'opérateur de groupement), alors l'opérateur d'égalité stricte appliqué entre les deux noms de fonction donnera true :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
(function downBelow() {});

var upTop = downBelow;

console.log(upTop === downBelow); // `true`

upTop.side = 1;
console.log(downBelow.side); // `1`

Ceci s'explique, car deux objets sont effectivement créés, mais après l'affectation, il n'en reste plus qu'un. Puisque les expressions de fonction nommée sont considérées comme des déclarations de fonction, pendant la phase d'entrée dans le contexte, downBelow est créée dans l'objet des variables. Après cela, lors de la phase d'exécution du code un second objet est créé : l'expression de fonction correspondant à downBelow. Aussitôt consommé, downBelow disparaît, car il n'est pas attaché à l'objet des variables. Il ne reste plus que la déclaration de fonction downBelow qui est assignée par référence à la variable upTop.

Troisièmement, en regardant la référence indirecte disponible via arguments.callee, on s'aperçoit que celle-ci prend la référence du nom par lequel la fonction est activée (la fonction a deux objets) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
var upTop = function downBelow() {

    console.log([
        arguments.callee === downBelow,
        arguments.callee === upTop
    ]);

};

downBelow(); // `[true, false]`
upTop(); // `[false, true]`

Quatrièmement, comme JScript traite les expressions de fonction nommée comme des déclarations de fonction, il n'est pas soumis aux règles des opérateurs conditionnels, c'est-à-dire que tout comme les déclarations de fonction, les expressions de fonction nommée sont créées pendant la phase d'entrée dans le contexte et c'est la dernière définition dans le code qui est utilisée :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
var upTop = function downBelow() {
    console.log(1);
};

if (false) {

    upTop = function downBelow() {
        console.log(2);
    };

}
downBelow(); // `2`
upTop(); // `1`

Ce comportement peut encore une fois être expliqué « logiquement ». Lors de la phase d'entrée dans le contexte, la dernière déclaration de fonction avec le nom downBelow est créée, c'est-à-dire la fonction avec console.log(2). Après cela, lors de la phase d'exécution du code, la nouvelle expression de fonction correspondant à downBelow est créée, et une référence est assignée à la variable upTop. Ensuite (car plus loin la structure de contrôle conditionnelle if avec l'expression false ne peut être atteinte), l'activation de upTop produit console.log(1). La logique est claire, mais cela est considéré comme un bogue IE. C'est pour cela que « logiquement » est entre guillemets, car cette implémentation biaisée ne dépend que d'un bogue IE.

Et le cinquième bogue de JScript est à propos de la création de propriétés dans l'objet global via l'affectation d'une valeur à un identifiant non qualifié (c'est-à-dire sans le mot-clé var). Comme les expressions de fonction nommée sont traitées comme des déclarations de fonction, elles sont stockées dans l'objet des variables affecté à un identifiant non qualifié (c'est-à-dire pas à une variable, mais à une propriété de l'objet global) et dans le cas où un nom de fonction est le même qu'un identifiant non qualifié, la propriété ne devient pas globale.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
(function () {

    // sans le mot-clé `var` ce n'est pas une variable du
    // contexte local, mais une propriété de l'objet globale

    upTop = function upTop() {};

})();

// cependant de l'extérieur
// l'expression de fonction nommée `upTop`
// est indisponible

console.log(typeof upTop); // `undefined`

Encore une fois la « logique » est claire : la déclaration de fonction upTop est ajoutée à l'objet d'activation du contexte local lors de la phase d'entrée dans le contexte. Et lors de la phase d'exécution du code, le nom upTop existe déjà dans l'objet d'activation, c'est-à-dire, _qu'il est traité comme une variable locale. D'après ECMA-262-3, l'affectation dans upTop est une opération de mise à jour d'une propriété existante dans l'objet d'activation upTop, mais pas une création de nouvelle propriété dans l'objet global.

II-D. Constructeur de fonctions

Ce troisième type de fonctions est traité séparément des déclarations de fonction et des expressions de fonction, car il a également ses propres fonctionnalités. Sa fonctionnalité principale est que sa propriété [[Scope]] ne contient que l'objet global :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
var adam = true;

function upTop() {

    var adam = false;
    var eden = true;

    var transWorld = new Function('console.log(adam); console.log(eden);');

    transWorld(); // `true`, « erreur : `eden` n'est pas défini(e) »

}

Nous pouvons voir que la propriété interne [[Scope]] de la fonction transWorld ne contient pas l'objet d'activation du contexte de upTop. La variable eden n'est pas accessible et la variable adam est résolue depuis le contexte global. En passant, vous pouvez remarquer que le constructeur Function peut être utilisé avec ou sans le mot-clé new de la même manière.

L'autre particularité de ce type de fonctions est liée aux productions de grammaires identiques (« equated grammar productions ») et aux objets joints (« joined objects »). Ce mécanisme est fourni par la spécification comme une suggestion d'optimisation (qui ne reste donc qu'une suggestion, non une obligation du standard). Par exemple, si vous avez un tableau de 100 éléments qui sont assignés dans une boucle depuis une expression de fonction, le mécanisme des objets joints sera utilisé :

 
Sélectionnez
1.
2.
3.
4.
5.
var transWorld = [];

for (var floor = 0; floor < 100; floor++) {
    transWorld[floor] = function () {}; // les objets joints peuvent être utilisés (même fonction pour les 100 objets)
}

Mais les fonctions créées via le constructeur Function ne sont jamais jointes (même sans new) :

 
Sélectionnez
1.
2.
3.
4.
5.
var transWorld = [];

for (var floor = 0; floor < 100; floor++) {
    transWorld[floor] = Function(''); // 100 fonctions différentes en mémoire
}

Encore un autre exemple avec les objets joints :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
function world() {

    function dualGravity(gravity) {
        return gravity * gravity;
    }

    return dualGravity;
}

var x = world();
var y = world();

Ici les implémentations peuvent utiliser la méthode des objets joints pour x et y (et utiliser la même objet de fonction), car les fonctions (ainsi que leur propriété [[Scope]]) retournées par world() ne peuvent être distinguées. Encore une fois, si les fonctions sont créées avec le constructeur Function, cela demande plus de ressources mémoire.

III. Algorithme de création de fonction

Maintenant que nous avons vu tous les types de fonctions existants, nous pouvons jeter un œil à l'algorithme de création de ces fonctions (sans la partie pour les objets joints). Cette description aide à comprendre plus en détail quels types de fonctions existent en JavaScript. L'algorithme est donc identique pour toute création de fonction quel que soit son type.

Pseudocode
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
// Un nouvel objet, est créé
F = new NativeObject()

// La propriété interne `[[Class]]`
// prend comme instance d'objet `"Function"`
//, car cet objet sera une fonction
F.[[Class]] = "Function"

// Association du prototype des fonctions
// à la fonction en cours de création
F.[[Prototype]] = Function.prototype

// Référence à la fonction elle-même dans `[[Call]]`
// `[[Call]]` est appelé quand `F` sera activé par
// une expression d'appel `F()` (sans `new`)
// cela crée un nouveau contexte d'exécution
F.[[Call]] = <référence à la fonction>

// Création du constructeur d'objets général dans `[[Construct]]`
// `[[Construct]]` est appelé quand `F` sera activé
// par une expression d'appel `new F()`
// c'est ça qui alloue la mémoire des nouveaux objets créés
// Puis `F.[[Call]]` est appelé pour l'initialisation
// de la valeur de this en tant que nouvel objet créé
F.[[Construct]] = internalConstructor

// Association de la chaîne des portées du contexte courant
// c'est-à-dire du contexte qui crée la fonction F
F.[[Scope]] = activeFunctionContext.Scope

// Si la fonction est créée
// via `new Function(...)` ou `Function(...)`, alors
if (createdByNewFunction) {
    F.[[Scope]] = globalContext.Scope
}

// Nombres de paramètres formels
F.length = countParameters

// prototype des objets créés par la fonction F
__objectPrototype = new Object()
__objectPrototype.constructor = F // {DontEnum}, n'est pas énumérable dans une boucle
F.prototype = __objectPrototype

return F

Notez que F.[[Prototype]] est le prototype de la fonction (constructeur) alors que F.prototype est le prototype des objets créés par cette fonction (faites bien attention à cela, car les articles expliquant que F.prototype est le prototype de la fonction constructeur se trompent).

IV. Conclusion

Ce tutoriel était plutôt long. Vous pourrez le comprendre encore mieux plus tard quand nous discuterons plus en détail des prototypes dans un prochain tutoriel.

V. Remerciements

Nous remercions Bruno Lesieur qui nous a autorisés à publier ce tutoriel.

Nous remercions également Laethy pour la mise au gabarit et Claude Leloup pour la correction orthographique.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2018 Nom Auteur. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.