JavaScript avancé - noelno/dovelei GitHub Wiki

Source

ES3 dans le détail, sur blog.lesieur.name, traduction / adaptation libre de la série d'articles ECMA-262-3 in detail de Dmitry Soshnikov

Contextes d'exécution

Cf. "Pile d'exécution" dans l'onglet "Débogueur" (Console Firefox), ou "Call Stack" dans l'onglet "Sources" (Console Chrome).

Les exemples qui suivent sont faits pour être testés dans votre console.

Un contexte global (root) par script / balise script / attribut JS sur une balise HTML.
(Chaque script, balise script ou attribut JS représente un programme - et pas l'ensemble des scripts de la page comme on pourrait le croire).

Un nouveau contexte s'empile par-dessus le contexte global à chaque appel de fonction :

<script>
function a(){
    b();
}
function b(){
    debugger;
}
a();
</script>

Dans cet exemple, on a une pile de trois contextes : le global, le contexte de a() et le contexte de b().
Le contexte au sommet de la pile est le contexte actif. Au moment où s'exécute debugger, le contexte de b() est le contexte actif.
Le contexte se dépile à chaque return ou fin de fonction.

Si une fonction s'appelle récursivement, l'empilement se poursuit autant de fois que la fonction s'appelle :

<script>
function a(i){
    if(i < 3){
        a(i + 1);
    }
    else{
         debugger;
    }
}
a(0);
</script>

Dans cet exemple on a une pile de cinq contextes : quatre fois celui de a(), et une fois le global.

Déclinaisons de code exécutable

  • code (de contexte) global : tout code s'exécutant hors d'une fonction
  • code (de contexte) de fonction : tout code s'exécutant dans une fonction, ou étant inclus via require ou include
  • contexte appelant : contexte qui appelle la fonction eval(). En ES3 et 4 le code déclaré dans eval() affecte le contexte appelant. En ES5 et +, eval() s'exécute dans un bac à sable.

Objet des variables

Cf. "Variables" dans l'onglet "Débogueur".
Chaque contexte d'exécution a son propre objet des variables (VO) lui permettant de stocker sous forme de propriétés :

  • les déclarations de variables (VD)
  • les déclarations de fonction (FD)
  • les paramètres formels (FP) de fonction s'il s'agit d'un contexte de fonction

("formels" = définis dans la signature de la fonction, "réels" = définis ET initialisés. Il est autorisé d'appeler une fonction en lui communiquant moins de paramètres que ce qui est formellement attendu, les autres paramètres non initialisés auront pour valeur undefined)

Objet Global

L'Objet Global (GO) est l'objet des variables commun à tous les contextes globaux de la page. Ce qui explique qu'en dépit du fait qu'il y ait un contexte global par programme, chaque programme ait accès aux variables et fonctions déclarées dans les programmes précédents.

L'OG est crée à l'initialisation du premier programme et est détruit au dernier programme.

Cet OG est initialisé avec des propriétés de base : Math, String, Date, parseInt… ainsi qu'une propriété étant une référence à lui-même = window dans un navigateur, ou global dans Node.js

À tester : console.log(window), puis cliquer sur l'objet window en vert pour voir le contenu du GO.

Objet d'Activation

L'Objet d'Activation (A0) est l'objet des variables spécifiques à une fonction. Il est crée lorsque l'on entre dans une fonction (quand cette fonction devient le contexte actif). Il contient au moins une propriété, l'Objet des arguments (ArgO).

L'ArgO contient trois propriétés : callee (équivalent du this), length (le nombre d'arguments réellement passés) et [properties-index] (les valeurs des arguments réellement passés).

L'AO d'une fonction a en commun avec son Objet Arguments les paramètres de fonction qui ont bien été initialisés à l'appel de fonction, et uniquement ceux-là.

À tester : console.log(mafonction()); pour visualiser le contenu de l'ArgO de cette fonction.

À partir de ES5, le concept de l'AO est remplacé par les environnements lexicaux.

Initialisation d'un VO

Exemple :

<script>
    function numbers(i,j){
	var numbers = [1,2];
	var toString = function (){
		return "Your numbers are " + i + " and " + j + ".";
	}
	function show(){
		console.log(toString());
	}
	(function k(){});
    }
numbers(3);
</script>

Ce qui se passe au moment de l'exécution de l'instruction numbers(3) :

  1. Entrée dans le contexte de fonction lié à numbers : AO(<number>) en pseudo-code
  2. [Uniquement pour les AO] Création d'une propriété pour chaque paramètre formel déclaré en signature de la fonction, dans AO(<number>)
   AO(<numbers>) = {
    i: 3,
    j: undefined
   }
  1. Création (ou surcharge si existant) d'une propriété pour chaque fonction déclarée dans AO(<number>). Les (II)FE (Expressions de Fonctions, comme k() dans l'exemple) ne sont pas concernées
   AO(<numbers>) = {
    i: 3,
    j: undefined,
    show: référence à la fonction déclarée dans numbers()
    toString: toString() //une des nombreuses fonctions par défaut. Les autres sont masquées ici par souci de simplification
	…
   }
  1. Création d'une propriété pour chaque variable déclarée dans AO(<number>), initialisée à undefined.
    Dans le cas où une fonction ou un paramètre de fonction porte déjà le même nom, cette propriété n'est pas remplacée.
   AO(<numbers>) = {
        i: 3,
        j: undefined,
	numbers: undefined,
	show: référence à la fonction déclarée dans numbers(),
	toString: toString()//pas d'effet sur toString
	…
	
   }
  1. Exécution du code de la fonction numbers()
   AO(<numbers>) = {
        i: 3,
        j: undefined,
	numbers: [1,2],
	show: référence à la fonction déclarée dans numbers(),
	toString: toString()//toString est surchargé
	…
	
   }

