ES3, Chap 5. — Les fonctions en JavaScript
Ce billet fait partie de la collection ES3 dans le détail et en constitue le Chapitre 5.
Dans cet article nous allons parler de l'un des objets principal en JavaScript – les fonctions.
Introduction
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. Nous répondrons à la question fréquemment posée qui est : « qu'elle est la différence entre une fonction créée comme cela :
Code JavaScript
var downBelow = function () {
/* ... */
};
et une fonction créée de la manière habituelle ? »
Code JavaScript
function upTop() {
/* ... */
}
ou encore, « pourquoi dans l'appel ci-dessous, la fonction est entourée avec des parenthèses ? » :
Code JavaScript
(function () {
/* ... */
})();
Parce que cet article va mentionner des éléments vus dans les précédents chapitres, 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 chapitre.
Nous allons également souvent évoquée « des positions ou une structure de contrôle est attendue » et « des positions ou une expression est attendue ». Si la différence entre ces deux types de positions n'est pas clair, vous pouvez consulter le billet sur expression versus structure de contrôle
C'est parti pour les types de fonctions.
Les types de fonctions
Il y a trois types de fonction en JavaScript et chacun d'entre eux offres ses propres fonctionnalités.
Déclaration de fonction
Une déclaration de fonction (dont la forme abrégée sera FD pour « function declaration ») est structure de contrôle :
- 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
Code JavaScript
function upTop() {
/* ... */
}
La fonctionnalité principale de ce type de fonction est qu'il est le seul a influencer l'objet des variables (car les variables sont stockées dans l'objet des variables des contextes). Cette fonctionnalité définie le second point important : la fonction est déjà disponible lors de la phase d'exécution du code (car les déclarations de fonctions 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 :
Code JavaScript
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éfinie dans une instruction qu'en dehors de toutes expressions (comme nous allons le décrire ci-dessous) :
Code JavaScript
// une déclaration de fonction peut-être faites :
// 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'endroit et nul part ailleurs (c.-à-d. 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.
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,
- et qui est créée lors de la phase d'exécution du code.
La fonctionnalité principale de ce type de fonction est qu'elle se trouvera toujours dans une expression. Voici ici un exemple avec expression d'affectation :
Code JavaScript
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 fonctions de posséder un nom mais celui-ci est facultatif :
Code JavaScript
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éder 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 fonctions se trouve toujours dans des expressions. Dans les exemples suivant nous pouvons voir que toutes ces fonctions sont des expressions de fonctions car elles se trouvent dans des expressions :
Code JavaScript
// 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 `;`)
0, 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 :
Code JavaScript
// 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 fonction ? ».
La réponse la plus simple, au dela du fait que ça permet de ne pas « polluer » l'objet des variables, est que c'est ce mécanisme qui permet au JavaScript de passer des fonctions à travers les paramètres des fonctions et qui permet l'utilisation des fonctions de rappel (« callback »). :
Code JavaScript
// Déclaration de fonction ajouté à 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 ou 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) :
Code JavaScript
// Cette expression de fonction est affectée à `downBelow`
// qui elle est ajouté à 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) :
Code JavaScript
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èque 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 :
Code JavaScript
(function () {
// Portée d'initialisation
})();
Notez qu'en ES6, déclarer une variable avec let dans une structure de contrôle de type bloc produit le même résultat.
var downBelow = {}; { let upMaterial = 10; downBelow.adam = function () { console.log(upMaterial); }; } downBelow.adam(); // `10`; console.log(upMaterial); // « erreur : `upMaterial` n'est pas défini(e) »
Voici encore un autre exemple d'expression de fonction créée conditionnellement à l'exécution et qui ne pollue pas l'objet des variables :
Code JavaScript
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 fonction 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.
Code JavaScript
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ée à 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.
Vous pourrez trouver plus d'informations sur les fonctions liées dans le chapitre approprié de la collection ES5 dans le détail.
À propos des parenthèses encadrantes dites 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 ? » :
Code JavaScript
(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.-à-d. 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) :
Code JavaScript
function () {
/* ... */
}();
// ou même avec un nom
function dualGravity() {
/* ... */
}();
cela va entrer en conflit avec la manière dont les déclaration de fonction sont créées. Les raisons de ce conflit varies.
Si nous prenons le cas d'un code global (c.-à-d. 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 :
Code JavaScript
// `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 ce 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 !
Code JavaScript
// 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 pourrions craindre une erreur de syntaxe :
Code JavaScript
if (true) function dualGravity() { console.log('Upside Down'); }
La construction ci-dessus en accord avec les spécifications est devrait être 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).
Code JavaScript
(function dualGravity(world) {
console.log(world);
})('Upside Down'); // Ici, nous avons 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écute 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 requière pas de parenthèse 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 :
Code JavaScript
var downBelow = {
adam: function (love) {
return love ? 'oui' : 'non';
}(true)
};
console.log(downBelow.adam); // `'oui'`
downBelow.adam est donc une chaîne de caractère 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 ou l'analyseur sait déjà résoudre cette fonction comme une expression de fonction c.-à-d. que la fonction est déjà à une position attendant une expression, les parenthèses ne sont pas obligatoire.
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 :
Code JavaScript
// Ceci indique que nous manipulons une expression de fonction
(function () {
console.log('La gravité partagée');
}());
// mais ceci aussi
0, 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épendue.
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.
Code JavaScript
// Deux expressions de fonctions toutes 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 () {}). Cet opération retourne à la syntaxe suivante sa référence. Puis nous utilisons les parenthèses d'appel () qui appliquée à la référence retournée exécute le code (car une paire de parenthèses suivants une référence demande 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 () {}()).
Implémentation : déclaration de fonction conditionnelle
Les exemples suivants montrent qu'aucunes des implémentations ne respectent les spécifications en matière de déclaration de fonction.
Code JavaScript
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 faites à l'intérieur d'une structure de contrôle (alors qu'ici les structure de contrôle if et else les contiennent). Comme nous l'avons dit, les déclarations de fonctions 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'analyses 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ée. 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 coté il ne considère pas ces fonctions comme des déclarations (c.-à-d. que la fonction est créée lors de la phase d'exécution du code) mais d'un autre côté se ne sont pas de vrai expression de fonction car elles ne pourrait pas être appelé 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 fonction supplémentaire (qui n'est pas l'un des trois types de fonction officiel).
Fonctionnalité : l'expression de fonction nommée
Dans le cas ou 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 :
Code JavaScript
(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 ou 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éé 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 chapitre 4) 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ée à 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 faites est de retirer cet objet spécial de la chaîne des portées parente. Voyons voir cet algorithme :
Pseudo-code
__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ée 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émentation, comme par exemple dans Mozilla Firefox (Rhino), cet 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.
Firefox et expression de fonction nommée
Jetons un œil sur différentes implémentions dédiées à cette problématique. Plusieurs versions de SpiderMonkey ont une fonctionnalité de l'objet spécial s'apparentant à un bug. 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 du au fait que dans SpiderMonkey l'objet global hérite de Object.prototype et le nom side est résolue :
Code JavaScript
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 le résoudre dans Object.prototype :
Code JavaScript
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.-à-d. n'atteint jamais l'objet d'activation de upSideDown car la valeur dans Object.prototype est trouvée avant :
Pseudo-code
AO(<upTop>) // rien, puis
AO(<upTop>).[[Prototype]] // trouvée, et vaut `3`. On s'arrête.
ou
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érité de Object.prototype, c'est exactement ce que nous constatons dans l'implémentation de SpiderMonkey (mais uniquement jusqu'à la version 1.7) :
Code JavaScript
function upTop() {
var side = 2;
(function downBelow() {
console.log(side); // `3`, et non `2`, car AO(<upTop>) n'est jamais atteind.
// `side` est résolue ainsi :
// __specialObject(<downBelow>) // rien puis
// __specialObject(<downBelow>).[[Prototype]] // trouvée, et vaut 3. On s'arrête.
})();
}
Object.prototype.side = 3;
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.
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 bugs en rapport avec les expressions de fonctions nommées. Beaucoup de ses bugs contredisent complètement le standard ECMA-262-3 ; et certain d'entre eux causent de sérieuses erreurs.
Premièrement, JScript brise la règle principale des expressions de fonctions nommées à savoir qu'elles ne doivent pas stocker le nom dans l'objet des variables. Le nom optionnel des expressions de fonctions nommées doit être stocké dans un objet spécial et accessible seulement à l'intérieur de la fonction nommée (et nul par ailleurs). Dans notre cas, elle se retrouve donc accessible directement dans l'objet des variables parent. Cependant, les expressions de fonctions sont traitées en JScript comme des déclarations de fonctions, c.-à-d. qu'elle 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 :
Code JavaScript
// 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 fonctions ; 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 ou l'on affecte une expression de fonction à une déclaration de variable, JScript créé deux différents objets de fonction. Il est difficil de dire que le comportement de ce nom est logique (surtout sachant qu'à l'extérieur de cette expression de fonction, ce nom ne devrait pas être accessible) :
Code JavaScript
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 faites 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 :
Code JavaScript
(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 fonctions nommées sont considérées comme des déclarations de fonctions, 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) :
Code JavaScript
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 fonctions nommées comme des déclarations de fonctions, il n'est pas soumis aux règles des opérateurs conditionnelles, c.-à-d. que tout comme les déclarations de fonctions, les expressions de fonctions nommées 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 :
Code JavaScript
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.-à-d. 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 clair, mais cela est considérée comme un bug IE. C'est pour cela que « logiquement » est entre guillemet car cette implémentation biaisée ne dépend que d'un bug IE.
Et le cinquième bug 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.-à-d. sans le mot-clé var). Comme les expressions de fonctions nommées sont traitées comme des déclarations de fonctions, elles sont stockées dans l'objet des variables affecté à un identifiant non qualifié (c.-à-d. pas à une variable mais à une propriété de l'objet global) et dans le cas ou un nom de fonction est le même qu'un identifiant non qualifié, la propriété ne devient pas globale.
Code JavaScript
(function () {
// sans le mot-clé `var` ce n'est pas une variable du
// contexte local, mais une propriété de l'objet global
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 clair : la déclaration de fonction upTop est ajoutée à l'objet d'activation du contexte locale lors de la phase d'entrée dans le contexte. Et lors de la phase d'exécution du code, le nom upTop exsite déjà dans l'objet d'activation, c.-à-d., _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.
Constructeur de fonction
Ce troisième type de fonction est traitée séparément des déclarations de fonctions et des expressions de fonctions car il a également ces propres fonctionnalités. Sa fonctionnalité principale est que sa propriété [[Scope]] ne contient que l'objet global :
Code JavaScript
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 et sans le mot-clé new de la même manière.
L'autre particularité de ce type de fonction est lié 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 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é :
Code JavaScript
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éée via le constructeur Function ne sont jamais jointent (mêne sans new) :
Code JavaScript
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 :
Code JavaScript
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ée 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émoires.
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 ses fonctions (sans la parties pour les objets joints). Cette description aide à comprendre plus en détail quels types de fonctions existe en JavaScript. Il est donc identiques pour toutes créations de fonctions quelque soit son type.
Pseudo-code
// 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éé 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.-à-d. du contexte qui créé 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éer 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).
Conclusion
Cet article était plutôt long. Vous pourrez le comprendre encore mieux plus tard quand nous discuterons plus en détail des prototypes dans un prochain chapitre.
Références
Section correspondante de la spécification ECMA-262-3 :
Ce texte est une libre adaptation française de l'excellent billet Тонкости ECMA-262-3. Часть 5. Функции. de Dmitry Soshnikov.