Aller au contenu

Javascript • Système de types • Fonction

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.

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;

Javascript est également faiblement typé. Il permet ainsi des conversions implicites de type.

const oneTwoThree = 123; // number
const fourFive = "45"; // string
const result = oneTwoThree + fourFive; // string
console.log(result); // ← "12345"

Le typage Javascript est divisé en deux mondes : les types de valeurs primitives (ou types primitifs) et les types objet. Les types Objet regroupent :

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.

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 function qui 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 :

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 valueInRadians via l’instruction return ;
  • Nous appelons la fonction degreesToRadians en lui passant 90 comme valeur de paramètre ;
  • la valeur retournée par la fonction est stockée dans la variable radians.

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.5707963267948966
const notDefined = degreesToRadians("45"); // ← undefined

La 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.

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 child de type object ;
  • Cet objet a 4 propriétés : firstName, lastName, dateOfBirth et age ;
  • La propriété age est une fonction ;
  • Dans le corps de la fonction age, nous utilisons le mot-clef this ;
  • Dans ce cas, this référence l’objet où est déclarée la fonction ;
  • Nous pouvons ainsi accéder à la propriété dateOfBirth de l’objet parent de la fonction ;
  • this est 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 undefined

Dans 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.

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 defined

Dans ce cas, l’objet arguments est undefined.

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.

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.

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.5707963267948966
const notDefined = degreesToRadians("45"); // ← undefined
  • Si le type du paramètre valueInDegrees n’est pas number, alors l’instruction return met fin à l’exécution de la fonction et undefined est retourné.
  • Par contre, si le type de valueInDegrees est bien number, alors l’exécution se poursuit et la valeur convertie de valueInDegrees est 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.5707963267948966

Si 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" }

Comme nous l’avons vu auparavant, nous pouvons déclarer des paramètres lorsque nous définissons une fonction.

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 }

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ètre likes est transformé en tableau ;
  • La première valeur "Lucas" est copiée dans le paramètre firstname ;
  • La seconde valeur "Miller" est copiée dans le paramètre lastName ;
  • Les 3 valeurs suivantes "Mangas", "Video Games" et "Soccer" sont ajoutées au tableau likes en respectant l’ordre ;
  • Le rest parameter est forcément le dernier paramètre déclaré.

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ètres age, firstName et lastName ;
  • 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 defined

Dans 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. Scope Javascript

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. Scope Javascript

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 defined
console.log(getcounter()); // ← 10
decrement();
decrement();
console.log(getcounter()); // ← 8
increment();
increment();
increment();
console.log(getcounter()); // ← 11

Dans la fonction counterManager nous avons déclaré :

  • une variable counter de type number ;
  • une variable increment de type function ;
  • une variable decrement de type function ;
  • une variable getCount de type function.

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 :

  • getcounter qui référence la fonction getCount ;
  • decrement qui référence la fonction decrement ;
  • increment qui référence la fonction increment.

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 de count ;
  • decrement() décrémente count ;
  • increment() incrémente count ;
  • count continue 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.

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.