Cela explique qu'une fonction puisse être appelée avant la ligne où elle est déclarée.

Exemple :

<script>
    function numbers(i,j){
        show(); //show s'exécute sans problème
        //…
	function show(){
		console.log(i + "," + j);
	}
        //…
    }
numbers(3);
</script>

Dans le cas des expressions de fonction dont la référence est sauvée dans une variable locale, on a vu plus haut que cette variable vaut undefined avant qu'une valeur ne lui soit affectée durant l'exécution du programme :

Exemple :

<script>
    function number(i){
        show(); //error "show is not a function"
	var show = function(i){
		console.log(i);
	}
        show(); //show est défini
    }
number(3);
</script>

Une variable déclarée dans une structure qui ne s'exécute jamais, est répertoriée comme les autres dans l'objet des variables, et retournera undefined si l'on tente d'y accéder - au lieu de "ReferenceError - a is not defined" comme on pourrait s'y attendre.

if(false){ 
    var a = 2; 
}
console.log(a); //retourne undefined

Une variable initialisée sans mot-clé (var ou let) est en fait une propriété de la fonction (ou du contexte global) et pas une vraie variable.
Elle n'est donc pas répertoriée dans le VO du contexte courant, et peut être supprimée avec l'opérateur delete - les variables portent un attribut {{DontDelete}} (ou [[Configurable]] à partir d'ES5) qui empêchent leur suppression.

Tenter d'accéder à cette propriété avant affectation d'une valeur donne une ReferenceError.

This

Dans le contexte global, this fait référence à l'objet global (window sur navigateur). i et this.i représentent la même propriété de l'objet global. Dans un contexte de fonction, la valeur de this dépend de la manière dont cette fonction est appelée.

Type Reference

Lorsqu'on assigne un objet à une variable ou une propriété, on lui assigne en fait une réference, autrement dit une valeur de type Reference, vers cet objet.

Une valeur de type Reference détient les propriétés suivantes :

  • base : le VO auquel appartient la variable ou propriété contenant la véritable valeur (soit GO, soit un AO)
  • propertyName : le nom de la propriété contenant la véritable valeur
  • strict (depuis ES5) : un booléen indiquant si la référence doit être résolue en mode strict

GetValue et résolution de nom

Le moteur js dispose d'une méthode interne [[GetValue]] lui permettant, à partir de l'identifiant d'une variable ou d'une propriété, de récupérer sa véritable valeur (résolution de nom). Cette méthode procède ainsi :

  • Si la valeur n'est pas de type Reference, on retourne directement cette valeur et c'est fini.

  • S'il s'agit bien d'une valeur de type Reference :

    • on récupère la base (l'objet des variables) de cette valeur afin de vérifier qu'elle n'est pas nulle
    • on récupère le propertyName de cette valeur
    • on utilise la méthode [[Get]] pour récupérer la valeur recherchée à partir du PropertyName, et on la retourne

Les cas où se déclenchent [[GetValue]] sont déterminants pour connaître la valeur de this dans un contexte donné.

Identifier la valeur de this

La valeur de this dépend de la forme de l'instruction d'appel :

  • Cas n°1 : il s'agit d'un identifiant pointant vers une variable, propriété ou fonction déclarée plus loin, et qui retournent systématiquement une Reference. Exemple : a; reset();

  • Cas n°2 : il s'agit d'un accesseur à une propriété d'un objet Exemples : dome.start(); user.cartridge.save()

  • Cas n°3 : il ne s'agit de ni l'un ni l'autre : par exemple une expression qui retourne une fonction / un objet…

Exemple 1

<script>
function a(){
	console.log(this);
}
a();
</script>

L'appel de la fonction a() est fait sous la forme a();. On est donc dans le cas d'un identifiant (cas n°1) qui retourne une réference vers la fonction a(). [[GetValue]] se déclenche, et remplace cette réference par une vraie valeur, ici de type Function.

La base d'un identifiant correspond au VO où est enregistré la vraie valeur de la référence : ici window puisque nous sommes dans le contexte global (root) du programme, et non à l'intérieur d'une autre fonction. 'a' est l'identifiant de la fonction a() dans GO.

Cette réference aReference peut donc être représentée de la façon suivante :

aReference = {
	base: GO,
	propertyName: 'a'
}

Dans cet exemple, this fait donc réference à window.

Exemple 2

<script>
function a(){
	console.log(this);
	debugger;
}
var b = {
	c: a
}; 
b.c();
</script>

L'appel de la fonction a() est fait sous la forme b.c();. On est ici dans le cas de l'accesseur de propriété (cas n°2), qui fait aussi appel à la résolution de nom de [[GetValue]].

Dans ce deuxième cas, la base d'un accesseur correspond au parent de la propriété, et le propertyName à l'identifiant de la propriété.

Cette réference peut être représentée de la façon suivante :

bcReference = {
	base: b,
	propertyName: 'c'
}

La base est b puisque c est une propriété de b. Le propertyName est 'c'.

Dans cet exemple, this fait réference à b.

Exemple 3

<script>
function a(){
	console.log(this);
	debugger;
}
var b = {
	c: a
}; 
d = b.c;
d();
</script>

L'exemple est quasiment le même que le précédent, sauf que l'appel de la fonction a() est fait sous la forme d();. On se retrouve dans le cas n°1 avec un identifiant. La base de dReference est window puisque c'est dans le contexte global qu'est déclaré a(). On voit bien avec ces deux exemples que pour une même fonction, selon la forme de l'instruction d'appel, this peut avoir des valeurs différentes.

Cette réference peut être représentée de la façon suivante :

dReference = {
	base: OG,
	propertyName: 'd'
}

Dans cet exemple, this fait réference à window.

Exemple 4

<script>
function a(){
	console.log(this);
	debugger;
} 
a.prototype.constructor(); //en js, Object() === Object.prototype.constructor()
</script>

L'appel de la fonction a() est fait sous la forme a.prototype.constructor();, donc cas de l'accesseur de propriété (cas n°2).

Cette réference peut être représentée de la façon suivante :

aprototypeconstructorReference = {
	base: 'prototype',
	propertyName: 'constructor'
}

Dans cet exemple, this fait réference à prototype.

Cas particuliers

Opérateur de groupement

<script>
(function () {
    console.log(this);
})();
</script>

L'opérateur de groupement est (). Tout code dans un opérateur de groupement est une expression : elle est parsée et exécutée sans affecter la VO du contexte courant.

L'opérateur de groupement, quand il ne contient qu'une seule valeur, se contente de retourner cette valeur telle qu'elle, sans résolution de nom. S'il s'agit d'un type Reference, il va renvoyer la réference et pas la "vraie" valeur.

Dans l'exemple ci-dessus, l'expression est (function () {console.log(this);}). Nous sommes dans le cas n°3 : il s'agit d'une expression qui retourne une fonction de type Object, pas une Reference. this vaut donc ici l'objet global.

Autres opérateurs

Certains opérations telles que x || y, x,y, x = y font une résolution des noms sur les opérandes, ce qui fait que le résultat ne renvoie plus une Reference mais directement la valeur. Ce qui nous renvoie au cas n°3.

<script>
var player = {
    start: function () {
        console.log(this);
    }
};
console.log((false || player.start)); //affiche function(){ console…
console.log((false || player.start)()); //affiche 'window'
</script>

Dans l'exemple ci-dessus, l'opérateur || résout player.start en remplaçant sa valeur Reference en une véritable valeur de type Object (Function). Ce qui revient à faire ceci :

<script>
var player = {
    start: function () {
        console.log(this);
    }
};
console.log(false || (function () {
        console.log(this);
    }));
console.log(false || (function () {
        console.log(this);
    })());
</script>

On a donc en retour une fonction et plus une référence, d'où le fait que this vale window.

Autre exemple :

var barrel = {
    lock: function () {
        console.log(this);
    }
};
console.log(barrel.lock()); // 1. affiche Object
console.log((barrel.lock)()); // 2. affiche Object
console.log((barrel.lock = barrel.lock)()); // 3. affiche window
console.log((false || barrel.lock)()); // 4. affiche window
console.log((barrel.lock, barrel.lock)()); // 5. affiche window

Dans les deux premiers cas, la console affiche l'objet barrel car on est dans le cas 2, et la base de barrellockReference est barrel.

Dans les trois derniers cas, la console affiche window car elle opère une résolution des noms sur barrel.lock(), ce qui crée temporairement une copie "globale" de la valeur réelle de barrel.lock en lieu et place de barrel.lock. Cette copie est une expression, et comme dit plus haut this vaut l'objet global dans le cas des expressions (cas n°3).

Cas des Objets d'Activation

<script>
var conteneur = {
    a: function () {
        b();

       function b() {
          console.log(this);
       }
    }
};
conteneur.a();//renvoie 'window'

</script>

Dans le cas d'une fonction b() déclarée et appelée dans une fonction a(), on pourrait s'attendre à ce que la base de conteneurbReference soit a(), mais les AO (objets de variable spécifique aux fonctions) ne peuvent pas être des bases. La base sera donc null = window.

Notons que cette règle s'applique même quand plusieurs contextes s'empilent : ici conteneur est une variable qui fait référence à un objet, et a donc une VO, et pourtant la base de conteneurbReference n'est pas conteneur puisque conteneur n'est pas le parent direct de b().

Cas des structures with et catch

Dans les anciennes versions de ES3, un appel de fonction dans une structure with ou dans le bloc catch d'un try…catch pouvait dans certaines conditions donner comme base un objet with ou un objet catch. L'utilisation de with étant déprécié, et le cas du catch n'étant qu'un bug corrigé depuis, on n'entrera pas dans les détails.

Cas des fonctions appelées en tant que constructeur

Les objets construits à partir d'une fonction ont chacun leur propre valeur de this.

function Happy() {
    console.log(this);
    this.halloween = 10;
}
var happy = new Happy(); // affiche une réference vers l'objet happy de type Happy
console.log(happy.halloween); // affiche `10`

Affectation manuelle de this pour les fonctions

Toutes les fonctions ont dans leur prototype les fonctions apply et call.

Au lieu d'appeler a(i,j), on peut appeler a.call(obj,i,j), obj étant la référence vers l'objet vers lequel pointera this, ou a.apply(obj,[i,j]) (apply nécessitant de transmettre les paramètres dans un tableau).

Chaîne de portée

Phase de création

A la phase de création de la fonction (du moment où elle est parsée jusqu'au moment où celle-ci est répertoriée dans son VO), la chaîne des portées, ou Scope, est la liste des VO des contextes parents au contexte courant (le VO courant n'est donc pas inclus).
Scope est une propriété du contexte, comme le sont les variables et fonctions référencées dans ce contexte.

Au passage une propriété [[scope]] est créée directement dans la fonction, et restera immuable. Cette fonction résout et retourne la chaîne de portées de ladite fonction (Scope), en y ajoutant en haut de pile le VO courant.

variable.[[Scope]] = réf. vers VO courante + Scope ou variable.[[Scope]] = réf. VO courante + __parent__.[[scope]]

Phase d'activation

A la phase d'activation de la fonction (à partir du moment où celle-ci est appelée), la fonction se sert de [[scope]] pour la résolution de nom / d'identifiant : on regarde dans chacune des VO référencées par [[scope]] si une propriété ne porte pas le nom que l'on recherche, en commençant par la VO du sommet de la pile, et en s'arrêtant à la première occurence trouvée.

Exemple 1

var i = 2;
function a(){
	function b(){
		console.log(i);
	}
        b();
}
a();

Dans l'exemple ci-dessus, console.log(i) affiche bien 2 bien que i ne fasse pas partie de son contexte. [[scope]] contient dans l'ordre : le VO de b(), le VO de a() et GO.

var i = 2;
function a(){
        var i = 3;
	function b(){
		console.log(i);//3
	}
        b();
}
a();

Le VO de a() arrivant avant le GO dans le scope, la valeur de i est 3.

Ici la chaîne de portées se compose de l'AO de b(), l'AO de a() et le GO

Exemple 2

    var i = 20;
	a();
	
    function a() {
        var i = 16;
        b();
    };
    function b() {
        console.log(i); //20
    }

Dans l'exemple précédent, la déclaration var i = 16 se fait dans le contexte de a(). Or c'est dans b() que se fait le console.log(i). La chaîne de portées du contexte de b() se limite au GO.

Exemple 3

var i = 20;
	
function a() {
    var i = 16;
    return function(){
        console.log(i); //16
    }
};

var b = a();
b();

Dans ce nouvel exemple, la propriété b appartient au même contexte que i = 20, mais une fois dans la fonction, c'est la chaîne de portée de a() qui prévaut.

Chaîne des prototypes

Au moment de la résolution des noms, on regarde dans chaque VO de la fonction si celle-ci contient l'identifiant recherché, et si ce n'est pas le cas avant de passer à la VO parente, on regarde si le prototype de la fonction ne contiendrait pas une propriété du même nom (déclarée directement dedans ou héritée de Function, ou Object). Le prototype n'est pas référencé comme variable dans la VO.

function a() {
    alert(b);
}
Object.prototype.b = 12;
a();//affiche 12

Cas particuliers

Function();

Le [[scope]] d'une fonction déclarée via l'objet Function se limite à GO.

Contexte global

Le [[scope]] du contexte global se limite à GO.

eval()

Le [[scope]] de tout code déclaré via eval() est égal au [[scope]] du contexte appelant.

with et catch

Comme vu plus haut dans la partie sur la valeur de this, with et catch peuvent ajouter au début de la chaîne de portée un objet with et un objet catch. On peut passer en paramètre de ces structures un objet contenant des propriétés, et c'est parmi ces propriétés que la résolution de nom commencera.

    var collectibles = { letters: 20, almanacs: 36 };
    var letters = 2;
    with (collectibles) {
        console.log(letters); // `20`
        console.log(almanacs); // `36`
    }

Par contre les valeurs de ces propriétés peuvent être modifiées durant l'exécution :

    var collectibles = { letters: 20, almanacs: 36 };

    with (collectibles) {
        console.log(letters); // `20`
        console.log(almanacs); // `36`
        var letters = 10;
        almanacs = 18;
        console.log(letters); // `10`
        console.log(almanacs); // `18`
    }
    //à cet endroit du code, les propriétés de collectibles modifiées arrivent en fin de vie.

Fonctions et expressions

Il existe trois types de fonctions :

  • les déclarations de fonction
  • les expressions de fonction
  • les constructeurs de fonction

Déclaration de fonction (FD)

La forme classique :

function maFonction(arguments){
  //…
}

Ce type de fonction se déclare soit au niveau programme, soit dans une autre fonction.

<script>
  //cette fonction est déclarée au niveau programme
  function a(){}
  
  if(true){
    function b(){} //ATTENTION : déclaration illicite ! Cette fonction n'est pas au niveau programme mais dans une structure !
  }
  
  function c(){ //cette fonction est déclarée au niveau programme
    
    function d(){} // cette fonction est déclarée dans une autre fonction
  }
  
</script>

Contrairement aux autres types de fonction, celle-ci est répertoriée dans son VO. Si son VO est l'objet global, cette fonction devient donc globale et accessible même au-dessus de sa déclaration.

Expression de fonction

Toutes les syntaxes où une expression est attendue, c'est-à-dire en tant qu' opérande, dans un initialisateur de tableau…
Nommer la fonction est facultatif : à part en cas de fonction récursive, rappeler la fonction est impossible vu qu'elle n'est pas dans le VO.

La principale caractéristique d'une expression de fonction est d'empêcher que les valeurs déclarées à l'intérieur ne soient mémorisées dans le VO courant.

A l'affectation (opérateur =)

var a = function b() {
  console.log(3)
};
a(); // affiche 3
b(); // ReferenceError : b is not defined

Dans une liste (opérateur ,)

1, function b() { console.log(3);}() // affiche 3
b(); // ReferenceError : b is not defined

Passée directement en paramètre d'une fonction

function a(callback) {
    callback();
}
a(function b() {
    console.log(3); 
}); // affiche 3
b(); // ReferenceError : b is not defined

Si on avait eu recours à une FD à la place pour déclarer b(), celle-ci aurait évidemment été accessible en dehors de a()

function a(callback) {
    callback();
}
function b() {
    console.log(3); 
}
a(b); // affiche 3
b(); // affiche 3

Dans un initialisateur de tableau

[function b() {}(console.log(3))]; // affiche 3
b(); // ReferenceError : b is not defined

En tant que IIFE (opérateur ())

(function b() { console.log(3);}()) // affiche 3
(function b() { console.log(3);})() // affiche 3
b(); // ReferenceError : b is not defined

On notera que deux syntaxes sont possibles : les parenthèses d'appel de fonction à l'intérieur de l'expression de fonction, et les parenthèses à l'extérieur.

Les parenthèses autour de la fonction sont obligatoires au niveau programme ou dans une fonction, pour que le moteur JS sache qu'il a bien affaire à une expression suivie de parenthèses d'appel, et non pas à une déclaration de fonction b() suivie d'un opérateur de groupement vide (ce qui retournerait une erreur vu que les opérateurs de groupement vides sont interdits.

Dans les cas où la fonction est en lieu et place d'une expression, les parenthèses autour de la fonction ne sont pas nécessaires.

En initialiseur de propriété

var a = {
    b: function (c) {
        return c ? 'oui' : 'non';
    }(true)
};

Au moment de son initialisation, b a pour valeur la chaîne de caractère 'oui', et non pas une fonction.

Appel récursif

(function a(i){
    if(i < 3){
        a(i + 1);
    }
    else{
         debugger;
    }
})(0)

Lorsque a() est analysée avant création, un objet spécial est ajouté au-dessus de la pile de la chaîne de portée parente.

Ensuite quand la fonction est activée, elle obtient sa propriété [[scope]], et ajoute dans l'objet spécial parent une référence à elle-même. Enfin à la fin de l'exécution de la fonction, cet objet spécial est retiré du scope parent. Ce qui explique que la résolution de nom soit possible durant l'exécution de l'IIFE.

Constructeur de fonctions (Function())

Comme vu précédemment, le scope des fonctions construites avec Function() se limite à l'objet global.

Autre différence notable avec les autres types de fonction : les différents navigateurs ont tendance, pour économiser de la place en mémoire, à utiliser le mécanisme des objets joints. Ce mécanisme consiste, pour x expressions de fonctions dont la définition est exactement la même, de se partager un emplacement mémoire plutôt que d'avoir chacun le sien. Avec Function() ce mécanisme n'est pas utilisé.

Algorithme de création des fonctions

Ce qu'il se passe lorsqu'une fonction est créée :

  1. Un objet natif inaccessible est créé.
  2. Cet objet natif se voit attribuer une propriété [[class]], qui est la chaîne de caractère "Function"
  3. Le prototype de Function est ajouté dans une nouvelle propriété de l'objet natif, [[Prototype]].
  4. La propriété [[call]] est définie et contient une référence vers la fonction
  5. La propriété [[construct]] est définie et contient une référence vers le constructeur de la fonction en cours de création
  6. La propriété [[scope]] est définie et contient une référence vers le scope du contexte parent, ou vers le scope global s'il s'agit d'une fonction créée à partir de Function
  7. La propriété length répertorie le nombre de paramètres attendus
  8. Le prototype des objets créés par cette fonction est initialisé

Principes de programmation fonctionnelle et des closures

Un argument fonctionnel est une fonction passée directement en paramètre d'une autre.
La fonction qui reçoit cet argument fonctionnel est une fonction d'ordre supérieur, ou fonction fonctionnelle.
Un argument fonctionnel peut aussi être retourné par une autre fonction. Une fonction retournant une autre fonction est une fonction avec valeur fonctionnelle. Une fonction de première classe est une fonction qui peut être envoyée en paramètre ou retournée, comme n'importe quelle autre donnée. En JavaScript c'est le cas de toutes les fonctions. Une fonction qui se retourne elle-même est une fonction auto-réplicative. Une fonction qui se reçoit elle-même en paramètre est une fonction auto-applicative.

L'intérêt d'une fonction auto-réplicative est de pouvoir enchaîner les appels de la dite fonction :

function starsList(modes) {
     modes.forEach(function (mode) {
         console.log(mode);
     });
}
starsList(['jill', 'chris', 'barry']); //affiche 'jill' sur une ligne, puis 'chris', puis 'barry'

function stars(mode) {
     console.log(mode);
     return stars; // on retourne la fonction elle-même
}
stars('jill')('chris')('barry'); //idem que pour l'appel de starsList.

Une variable libre est une variable déclarée dans un contexte ancêtre de la fonction qui l'utilise. (elle n'est pas passée en paramètre).

function a(){
	var variableLibre = 2;
	function b(){
		console.log(variableLibre);
	}
}

Une fermeture, ou closure, est la fonction enfant qui utilise une ou plusieurs variables libres.

Toutes les fonctions sont des closures (sauf celles créées avec Function()) vu qu'elles ont toutes un AO tout en ayant accès aux variables de l'objet global.

Problématiques des closures

Problématique du scope

Toutes les fonctions créées dans le même contexte se partage le même scope parent. Les variables libres peuvent être modifiées par des instructions présentes dans des contextes enfants (ce qui peut notamment poser problème lorsque l'on souhaite encapsuler des données).

var variableLibre = 1;
function a(){
	variableLibre++;
	console.log(variableLibre);
}
function b(){
	variableLibre++;
	console.log(variableLibre);
}
a(); // 2
b(); // 3
a(); // 4
console.log(variableLibre); // 4

Autre exemple :

var a;
var b;

function foo() {

  var x = 1;

  a = function () { return ++x; };
  b = function () { return --x; };

  x = 2; // les deux fonctions ont pour scope l'AO de foo, qui contient x

  console.log(a());
}

foo(); // x = 3

console.log(a()); // x = 4 (incrémentation)
console.log(b()); // x = 3 (décrémentation)

D'où le problème suivant lorsque l'on créé une fonction dans une boucle :

for (var a = 0;a < 10;a++){
	window.setTimeout(function () {
		console.log(a); // affiche 10 fois '10' au lieu de '1','2','3'… '10'
    }, a * 1000);
}

Dans cet exemple, a est déclaré dans le contexte global, et vaut 0,1,2,3…10 pendant la boucle. Après la sortie de boucle, a vaut toujours 10 dans le contexte global. La fonction passée en paramètre de setTimeout se déclenche après la fin de la boucle, quand a vaut 10.

Ce problème peut être évité en ajoutant un scope intermédiaire :

Avec let (ES6)

for (let a = 1;a <= 10;a++){ 
	window.setTimeout(function () {
		console.log(a); // affiche '1','2','3'… '10'
    }, a * 1000);
}

Avec le mot-clé let, a n'existe pas dans l'objet global mais uniquement dans le scope de l'itération en cours. A chaque appel de l'argument de fonction, celui-ci s'exécute dans un sous-scope différent, celui de l'itération en cours, avec sa propre valeur de a.

Avec une IIFE

for (var a = 1;a <= 10;a++){
	(function(a){
		window.setTimeout(function () {
			console.log(a); // affiche '1','2','3'… '10'
		}, a * 1000);
	})(a);
}

Sur le même principe, il suffit d'isoler l'argument de fonction du a global en l'encapsulant dans une IIFE. A chaque tour de boucle, la valeur du a global est passée en paramètre de l'IIFE et cette copie se retrouve donc accessible depuis le scope de l'argument de fonction.

Problématique du return

En javascript, faire un return dans une sous-fonction ne quitte pas la fonction parente.

function getElement() {

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

    if (element % 2 == 0) {
      console.log('found: ' + element); // found: 2
      return element;
    }

  });

  return null;
}

