JS: Les tableaux et objets

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 :


    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:

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 :

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 :

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" :

un objet fonction en JS
La fonction tva affichée en console
un objet fonction en JS
La fonction tva affichée dans le debugger

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:

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}:

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

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

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:

NomPrénomInterro /20QCM /10Projet /20Examen /50
BakaireJoséphina15813.938.2
DhourYoussunn164.614.731.9
DupontMarcel8.5712.626.8
MeiLinn67.51322.6
PotteHenriPP2.68.216.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}
];
Analyse des résultats
Le résultat attendu affiché en console

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