Introduction
Dans ce chapitre vont être présentés les éléments suivants concernant la syntaxe de base de JavaScript:
- la déclaration et les opérations de base sur les tableaux,
- les fonctions spécifiques sur les tableaux en JS,
- le concept d'objet en JS,
- le format JSON et le clonage,
- l'affectation par décomposition,
- l'invocation indirecte,
- le mot-clef "this",
- les fermetures
- les collections et les concepts d'itérateur et d'itérable
Les tableaux
Les tableaux en JS
Déclaration de tableaux
Tout comme en PHP, les tableaux en JavaScript peuvent contenir des données de types différents. Par exemple:
let tab = ["a", 2, 3.2, function(x){ return ++x;}];
Dans l'exemple ci-dessus, notez la présence d'une fonction dans le tableau. En effet, en JavaScript une fonction est un objet, elle peut donc être placée dans un tableau comme n'importe quel autre objet.
Accès à un élément selon son indice
L'accès à l'élément d'indice 'i' s'effectue selon la syntaxe classique:
console.log(tab[0]); //"a"
console.log(tab[3](5));//6
La méthode at() permet l'accès à un élément selon sa position relative à la fin du tableau:
let tab = ['a','b','c','d','e','f','g','h'];
console.log(`${tab.at(2)}${tab.at(-1)}${tab.at(4)}${tab.at(-3)}`); //chef
Longueur d'un tableau
La longueur d'un tableau est obtenue par la propriété "length".
console.log(tab.length); //4
Ajouter un élément
Tout comme en PHP, les tableaux sont dynamiques. L'ajout d'un élément est possible soit en affectant une valeur à l'indice égale à la longueur actuelle du tableau ou en utilisant la méthode "push(elem, elem, ...)".
let tab = [];
tab[tab.length] = 3; ou tab.push(3);
l'opérateur "spread" (...)
l'opérateur spread (symbole composé de trois points) "..." permet de scinder un objet itérable, donc aussi les tableaux, en ses valeurs individuelles.
let array1 = [2,3,4];
let array2 = [1, ...array1, 5, 6, 7];
console.log(array2.toString()); //=> "1, 2, 3, 4, 5, 6, 7"
let array3 = [2,3,4];
let array4 = [1];
array4.push(...array3);
console.log(array4.toString()); //=> "1, 2, 3, 4"
function myFunction(a, b) {
return a + b;
}
let data = [1, 4];
let result = myFunction(...data);
console.log(result); //=> "5"
Supprimer un élément
L'opérateur delete supprime la valeur contenue dans le tableau et la remplace par "undefined". La case du tableau existe donc toujours (Cette technique est peu recommandée, il vaut mieux utiliser "splice" ou "pop").
La méthode pop() retourne le dernier élément du tableau après avoir supprimé la dernière case du tableau.
Pour retirer un ou plusieurs éléments à partir de n'importe quelle position d'un tableau, il faut utiliser la méthode splice
Suppression d'éléments dans un tableau
let tab = [1,2,3,4];
tab[tab.length] = 5;
delete tab[1];
console.log(tab.length);
console.log(tab[1]);
let a = tab.pop();
console.log(tab.length);
tab.splice(1,2);
console.log(tab.length);
console.log(tab.toString());
Sans exécuter le code ci-dessous, qu'est-ce qui sera affiché en console ?
Réponse...
5 undefined 4 2 "1,4" car tab[1] vaut undefined suite à l'appel à delete; la dernière valeur ajoutée (5) est supprimée par l'appel à "pop" et l'appel à "splice" retire 2 éléments à partir de l'indice 1 compris.
D'autres fonctions utiles sur les tableaux
Comme tout langage de programmation, JavaScript dispose de bien d'autres fonctions et méthodes de manipulation de tableaux, comme : join(), reverse(), sort(), concat(), slice(), splice(), unshift(), shift(),….
Consultez la documentation pour découvrir les propriétés et méthodes des tableaux.
Fonctions spécifiques pour le parcours de tableaux
Certaines opérations fréquentes nécessitent de parcourir l'ensemble des éléments d'un tableau. Voici quelques méthodes qui rendent la manipulation des tableaux en JavaScript élégante et facile.
forEach
Comme son nom l'indique, "forEach(function(valeur, index, tab){})" permet de parcourir un tableau et d'appliquer une fonction sur chaque élément.
Cette fonction peut prendre de 1 à trois paramètres; dans l'ordre: la valeur et l'indice de l'élément courant et la référence du tableau.
Veuillez noter qu'il est possible de modifier directement le tableau en utilisant la référence du tableau !
let data = [1,2,3,4,5];
let sum = 0;
data.forEach(function(value) { sum += value; });
console.log(sum); // => 15
data.forEach(function(v, i, a) { a[i] = v + 1; });
console.log(data.toString()); // => "2,3,4,5,6"
map
La méthode "map(function(valeur, index, tab){})" est presque identique à la méthode forEach à une différence fondamentale près: la méthode retourne un nouveau tableau avec le résultat retourné par chaque appel de la fonction fournie en argument.
let data = [1,2,3,4,5];
let sum = 0;
data.map(function(value) { sum += value; });
console.log(sum); // => 15
data.map(function(v, i, a) { a[i] = v + 1; });
console.log(data.toString()); // => "2,3,4,5,6"
let dataCopy = data.map(v => v * 2);
console.log(data.toString()); // => "2,3,4,5,6"
console.log(dataCopy.toString()); // => "4,6,8,10,12"
filter
La méthode "filter(function(valeur, index, tab){})" appliquée à un tableau retourne un nouveau tableau, après filtrage des éléments pour lesquels la fonction passée en argument renvoie true.
a = [5, 4, 3, 2, 1];
smallvalues = a.filter(x => x < 3); // [2, 1]
everyother = a.filter((x,i) => i%2==0); // [5, 3, 1]
find
La méthode "find(function(valeur, index, tab){})" retourne la première valeur du tableau pour laquelle la fonction passée en argument renvoie true.
Si aucun élément n'est trouvé, la méthode retourne "undefined".
let a = [1,2,3,4,5];
let four = a.find(x => x > 3); // 4
let seven = a.find(x => x > 6); //undefined
findIndex
La méthode "findIndex(function(valeur, index, tab){})" retourne le premier indice de l'élément du tableau pour lequel la fonction passée en argument renvoie true.
Si aucun élément n'est trouvé, la méthode retourne "-1".
let a = [1,2,3,4,5];
let posFour = a.findIndex(x => x > 3); // 3
let posSeven = a.findIndex(x => x > 6); // -1
every
La méthode "every(function(valeur, index, tab){})" retourne true si la fonction passée en argument retourne true pour tous les éléments du tableau.
a = [1,2,3,4,5];
a.every(function(x) { return x < 10; }); // => true
a.every(function(x) { return x % 2 === 0; }); // => false
some
La méthode "some(function(valeur, index, tab){})" retourne true si la fonction passée en argument retourne true pour au moins un élément du tableau.
a = [1,2,3,4,5];
a.some(function(x) { return x%2===0; }); // => true
a.some(isNaN);// => false
reduce
La méthode "reduce(function(accumulateur, valeur, index, tab){}, valeur_initiale)" applique une fonction qui accumule le résultat obtenu sur chaque élément pour réduire le tableau à une seule valeur.
La fonction prend au minimum en arguments la variable accumulateur qui contient la valeur cumulée et la variable représentant la valeur de l'élément courant.
La méthode "reduce" peut prendre en second paramètre la valeur initiale de l'accumulateur. Si celle-ci n'est pas spécifiée, l'accumulateur prend la valeur du premier élément et commence l'itération à partir du second élément.
let a = [1,2,3,4,5];
let sum = a.reduce((x,y) => x+y, 5); //5 +1 +2 +3 +4 +5 = 20
let product = a.reduce((x,y) => x*y, 1); //1 *2 *3 *4 *5 = 120
let max = a.reduce((x,y) => (x>y) ? x : y); //1 >2? >3? >4? >5? = 5
let sub1 = a.reduce((x,y) => x-y); //1 -2 -3 -4 -5 = -13
let sub2 = a.reduce((x,y) => x-y, 0); //0 -1 -2 -3 -4 -5 = -15
let sum2 = a.reduce((acc, v, i, t) => acc+t[i], 10); //10 +1 +2 +3 +4 +5 = 25
reduceRight
la méthode "reduceRight(function(accumulateur, valeur, index, tab){}, valeur_initiale)" est identique à "reduce" sauf qu'elle parcourt le tableau du dernier élément jusqu'au premier.
let a = [1,2,3,4,5];
let sumbis = a.reduceRight((x,y) => x+y); //5 +4 +3 +2 +1 = 15
Tri de tableaux
La méthode "sort(function(a,b){})" d'un tableau trie les éléments du tableau par ordre croissant.
let valeurs= [5,9,68,23,2,1,68];
valeurs.sort();
console.log(valeurs.join(', ')); //1, 2, 23, 5, 68, 68, 9
Cette méthode peut recevoir une fonction en argument qui permet de comparer deux éléments a et b du tableau. La valeur retournée est positive si a < b, négative si a > b et nulle si a == b.
let valeurs = [5, 9, 68, 23, 2, 1, 68];
valeurs.sort((a, b) => b - a);
console.log(valeurs.join(', ')); //68, 68, 23, 9, 5, 2, 1
Tri de tableau d'objets
let eleves = [
{nom: 'DUPONT', prenom: 'Jeanne', classe: 'A'},
{nom: 'ALTAIR', prenom: 'Luc', classe: 'A'},
{nom: 'DUPONT', prenom: 'Pierre', classe: 'B'},
{nom: 'DUPONT', prenom: 'Arthur', classe: 'A'},
{nom: 'PIONG', prenom: 'Chang', classe: 'B'},
{nom: 'POPOULOS', prenom: 'Dimitrius', classe: 'B'},
{nom: 'ATAC', prenom: 'Djamila', classe: 'A'}
];
Comment trier le tableau des élèves par classe, nom et prénom ? (Astuce: String.localeCompare()) permet de comparer 2 chaînes. Réalisez l'affichage en console de chaque éléve (classe nom prenom), 1 élève par ligne.
Solution...
eleves.sort((eleveA, eleveB) => eleveA.classe.localeCompare(eleveB.classe) || eleveA.nom.localeCompare(eleveB.nom) || eleveA.prenom.localeCompare(eleveB.prenom));
console.log(eleves.map(e => `${e.classe} ${e.nom} ${e.prenom}`).join('\n'));
Les objets
Initialisation d'objets
Un objet est créé par l'affectation de couples clé-valeur:
let obj = {
propriete1: false,
propriete2: 42,
propriete3: {x: 3, y: 4},
propriete4: function(x){ return ++x; }
}
La valeur d'une propriété peut être de n'importe quel type. Elle peut être elle-même un objet (propriete3) ou encore une fonction anonyme (propriete4); ce qui permet de déclarer des méthodes à un objet.
Appel de propriété / méthode
Explicite
console.log(obj.propriete2); //42
console.log(obj.propriete4(5)); //6
Dynamique
L'objet peut être vu comme un "tableau associatif" dont les clés sont les noms des propriétés.
console.log(obj["propriete2"]); //42
console.log(obj["propriete4"](5)); //6
Conditionnelle
Les objets JS sont dynamiques et peuvent avoir des propriétés qui sont ajoutées ou retirées en cours d'exécution. L'accès à une propriété non définie d'un objet retourne undefined. Mais l'accès à une propriété d'une propriété non définie d'un objet provoque une erreur et l'interruption du code exécuté. L'opérateur ?. retourne la valeur de la propriété d'un objet si elle est définie, "undefined" sinon. Évidemment, il ne faut pas utiliser systématiquement cet opérateur lorsque ce n'est pas nécessaire afin de ne pas alourdir le code.
let eleveA = { matricule: "e123456", bulletin: {math: 15, francais: 16}};
let eleveB = { matricule: "e125789" };
console.log(`(${eleveA.matricule}) cote: ${eleveA.bulletin.math}`);
//(e123456) cote: 15
//console.log(`(${eleveB.matricule}) cote: ${eleveB.bulletin.math}`);
//console.log(`(${eleveB.matricule}) cote: ${eleveB?.bulletin.math}`);
//Les lignes de code ci-dessus provoquent une erreur et une interruption du code si décommentées...
console.log(`(${eleveB.matricule}) cote: '${eleveB.bulletin?.math}'`);
//(e125789) cote: undefined
console.log(`(${eleveB.matricule}) cote: '${eleveB.bulletin?.math ?? "manquante"}'`);
//(e125789) cote: 'manquante'
Suppression d'une propriété d'un objet
La fonction "delete" supprime une propriété d'un objet.
let student = {
lastName: "John",
firstName: "Doe",
birthDate: "05/06/1978"
}
delete(student.birthDate);
console.log(student.birthDate); // undefined
Vérification si un objet possède une propriété
En JavaScript, les objets peuvent voir leurs propriétés ajoutées ou supprimées dynamiquement. Il est donc parfois utile de vérifier si une propriété existe avec "hasOwnProperty".
if (student.hasOwnProperty('birthDate')) {
console.log(student.birthDate);
};
Parcours des propriétés d'un objet
for(prop in obj){
console.log(prop + " : " + obj[prop]);
}
Il est cependant préférable d'utiliser "Object.keys" qui ne prend en compte que les propriétés de l'objet sans tenir compte des propriétés héritées par la chaine de prototypes (En JS, un objet est construit à partir d'un autre et possède ses propriétés en plus des siennes. Nous verrons en détail ce mécanisme dans le chapitre Classes et modules.); ce qui en général est préférable pour une bonne découpe (responsabilité) du code.
Object.keys(obj).forEach(function (prop) {
console.log(prop + " : " + obj[prop]);
});
Sérialisation
La sérialisation consiste à obtenir une représentation textuelle d'un objet; la désérialisation étant l'opération inverse.
La syntaxe de déclaration d'objets JavaScript a été dérivée pour définir la sérialisation d'objets au format JSON (JavaScript Object Notation).
Les fonctions sont :
- JSON.stringify(obj): sérialisation d'un objet obj,
- JSON.parse(txt): désérialisation d'une chaîne textuelle txt
let elem = {
num:1,
obj: { tab: [false,null,""] },
fct: function(i) { return ++i },
und: undefined,
nan: NaN,
dat: new Date('2019-02-20T10:02:00')
};
console.log(elem.dat); //Wed Feb 20 2019 10:02:00 GMT+0100 (heure normale d’Europe centrale)
console.log(elem.dat.toDateString()); //Wed Feb 20 2019
let elemStr = JSON.stringify(elem);
console.log(elemStr); //{"num":1,"obj":{"tab":[false,null,""]},"nan":null,"dat":"2019-02-20T09:02:00.000Z"}
let elemCopy = JSON.parse(elemStr);
console.log(elemCopy.dat); //2019-02-20T09:02:00.000Z
console.log(elemCopy.dat.toDateString()); //TypeError: elemCopy.dat.toDateString is not a function
Comme vous pouvez le constater ci-dessus, les méthodes et certaines propriétés, en fonction de leur valeur, ne sont pas conservée lors de la sérialisation:
- Sérialisés: objets, tableaux, chaines de caractères, nombres, booléens et null,
- Remplacés: NaN, Infinity, -Infinity => null; Date => format ISO,
- Supprimés : fonctions, RegExp, Error, undefined
Pour restaurer toutes les propriétés et méthodes d'un objet sérialisé, il est parfois nécessaire de créer une fonction spécifique :
let elemClone = JSONreviver(elemCopy);
console.log(elemClone.dat);
console.log(elemClone.dat.toDateString());
function JSONreviver(obj){
let e = obj;
e.fct = function(i) { return ++i };
if(!obj.hasOwnProperty("und")){
e.und = undefined;
}
if(!obj.hasOwnProperty("nan")){
e.nan= NaN;
}
e.dat = new Date(obj.dat);
return e;
}
Pour vérifier vos chaînes JSON, utilisez un validateur online
Clonage d'objet
La copie d'un objet n'est pas aussi évidente qu'elle n'y parait. En effet, un objet peut en contenir d'autres si certaines de ses propriétés sont elles-mêmes des objets.
Une simple copie de la variable elle-même provoque la copie non pas de l'objet lui-même mais bien de sa référence comme le montre l'exemple ci-dessous !
let a = {
pos: {x: 1, y:1}
nbr: 2
}
let b = a;
b.nbr = 3;
console.log(a.nbr); // 3 !
La copie en profondeur... incomplète
La première technique pour réaliser une copie en profondeur (deep copy) pourrait être de sérialiser et désérialiser l'objet.
let a = {
pos: {x: 1, y:1},
nbr: 2,
print: function(){ console.log("nbr " + this.nbr); }
}
let b = JSON.parse(JSON.stringify(a));
b.nbr = 3;
b.pos.x = 9;
console.log(a.nbr); // 2
console.log(a.pos.x); // 1
b.print() //Exception: méthode print() inexistante !
Cependant, comme nous l'avons vu précédemment, la sérialisation provoque la perte des méthodes et de certaines propriétés en fonction de leur valeur.
Le clonage n'est donc pas complet!
Clonage via JSON
let elem = {
num:1,
obj: { tab: [false,null,""] },
fct: function(i) { return ++i },
und: undefined,
nan: NaN,
dat: new Date('2019-02-20T10:02:00')
};
function cloneObj(src) {
return JSON.parse(JSON.stringify(src));
}
let clone = cloneObj(elem);
clone.num = 2;
console.log(elem.num);
console.log(clone.num);
clone.obj.tab[0] = true;
console.log(elem.obj.tab[0]);
console.log(clone.obj.tab[0]);
console.log(clone.fct(5));
Quel est le résultat affiché en console par les lignes ci-dessus ?
Réponse...
"1 2 false true TypeError: clone.fct is not a function" puisque les méthodes sont ignorées lors de la sérialisation. Par contre, le tableau a bien été copié par valeur.
La copie complète... en surface
Une autre technique, appelée copie peu profonde (shallow copy) permet de conserver toutes les propriétés, méthodes comprises.
let a = {
pos: {x: 1, y:1},
nbr: 2,
print: function(){ console.log("nbr " + this.nbr); }
}
let b = Object.assign({}, a);
b.nbr = 3;
b.pos.x = 9;
console.log(a.nbr); // 2
console.log(a.pos.x); // 9 !
b.print(); // "nbr 3"
Comme vous pouvez le constater, les valeurs des propriétés sont passées par référence et non copiées !
La syntaxe suivante, exploitant l'opérateur ... (spread) est également équivalent à Object.assign():
let a = {
pos: {x: 1, y:1},
nbr: 2,
print: function(){ console.log("nbr " + this.nbr); }
}
let b = {... a};
b.nbr = 3;
b.pos.x = 9;
console.log(a.nbr); // 2
console.log(a.pos.x); // 9 !
b.print(); // "nbr 3"
Clonage via ...
let elem = {
num:1,
obj: { tab: [false,null,""] },
fct: function(i) { return ++i },
und: undefined,
nan: NaN,
dat: new Date('2019-02-20T10:02:00')
};
function cloneObj(src) {
return Object.assign({}, src);
}
let clone = cloneObj(elem);
clone.num = 2;
console.log(elem.num);
console.log(clone.num);
clone.obj.tab[0] = true;
console.log(elem.obj.tab[0]);
console.log(clone.obj.tab[0]);
console.log(clone.fct(5));
Quel est le résultat affiché en console par les lignes ci-dessus ?
Réponse...
"1 2 true true 6" car un tableau est un objet; la valeur de la propriété obj est donc passée par référence; il s'agit donc du même tableau.
Copie profonde et complète
Il faut itérer sur les différents niveaux de profondeur de l'objet pour effectuer un clonage complet. De nombreuses librairies proposent leur propre version de clonage, comme par exemple lodash.js
const clone = require('lodash.clonedeep');
let a = {
pos: {x: 1, y:1},
nbr: 2,
print: function(){ console.log("nbr " + this.nbr); }
}
let b = clone(a);
b.nbr = 3;
b.pos.x = 9;
console.log(a.nbr); // 2
console.log(a.pos.x); // 1
b.print(); // "nbr 3"
Affectation par décomposition
L'affectation des différentes valeurs d'un tableau ou des propriétés d'un objet à des nouvelles variables se réalise grâce à la syntaxe suivante :
let {prenom, nom} = { nom: "Gendry", prenom: "Lucie", age: 22};
console.log(prenom); //"Lucie";
let dossards = ["#5687", "#4857", "#3879", "#2198", "#7832"];
let [premier, second, ...peloton] = dossards;
console.log("Gagnants: " + premier + " et " + second); // Gagnants: #5687 et #4857
console.log("et " + peloton.length + " finishers"); // et 3 finishers
Utilisation: fonction retournant plusieurs valeurs
function limits(val, ecart){
return [val - ecart, val + ecart];
}
let [min, max] = limits(15, 5);
console.log("Limites: " + min + " et " + max); //Limites: 10 et 20
La différence entre let {var1, var2}
et let [var3, var4]
c'est que var1
correspont à la propriété "var1" de l'objet décomposé tandis que var3
correspond au premier élément du tableau décomposé.
Arguments par défaut d'une fonction
Afin de simplifier les appels de fonctions, l'ensemble des arguments nécessaires sont regroupés au sein d'un seul objet. l'affectation par décomposition peut alors être exploitée pour définir des valeurs par défaut aux différentes propriétés :
function montantTVAC({montant = 100, tva = 0.21, reduction = 0.01} = {}){
return montant * (1 + tva - reduction);
}
console.log(montantTVAC({montant: 200, reduction: 0})); // 242
Invocation indirecte de méthode
Une fonction peut être interprétée comme une méthode pour un objet passé en argument; c'est une invocation indirecte de méthode.
Trois méthodes sont possibles :
- call: les arguments de la fonction sont passés en liste d'arguments,
- apply: les arguments de la fonction sont passés comme valeurs d'un tableau.
- bind: création d'une copie de la fonction pour laquelle this est un objet donné.
let paramsParticuliers = {
reduction: 0,
tva: 0.21
};
let paramsEntreprises = {
reduction: 0.02,
tva: 0.06
};
function montantTVAC(montant, remise){
return (montant * ( 1 + this.tva - this.reduction)) - remise;
}
console.log(montantTVAC.call(paramsParticuliers, 100, 5)); // => 116
console.log(montantTVAC.apply(paramsEntreprises, [100, 5])); // => 99
let montantTVACEntreprises = montantTVAC.bind(paramsEntreprises);
console.log(montantTVACEntreprises(100, 5)); // => 99
En fonction de l'exemple précédent, que donnerait le code ci-dessous ?
console.log(montantTVAC(100, 5));
Réponse...
"NaN" car ni this.tva ni this.reduction ne sont alors définis.
Illustrations de différentes syntaxes équivalentes:
let vals = [1,5,2];
console.log(Math.max(1,5,2));
console.log(Math.max(...vals));
console.log(Math.max.call(null, ...vals));
console.log(Math.max.apply(null, vals));
console.log(Math.max.bind(null)(1,5,2));
console.log(Math.max.bind(null)(...vals));
Le motclef "this"
Le motclef this, contrairement à d'autres langages, ne représente pas la portée lexicale de la fonction ou la fonction elle-même mais plutôt le contexte d'invocation de la fonction.
La liaison par défaut: this = objet global
function getCompte(){
let montant = 1000;
this.printLog();
}
function printLog(){
console.log("Affichage");
console.log(this.montant);
}
getCompte(); //"Affichage" undefined
Dans l'exemple ci-dessus, "this" dans getCompte() se réfère à l'objet global, le contexte d'exécution de getCompte(). C'est pareil pour "this" dans la fonction printLog(), or montant n'est pas définit dans l'objet global mais au sein de la fonction getCompte() => this.montant est non défini !
Attention: ceci n'est vrai que dans l'exécution dans un navigateur ou dans l'environnement node.js pour autant que le fichier ne soit pas interprété comme un module ! Dans PHPStorm, il est possible que this soit interprété comme l'objet module.export et non global... dans ce cas, this.printLog n'est pas défini !
Dans un script exécuté par un navigateur, l'objet global est l'objet Window.
function printLog(){
console.log(this.val);
}
var val = 10;
printLog(); //10
La liaison implicite: this = objet courant
function printLog(){
console.log("Affichage");
console.log(this.montant);
}
var montant = 2000;
let facture = {
montant: 1000,
print: printLog
}
facture.print(); // 1000
printLog(); //2000
L'appel à facture.print() provoque l'appel de printLog() avec facture comme contexte d'exécution. => this.montant = 1000. Par contre, un appel direct à printLog() s'effectue avec le contexte d'exécution par défaut.. l'objet global => this.montant = 2000.
Impact de let
function printLog(){
console.log("Affichage");
console.log(this.montant);
}
let montant = 2000; //déclaration avec let au lieu de var
let facture = {
montant: 1000,
print: printLog
}
facture.print(); // 1000
printLog(); //??
Qu'affichera en console le dernier appel à printLog() ?
Tout comme const, let ne crée pas de propriété sur l'objet global quand les variables sont déclarées au niveau global => this.montant retourne undefined.
La liaison implicite
Analysez le code ci-dessous :
let facture = {
montant: 1000,
tva: function(taux) { return this.montant * taux; }
}
console.log(facture.tva(0.21));
let getTvaC = facture.tva;
console.log(getTvaC(0.06));
Qu'est ce qui sera affiché en console ?
facture.tva(0.21) retourne 210 puisque this = objet facture. Par contre, getTvaC(0.06) retourne NaN puisque dans ce cas, this = objet global !
La liaison explicite: this = objet fourni en argument
L'utilisation de call() ou apply() permet de spécifier l'objet qui devient le contexte d'exécution de la méthode.
La liaison explicite
let facture = {
montant: 1000
}
function tva(taux) { return this.montant * taux; }
console.log(tva.call(facture, 0.21));
console.log(tva(0.06));
console.log(tva.bind(facture)(0.33));
Qu'est ce qui sera affiché en console ?
Réponse...
tva.call(facture, 0.21) retourne 210 puisque this se réfère à facture. Par contre, l'appel direct à tva(0.06) retourne NaN puisque dans ce cas, this est l'objet global!
tva.bind(facture)(0.33)) retourne 330 puisque this se réfère à facture.
La liaison par instanciation
L'utilisation du motclef new qui permet de créer un objet par copie d'un prototype (nous en reparlerons plus loin), le nouvel objet devient le contexte d'exécution pour les méthodes de l'objet.
class Facture {
constructor() {
this.montant = 1000;
}
tva(taux) {
return this.montant * taux;
}
logName(){
console.log(this.nom);
}
}
var nom = "John";
let facture = new Facture();
console.log(facture.tva(0.21)); // 210
facture.logName(); // undefined
Au sein des méthodes tva(taux) et logName(), this représente l'objet facture et non l'objet global; this.nom est donc indéfini.
this et les fonctions fléchées
Au sein d'une fonction fléchée, qui est toujours définie au sein d'une fonction parente, this représente l'environnement lexical (ensemble des variables accessibles) de la fonction parente.
Version function
reduction = 10;
(() => {
var reduction = 5;
let facturier = {
reduction: 100,
factures: [1000, 2000, 500],
print: function(tva){
this.factures.forEach(function (facture) {
console.log(((facture * tva) - this.reduction));
});
}
}
facturier.print(0.21); //Affiche: 200 410 95
})();
Au sein de la boucle "forEach", "this" représente l'objet global.
Version =>
reduction = 10;
(() => {
var reduction = 5;
let facturier = {
reduction: 100,
factures: [1000, 2000, 500],
print: function(tva){
this.factures.forEach((facture) => {
console.log(((facture * tva) - this.reduction));
});
}
}
facturier.print(0.21); //Affiche: 110 320 5
})();
Au sein de la boucle "forEach", "this" représente l'objet "facturier".
L'objet globalThis
L'objet global dépend de l'environnement d'exécution de JS. En effet, dans un navigateur, il s'agit de window. Dans l'environnement node.js, il s'agit de global; et pour un web worker, il s'agit de self.
Afin d'uniformiser l'écriture de code indépendamment de l'environnement, ES2020 a introduit l'alias globalThis qui représente l'objet global de l'environnement d'exécution.
if (globalThis === window) {
console.log('Vous êtes dans un navigateur')
} else if (globalThis === global) {
console.log('Vous êtes dans node.js')
}
Les closures
Rappel de notions importantes
Avant de parler des fermetures(closure), il est bon rappeler certaines notions :
- la portée d'une variable (scope) est l'ensemble des lignes de code où cette variable est définie,
- l'enchaînement de portées (scope chain) d'une ligne de code est l'ensemble des variables dont la portée comprend cette ligne de code;
- En dehors de tout bloc, l'enchaînement de portées d'une ligne de code est l'objet global,
- Au sein d'une fonction non imbriquée, l'enchaînement de portées est l'objet global augmenté des propriétés de l'objet fonction (paramètres et variables locales),
- Au sein d'une fonction imbriquée, l'enchaînement de portées est l'objet global augmenté des propriétés de l'objet fonction "conteneur" et de l'objet fonction imbriquée.
En langage informatique, une fermeture est la fonction (≈ corps de la fonction) et son enchaînement de portées.
En JavaScript, une fonction est un objet qui contient le corps de la fonction et son enchaînement de portées, toute fonction est donc une fermeture.
Si vous exécutez le code ci-dessous dans votre navigateur et...
let tva = function(montant){
let tva = 0.21;
return montant * tva;
}
console.log(tva);
debugger;
... si vous regardez la valeur de la variable "tva" :
Enchaînements de portées à la définition vs à l'invocation
En général, l'enchaînement de portées à la définition est égale à l'enchaînement de portées à l'invocation.
Mais en JavaScript, l'exécution d'une fonction se fait toujours en tenant compte de l'enchaînement de portées à la définition !!!
Différencier l'enchaînement de portées à la définition vs à l'invocation
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() { return scope; }
return f();
}
checkscope(); // => ??
Que vaut cette dernière ligne ?
Réponse...
L'appel de "checkscope()" provoque l'appel de la fonction f() qui retourne la valeur de la variable scope redéfinie localement. Donc la valeur de la dernière ligne est "local scope".
Normalement, rien de surprenant pour vous si vous comprenez les portées des variables en JavaScript.
À présent modifions légèrement le code comme suit pour que la fonction "checkscope()" retourne une fonction plutôt que l'appel de cette fonction:
var scope = "global scope";
function checkscope() {
var scope = "local scope";
var f = function () { return scope; }
return f;
}
checkscope()(); //=> ??
Que vaut à présent cette dernière ligne ?
Réponse...
L'appel de "checkscope()" retourne la fonction "f", et donc, l'appel à "checkscope()()" est comme remplacé par "f()". A ce moment, l'enchainement de portées à l'invocation de "f" comprend la variable "scope" globale ! Certes, mais l'enchainement de portées à la définition de "f" (quand l'interpréteur a découvert la définition de la fonction pour la première fois) quant à elle comprend la variable "scope" locale... Donc la valeur de la dernière ligne est toujours "local scope".
Un dernier exemple...
var options = { start: 10 };
var uniqueInteger = (
function() {
var counter = options.start;
return function() { return counter++; };
}(options)
);
console.log(uniqueInteger()); // => 10
console.log(uniqueInteger()); // => 11
options.start = 20;
console.log(uniqueInteger()); //=> ??
console.log(uniqueInteger()); //=> ??
Qu'affichent ces deux dernières lignes dans la console ?
Réponse...
Au premier appel d'"uniqueInteger()", l'interpréteur découvre la définition de la fonction et lui ajoute, dans son enchainement de portées, la variable "counter" qui vaut la copie de la valeur de "options.start", soit 10 puis l'incrémente. Au second appel, la valeur de "counter" est affichée, soit 11 puis incrémentée.
Les appels successifs à "uniqueInteger()" ne peuvent pas être impactés par une modification de "options.start" puisque cette variable ne fait pas partie de l'enchainement de portées à la définition de la fonction. "counter" avait été initialisé avec la valeur d'"options.start" et non sa référence.
Les deux dernières lignes valent donc respectivement 12 et 13.
Les collections
Les itérateurs et les itérables
Objets itérables
Sans rentrer dans les détails, un objet est itérable s'il définit un comportement lors d'une itération ("parcours des valeurs").
En JavaScript, les objets suivants sont, par défaut, itérables:
- les tableaux (Array),
- les chaines de caractères (String),
- les ensembles (Set),
- les dictionnaires (Map),
Objets itérateurs
Un objet itérateur est un objet qui implémente la méthode next() qui permet de parcourir les éléments d'un objet itérable; cet objet peut être lui-même. Un itérateur peut être itérable.
La méthode next() doit retourner un objet {done: booléen, value: objet/primitif}:
- done: True si plus aucun élément restant à parcourir dans l'itérable,
- value: n'importe quelle valeur retournée par l'itérateur, facultative si done === True
Les fonctions génératrices sont des exemples d'objets itérateurs:
function* voyelles(){
yield* ['a','e','i','o','u','y'];
}
let voyelle = voyelles();
console.log(voyelle.next()); //{value: 'a', done: false}
console.log(voyelle.next()); //{value: 'e', done: false}
console.log(voyelle.next()); //{value: 'i', done: false}
console.log(voyelle.next()); //{value: 'o', done: false}
console.log(voyelle.next()); //{value: 'u', done: false}
console.log(voyelle.next()); //{value: 'y', done: false}
console.log(voyelle.next()); //{value: undefined, done: true}
Convertir un itérable en tableau
La méthode Array.from(itérable) crée un nouveau tableau contenant les valeurs de l'itérable.
console.log(Array.from('aeiouy')); //Array ["a","e","i","o","u","y"]
Les dictionnaires
Les dictionnaires (objets Map ) conservent des couples "clé"-"valeur". Chaque "clé" est unique pour un même dictionnaire et peut être de n'importe quel type.
Propriétés et méthodes principales d'un dictionnaire
- size: nombre de couples clé/valeur présents dans le dictionnaire,
- clear(): suppression de tous les couples clé/valeur,
- get(clé): retourne la valeur associée à la clé,
undefined
si clé non trouvée, - has(clé): true si clé présente, false sinon,
- set(clé, valeur): insère un couple clé/valeur,
- delete(clé): supprime un couple clé/valeur,
- keys(): retourne un objet itérateur et itérable qui contient les clés,
- values(): retourne un objet itérateur et itérable qui contient les valeurs
- entries(): retourne un objet itérateur et itérable qui contient des tableaux [clé, valeur]
Démonstration d'utilisation d'un dictionnaire
let eleves = new Map();
eleves.set('s203468', {nom: 'DUPONT', prenom: 'Jeanne'});
eleves.set('s206987', {nom: 'ALTAIR', prenom: 'Luc'});
eleves.set('s204385', {nom: 'PIONG', prenom: 'Chang'});
console.log(`Matricules: ${Array.from(eleves.keys()).join(', ')}`);
if (eleves.has('s206987')){
console.log(`Elève ${eleves.get('s206987').nom} trouvé, puis supprimé`);
eleves.delete('s206987');
}
eleves.set('s203468', {nom: 'POPOULOS', prenom: 'Dimitrius'});
eleves.forEach(e => console.log(`${e.prenom[0]}. ${e.nom}\n`));
eleves.clear();
if(eleves.size === 0){
console.log('Tous les élèves sont supprimés');
}
Sans exécuter le code ci-dessus, qu'est-ce qui sera affiché en console ?
Réponse...
Matricules: s203468, s206987, s204385
Elève ALTAIR trouvé, puis supprimé
D. POPOULOS
C. PIONG
Tous les élèves sont supprimés
Les ensembles
Les ensembles (objets Set) conservent des valeurs. Chaque valeur est unique pour un même ensemble et peut être de n'importe quel type.
Propriétés et méthodes principales d'un ensemble
- size: nombre de valeurs présentes dans l'ensemble,
- clear(): suppression de toutes les valeurs,
- has(valeur): true si valeur présente, false sinon,
- add(valeur): ajoute une valeur,
- delete(valeur): supprime une valeur,
- values(): retourne un objet itérateur et itérable qui contient les valeurs,
- keys(): alias de values(),
- entries(): retourne un objet itérateur et itérable qui contient des tableaux [valeur, valeur]
Démonstration d'utilisation d'un ensemble
let eleves = new Set();
eleves.add({matricule: 's203468', nom: 'DUPONT', prenom: 'Jeanne'});
eleves.add({matricule: 's206987', nom: 'ALTAIR', prenom: 'Luc'});
eleves.add({matricule: 's204385', nom: 'PIONG', prenom: 'Chang'});
console.log(`Matricules: ${Array.from(eleves.values()).map(e => e.matricule).join(', ')}`);
if (eleves.has({matricule: 's206987', nom: 'ALTAIR', prenom: 'Luc'})){
console.log(`Elève ALTAIR trouvé, puis supprimé`);
eleves.delete({matricule: 's206987', nom: 'ALTAIR', prenom: 'Luc'});
}
eleves.add({matricule: 's203468', nom: 'POPOULOS', prenom: 'Dimitrius'});
eleves.add({matricule: 's203468', nom: 'POPOULOS', prenom: 'Dimitrius'});
eleves.forEach(e => console.log(`(${e.matricule}) ${e.prenom[0]}. ${e.nom}\n`));
eleves.clear();
if(eleves.size === 0){
console.log('Tous les élèves sont supprimés');
}
Sans exécuter le code ci-dessus, qu'est-ce qui sera affiché en console ?
Réponse...
Matricules: s203468, s206987, s204385
(s203468) J. DUPONT
(s206987) L. ALTAIR
(s204385) C. PIONG
(s203468) D. POPOULOS
(s203468) D. POPOULOS
Tous les élèves sont supprimés
Attention, tout comme d'en d'autres langages, il ne faut pas confondre référence d'un objet et les données qu'il contient lui-même. Testez et comparez à présent avec le code ci-dessous:
let eleves = new Set();
let altair = {matricule: 's206987', nom: 'ALTAIR', prenom: 'Luc'};
eleves.add({matricule: 's203468', nom: 'DUPONT', prenom: 'Jeanne'});
eleves.add(altair);
eleves.add({matricule: 's204385', nom: 'PIONG', prenom: 'Chang'});
console.log(`Matricules: ${Array.from(eleves.values()).map(e => e.matricule).join(', ')}`);
if (eleves.has(altair)){
console.log(`Elève ALTAIR trouvé, puis supprimé`);
eleves.delete(altair);
}
let popoulos = {matricule: 's203468', nom: 'POPOULOS', prenom: 'Dimitrius'};
eleves.add(popoulos);
eleves.add(popoulos);
eleves.forEach(e => console.log(`(${e.matricule}) ${e.prenom[0]}. ${e.nom}\n`));
eleves.clear();
if(eleves.size === 0){
console.log('Tous les élèves sont supprimés');
}
Exercices
Exercices
Exercice js.arr1
Créez la fonction multiplesOccurences() qui reçoit un tableau d'entiers et retourne un tableau qui contient les valeurs présentes plus d'une fois.
S'il n'y en a pas, la fonction retourne un tableau vide.
let tab = [5,2,36,9,45,5,7,6,12,9,26];
multiplesOccurences(tab); // [5,9]
let tab2 = [5,2];
multiplesOccurences(tab2); // []
Exercice js.arr2
Créez la fonction differencesTables() qui reçoit deux tableaux de caractères et retourne un tableau avec les caractères qui ne sont pas présents en même temps dans les deux tableaux.
let a = ["a","b","c","d","e","f"];
let b = ["a","b","j","j"];
let c = ["b","a"];
let d = [];
console.log(differencesTables(a,b)); //[ 'c', 'd', 'e', 'f', 'j', 'j' ]
console.log(differencesTables(a,c)); //[ 'c', 'd', 'e', 'f' ]
console.log(differencesTables(a,d)); //[ 'a', 'b', 'c', 'd', 'e', 'f' ]
console.log(differencesTables(a,a)); //[]
Exercice js.arr3
Lors d'une manifestation de scouts d'une même troupe, vous recevez les participations des enfants aux activités d'orientation et du jeu de pistes sous forme de tableaux d'objets :
let course = [
{totem: "Raton", nom: "PAHAYT", prenom: "Luc"},
{totem: "Epagneul", nom: "MARTIJN", prenom: "Sven"},
{totem: "Colibri", nom: "LING", prenom: "Riu"},
{totem: "Ours", nom: "NGOMBE", prenom: "Dieumerci"},
{totem: "Faucon", nom: "MALTESE", prenom: "Kurt"},
{totem: "Tarentule", nom: "ARNEM", prenom: "Marthe"}
];
let pistes = [
{totem: "Impala", nom: "KALHDOUN", prenom: "Mohammed"},
{totem: "Raton", nom: "PAHAYT", prenom: "Luc"},
{totem: "Bison", nom: "VANDENHOUT", prenom: "Lauren"},
{totem: "Epagneul", nom: "MARTIJN", prenom: "Sven"},
{totem: "Kiwi", nom: "BENASSOUR", prenom: "Sarah"},
{totem: "Faucon", nom: "MALTESE", prenom: "Kurt"},
];
En vous inspirant de l'exercice précédent, obtenez le listing des scouts qui n'ont pas participé aux deux activités. Triez ce listing selon les totems (Vous pouvez considérer que le totem identifie l'enfant).
console.log(differencesTablesActivites(course, pistes));
/* résultat affiché en console:
[
{ totem: 'Bison', nom: 'VANDENHOUT', prenom: 'Lauren' },
{ totem: 'Colibri', nom: 'LING', prenom: 'Riu' },
{ totem: 'Impala', nom: 'KALHDOUN', prenom: 'Mohammed' },
{ totem: 'Kiwi', nom: 'BENASSOUR', prenom: 'Sarah' },
{ totem: 'Ours', nom: 'NGOMBE', prenom: 'Dieumerci' },
{ totem: 'Tarentule', nom: 'ARNEM', prenom: 'Marthe' }
]
*/
Exercice js.arr4
Un enseignant doit déterminer les cotes de fin d'année de ses étudiants:
Nom | Prénom | Interro /20 | QCM /10 | Projet /20 | Examen /50 |
---|---|---|---|---|---|
Bakaire | Joséphina | 15 | 8 | 13.9 | 38.2 |
Dhour | Youssunn | 16 | 4.6 | 14.7 | 31.9 |
Dupont | Marcel | 8.5 | 7 | 12.6 | 26.8 |
Mei | Linn | 6 | 7.5 | 13 | 22.6 |
Potte | Henri | PP | 2.6 | 8.2 | 16.6 |
Stockez les données dans un tableau d'objets et créez les fonctions nécessaires pour déterminer les cotes globales de chaque étudiant, la moyenne de la classe ainsi que la cote la plus haute et la plus basse parmi les étudiants qui ont passé toutes les épreuves.
La moyenne considère que les étudiants qui n'ont pas tout passé, ont une cote nulle.
let resultats = [
{nom: "Bakaire", prenom: "Joséphina", interro: 15, qcm: 8, projet: 13.9, examen: 38.2},
{nom: "Dhour", prenom: "Youssunn", interro: 16, qcm: 4.6, projet: 14.7, examen: 31.9},
{nom: "Dupont", prenom: "Marcel", interro: 8.5, qcm: 7, projet: 12.6, examen: 26.8},
{nom: "Mei", prenom: "Linn", interro: 6, qcm: 7.5, projet: 13, examen: 22.6},
{nom: "Potte", prenom: "Henri", interro: "PP", qcm: 2.6, projet: 8.2, examen: 16.6}
];
Exercice js.arr5
Calculer la moyenne des notes arrondies qui sont >= à 10. (c-à-d arrondir, garder supérieur ou égal à 10, calculer la moyenne)
Exemple: pour [10.7, 9.4, 8.7, 12.3, 9.8, 6.5, 13.7] vous devez obtenir le résultat de 11,75.
Exercice js.arr6
En fin d'une soirée "team building" bien arrosée, suite à un pari perdu, vous vous êtes engagé à rembourser vos collègues du montant de l'entrée du concert, soit 25€. Vous avez utilisé une application sur votre smartphone pour que vos collègues vous communiquent leur numéro de compte bancaire. Voici ce que vous avez obtenu:
[
{titulaire: 'Lucie LIU', compte: '972-487-086'},
{titulaire: 'Marc KROSS', compte: '12-34567812-3456 70'},
{titulaire: 'Désiré NGOMBE', compte: '4084-9027-8919-7157'},
{titulaire: 'Lucie LIU', compte: '972-487-068'},
{titulaire: 'Marc KROSS', compte: '12-34567812-3456 70'},
{titulaire: 'Djamila HASSAN', compte: '4084-9027-8919-7175'},
{titulaire: 'John FLY', compte: '499 273-987 16'},
{titulaire: 'Leif HARALDSON', compte: '499 273-987 17'},
{titulaire: 'Désiré NGOMBE', compte: '4084-9027-8919-7157'},
{titulaire: 'Natalia PETROCHKAIA', compte: '12-34567812-3456 78'},
{titulaire: 'Marc KROSS', compte: '12-34567812-3456 70'}
]
Avant d'effectuer vos versements, vous souhaitez pouvoir filtrer cette liste pour retirer les doublons ainsi que les numéros de compte incorrects (La vérification des numéros de compte utilise l'algorithme de Luhn. Essayez de programmer cet algorithme par vous-même au lieu de copier une des solutions présentes sur http://rosettacode.org.).
Voici ce que vous devriez obtenir:
Le numéro de compte 972-487-086 de Lucie LIU est valide
Le numéro de compte 12-34567812-3456 70 de Marc KROSS est valide
Le numéro de compte 4084-9027-8919-7157 de Désiré NGOMBE est valide
Le numéro de compte 499 273-987 16 de John FLY est valide