console.log(getElement()); // null, pas 2

Pour contourner cela, on peut placer la sous-fonction dans un try, y lancer une exception, et faire le return dans un catch.

var $break = {};

function getElement() {

  try {

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

      if (element % 2 == 0) {
        console.log('found: ' + element); // found: 2
        $break.data = element;
        throw $break;
      }

    });

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

  return null;
}

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

Quelques exemples de closures en arguments de fonction

  • array.sort() accepte en paramètre une fonction contenant les conditions du tri du tableau
  • array.find() accepte en paramètre une fonction contenant les critères de recherche
  • array.map() accepte en paramètre une fonction contenant les traitements à appliquer à chaque entrée du tableau, et retourne le tableau après traitement
  • array.forEach() accepte en paramètre une fonction qui applique une série d'instructions pour chaque item du tableau
  • window.setTimeout() accepte en paramètre une fonction qui se déclenchera dans un intervalle de temps donné
  • xmlHttpRequestObject.onreadystatechange() accepte en paramètre une fonction qui se déclenchera lors d'un événement précis

Types

Présentation

Cinq types primitifs : undefined, null, boolean, string, number Un type : object.

Trois types réservés :

  • Reference : lié à la valeur et au comportement de delete, typeof, this
  • List : lié au comportement des listes d'arguments : new Objet(arglist) ou fonction(arglist)
  • Completion : lié au comportement de break, continue, return, throw

