Javascript • Système de types • Fonction
Introduction
Section intitulée « Introduction »Dans cette série d’articles, nous faisons un focus sur les fonctionnalités de base de Javascript.
Cet article concerne le système de types proposé par Javascript.
Nous allons détailler maintenant le type function.
Système de types en Javascript
Section intitulée « Système de types en Javascript »Typage dynamique
Section intitulée « Typage dynamique »Javascript possède un typage dynamique. Qu’est-ce-que cela signifie exactement ?
🔑 Nous ne déclarons pas le type d’une variable avant de l’utiliser.
🔑 Le type d’une variable est déterminé à l’exécution (et pas à la compilation).
🔑 Le type d’une variable peut changer pendant sa durée de vie.
// toto est une chaine de caractères (string)let toto = "text";
// toto est maintenant un nombre (number)toto = 69;
// toto est maintenant un booléen (boolean)toto = true;Typage faible
Section intitulée « Typage faible »Javascript est également faiblement typé. Il permet ainsi des conversions implicites de type.
const oneTwoThree = 123; // numberconst fourFive = "45"; // stringconst result = oneTwoThree + fourFive; // stringconsole.log(result); // ← "12345"Les types Objet
Section intitulée « Les types Objet »Le typage Javascript est divisé en deux mondes : les types de valeurs primitives (ou types primitifs) et les types objet. Les types Objet regroupent :
- le type objet (Object) ;
- le type tableau (Array) ;
- le type fonction (Function) ;
- le type class (Class).
Les types objet sont des types référence (reference type) : une variable d’un type object contient la référence à la valeur. Nous pouvons aussi dire qu’elle pointe vers cette valeur.
Les variables d’un type objet sont muables (mutable ) : nous pouvons changer la valeur d’un objet après sa création, contrairement aux valeurs primitives.
Nous allons maintenant nous concentrer dans cet article sur le type function.
Type function
Section intitulée « Type function »Le type function permet de nommer et regrouper un ensemble d’instructions Javascript.
Une fonction possède :
- un nom : il représente la variable de type
functionqui référence la fonction ; - un corps : il contient les instructions de la fonction ;
- aucun, un seul, ou plusieurs paramètre(s) ;
- aucune ou une valeur de retour.
Une fois déclarée, la fonction
peut être appelée en ajoutant des parenthèses () :
- soit à l’extérieur de la fonction, dans le scope de sa déclaration ;
- soit à l’intérieur de la fonction, il s’agit alors d’une fonction récursive (qui s’appelle elle-même).
En appelant une fonction, les instructions de son corps sont exécutées.
L’object wrapper associé à function est :
Function.
En tant que type objet, une fonction a les mêmes propriétés que n’importe quel objet :
- elle est traitée comme n’importe quelle variable ;
- elle peut être passée en paramètre d’un autre fonction ;
- elle peut être retournée par une fonction.
On dit qu’elle est de première classe.
Javascript propose deux façons de déclarer une fonction :
- via l’instruction function ;
- via l’expression de fonction fléchée ou arrow function.
Instruction function
Section intitulée « Instruction function »L’instruction function permet de déclarer une nouvelle fonction.
Tout comme les instructions var,
let,
et const,
l’instruction function
permet de définir une liaison de nom (binding)
associant un identifiant (identifier)
à la fonction que nous souhaitons déclarer.
function degreesToRadians(valueInDegrees) { const valueInRadians = valueInDegrees * Math.PI / 180; return valueInRadians;}
const radians = degreesToRadians(90); // ← 1.5707963268- Nous avons déclaré une nouvelle fonction nommée
degreesToRadians; - Cette fonction a un seul paramètre :
valueInDegrees; - Le corps de la fonction contient deux instructions ;
- La fonction retourne la valeur de
valueInRadiansvia l’instruction return ; - Nous appelons la fonction
degreesToRadiansen lui passant90comme valeur de paramètre ; - la valeur retournée par la fonction est stockée dans la variable
radians.
Arrow function
Section intitulée « Arrow function »Javascript propose une alternative pour déclarer une nouvelle fonction en utilisant l’expression de fonction fléchées.
const degreesToRadians = (valueInDegrees) => { if (typeof valueInDegrees !== "number") return; const valueInRadians = valueInDegrees * Math.PI / 180; return valueInRadians;}
const radians = degreesToRadians(90); // ← 1.5707963267948966const notDefined = degreesToRadians("45"); // ← undefinedLa fonction degreesToRadians déclarée avec l’expression fléchée () => {}
a le même comportement que la fonction que nous avons déclarée via l’instruction function.
La plupart du temps, nous ne verrons pas de différence entre les deux types de déclaration. Pourtant, il existe quelques différences que nous allons détailler maintenant.
Function ↔ Arrow function
Section intitulée « Function ↔ Arrow function »Mot-clef this
Section intitulée « Mot-clef this »const child = { firstName: "Lucas", lastName: "MIller", dateOfBirth: new Date("2015-05-24"), age: function() { const now = new Date(); const age = now.getFullYear() - this.dateOfBirth.getFullYear(); return age; }}
console.log(child.age()); // ← 9- Nous avons déclaré une variable
childde typeobject; - Cet objet a 4 propriétés :
firstName,lastName,dateOfBirthetage; - La propriété
ageest une fonction ; - Dans le corps de la fonction
age, nous utilisons le mot-clef this ; - Dans ce cas,
thisréférence l’objet où est déclarée la fonction ; - Nous pouvons ainsi accéder à la propriété
dateOfBirthde l’objet parent de la fonction ; thisest automatiquement bind à l’objet où est déclarée la fonction.
Maintenant utilisons une arrow function pour la propriété age.
const child = { firstName: "Lucas", lastName: "Miller", dateOfBirth: new Date("2015-05-24"), age: () => { console.log(this); const now = new Date(); const age = now.getFullYear() - this.dateOfBirth.getFullYear(); return age; }}
console.log(child.age()); // 🔴 Error: can't access property "getFullYear", this.dateOfBirth is undefinedDans ce cas, this ne référence pas l’objet parent de la propriété age, et dateOfBirth est alors undefined :
il n’y a pas de binding automatique.
this référence l’objet global Window si le code Javascript est exécuté dans le navigateur. C’est sa valeur par défaut.
Objet arguments
Section intitulée « Objet arguments »function log() { console.log(arguments);}
log("I", "have", 10 , false); // ← Object { 0: "I", 1: "have", 2: 10, 3: false }L’objet arguments est disponible dans le corps de la fonction log.
Chaque propriété de arguments correspond à un argument passé à la fonction.
Ces propriétés ont le même ordre que les arguments.
La clef de la propriété est égale à l’index de l’argument (0, 1, 2, 3, 4, etc.).
La valeur de la propriété est égale à la valeur de l’argument.
Maintenant utilisons une arrow function pour déclarer log.
const log = () => { console.log(arguments);}
log("I", "have", 10 , false); // 🔴 ReferenceError: arguments is not definedDans ce cas, l’objet arguments est undefined.
Déclaration et utilisation
Section intitulée « Déclaration et utilisation »square(3); // ← 9
function square(x) { return (x * x);}La fonction square peut être appelée avant sa déclaration.
C’est le principe de remontée (hoisting) :
les fonctions déclarées avec function (ou async function) sont remontée en haut de leur scope.
Elles sont alors disponibles quel que soit l’endroit où elles sont déclarées.
Maintenant utilisons une arrow function pour déclarer square.
square(3); // 🔴 ReferenceError: can't access lexical declaration 'square' before initialization
const square = (x) => { return (x * x);}Les variables déclarées avec const ou let ne bénéficient pas du hoisting.
Il faut donc les déclarer avant de les utiliser.
Autres différences
Section intitulée « Autres différences »Les arrow functions :
- n’ont pas de binding vers
arguments, super et this ; - ne peuvent pas être utilisées comme constructeur d’une classe ;
- peuvent être utilisée comme méthode d’une classe, mais elles n’ont pas accès à this (cela limite leur utilité) ;
- ne peuvent pas utiliser l’instruction yield.
Instruction return
Section intitulée « Instruction return »L’instruction return met fin à l’exécution de la fonction et rend la main au code qui l’a appelée.
Nous pouvons utiliser l’instruction return aucune, une ou plusieurs fois dans le corps de la fonction.
Si une valeur suit l’instruction return alors cette valeur est retournée.
Si aucune valeur ne suit l’instruction return alors undefined est retourné.
undefined est également retourné s’il n’y a pas d’instruction return dans le corps de la fonction.
function degreesToRadians(valueInDegrees) { if (typeof valueInDegrees !== "number") return; const valueInRadians = valueInDegrees * Math.PI / 180; return valueInRadians;}
const radians = degreesToRadians(90); // ← 1.5707963267948966const notDefined = degreesToRadians("45"); // ← undefined- Si le type du paramètre
valueInDegreesn’est pasnumber, alors l’instructionreturnmet fin à l’exécution de la fonction etundefinedest retourné. - Par contre, si le type de
valueInDegreesest biennumber, alors l’exécution se poursuit et la valeur convertie devalueInDegreesest retournée.
Lorsque nous utilisons la syntaxe des arrow functions,
l’instruction return peut être omise si le corps de la fonction ne contient qu’une seule instruction.
const degreesToRadians = (valueInDegrees) => valueInDegrees * Math.PI / 180;const radians = degreesToRadians(90); // ← 1.5707963267948966Si l’instruction à retourner est un objet, alors nous pouvons l’entourer de parenthèses. Javascript comprend ainsi que c’est un objet et pas un ensemble d’instructions.
const createChild = (firstName, lastName) => ({ firstName, lastName });const lucas = createChild("Lucas", "Miller"); // ← Object { firstName: "Lucas", lastName: "Miller" }Paramètres
Section intitulée « Paramètres »Comme nous l’avons vu auparavant, nous pouvons déclarer des paramètres lorsque nous définissons une fonction.
Valeur par défaut
Section intitulée « Valeur par défaut »Les paramètres ont implicitement undefined comme valeur par défaut.
Javascript n’impose pas de passer une valeur à l’ensemble des paramètres déclarés lorsque nous appelons une fonction.
const createChild = (firstName, lastName) => ({ firstName, lastName });
const unknown = createChild(); // ← Object { firstName: undefined, lastName: undefined }const lucas = createChild("Lucas"); // ← Object { firstName: "Lucas", lastName: undefined }const lucasMiller = createChild("Lucas", "Miller"); // ← Object { firstName: "Lucas", lastName: "Miller" }Nous pouvons également spécifier une valeur par défaut pour certains paramètres (ou pour tous).
const createChild = (firstName = "", lastName = "") => ({ firstName, lastName });
const unknown = createChild(); // ← Object { firstName: "", lastName: "" }const lucas = createChild("Lucas"); // ← Object { firstName: "Lucas", lastName: "" }const lucasMiller = createChild("Lucas", "Miller"); // ← Object { firstName: "Lucas", lastName: "Miller" }const createChild = (firstName, lastName, age = 0) => ({ firstName, lastName, age });
const unknown = createChild(); // ← Object { firstName: undefined, lastName: undefined, age: 0 }const lucas = createChild("Lucas"); // ← Object { firstName: "Lucas", lastName: undefined, age: 0 }const lucasMiller = createChild("Lucas", "Miller"); // ← Object { firstName: "Lucas", lastName: "Miller", age: 0 }const lucasMiller8 = createChild("Lucas", "Miller", 8); // ← Object { firstName: "Lucas", lastName: "Miller", age: 8 }Rest Parameter
Section intitulée « Rest Parameter »Nous pouvons utiliser la syntaxe ... afin de déclarer un rest parameter.
const createChild = (firstName, lastName, ...likes) => ({ firstName, lastName, likes });
// ← Object {// firstName: "Lucas",// lastName: "Miller",// likes: Array(3) [ "Mangas", "Video Games", "Soccer" ]// }const lucas = createChild("Lucas", "Miller", "Mangas", "Video Games", "Soccer");- Grâce à la syntaxe
..., le paramètrelikesest transformé en tableau ; - La première valeur
"Lucas"est copiée dans le paramètrefirstname; - La seconde valeur
"Miller"est copiée dans le paramètrelastName; - Les 3 valeurs suivantes
"Mangas","Video Games"et"Soccer"sont ajoutées au tableaulikesen respectant l’ordre ; - Le rest parameter est forcément le dernier paramètre déclaré.
Destructuring
Section intitulée « Destructuring »Nous pouvons utiliser la syntaxe de décomposition (destructuring) lors de la déclaration des paramètres.
const createChild = (firstName = "", lastName = "", age = 0) => ({ firstName, lastName, age });const getChildInfo = ({ age, firstName, lastName }) => `${firstName} ${lastName} is ${age} year(s) old.`;
const lucas = createChild("Lucas", "Miller", 8); // ← Object { firstName: "Lucas", lastName: "Miller", age: 8 }const lucasInfos = getChildInfo(lucas); // ← "Lucas Miller is 8 year(s) old."- La fonction
createChild()permet de créer un objet{ firstName, lastName, age }; - La fonction
getChildInfo()a pour paramètre un objet avec les paramètresage,firstNameetlastName; - La syntaxe de décomposition permet d’assigner chaque propriété de l’objet à une variable du même nom ;
- Les variables sont utilisées pour construire la chaine de caractères retournée.
La portée (ou scope) représente le contexte d’exécution courant.
Le scope est comme une bulle dans laquelle les variables se voient et se connaissent. À l’intérieur de cette bulle, nous pouvons accéder aux variables que nous avons créées et nous pouvons appeler les fonctions que nous avons déclarées. Par contre, ces variables ne sont pas visibles et accessibles à l’extérieur du scope.
En Javascript, il existe 4 types de scope :
- le scope global : scope par défaut pour le code exécuté en mode
script; - le scope module : scope par défaut pour le code exécuté en mode
module; - le scope fonction : scope associé au corps d’une fonction ;
- le scope bloc : scope crée par un bloc,
c’est-à-dire du code entre
{}.
Les variables du scope courant ont accès à celles de leur scope et celles du scope parent. L’inverse n’est pas vrai : le scope courant n’a pas accès au scope de ses enfants.
const fullName = (firstName, lastName) => `${firstName} ${lastName}`;
const createFamily = (lastName, ...firstNames) => { const count = firstNames.length;
const separator = (index) => { switch (index) { case 0: return " "; case (count - 1): return " and "; default: return ", "; } }
let familyText = `Family ${lastName} have ${count} member(s):`;
firstNames.forEach((firstName, index) => { const member = fullName(firstName, lastName); familyText += `${separator(index)}${member}`; });
return `${familyText}.`;}
const familyName = "Miller";
// ← "Family Miller have 3 member(s): Lucas Miller, Nathan Miller and Thomas Miller."const family = createFamily(familyName, "Lucas", "Nathan", "Thomas");
separator(2); // 🔴 ReferenceError: separator is not definedDans le code ci-dessus, les variables sont réparties sur trois scopes :
- le scope global ;
- le scope de la fonction
createFamily; - le scope de la fonction anonyme associée à
forEach.
Le scope de la fonction anonyme a accès au scope de la fonction createFamily,
qui a lui-même accès au scope global.
const counterManager = (initialValue = 0) => { let count = Number(initialValue) ?? 0;
const increment = () => { count = count + 1; }
const decrement = () => { count = count - 1; }
const getCount = () => count;
return { getcounter: getCount, decrement, increment }}
const { getcounter, decrement, increment } = counterManager(10);console.log(count); // 🔴 ReferenceError: count is not definedconsole.log(getcounter()); // ← 10decrement();decrement();console.log(getcounter()); // ← 8increment();increment();increment();console.log(getcounter()); // ← 11Dans la fonction counterManager nous avons déclaré :
- une variable
counterde typenumber; - une variable
incrementde typefunction; - une variable
decrementde typefunction; - une variable
getCountde typefunction.
Comme nous l’avons vu dans le chapitre précédent,
toutes ces variables appartiennent au scope de la fonction counterManager
et sont donc inaccessibles au scope global.
Ensuite, nous appelons la fonction counterManager en lui passant le paramètre 10.
Les instructions de cette fonction sont exécutées et elle retourne un object avec les propriétés suivantes :
getcounterqui référence la fonctiongetCount;decrementqui référence la fonctiondecrement;incrementqui référence la fonctionincrement.
Une fois la fonction exécutées, nous pourrions penser que les variables appartenant au scope de la fonction ne soient plus disponibles.
En effet, les variables du scope de la fonction n’ont plus de raison d’exister une fois celle-ci exécutée.
En particulier la variable count : elle a un type primitif et elle n’est pas référencée directement par l’objet retourné.
Pourtant :
getcounter()retourne la valeur courante decount;decrement()décrémentecount;increment()incrémentecount;countcontinue donc d’exister.
En Javascript, les fonctions forment une closure.
Une closure représente la combinaison d’une fonction et des variables qui l’entourent,
c’est-à-dire les variables du scope accessibles au moment de la création de la fonction.
Dans notre exemple, chaque fonction decrement, increment et getCount est une closure qui a accès à la variable count.
Tant qu’une référence vers l’une de ses fonctions existe, alors Javascript conserve la variable count.
Conclusion
Section intitulée « Conclusion »En Javascript, une fonction :
- hérite du type
object; - est une variable comme un autre ;
- créé son propre scope
- forme une closure.
Cela représente beaucoup de super-pouvoirs ! Mais ce n’est pas étonnant, car Javascript est plutôt un langage de programmation fonctionnelle, même si il possède quelques mécaniques de programmation orientée objet.
Quoi qu’il en soit, les fonctions sont un atout indispensable pour bien architecturer notre code.