Crédits de l’image: Ahmad Awais ⚡️
Ce document est un condensé de tout ce que vous devriez savoir sur le JavaScript que vous allez rencontrer dans des projets modernes.
Ce guide n’a pas été conçu pour vous apprendre JavaScript à partir de rien, mais pour aider les développeurs ayant des connaissances basiques qui ont des problèmes avec les codebases modernes (ou pour apprendre React, par exemple) à cause des concepts JavaScript utilisés.
De plus, je vous donnerai parfois des conseils personnels qui pourraient porter à débat, mais je prendrai le soin de le mentionner lorsque je le ferai.
Note : La plupart des concepts introduits ici viennent d’une mise à jour du langage JavaScript (ES2015, souvent appelé ES6. Vous pouvez trouver les nouvelles fonctionnalités. ajoutées par cette mise à jour ici (ce site est vraiment bien fait !).
Lorsque vous avez de la peine à comprendre une notion, je vous suggère de chercher des réponses dans les ressources suivantes :
- MDN (Mozilla Developer Network)
- You don't know JS (livre)
- ES6 Features with examples
- WesBos blog (ES6)
- Javascript Basics for Beginners - a free Udacity course
- Reddit (JavaScript)
- Google pour trouver des blogs et des ressources spécifiques
- StackOverflow
Note : Presque toutes les ressources externes listées dans ce document sont en anglais, à l’exception des liens menant vers le Mozilla Developer Network (MDN) qui lui est traduit en français.
- Modern JavaScript Cheatsheet
- Introduction
- Table des matières
- Notions
- Déclaration des variables : var, const, let
- Fonction fléchée
- Valeur par défaut d'un paramètre dans une fonction
- Déstructurer des objets et des tableaux
- Méthodes de tableau : map / filter / reduce
- Opérateur de décomposition "..."
- Raccourci pour les propriétés d'objet
- Promesses
- Littéraux de modèle
- Littéraux de modèle étiquetés
- Imports / exports
- this en JavaScript
- Classe
- Mots clés extends et
super
- Async Await
- Vérité / fausseté
- Méthodes statiques
- Glossaire
En JavaScript, il y a trois mots-clés disponibles pour déclarer une variable, et chacun a ses différences. Ces mots-clés sont var
, let
et const
.
Les variables déclarées avec le mot-clé const
ne peuvent pas être réassignées, alors que celles déclarées avec let
et var
le peuvent.
Je vous recommande de toujours déclarer vos variables avec const
par défaut, et avec let
si vous avez besoin de la muter ou de la réassigner plus tard.
Portée | Réassignable | Mutable | Temporal Dead Zone | |
---|---|---|---|---|
const | Bloc | Non | Oui | Oui |
let | Bloc | Oui | Oui | Oui |
var | Fonction | Oui | Oui | Non |
const person = "Nick";
person = "John" // Déclenchera une erreur, person ne pouvant pas être réassigné
let person = "Nick";
person = "John";
console.log(person) // "John", le réassignement est permis avec let
La portée d’une variable définit "où la variable est disponible dans le code".
La portée des variables déclarées avec var
s’étend à la fonction qui la contient, ce qui signifie que lorsqu‘une variable est créée dans une fonction, tout dans cette fonction peut y accéder. De plus, on ne pourra pas accéder à cette variable de l’extérieur de cette fonction.
function myFunction() {
var myVar = "Nick";
console.log(myVar); // "Nick" - myVar est accessible à l'intérieur de la fonction
}
console.log(myVar); // Émet une ReferenceError, myVar n'est pas accessible en dehors de la fonction
Voici un exemple plus subtil, toujours sur la portée des variables :
function myFunction() {
var myVar = "Nick";
if (true) {
var myVar = "John";
console.log(myVar); // "John"
// La portée de myVar s'étendant à toute la fonction, nous venons d'écraser la valeur précédente de myVar ("Nick"), qui est devenue "John"
}
console.log(myVar); // "John" - les instructions dans le bloc if ont affecté cette valeur
}
console.log(myVar); // Produit une ReferenceError, myVar n'étant pas disponible en dehors de la fonction
De plus, les variables déclarées avec var sont déplacées tout en haut de leur portée à l'exécution. Ce comportement est appelé var hoisting.
Ce bout de code :
console.log(myVar) // undefined -- aucune erreur produite
var myVar = 2;
est compris à l'exécution comme :
var myVar;
console.log(myVar) // undefined -- aucune erreur produite
myVar = 2;
var
et let
sont presque identiques, mais les variables déclarées avec let
- ont leur portée qui est limitée au bloc qui les entoure
- ne sont pas accessibles avant d'être assignées
- ne peuvent pas être redéclarées dans la même portée
Observons l'impact de la nouvelle portée dans l'exemple précédent :
function myFunction() {
let myVar = "Nick";
if (true) {
let myVar = "John";
console.log(myVar); // "John"
// la portée de myVar étant limitée au bloc if, nous venons de crééer une nouvelle variable myVar.
// cette variable n'est pas accessible en dehors du bloc if
// et est totalement indépendante du premier myVar créé.
}
console.log(myVar); // "Nick" - les instructions dans le bloc if n'ont PAS affecté cette valeur
}
console.log(myVar); // Produit une ReferenceError, myVar n'étant pas disponible en dehors de la fonction
Maintenant, observons ce que signifie pour les variables créées avec let (et const) de ne pas être accessibles avant d'avoir été assignées :
console.log(myVar) // déclenche une ReferenceError !
let myVar = 2;
Contrairement aux variables var, si vous essayez de lire ou d'écrire dans une variable let ou const avant d'être assignées une erreur se produira. Ce phénomène est souvent appelé Temporal dead zone ou TDZ.
Note : Techniquement, les déclarations de variables let and constsont aussi affectées par le var hoisting, mais pas leur assignation. Vu qu'elles sont faite pour ne pas pouvoir être utilisées avant l'assignation, on a l'impression qu'il n'y a pas de hoisting, mais il y en a un. Si vous voulez en savoir plus, vous pouvez lire cette explication très détaillée.
Par ailleurs, vous ne pouvez pas re-déclarer une variable let :
let myVar = 2;
let myVar = 3; // Produit une SyntaxError
Les variables déclarées avecconst
se comportent comme des variables let, mais elles ne peuvent pas être réassignées.
Pour résumer, les variables const :
- ont leur portée qui est limitée au bloc qui les entoure
- ne sont pas accessibles avant leur assignation
- ne peuvent pas être redéclarées dans la même portée
- ne peuvent pas être réassignées
const myVar = "Nick";
myVar = "John" // cause une erreur, le réassignement n'est pas permis
const myVar = "Nick";
const myVar = "John" // cause une erreur, la redéclaration n'est pas permise
Mais il y a une subtilité : les variables const
ne sont pas immutables ! Concrètement, cela signifie que les objets et tableaux const
peuvent être mutées.
Pour les objets :
const person = {
name: 'Nick'
};
person.name = 'John' // ceci fonctionnera ! la variable person n'est pas complètement réassignée, mais mutée
console.log(person.name) // "John"
person = "Sandra" // cause une erreur, car le réassignement n'est pas autorisé avec les variables déclarées avec const
Pour les tableaux :
const person = [];
person.push('John'); // ceci fonctionnera ! la variable person n'est pas complètement réassignée, mais mutée
console.log(person[0]) // "John"
person = ["Nick"] // cause une erreur, car le réassignement n'est pas autorisé avec les variables déclarées avec const
Avec la mise à jour ES6 de JavaScript, nous pouvons utiliser des fonctions fléchées, qui sont un autre moyen de déclarer et d'utiliser des fonctions. Voici les bénéfices qu'elles apportent :
- Plus concis
- this se réfère à l'extérieur de la fonction fléchée
return
implicite
- Concision et retour implicite
function double(x) { return x * 2; } // Manière traditionnelle
console.log(double(2)) // 4
const double = x => x * 2; // Même fonction écrite sous forme de fonction fléchée avec un retour implicite
console.log(double(2)) // 4
- Référence de this
Dans une fonction fléchée, this est égal à la valeur de this dans le contexte d'exécution alentour. Basiquement, avec les fonctions fléchées, vous n'êtes plus obligé d'utiliser l'astuce "that = this" avant d'appeler une fonction à l'intérieur d'une fonction.
function myFunc() {
this.myVar = 0;
setTimeout(() => {
this.myVar++;
console.log(this.myVar) // 1
}, 0);
}
Les fonctions fléchées sont plus concises que les fonctions traditionnelles par plusieurs aspects. Voyons tous les cas possibles :
- Retour implicite VS explicite
Un retour explicite est une fonction où le mot-clé return
est utilisé dans son corps.
function double(x) {
return x * 2; // cette fonction retourne explicitement x * 2, le mot-clé return est utilisé
}
Dans l'écriture de foncitons traditionnelle, le retour était toujours explicite. Mais avec les fonctions fléchées, vous pouvez faire un retour implicite ce qui signifie que vous n'êtes pas obligés d'utiliser le mot-clé return pour retourner une valeur.
Pour faire un retour implicite, le code doit être écrit en une ligne.
const double = (x) => {
return x * 2; // Ce retour est explicite
}
Comme il n'y a qu'un retour de valeur ici, nous pouvons utiliser un retour implicite.
const double = (x) => x * 2;
Pour ce faire, il nous suffit de supprimer les accolades et le mot-clé return. C'est pour cette raison qu'on qualifie ce retour d'implicite : le mot-clé return n'est pas là. mais la fonction va bien retourner x * 2
.
Note : Si votre fonction ne retourne aucune valeur (avec des effets secondaires), elle ne fait ni retour implicite ni explicite.
De plus, si vous voulez retourner un objet vous devez mettre des parenthèses autour pour éviter un conflit avec les accolades de bloc :
const getPerson = () => ({ name: "Nick", age: 24 })
console.log(getPerson()) // { name: "Nick", age: 24 } -- l'objet est implicitement retourné par la fonction fléchée
- Uniquement un argument
Si votre fonction ne prend qu'un paramètre, vous pouvez omettre les parenthèses autour. En reprenant un exemple au dessus :
const double = (x) => x * 2; // cette fonction fléchée ne prend qu'un paramètre
Les parenthèses autour du paramètre peuvent être évitées :
const double = x => x * 2; // cette fonction fléchée ne prend qu'un paramètre
- Pas de paramètres
Lorsqu'une fonction fléchée ne prend aucun paramètre, vous devez obligatoirement mettre des parenthèses pour que la syntaxe soit valide :
() => { // avec des parenthèses, tout fonctionne
const x = 2;
return x;
}
=> { // pas de parenthèses, ceci ne fonctionnera pas !
const x = 2;
return x;
}
Pour comprendre cette subtilité des fonctions fléchées, vous devez savoir comment this fonctionne en JavaScript.
Dans une fonction fléchée, this est égal à la valeur de this dans le contexte d'exécution alentour. Cela signifie qu'une fonction fléchée ne créée pas de nouveau this, mais le prend autour à la place.
Sans une fonction fléchée, pour accéder à une variable présente dans this dans une fonction à l'intérieur d'une fonction, vous deviez utiliser l'astuce de that = this ou self = this.
Par exemple, voici comment utiliser setTimeout à l'intérieur de myFunc :
function myFunc() {
this.myVar = 0;
var that = this; // astuce that = this
setTimeout(
function() { // Un nouveau *this* est créé dans la portée de cette fonction
that.myVar++;
console.log(that.myVar) // 1
console.log(this.myVar) // undefined -- voir la déclaration de fonction au-dessus
},
0
);
}
Mais avec une fonction fléchée, this est pris de l'extérieur :
function myFunc() {
this.myVar = 0;
setTimeout(
() => { // this pris de l'extérieur, signifiant myFunc ici
this.myVar++;
console.log(this.myVar) // 1
},
0
);
}
À partir de la mise à jour de JavaScript ES2015, vous pouvez assigner une valeur par défaut à un paramètre de fonction en utilisant la syntaxe suivante
function myFunc(x = 10) {
return x;
}
console.log(myFunc()) // 10 -- aucune valeur n'étant donnée, la valeur par défaut est utilisée
console.log(myFunc(5)) // 5 -- une valeur est donnée donc x est égal à 5 dans myFunc
console.log(myFunc(undefined)) // 10 -- la valeur undefined est donnée, donc x est assigné à la valeur par défaut
console.log(myFunc(null)) // null -- une valeur (null) étant donnée, voir plus bas pour plus de détails
Le paramètre par défaut est utilisé dans deux et uniquement deux situations :
- Aucun paramètre n'est fourni
- Le paramèter undefined est fourni
Autrement dit, si vous passez null le paramètre par défaut ne sera pas appliqué.
Note : Un assignement de valeur par défaut peut être utilisé avec des paramètres déstructurés également (voyez la notion suivante pour avoir un exemple).
La déstructuration un moyen pratique de créer de nouvelles variables en extrayant des valeurs des données enregistrées dans des objets ou des tableaux.
Pour nommer quelque cas d'utilisation, la déstructuration peut être utilisée pour déstructurer des paramètres de fonction ou this.props dans des projets React par exemple.
- Objet
Prenons l'objet suivant pour tous les exemples
const person = {
firstName: "Nick",
lastName: "Anderson",
age: 35,
sex: "M"
}
Sans déstructuration :
const first = person.firstName;
const age = person.age;
const city = person.city || "Paris";
Avec la déstructuration, tout en une ligne :
const { firstName: first, age, city = "Paris" } = person; // Et voilà !
console.log(age) // 35 -- Une nouvelle variable age est créée et est égale à person.age
console.log(first) // "Nick" -- Une nouvelle variable first est créée et est égale à person.firstName
console.log(firstName) // ReferenceError -- person.firstName existe MAIS la nouvelle variable créée est nommée first
console.log(city) // "Paris" -- Une nouvelle variable city est créée et comme person.city n'est pas défini, city est égal à la valeur par défaut donnée, à savoir "Paris"
Note : Dans const { age } = person;
, les accolades après le mot-clé const ne sont pas utilisées pour déclarer un objet ou un bloc, mais font partie de la syntaxe de déstructuration.
- Paramètres de fonction
La destructuring est souvent utilisée pour déstructurer les paramètres de fonction sous la forme d'objets.
Sans déstructuration :
function joinFirstLastName(person) {
const firstName = person.firstName;
const lastName = person.lastName;
return firstName + '-' + lastName;
}
joinFirstLastName(person); // "Nick-Anderson"
En déstructurant le paramètre objet person, nous obtenons une fonction plus concise :
function joinFirstLastName({ firstName, lastName }) { // nous crééons les variables firstName et lastName en déstructurant le paramètre person
return firstName + '-' + lastName;
}
joinFirstLastName(person); // "Nick-Anderson"
La déstructuration est encore plus agréable à utiliser avec les fonctions fléchées :
const joinFirstLastName = ({ firstName, lastName }) => firstName + '-' + lastName;
joinFirstLastName(person); // "Nick-Anderson"
- Tableau
Utilisons le tableau suivant :
const myArray = ["a", "b", "c"];
Sans la destructuration :
const x = myArray[0];
const y = myArray[1];
Avec :
const [x, y] = myArray; // Et voilà !
console.log(x) // "a"
console.log(y) // "b"
Map, filter et reduce sont des méthodes de tableau qui viennent avec un paradigme de programmation appelé la programmation fonctionnelle.
Pour résumer :
- Array.prototype.map() prend un tableau, fait quelque chose sur ses éléments et retourne un tableau contenant les éléments transformés.
- Array.prototype.filter() prend un tableau, décide élément par élément s'il faut le garder ou non et retourne un tableau contenant uniquement les éléments conservés.
- Array.prototype.reduce() prend un tableau et agrège ses éléments en une seule valeur (qui est retournée).
Je recommande de les utiliser autant que possibles pour suivre les principes de la programmation fonctionelle car ils sont composables, concis et élégants.
Avec toutes ces méthodes, vous pouvez éviter l'utilisation des boucles for et forEach dans la plupart des situations. Vous pouvez avoir de la peine au début à les utiliser car ils vous obligent à apprendre une nouvelle façon de penser, mais une fois que vous avez compris comment cela fonctionne tout devient plus facile.
const numbers = [0, 1, 2, 3, 4, 5, 6];
const doubledNumbers = numbers.map(n => n * 2); // [0, 2, 4, 6, 8, 10, 12]
const evenNumbers = numbers.filter(n => n % 2 === 0); // [0, 2, 4, 6]
const sum = numbers.reduce((prev, next) => prev + next, 0); // 21
Calculer la somme des notes (grades
) des étudiants ayant eu plus que 10 en utilisant map, filter et reduce :
const students = [
{ name: "Nick", grade: 10 },
{ name: "John", grade: 15 },
{ name: "Julia", grade: 19 },
{ name: "Nathalie", grade: 9 },
];
const aboveTenSum = students
.map(student => student.grade) // nous transformons le tableau students en un tableau contenant leurs notes
.filter(grade => grade >= 10) // nous filtrons le tableau des notes pour ne conserver que celles supérieures ou égales à 10
.reduce((prev, next) => prev + next, 0); // nous additionnons toutes les notes au dessus de 10 une par une
console.log(aboveTenSum) // 44 -- 10 (Nick) + 15 (John) + 19 (Julia), Nathalie étant ignorée car sa note est inférieure à 10
Prenons le tableau de nombres pour nos exemples :
const numbers = [0, 1, 2, 3, 4, 5, 6];
const doubledNumbers = numbers.map(function(n) {
return n * 2;
});
console.log(doubledNumbers); // [0, 2, 4, 6, 8, 10, 12]
Que se passe-t-il ici ? Nous utilisons .map sur le tableau numbers, le map itère sur chaque élément du tableau et le passe à notre fonction. Le but de la fonction est de produire et de retourner une nouvelle valeur pour chaque valeur donnée pour que le map puisse la remplacer.
Extrayons cette fonction pour la rendre plus claire, juste pour cette fois-ci :
const doubleN = function(n) { return n * 2; };
const doubledNumbers = numbers.map(doubleN);
console.log(doubledNumbers); // [0, 2, 4, 6, 8, 10, 12]
numbers.map(doubleN)
produit [doubleN(0), doubleN(1), doubleN(2), doubleN(3), doubleN(4), doubleN(5), doubleN(6)]
ce qui est égal à [0, 2, 4, 6, 8, 10, 12]
.
Note : Si vous n'avez pas besoin d'un nouveau tableau et que vous voulant juste avoir une boucle ayant des effets secondaires, vous pourriez juste avoir besoin d'une boucle for / forEach à la place d'un map.
const evenNumbers = numbers.filter(function(n) {
return n % 2 === 0; // true si "n" est pair, false si "n" est impair
});
console.log(evenNumbers); // [0, 2, 4, 6]
Nous utilisons .filter sur le tableau numbers : filter itère sur chaque élément du tableau et le passe à notre fonction. Le but de la fonction est de retourner un booléen qui déterminera si la valeur actuelle sera conservée. Filter retourne ensuite le tableau avec uniquement les valeurs conservées.
Le but de la méthode reduce est de réduire tous les éléments du tableau sur lequel elle itère à une seule valeur. Vous décidez de la manière dont les éléments sont agrégés.
const sum = numbers.reduce(
function(acc, n) {
return acc + n;
},
0 // valeur de l'accumulateur à la première itération
);
console.log(sum) //21
Tout comme pour les méthodes .map et .filter, .reduce est appliqué sur un tableau et prend une fonction en premier paramètre.
Cette fois cependant, il y a des changements :
- .reduce prend deux paramètres
Le premier paramètre est une fonction qui sera appelée à chaque étape d'itéraiton.
Le second paramètre est la valeur de la variable d'accumulation (acc ici) à la première étape d'itération (lisez le point suivant pour comprendre).
- Paramèters de fonction
La fonction que vous passez comme premier paramètre de .reduce prend deux paramètres. Le premier (acc ici) est la variable d'accumulateur, tandis que le second paramètre (n) est l'élément actuel.
La variable d'accumulateur est égale à la valeur retournée par votre fonction à l'étape d'itération précédente. À la première étape de l'itération, acc est égale à la valeur que vous avez passé au second paramètre de .reduce.
acc = 0
car nous avons passé 0 comme second paramètre à reduce.
n = 0
premier élément du tableau number
La fonction retourne acc + n --> 0 + 0 --> 0
acc = 0
car c'est la valeur retournée par la fonction à l'étape d'itération précédente
n = 1
deuxième élément du tableau number
La fonction retourne acc + n --> 0 + 1 --> 1
acc = 1
car c'est la valeur retournée par la fonction à l'étape d'itération précédente
n = 2
troisième élément du tableau number
La fonction retourne acc + n --> 1 + 2 --> 3
acc = 3
car c'est la valeur retournée par la fonction à l'étape d'itération précédente
n = 3
quatrième élément du tableau number
La fonction retourne acc + n --> 3 + 3 --> 6
acc = 15
car c'est la valeur retournée par la fonction à l'étape d'itération précédente
n = 6
dernier élément du tableau number
La fonction retourne acc + n --> 15 + 6 --> 21
Comme c'est la dernière étape d'itération, .reduce retourne 21.
L'opérateur de décomposition ...
existe depuis ES2015 et est utilisé pour décomposer les éléments d'un itérable (comme un tableau) dans des emplacements qui peuvent contenir plusieurs éléments.
const arr1 = ["a", "b", "c"];
const arr2 = [...arr1, "d", "e", "f"]; // ["a", "b", "c", "d", "e", "f"]
function myFunc(x, y, ...params) {
console.log(x);
console.log(y);
console.log(params)
}
myFunc("a", "b", "c", "d", "e", "f")
// "a"
// "b"
// ["c", "d", "e", "f"]
const { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
console.log(x); // 1
console.log(y); // 2
console.log(z); // { a: 3, b: 4 }
const n = { x, y, ...z };
console.log(n); // { x: 1, y: 2, a: 3, b: 4 }
Si nous avons les deux tableaux suivants :
const arr1 = ["a", "b", "c"];
const arr2 = [arr1, "d", "e", "f"]; // [["a", "b", "c"], "d", "e", "f"]
Le premier élément d'arr2 est un tableau, car arr1 est injecté tel quel dans arr2. Cependant, nous voulons que arr2 soit un tableau de lettres. Pour ce faire, nous pouvons décomposer les éléments d'arr1 dans arr2.
Avec l'opérateur de décomposition :
const arr1 = ["a", "b", "c"];
const arr2 = [...arr1, "d", "e", "f"]; // ["a", "b", "c", "d", "e", "f"]
Dans les paramètres d'une fonction, nous pouvons utiliser l'opérateur de reste pour injecter les paramètres dans un tableau dans lequel nous pouvons faire une bocle. Il existe déjà un objet arguments lié à chaque fonction, qui est un tableau contenant tous les paramètres passés à la fonction.
function myFunc() {
for (var i = 0; i < arguments.length; i++) {
console.log(arguments[i]);
}
}
myFunc("Nick", "Anderson", 10, 12, 6);
// "Nick"
// "Anderson"
// 10
// 12
// 6
Mais imaginons que nous voulons que cette fonction créée un nouvel édudiant avec ses notes et sa note moyenne. Ne serait-il pas plus pratique d'extraire les deux premiers paramètres dans deux variables séparées, et d'avoir les notes dans un tableau sur lequel nous pourrions effectuer une boucle ?
C'est exactement ce que l'opérateur de reste nous permet de faire !
function createStudent(firstName, lastName, ...grades) {
// firstName = "Nick"
// lastName = "Anderson"
// [10, 12, 6] -- "..." prend tous les autres paramètres passés, créée une variable grades qui contient un tableau contenant les paramètres
const avgGrade = grades.reduce((acc, curr) => acc + curr, 0) / grades.length; // calculer la note moyenne à partir des notes
return {
firstName: firstName,
lastName: lastName,
grades: grades,
avgGrade: avgGrade
}
}
const student = createStudent("Nick", "Anderson", 10, 12, 6);
console.log(student);
// {
// firstName: "Nick",
// lastName: "Anderson",
// grades: [10, 12, 6],
// avgGrade: 9,33
// }
Note : la fonction createStudent est mauvaise car nous ne vérifions pas si grades.length existe ou est différent de 0. Cependant, je n'ai pas prévu ce cas pour rendre l'exemple plus facile à lire.
Pour celui-ci, je vous recommande de lire les explications précédentes concernant l'opérateur de reste sur les itérables et les paramètres de fonction auparavant.
const myObj = { x: 1, y: 2, a: 3, b: 4 };
const { x, y, ...z } = myObj; // l'objet est déstructuré ici
console.log(x); // 1
console.log(y); // 2
console.log(z); // { a: 3, b: 4 }
// z est le reste de l'objet déstructuré : c'est myObj sans les propriétés x et y qui ont été déstructurées
const n = { x, y, ...z };
console.log(n); // { x: 1, y: 2, a: 3, b: 4 }
// Ici, les propriétés de l'objet z sont étendues dans n
- TC39 - Object rest/spread
- Spread operator introduction - WesBos
- JavaScript & the spread operator
- 6 Great uses of the spread operator
Lorsque l'on assigne une variable à la propriété d'un objet, si le nom de la variable est le même que le nom de la propriété, vous pouvez faire la chose suivante :
const x = 10;
const myObj = { x };
console.log(myObj.x) // 10
D'habitude (avant ES2015), lorsque l'on déclare un litéral d'objet et que l'on veut utiliser des variables comme propriétés de l'objet, on écrit ce genre de code :
const x = 10;
const y = 20;
const myObj = {
x: x, // on assigne le contenu de la variable x à myObj.x
y: y // on assigne le contenu de la variable y à myObj.y
};
console.log(myObj.x) // 10
console.log(myObj.y) // 20
Comme vous pouvez le voir, c'est assez répétitif car les noms des propriétés de myObj sont identiques aux noms des variables que vous voulez assigner à ces propriétés.
Avec ES2015, quand le nom de la variable est identique au nom de la propriété, vous pouvez utiliser ce raccourci :
const x = 10;
const y = 20;
const myObj = {
x,
y
};
console.log(myObj.x) // 10
console.log(myObj.y) // 20
Une promesse est un objet qui peut être retourné de manière synchrone depuis une fonction asynchrone (ref).
Les promesses peuvent être utilisées pour éviter un enfer de callbacks, et elles sont rencontrées de plus en plus fréquemment dans les projets JavaScript modernes.
const fetchingPosts = new Promise((res, rej) => {
$.get("/posts")
.done(posts => res(posts))
.fail(err => rej(err));
});
fetchingPosts
.then(posts => console.log(posts))
.catch(err => console.log(err));
Quand vous faites une requête AJAX, la réponse n'est pas synchrone car vous voulez une ressource qui prend du temps à venir. Elle ne pourriat même jamais arriver si la ressource que vous avez demandée est indisponible pour une raison quelconque (404).
Pour gérer ce type de situations, ES2015 nous a offert les promesses. Les promesses peuvent avoir trois états différents :
- En attente
- Complété
- Rejeté
Imagions que nous voulions utiliser les promesses pour gérer une requête AJAX pour récupérer la ressource X.
Nous allons commencer par crééer une promesse. Nous allons utiliser la méthode $.get()
de jQuery pour faire notre requête AJAX vers X.
const xFetcherPromise = new Promise( // Créer une promesse avec le mot-clé new et la stocker dans une variable
function(resolve, reject) { // Le constructeur de Promise prend une fonction comme paramètre qui a elle-même deux paramètres, resolve et reject
$.get("X") // Démarrer la requête AJAX
.done(function(X) { // Quand la requête est terminée...
resolve(X); // ...résoudre la promesse avec la valeur X comme paramètre
})
.fail(function(error) { // Si la requête a échoué...
reject(error); // ...rejeter la promesse avec l'erreur comme paramètre
});
}
)
Comme vu dans l'exemple ci-dessus, l'objet Promise prend une fonction exécuteur qui prend les deux paramètres resolve et reject. Ces paramètres sont des fonctions qui lorsqu'elles sont appelées, vont modifier l'état de la promesse d'en attente à respectivement l'état complété et résolu.
La promesse est dans l'état en attente à sa création et sa fonction éxécuteur est appelée immédiatement. Quand une des deux fonctions resolve ou reject est appelée depuis la fonction éxécuteur, la promesse appelera les déclencheurs associés.
Pour obtenir le résultat (ou l'erreur) de la promesse, nous devons y attacher des déclencheurs en faisant la chose suivante :
xFetcherPromise
.then(function(X) {
console.log(X);
})
.catch(function(err) {
console.log(err)
})
Si la promesse réussit, resolve()
sera exécuté et la fonction passée à .then
est appelée.
Si elle échoue, reject()
sera exécuté et la fonction passée à .catch
est appelée.
**Note : ** Si la promesse a déjà été accomplie ou rejetée lorsqu'un déclencheur y est attaché, le déclencheur sera appelé, afin de ne pas avoir une course entre une opération asynchrone se complétant et ses déclencheurs s'y faisant attacher (référence: MDN).
- JavaScript Promises for dummies - Jecelyn Yeen
- JavaScript Promise API - David Walsh
- Utiliser les promesses - MDN
- What is a promise - Eric Elliott
- JavaScript Promises: an Introduction - Jake Archibald
- Documentation sur l'objet Promise - MDN
Les littéraux de modèle (template literals) sont une interpolation d'expression pour les chaînes de caractère d'une ligne ou multilignes.
Autrement dit, c'est une nouvelle syntaxe de chaîne de caractère qui est particulièrement pratique pour y utiliser des expressions JavaScript (des variables, par exemple).
const name = "Nick";
`Bonjour ${name}, l'expression suivante vaut 4 : ${2+2}`;
// Bonjour Nick, l'expression suivante vaut 4 : 4
Les étiquettes de modèle sont des fonctions qui peuvent être préfixées à un littéral de modèle. Quand une fonction est appelée ainsi, le premier paramètre est un tableau des chaînes de caractère qui apparaissent entre les variables interpolées du modèle, et les paramètres suivants sont les valeurs interpolées. Vous pouuvez utiliser l'opérateur de décomposition ...
pour les récupérer. (Références: MDN).
Note : Certaines librairies, comme styled-components, se basent entièrement sur cette fonctionnalité.
Voici un petit exemple de leur fonctionnement :
function highlight(strings, ...values) {
const interpolation = strings.reduce((prev, current) => {
return prev + current + (values.length ? "<mark>" + values.shift() + "</mark>" : "");
}, "");
return interpolation;
}
const condiment = "confiture";
const meal = "toast";
highlight`J'aime mettre de la ${condiment} sur mon ${meal}.`;
// "J'aime mettre de la <mark>confiture</mark> sur mon <mark>toast</mark>."
Un autre exemple plus intéressant :
function comma(strings, ...values) {
return strings.reduce((prev, next) => {
let value = values.shift() || [];
value = value.join(", ");
return prev + next + value;
}, "");
}
const snacks = ['pommes', 'bananes', 'cerises'];
comma`J'aime grignoter des ${snacks}.`;
// "J'aime grignoter des pommes, bananes, cerises."
Les modules ES6 sont utilisés pour accéder à des variables et à des fonctions explicitement exportées depuis les modules importés.
Je vous recommande de regardes la documentation sur MDN concernant import/export (voir les ressources externes plus bas), elles sont à la fois simples et complètes.
Les exports nommés sont utilisés pour exporter plusieurs valeurs d'un module.
Note : Vous ne pouvez exporter avec un nom que des objets de première classe qui ont un nom.
// mathConstants.js
export const pi = 3.14;
export const exp = 2.7;
export const alpha = 0.35;
// -------------
// myFile.js
import { pi, exp } from './mathConstants.js'; // Import nommé -- syntaxe ressemblant à la destructuration
console.log(pi) // 3.14
console.log(exp) // 2.7
// -------------
// mySecondFile.js
import * as constants from './mathConstants.js'; // Injecter toutes les valeurs exportées dans la variable constants
console.log(constants.pi) // 3.14
console.log(constants.exp) // 2.7
Bien que les imports nommés ressemblent à une destructuration, ils ont une syntaxe différente et ne sont pas identiques. Ils ne supportent ni les valeurs par défaut ni la destructuration profonde.
De plus, vous pouvez créer des alias mais la syntaxe est différente de celle utilisée avec la destructuration :
import { foo as bar } from 'myFile.js'; // foo est importé et injecté dans une nouvelle variable bar
Concernant l'export par défaut, il ne peut y en avoir qu'un seul par module. Un export par défaut peut être une fonction, une classe, un objet, ou quoi que ce soit d'autre. Cette valeur est considérée comme étant la valeur exportée "principale" car elle sera la plus simple à importer. Concerning the default export, there is only a single default export per module. A default export can be a function, a class, an object or anything else. This value is considered the "main" exported value since it will be the simplest to import. Référence: MDN
// coolNumber.js
const ultimateNumber = 42;
export default ultimateNumber;
// ------------
// myFile.js
import number from './coolNumber.js';
// L'export par défaut, peu importe son nom, est automatiquement injecté dans la variable number
console.log(number) // 42
Exporter une fonction :
// sum.js
export default function sum(x, y) {
return x + y;
}
// -------------
// myFile.js
import sum from './sum.js';
const result = sum(1, 2);
console.log(result) // 3
- ES6 Modules in bulletpoints
- Export - MDN
- Import - MDN
- Understanding ES6 Modules
- Destructuring special case - import statements
- Misunderstanding ES6 Modules - Kent C. Dodds
- Modules in JavaScript
L'opérateur this se comporte différemment que dans d'autres langages et est dans la plupart des cas déterminé par la manière dont une fonction est appelée. (Ref: MDN).
Cette notion a beaucoup de subtilités et comme elle est assez complexe, je vous recommande donc fortement de lire avec attention les ressources externes ci-dessous. Par conséquent, je vais utiliser l’idée personnelle que j’ai en tête pour déterminer ce qu’est this. J’ai appris cette astuce de cet article écrit par Yehuda Katz.
function myFunc() {
...
}
// Après chaque ligne, vous trouverez la valeur de *this* dans myFunc
myFunc.call("myString", "hello") // "myString" -- la valeur du premier paramètre de .call est injectée dans *this*
// En mode non-strict
myFunc("hello") // window -- myFunc() est un sucre syntaxique pour myFunc.call(window, "hello")
// En mode strict
myFunc("hello") // undefined -- myFunc() est un sucre syntaxique pour myFunc.call(undefined, "hello")
var person = {
myFunc: function() { ... }
}
person.myFunc.call(person, "test") // objet person -- le premier paramètre de call est injecté dans *this*
person.myFunc("test") // objet person -- person.myFunc() est un sucre syntaxique pour person.myFunc.call(person, "test")
var myBoundFunc = person.myFunc.bind("hello") // Créée une nouvelle fonction dans laquelle nous injectons “hello” comme valeur de *this*
person.myFunc("test") // objet Person -- La méthode bind n’a aucun effet sur la méthode originale
myBoundFunc("test") // "hello" -- myBoundFunc est person.myFunc avec “hello” comme valeur de *this*
JavaScript est un language basé sur les prototypes (alors que Java, par exemple, est basé sur les classes). ES6 a introduit les classes JavaScript qui sont un sucre syntaxique pour de l’héritage et non un nouveau modèle d’héritage basé sur des classes
JavaScript is a prototype-based language (whereas Java is class-based language, for instance). ES6 has introduced JavaScript classes which are meant to be a syntactic sugar for prototype-based inheritance and not a new class-based inheritance model (référence : MDN).
Le mot classe est évidemment source de confusion si vous êtes habitué aux classes dans d’autres langages. Dans ce cas, évitez de penser que les classes JavaScript fonctionnent de la même manière et considérez-les comme une notion totalement différente.
Comme ce document n’est pas fait pour vous apprendre le langage à partir de zéro, je vais partir du fait que vous savez déjà ce que sont les prototypes et comment ils se comportement. Dans le cas contraire, voyez les ressources externes listées en dessous des exemples.
Avant ES6, la syntaxe de prototype :
var Person = function(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.stringSentence = function() {
return "Hello, my name is " + this.name + " and I'm " + this.age;
}
Avec la syntaxe ES6 de classe :
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
stringSentence() {
return "Hello, my name is " + this.name + " and I'm " + this.age;
}
}
const myPerson = new Person("Manu", 23);
console.log(myPerson.age) // 23
console.log(myPerson.stringSentence()) // "Hello, my name is Manu and I'm 23
Pour comprendre les prototypes :
- Understanding Prototypes in JS - Yehuda Katz
- A plain English guide to JS prototypes - Sebastian Porto
- Héritage et chaîne de prototypes - MDN
Pour comprendre les classes
Le mot-clé extends
est utilisé dans les déclarations de classe ou les expressions de classe pour créer une classe qui est une fille d’une autre classe (référence: MDN). La sous-classe hérite de toutes les propriétés de la classe supérieure et peut aussi ajouter de nouvelles propriétés ou modifier celles héritées.
Le mot-clé super
est utilisé pour appeler des fonctions sur le parent d’un objet, y compris son constructeur.
- Le mot-clé
super
doit être utilisé avant que le mot-cléthis
soit utilisé dans le constructeur - Invoquer
super()
appelle le constructeur de la classe parente. Si vous voulez passer des arguments au constructeur de la classe parente, vous pouvez l’appeler avecsuper(arguments)
. - Si la classe parente a une méthode (même statique) appelée
X
, vous pouvez utilisersuper.X()
pour l’appeler depuis la classe fille.
class Polygon {
constructor(height, width) {
this.name = 'Polygon';
this.height = height;
this.width = width;
}
getHelloPhrase() {
return `Hi, I am a ${this.name}`;
}
}
class Square extends Polygon {
constructor(length) {
// Ici, le constructeur de la classe parente est appelé avec
// les longueurs données pour la hauteur et largeur du Polygon
super(length, length);
// Note : dans les classes dérivées, super() doit être appelé avant que vous ne
// puissiez utiliser 'this'. Ne pas respecter cette règle causera une ReferenceError.
this.name = 'Square';
this.length = length;
}
getCustomHelloPhrase() {
const polygonPhrase = super.getHelloPhrase(); // accéder à une méthode de la classe parente avec la syntaxe super.X()
return `${polygonPhrase} with a length of ${this.length}`;
}
get area() {
return this.height * this.width;
}
}
const mySquare = new Square(10);
console.log(mySquare.area) // 100
console.log(mySquare.getHelloPhrase()) // 'Hi, I am a Square' -- Square hérite de Polygon et a accès à ses méthodes
console.log(mySquare.getCustomHelloPhrase()) // 'Hi, I am a Square with a length of 10'
Note : Si nous avions essayé d’utiliser this
avant d’appeler super()
dans la classe Square, une ReferenceError aurait été déclenchée :
class Square extends Polygon {
constructor(length) {
this.height; // ReferenceError, super doit être appelé d’abord !
// Ici, le constructeur de la classe parente est appelé avec
// les longueurs données pour la hauteur et largeur du Polygon
super(length, length);
// Note : dans les classes dérivées, super() doit être appelé avant que vous ne
// puissiez utiliser 'this'. Ne pas respecter cette règle causera une ReferenceError.
this.name = 'Square';
}
}
En plus des Promises, il y a une nouvelle syntaxe que vous pourriez rencontrer pour gérer le code asynchrone nommée async / await.
Le but des fonctions async/await est de simplifier le comportement de l’utilisation des promesses de manière synchrone et d’exécuter un comportemnet sur un groupe de promesses. Tout comme les promesses sont similaires aux callbacks structurés, asnyc/await est similaire à la combinaison de générateurs et de promesses. Les fonctions asynchrones renvoient toujours une Promise. (référence: MDN)
Note : Vous devez comprendre ce que sont les promesses et comment elles fonctionnent avant d’essayer de comprendre async/await car elles dépendent dessus.
Note 2: await doit être utilisé dans une fonction async, ce qui signifie que vous ne pouvez pas utiliser await dans le premier niveau de votre code car ce n’est pas une fonction async.
async function getGithubUser(username) { // le mot-clé async autorise l’utilisation de await dans la fonction et signifie que la fonction retourne une promesse
const response = await fetch(`https://api.github.com/users/${username}`); // L’exécution est mise en pause ici jusqu’à ce que la Promise retournée par fetch soit résolue
return response.json();
}
getGithubUser('mbeaudru')
.then(user => console.log(user)) // afficher dans la console la réponse - on ne peut pas utiliser la syntaxe await car ce code n’est pas une fonction async
.catch(err => console.log(err)); // si notre fonction async lève une erreur, nous la récupérons ici
Async / Await fonctionnent avec les promises mais ils permettent d’utiliser un style de code plus impératif.
L’opérateur async marque une fonction comme étant asynchrone et retournera toujours une Promise. Vous pouvez utiliser l’opérateur await dans une fonction async pour mettre en pause l’exécution sur cette ligne jusqu’à ce que la promise retournée par l’expression se fasse résoudre ou rejeter.
async function myFunc() {
// nous pouvons utiliser l’opérateur await car cette fonction est asynchrone
return "hello world";
}
myFunc().then(msg => console.log(msg)) // "hello world" -- la valeur de retour de myFunc se fait transformer en promesse à cause de l’opérateur async
Lorsque l’expression return d’une fonction async est atteint, la Promise est remplie avec la valeur retournée. Si une erreur est levée à l’intérieur de la fonction async, la l’état de la promesse passera à rejeté. Si aucune valeur n’est retournée par une fonction async, une promesse est quand même retournée et sera résolue sans valeur lorsque l’exécution de la fonction async est terminée.
L’opérateur await est utilisé pour attendre qu’une primesse se fasse résoudre et ne peut être utilisé qu’à l’intérieur du corps d’une fonction async. Lorsqu’il est rencontré, l’exécution du code est mise en pause jusqu’à ce que la promise se fasse résoudre.
Note : fetch est une fonction qui retourne une Promise qui nous permet de faire une requête AJAX
Voyons d’abord comment nous pourrions récupérer un utilisateur sur GitHub avec des promesses.
function getGithubUser(username) {
return fetch(`https://api.github.com/users/${username}`).then(response => response.json());
}
getGithubUser('mbeaudru')
.then(user => console.log(user))
.catch(err => console.log(err));
Voici l’équivalent avec async/await
async function getGithubUser(username) { // utilisation d’une promesse + du mot-clé await autorisé
const response = await fetch(`https://api.github.com/users/${username}`); // L’exécution s’arrête ici jusqu’à ce que la promesse fetch soit résolue
return response.json();
}
getGithubUser('mbeaudru')
.then(user => console.log(user))
.catch(err => console.log(err));
La syntaxe async/await est particulièrement utile lorsque vous avez besoin d’enchaîner des promesses qui sont mutuellement dépendantes.
Par exemple, si vous avez besoin d’obtenir un jeton d’accps pour pouvoir récupérer un post de blog sur une base de données puis les informations sur l’auteur :
Note : les expressions await ont besoin d’êtres entourées de parenthèses pour appeler leur valeur résolue et leurs propriétés sur la même ligne.
async function fetchPostById(postId) {
const token = (await fetch('token_url')).json().token;
const post = (await fetch(`/posts/${postId}?token=${token}`)).json();
const author = (await fetch(`/users/${post.authorId}`)).json();
post.author = author;
return post;
}
fetchPostById('gzIrzeo64')
.then(post => console.log(post))
.catch(err => console.log(err));
À moins d’ajouter des blocs try / catch autour des expressions await, les exceptions non gérées – peu importe de si elles ont été levées dans le corps de votre fonction async ou lorsque le code est en pause pendant await – vont rejeter la promesse retournée par la fonction async. Utiliser l’expression thrown
dans une fonction asynchrone donne le même résultat que retourner une promesse qui se fait rejeter (référence : PonyFoo).
Note : Les promesses se comportent de la même manière !
Avec les promesses, voici comment on gérerait la chaîne d’erreur :
function getUser() { // Cette promesse se fera rejeter !
return new Promise((res, rej) => rej("Utilisateur non trouvé !"));
}
function getAvatarByUsername(userId) {
return getUser(userId).then(user => user.avatar);
}
function getUserAvatar(username) {
return getAvatarByUsername(username).then(avatar => ({ username, avatar }));
}
getUserAvatar('mbeaudru')
.then(res => console.log(res))
.catch(err => console.log(err)); // "Utilisateur non trouvé !"
Équivalent avec async / await:
async function getUser() { // La promesse retournée se fera rejeter
throw "Utilisateur non trouvé !";
}
async function getAvatarByUsername(userId) => {
const user = await getUser(userId);
return user.avatar;
}
async function getUserAvatar(username) {
var avatar = await getAvatarByUsername(username);
return { username, avatar };
}
getUserAvatar('mbeaudru')
.then(res => console.log(res))
.catch(err => console.log(err)); // "Utilisateur non trouvé !"
- Async/Await - JavaScript.Info
- ES7 Async/Await
- 6 Reasons Why JavaScript’s Async/Await Blows Promises Away
- JavaScript awaits
- Using Async Await in Express with Node 8
- Async Function
- Await
- Using async / await in express with node 8
En JavaScript, une valeur truthy
ou falsy
est une valeur qui se fait caster en un booléen lorsqu’elle est évaluée dans un contexte booléen. Un exemple de contexte booléen serait l’évaluation d’une condition if
.
Chaque valeur sera transformée en true
, à part si elles sont égales à :
false
0
""
(chaîne de caractères vide)null
undefined
NaN
Voici des exemples de contexte booléen :
- Évaluation de condition
if
if (myVar) {}
myVar
peut être n’importe quel objets de première classe (variable, fonction, booléen) mais il sera transformé en booléen car il est évalué dans un contexte booléen.
- Après l’opérateur logique NOT
!
Cet opérateur retourne false
si son opérande peut être converti à true
; dans le cas contraire, il retourne true
!0 // true -- 0 est falsy, donc retourne true
!!0 // false -- 0 est falsy, donc !0 retourne true, et donc !(!0) retourne false
!!"" // false -- une chaîne vide est falsy donc NOT (NOT false) est égal à false
- Avec le constructeur d’objet Boolean
new Boolean(0) // false
new Boolean(1) // true
- Dans une expression ternaire
myVar ? "truthy" : "falsy"
myVar se fait évaluer dans un contexte booléen.
Le mot-clé static
est utilisé dans les classes pour déclarer des méthodes statiques. Les méthodes statiques sont des fonctions dans une classe qui appartiennent à l’objet classe et ne sont pas disponible dans les instances de la classe.
class Repo {
static getName() {
return "Le nom du repo est modern-js-cheatsheet"
}
}
// Notez que nous n’avons pas créé d’instance de la classe Repo
console.log(Repo.getName()) // "Le nom du repo est modern-js-cheatsheet"
let r = new Repo();
console.log(r.getName()) // Uncaught TypeError: repo.getName is not a function
Les méthodes statiques peuvent être appelées à l’intérieur d’une autre méthode statique en utilisant le mot-clé this
. Ceci ne fonctionne pas avec les méthodes non statiques, qui ne peuvent pas y accéder directement en utilisant le mot-clé this
.
Pour appeler une méthode statique depuis une autre méthode statique, le mot-clé this
peut être utilisé comme ceci :
class Repo {
static getName() {
return "Le nom du repo est modern-js-cheatsheet"
}
static modifyName(){
return this.getName() + '-ajout'
}
}
console.log(Repo.modifyName()) // Le nom du repo est modern-js-cheatsheet-ajout
Les méthodes non statiques peuvent appeler des méthodes statiques de deux façons :
Pour accéder à une méthode statique depuis une méthode non statique, nous pouvons utiliser le nom de la classe et appeler la méthode statique comme une propriété, par exemple NomClasse.NomMethodeStatique()
class Repo {
static getName() {
return "Le nom du repo est modern-js-cheatsheet"
}
useName() {
return Repo.getName() + ' et il contient des trucs vraiment importants'
}
}
// Nous devons instancier la classe pour utiliser les méthodes non statiques
let r = new Repo()
console.log(r.useName()) // Le nom du repo est modern-js-cheatsheet et il contient des trucs vraiment importants
Les méthodes statiques peuvent être appelées en tant que propriétés sur l’objet constructor
class Repo {
static getName() {
return "Repo name is modern-js-cheatsheet"
}
useName() {
// Appelle la méthode statique comme une propriété du constructeur
return this.constructor.getName() + ' et il contient des trucs vraiment importants'
}
}
// Nous devons instancier la classe pour utiliser les méthodes non statiques
let r = new Repo()
console.log(r.useName()) // Le nom du repo est modern-js-cheatsheet et il contient des trucs vraiment importants
Le contexte dans lequel les valeurs et expressions sont “visibles”, ou peuvent être référencées. Si une variable ou une autre expression n’est pas “dans la portée actuelle”, alors son utilisation ne sera pas possible.
Source : MDN
On dit d’une variable qu’elle a été mutée lorsque sa valeur initiale a été changée après coup.
var myArray = [];
myArray.push("firstEl") // myArray s’est fait muter
Une variable est dite immutable si elle ne peut être mutée.
Vous pouvez lire l’article Mutable de MDN pour plus de détails.