Les types primitifs n'ont ni constructeur ni prototype.

typeof lit dans un tableau une chaîne de caractère associée à la valeur, d'où des retours étonnants comme typeof null === "Object".

Un objet est une collection non ordonnée de paires de clés/valeurs appelées propriétés (ou méthodes quand la valeur est une fonction).

En JS les objets sont dynamiques, c'est-à-dire que leurs propriétés peuvent être modifiées, ajoutées ou supprimées à la volée (durant l'exécution).
Certaines propriétés ne peuvent être modifiées ou supprimées car porteuses d'un attribut read-only ou non-configurable.

En ES5, il est possible de geler un objet, c'est-à-dire empêcher toute création, modification ou suppression de ses propriétés, avec Object.freeze(monObjet). Si c'est juste la création que l'on souhaite empêcher, on peut utiliser Object.preventExtensions(monObjet), et si on ne souhaite limiter que certaines propriétés et pas l'objet entier, on peut utiliser Object.defineProperty(monObjet,maPropriete,objDescripteur)

Un objet natif est un objet fourni par l'implémentation JS.
Un objet pré-conçu est un sous-objet natif créé par l'implémentation en début de programme : parseInt, Math
Un objet hôte est un objet fourni par l'environnement, par ex. le navigateur : window, console

Pour certains types primitifs, il existe des objets englobants spéciaux : Boolean, String, Number

