Programmation Fonctionnelle et Asynchrone
Romuald THION
Semestre printemps 2023-2024 UCBL
Glossary of Modern JavaScript Concepts (2017)
☠️ ☠️var
: portée fonction
⚠️ let
/const
: portée lexicale
(bloc) ⚠️
👍 let
/const
, la portée de i
est la boucle for
.
for (let i = 0; i < 3; i++) {
console.log(i); // 0, then 1, then 2
}
console.log(i); // Uncaught ReferenceError: i is not defined
👎 var
, la portée de i
est la fonction
englobante.
// déclaration de funct
function funct(arg1, arg2, ..., argN) {
// instructions
return expression;
}
// appel
const res = funct(p1, p2, ..., pN);
Ici, on crée une fonction avec une instruction
(statement 🇬🇧) function
qui ne produit
pas de valeur de retour (MDN).
// NFE affectée à funct
const funct = function (arg1, arg2, ..., argN) {
// instructions
return expression;
};
// appel
const res = funct(p1, p2, ..., pN);
👉 Ici, on crée une fonction comme une expression
avec laquelle on initialise une variable funct
,
c’est une Named Function Expression - (NFE) (javascript.info, MDN).
A.k.a., arrow functions, fat arrows ou lambdas (MDN).
// arrow avec bloc d'instructions et return explicite
const funct = (arg1, arg2, ..., argN) => {
// instructions
return expression;
};
// appel
const res = funct(p1, p2, ..., pN);
⚠️ Sans return
, funct
renvoie
undefined
. ⚠️
// arrow affectée à funct
const funct = (arg1, arg2, ..., argN) => expression;
// appel
const res = funct(p1, p2, ..., pN);
Quasi-équivalente à la NFE suivante :
Et à l’expression du λ-calcul :
\lambda a_1 \ldots \lambda a_n . e
⚠️ NFE et arrows ne sont pas
équivalentes, car une arrow a this
dans sa
fermeture statique, mais pas la NFE
qui l’obtient dynamiquement. ⚠️
Arrow functions do not have
this
. Ifthis
is accessed, it is taken from the outside. (Arrow functions, the basics et Arrow functions revisited)
Inside a function, the value of
this
depends on how the function is called. Think about this as a hidden parameter of a function - just like the parameters declared in the function definition,this
is a binding that the language creates for you when the function body is evaluated. (MDN)
🪳 Si vous utilisez this
dans un handler avec
une arrow, this
ne sera pas
l’élément auquel on attache le handler. 🪳
const fNFE = function (x) {
return console.log(`this=${this} id=${this.id} x=${x}`);
};
const fArrow = (x) => console.log(`this=${this} id=${this.id} x=${x}`);
const object = { id: 42, fNFE, fArrow };
object.fNFE(3); // this = object
object.fArrow(3); // this = globalThis
const testButton = document.querySelector("#test");
testButton.addEventListener("click", fNFE); // this = testButton
testButton.addEventListener("click", fArrow); // this = globalThis
testButton.click();
Voir exemple.
this=[object Object] id=42 x=3
this=[object Window] id=undefined x=3
this=[object HTMLButtonElement] id=test x=[object MouseEvent]
this=[object Window] id=undefined x=[object MouseEvent]
Terme utilisé en JS pour (\lambda x.e) v : l’expression e dans laquelle x prend la valeur à v.
function hello(person) {
return function (content) {
return `Hi ${person}, ${content}`;
};
}
const helloBuddy = hello("Buddy");
const helloGuys = hello("Guys");
console.log(helloBuddy("c'mon."));
console.log(helloBuddy("welcome."));
console.log(helloGuys("how do you do?"));
💡 helloBuddy()
et helloGuys()
sont des
fermetures (closures 🇬🇧) où
person
est liée. 💡
En λ-calcul (avec \mathsf{++} la concaténation) :
\begin{align*} \mathsf{hello} &\equiv \lambda p . \lambda c . p \mathsf{++} c\\ \mathsf{helloBuddy} &\equiv (\mathsf{hello}\; \mathtt{Buddy})\\ &\equiv (\lambda p . \lambda c . p \mathsf{++} c)\; \mathtt{Buddy}\\ &\equiv (\lambda c . p \mathsf{++} c)[p := \mathtt{Buddy}] \end{align*}
\begin{align*} \mathsf{helloBuddy}\; \mathtt{OK} &\equiv ((\lambda c . p \mathsf{++} c)[p := \mathtt{Buddy}])\mathtt{OK} \\ &\equiv (p \mathsf{++} c)[p := \mathtt{Buddy}, c := \mathtt{OK}] \end{align*}
💡 (\lambda c . p \mathsf{++} c)[p := \mathtt{Buddy}] est une fermeture :
Ici le calcul termine effectivement avec (p \mathsf{++} c)[p := \mathtt{Buddy}, c := \mathtt{OK}] évalué en \mathtt{Buddy} \mathsf{++} \mathtt{OK}.
📖 La sémantique de JS est informelle ECMA-262, sa formalisation se construit a posteriori, e.g. KJS: A Complete Formal Semantics of JavaScript - PLDI’15, voir aussi TypeScript.
function fabriqueRedimensionneur(taille) {
return function () {
// 🚨 taille est capturée 🚨
document.body.style.fontSize = taille + "px";
};
}
const taille12 = fabriqueRedimensionneur(12);
const taille14 = fabriqueRedimensionneur(14);
document.querySelector("#t-12").onclick = taille12;
document.querySelector("#t-14").onclick = taille14;
💡 La variable taille
est liée.
La fermeture de M. Jourdain (TP2) :
const $anchors = document.querySelectorAll("#images-list li > a");
const $display = document.querySelector("#image-container");
for (const $a of $anchors) {
$a.addEventListener("click", function clickButton(event) {
// 🚨 $a est capturée car portée block 🚨
event.preventDefault();
const $img = document.createElement("img");
$img.src = $a.href;
$display.replaceChildren($img);
});
}
const fsMap = [1, 2, 3].map((x) => (y) => y + x);
console.log(fsMap.map((f) => f(1)));
// [ 2, 3, 4 ] 👍
const fsConst = [];
for (const a of [1, 2, 3]) fsConst.push((x) => x + a);
console.log(fsConst.map((f) => f(1)));
// [ 2, 3, 4 ] 👍
const fsVar = [];
for (var a of [1, 2, 3]) fsVar.push((x) => x + a);
console.log(fsVar.map((f) => f(1)));
// [ 4, 4, 4 ] 🤯
Voir l’exemple.
🤔 let
/const
n’ont été introduits qu’avec
ES6 / ECMAScript 2015, comment faisait-on depuis 1995 ?
An IIFE (Immediately Invoked Function Expression) is a JavaScript function that runs as soon as it is defined MDN.
📜 Permet de limiter la portée des variables avec une closure : méthode avant la standardisation des modules ESM (ES6) pour encapsuler.
💡 Les IIFEs permettent de résoudre le problème du tableau de
fonctions avec var
.
const functs = [];
for (var a of [1, 2, 3])
(function (y) {
functs.push((x) => x + y);
})(a); // IIFE
console.log(functs.map((f) => f(1)));
// [ 2, 3, 4 ] 😌
Ici, on introduit y
pour capturer a
via
l’IIFE :
(\lambda x. x + y)[y := a]
Closures and classes behave differently in JavaScript with a fundamental difference: closures support encapsulation, while JavaScript classes don’t support encapsulation.
Closures offer simplicity, since we don’t have to worry about the context that
this
is referring to. (How to decide between classes v. closures in JavaScript)
Class
, style ES6+
Voir class-vs-closure.js.
📝 LIFWEB n’est pas tout à fait une UE sur JavaScript et les
subtilités de son modèle objet : on sera utilisateurs des
objets et on évitera this
en privilégiant le style
fonctionnel. ⚠️
Source : JavaScript Visualized: Event Loop
onevent
const $output = document.querySelector("#output");
const $input = document.querySelector("#input");
const $eval = document.querySelector("#eval");
function work() {
// 💡 ici, $input.value n'est pas dans la closure
// la variable n'est PAS dans la fermeture
$output.textContent += `${2n ** BigInt($input.value)}\n`;
}
$eval.addEventListener("click", work);
Voir l’exemple.
console.log("Start"); // (A)
setTimeout(
// (T1)
() => console.log("Call back #1"), // (CB1)
);
console.log("Middle"); // (B)
setTimeout(
// (T2)
() => console.log("Call back #2"), // (CB2)
);
console.log("End"); // (C)
🧠 setTimeout(fct, delay=0, ...params)
rend
fct
asynchrone : les alarmes sont
déclenchées au début du tour suivant de la boucle
d’événements.
❓ Qu’est-ce qui s’affiche dans la console ? (exemple)
const s = new Date().getSeconds();
setTimeout(function () {
console.log(`Elapsed: ${new Date().getSeconds() - s} seconds`);
}, 0);
while (true) {
if (new Date().getSeconds() - s >= 2) {
console.log("Good, looped for 2 seconds");
break;
}
}
⚠️ Ici, l’alarme sera déclenchée après au moins deux secondes (exemple).
💀 Ne réalisez pas vous-même cette cascade.
Voir sur javascript.info, MDN ou Node.js.
💡 setTimeout(fct, delay=0, ...params)
fct
devient asynchrone ;fct
doit être une
fonction ;fct(...params)
après delay
ms.👎 Ce n’est pas la meilleure façon de rendre une fonction asynchrone. On en verra d’autres.
On veut exécuter work(2)
en asynchrone.
// 🔥 typeof(work(2)) == "number"
setTimeout(work(2), 0);
// ❎ pas de paramètre
setTimeout(work, 0);
// 👎 spécifique à setTimeout
setTimeout(work, 0, 2);
// 👌 fonction explicite
setTimeout(function () {
return work(2);
}, 0);
// 👌 fat arrow
setTimeout(() => work(2), 0);
🚨 Comment obtenir la valeur de retour de
work(n)
?
How do I return the response from an asynchronous call? - SO
return
function work(x) {
console.info(`work(${x})`);
return x ** 2;
}
function asyncWork(n) {
return setTimeout(() => work(n), 3000);
}
const r1 = asyncWork(12);
console.log(r1);
❎ Ici, r1
est l’identifiant de l’alarme obtenue de
façon synchrone, pas le retour de work(n)
(exemple) !
function work(x) {
console.info(`work(${x})`);
return x ** 2;
}
function asyncWork(n, callback) {
return setTimeout(() => {
const r = work(n);
callback(r);
}, 3000);
}
const r1 = asyncWork(3, (r) => console.log(r));
console.log(r1);
✅ Ici, r1
est toujours l’alarme, mais on a bien exécuté
(r) => console.log(r)
avec la valeur de retour
de work(n)
après 3000 ms (exemple) !
Phénomène appelé pyramid of doom ou callback hell
function asyncWork(n, callback) {
return setTimeout(() => callback(n ** 2), 3000);
}
asyncWork(3, (r1) => {
console.log(r1);
asyncWork(r1, (r2) => {
console.log(r2);
asyncWork(r2, (r3) => console.log(r3));
});
});
✅ Ici, on parvient à chaîner les asyncWork
, mais il
faut imbriquer des fonctions (exemple) !
return
renvoie une valeur synchrone
:
throw
, mais pour un succès.