var objBool = new Boolean(true);
var primitiveBool = Boolean(objBool); // pas de new : conversion implicite
console.log(objBool); // affiche un objet Boolean
console.log(primitiveBool); // affiche true

var newObjBool = Object(primitiveBool);
console.log(newObjBool); // affiche un objet Boolean

Constructeurs spéciaux préconçus : Function, Array, RegExp, Date, Math… construisent des objets (type Object)

Initialiseurs d'objet, de tableau, d'expression régulière : [a => 0,b => 1], {a: 0, b:1}, /*/

Dans le cas de l'initialiseur d'expression régulière, en ES3 celui-ci crée une seule instance d'une expression régulière pour tout le script.

/ain/g === /ain/g // vaut 'true' en ES3, 'false' en ES5

Les objets diffèrent des tableaux associatifs en ces points :

  • un objet en JS, même initialisé vide, hérite du prototype de la classe parente et n'est donc jamais vraiment vide, contrairement aux tableaux associatifs.
var newObject = {};
console.log(newObject) //affiche tout plein de propriétés…

En ES5 il est devenu possible d'initialiser un Object vraiment vide grâce à la syntaxe Object.create(null).

  • la syntaxe ["propriete"] et .propriete pointent sur le même résultat, ce qui n'est pas le cas en Ruby, en PHP…
newObject["propriete"] = "Oh, une valeur !"
console.log(newObject.propriete) // affiche "Oh, une valeur !"
  • les propriétés peuvent être accessibles directement sans forcément être modifiables :
var name = new String("Marie");
name['length'] = 10;
console.log(name['length']); // affiche '5'

Conversions

Les conversions d'un type Object à un type primitif se font à l'aide de la fonction valueOf interne à chaque objet. C'est cette fonction qui est appelée lors d'une conversion implicite : var primitiveBool = Boolean(objBool);, mais elle peut aussi être appelée explicitement.

var number = new Number(7);
var primitiveNumber = Number(number); // appel implicite de `valueOf`
var alsoPrimitiveNumber = number.valueOf(); // appel explicite de `valueOf`

console.log([
    typeof number, // `"object"`
    typeof primitiveNumber, // `"number"`
    typeof alsoPrimitiveNumber // `"number"`
]);

Les conversions implicites ont notamment lieu lors de calculs arithmétiques.

var number = new Number(7);
console.log(number + 1); //affiche '8'

Il est possible de déclarer dans un objet standard une fonction valueOf() pour que celle-ci retourne une valeur primitive dans une opération, exactement comme pour toString() dans une concaténation.

var euros = {
    total : 350.0,
    taux : 1,
    valueOf: function (){
        return this.total * this.taux;
    }
}
var dollars = {
    total : 170.0,
    taux : 0.83,
    valueOf: function(){
        return this.total * this.taux;
    }
}
console.log(dollars + euros); //affiche 491.10 

La méthode toString() est d'ailleurs appelée en lieu et place de valueOf() quand celle-ci n'est pas définie.

Attributs de propriétés

En ES3 les attributs suivants existent :

  • {ReadOnly} : non modifiable par l'utilisateur (mais modifiable par l'hôte - le navigateur)
  • {DontEnum} : non énumérable dans une boucle for…in
  • {DontDelete} : non supprimable avec l'opérateur delete
  • {Internal} : totalement inaccessible

En ES5 leur tournure a été changée pour qu'elles soient au positif et plus au négatif :

  • [[Writable]] : modifiable par l'utilisateur (mais modifiable par l'hôte - le navigateur)
  • [[Enumerable]] : énumérable dans une boucle for…in
  • [[Configurable]] : supprimable avec l'opérateur delete
  • {Internal} : idem

Il est possible de récupérer tous ces attributs (sauf {internal}) en un seul objet descripteur avec Object.getOwnPropertyDescriptor(monObjet);.

var monObjet = {"maPropriete": 101}
Object.defineProperty(monObjet, "maPropriete", {
    value: 101,
    writable: true,
    enumerable: false,
    configurable: true
});

var desc = Object.getOwnPropertyDescriptor(monObjet, "maPropriete");
console.log(desc.enumerable); // `false`
console.log(desc.writable); // `true`

Méthodes et propriétés internes

  • [[Prototype]] prototype de l'objet
  • [[Class]] retourne le type de l'objet
  • [[Get]] récupère la valeur d'une propriété
  • [[Put]] attribue une valeur à une propriété
  • [[CanPut]] indique de quelle manière cette propriété peut être redéfinie
  • [[HasProperty]] indique si cette propriété appartient déjà à l'objet
  • [[Delete]] retire une propriété de l'objet
  • [[DefaultValue]] retourne une valeur primitive correspondant à l'objet, ou lève une exception TypeError

Object.prototype.toString() retourne une chaîne "[object " + [[Class]] + "]", mais selon les implémentations cette valeur n'est pas toujours fiable.

Constructeurs et méthodes

Un objet est créé à l'aide d'un constructeur.
Un constructeur est une fonction qui créé et initialise un objet.
Créer un objet consiste à lui allouer de la mémoire à l'aide d'une méthode interne [[Construct]].
Initialiser un objet consiste à lui attribuer des valeurs à l'aide d'une méthode interne [[Call]], qui va elle-même faire appel au constructeur.
L'objet est accessible dans le programme à partir du début de son initialisation, dans le constructeur : c'est this.

function Player() {
    this.level = 10;
    return this;
}
var player = new Player;
console.log(player) // affiche {level: 10}

On a vu précédemment qu'un objet natif est un objet fourni par l'implémentation JS (défini par l'utilisateur dans le code ou apparaîssant dans les spécifications), par opposition à un objet hôte qui lui est fourni par le navigateur.
On a aussi vu l'algorithme de création des fonctions : la première étape de la création d'une fonction est la création et l'initialisation d'un objet natif inaccessible dans l'implémentation.

F = new NativeObject();
F.[[Class]] = "Function"
…
F.[[Call]] = Référence à la fonction (pas à l'objet natif F)
F.[[Construct]] = Référence au constructeur général qui gère l'allocation mémoire

Un objet qui peut être activé par deux parenthèses est une fonction. Une fonction a une propriété [[Call]] ainsi qu'une propriété [[Class]] qui vaut "Function", et typeof retourne "function".

Dans le cas des fonctions hôtes, typeof peut en fonction de l'implémentation ne pas retourner "function" mais "Object". Par exemple sous IE8 console.log(typeof window.console.log) retourne object.

Pour initialiser un objet à partir d'une fonction, il faut impérativement utiliser le mot-clé new. Les parenthèses sont optionnelles quand il n'y a aucun argument à passer.

Création d'une fonction

  1. Création d'un nouvel objet natif O
  2. [[Class]] de l'objet natif = "Object"
  3. Récupération de la propriété prototype de la fonction F, ou à défaut du prototype de Object
  4. Appel de Function.[[Call]] pour affecter this et les arguments passés au constructeur
  5. Retourne l'objet créé (explicitement avec return, ou this par défaut)

Prototype

Le prototype d'un objet peut être un objet ou la valeur null.
La propriété interne [[Prototype]] permet d'y accéder indirectement.

Character.prototype = {
    constructor: Character,
    hp: 100
};

Le prototype a une propriété constructor qui fait référence à la fonction qui construit l'objet.
Les propriétés constructor et prototype peuvent être redéfinies après la création de la fonction :

function Character() {}
Character.prototype = {
    level: 10
};

Penser à réaffecter constructor après avoir changé le prototype, sinon la référence vers le constructeur (via monObjet.constructor) sera perdue. Par défaut les propriétés de O.constructor sont non-énumérables, mais après réaffectation du prototype elles le sont de nouveau.

Les objets qui auront été créés avant le changement de prototype ne seront pas affectés et garderont le prototype initial. Le seul moyen de modifier le prototype en répercutant les modifications dans les instances existantes, est de modifier directement les propriétés existantes dans le prototype plutôt que de réaffecter un nouvel objet prototype.

Certaines implémentations JS fournissent la propriété non standard __proto__ qui permet de modifier directement le prototype d'une instance en lui affectant une valeur.
En ES5 Object.getPrototypeOf permet de récupérer en lecture la valeur de [[prototype]].

Détruire la fonction construtrice, par exemple en la mettant à null, n'empêche pas de pouvoir continuer à construire d'autres objets de même type grâce à la propriété constructor que possèdent les instances.

function Character() {}
Character.prototype.level = 10;
var player1 = new Character;
Character = null;
var player2 = new player1.constructor();
console.log(player1.level); //10
console.log(player2.level); //10

delete player1.constructor.prototype.constructor;
var player3 = new player1.constructor(); 
console.log(player3.level); //undefined
console.log(player1.level); //10

monObjet instanceof Character : instanceof regarde si le prototype de Character (Character.prototype) est présent dans monObjet.[[prototype]] grâce à la méthode interne [[HasInstance]].
instanceof ne permet donc pas de savoir avec certitude par quel constructeur a été construit l'objet, puisque les prototypes peuvent être modifiés après coup avec __proto__.

function Animal() {}
function Mammal() {}

var cat = new Animal;
cat.__proto__ = Mammal.prototype;

console.log(cat instanceof Animal); //false
console.log(cat instanceof Mammal); //true

Le prototype est particulièrement utile pour mettre en commun des méthodes, des états par défaut et des propriétés partagées entre toutes les instances.

function Character(stat) {
    this.stat = stat || 100;
}
Character.prototype = (function () {
    var sharedStat = 500;
    function helper() {
        console.log('stat partagée : ' + sharedStat);
    }
    function attack() {
        console.log('attaque : ' + this.stat);
    }
    function defence() {
        console.log('défense : ' + this.stat);
        helper();
    }
    // le prototype lui-même.
    return {
        constructor: Character,
        attack: attack,
        defence: defence
    };
})();

La méthode interne [[Get]] résout le nom de propriété demandé en regardant dans l'ordre dans :

  • les propriétés de l'objet courant
  • les propriétes du prototype de l'objet courant (et retourne undefined si le prototype est nul)
  • les propriétes du prototype du prototype de l'objet courant…
  • et récursivement jusqu'à ce que le dernier prototype en bout de chaîne soit nul

Si une propriété n'existe pas, elle vaut undefined (et équivaut à false après conversion en booléen).
On préfèrera utiliser l'opérateur in pour tester la présence d'une propriété dans un objet, car if(window.monObjet) retourne false même si la propriété est présente si celle-ci vaut false ou 0.

Méthode Put

Modifie la propriété de l'objet… ou s'il n'a pas le droit (propriété en lecture seule, la méthode interne monObjet.canPut retourne false), ne fait rien en ES3, ou lève une erreur TypeError en mode strict… ou si elle n'existe pas, la crée en la laissant modifiable, énumérable, accessible et supprimable.

Ce qui implique qu'une propriété héritée dans le prototype n'est pas supprimée quand une affectation est faite directement dans l'objet mais juste masquée. Les propriétés héritée en prototype en lecture seule ne peuvent pas être surchargées.

var luxendarc = new String("Abc"); console.log(luxendarc.length); // 3, la longeur de abc // essayons de la masquer luxendarc.length = 5; console.log(luxendarc.length) // toujours 3

toObject()

Lorsque l'on tente d'appeler les méthodes Get et Put sur des primitives, la méthode toObject() convertit juste le temps de l'instruction la primitive en objet

var level = 10; // valeur primitive console.log(level.toString()); // "10"

toString opère en fait sur un nouvel objet équivalent à new Number(level), et cet objet est supprimé juste après l'exécution du toString() (toString est hérité du prototype de Number). Ce qui explique pourquoi créer une nouvelle propriété sur cette primitive n'a aucun effet :

var level = 10; // valeur primitive level.test = "test"; console.log(level.test); // affiche undefined

1..toString() équivaut à faire 1.0.toString(), ou new Number(1.0).toString(). (et affiche "1") La syntaxe 1.toString() n'est pas autorisée car il y a une ambiguité sur le rôle du point : point décimal ou point d'appel d'une propriété ? Par contre (1).toString() ne laisse pas de place au doute et est autorisé, tout comme 1 .toString(), 1'toString'; …

Chaîne de prototype

Enchaîner les prototypes

    Object.create ||
    Object.create = function (parent, properties) {
        function F() {}
        F.prototype = parent;
        var child = new F;
        for (var k in properties) {
            child[k] = properties[k].value;
        }
        return child;
    }
    var monster = { attack: 10 };
    var goblin = Object.create(monster, { defence: { value: 20 } });
    console.log(goblin.attack, goblin.attack); // `10`, `20`

En ES6 :

class Monster {
    constructor(name) {
       this._name = name;
    }
    getName() {
        return this._name;
    }
}
class Humanoid extends Monster {
    getName() {
        return super.getName() + ' Archer';
    }
}
var goblin = new Humanoid('Goblin');
console.log(goblin.getName()); // `"Goblin Archer"`

Stratégie d'évaluation.

Mode de passage des valeurs en paramètre d'une fonction.

Souvent simplifiée comme étant « par valeur quand celle-ci est primitive, ou par référence quand celle-ci est un objet », en réalité les objets sont passés par partage, c'est à dire qu'une copie de référence est communiquée. Les propriétés de cet objet sont mutables (les modifications se répercutent à l'extérieur de la fonction), mais changer directement la valeur de l'objet lui-même n'a pas d'effet sur la valeur en dehors de la fonction puisqu'il s'agit d'une référence.

var a = {test: "Test";} a = b; // simple affectation où il y a deux objets différents, mais avec une valeur identique (la copie d'adresse).

⚠️ **GitHub.com Fallback** ⚠️