JS Warmup - Observables
JS Warmup
Ça va bien se passer
Exercice 1 - On commence doucement
Codez une fonction sum
prenant un nombre indéfini de paramètres et retournant la somme des nombres.
Correction :
// utilisation des rest paramters
function sum(...number) {
// les rest parameters s'utilisent comme des tableaux
// on peut donc utiliser la méthode "reduce" du prototype Array
// pour faire la somme
return numbers.reduce((acc, curr) => acc + curr, 0);
}
Note : pour la méthode reduce
const tableau = [4, 5, 6];
const reducerCallback = (accumulateur, valeurCourante) => { return accumulateur + valeurCourante }
tableau.reduce(reducerCallback, 0); // ici, je spécifie la valeur de départ
- 1er tour de boucle :
accumulateur
vaut 0 (valeur de départ) etvaleurCourante
vaut 4 (première case du tableau) etreducerCallback
retourne 4 - 2ème tour de boucle :
accumulateur
vaut 0 + 4 = 4 etvaleurCourante
vaut 5 (deuxième case du tableau) etreducerCallback
retourne 9 - 3ème tour de boucle :
accumulateur
vaut 4 + 5 = 9 etvaleurCourante
vaut 6 (troisième case du tableau) etreducerCallback
retourne 15 - Après le troisième tour, c’est terminé, le tableau est parcouru. Et le résultat final est donc 15
Exercice 2 - Escalade Véloce 🚀
2A
Ecrivez une fonction isFunction
qui prend un paramètre et qui vérifie qu’il s’agisse bien d’une fonction. Aidez vous de l’opérateur typeof
.
isFunction
devra fonctionner comme suit :
const f1 = () => {};
function f2() {}
const nombre = 12;
isFunction(f1); // retourne true
isFunction(f2); // retourne true
isFunction(nombre); // retourne false
Correction :
function isFunction(f) {
// un simple "typeof" suffit
return typeof f === 'function';
}
2B
Ecrivez une fonction filterNonFunctions
qui prend en paramètre un nombre indéfini d’arguments sous la forme d’un rest parameter et qui retourne un tableau ne contenant que les arguments étant des fonctions.
Pour cela, aidez-vous de la méthode filter
du prototype Array
.
Elle devra fonctionner comme suit :
const f1 = () => {};
function f2() {}
const nombre = 12;
const functions = filterNonFunctions(f1, nombre, f2); // [f1, f2]
Correction :
function filterNonFunctions(...functions) {
// il suffit d'utiliser la méthode "filter"
// du prototype Array et la méthode isFunction précédemment écrite
return functions.filter(isFunction);
}
2C
Ecrivez désormais une fonction pipe
qui fonctionne comme suit :
- nombre indéfini de fonctions en entrée ;
- filtre les arguments qui ne sont pas des fonctions ;
- stocke le tableau résultant du filtre ;
- retourne une nouvelle fonction
- prenant un nombre indéfini d’arguments ;
- exécute chaque fonction
n
en lui passant en argument le résultat de l’appeln - 1
; - retourne le résultat final.
Exemple :
// On a des fonctions de base
const multiply = (x, y) => x * y;
const square = (x) => x * x;
const double = (y) => y * 2;
const addTen = (z) => z + 10;
// pipe prend un nombre indéfini de fonctions en argument
// Ici, on crée trois nouvelles fonctions
const piped1 = pipe(multiply, square, double, addTen);
const piped2 = pipe(square, double);
const piped3 = pipe();
// piped1(2, 3) va exécuter à la suite :
// multiply(2, 3), square(6), double(36), addTen(72)
const result1 = piped1(2, 3);
// piped2(4) va exécuter à la suite :
// square(4), double(16)
const result2 = piped2(4);
// piped3 ne va rien exécuter
const result3 = piped3();
console.log(result1); // affiche 86
console.log(result2); // affiche 32
console.log(result3); // undefined
Correction :
function pipe(...functions) {
// d'abord on filtre les fonctions
const fns = filterNonFunctions(...functions);
// on retourne une nouvelle fonction
return (...args) => {
// si nous avons au moins une fonction
if (fns.length > 0) {
// on va faire un "reduce" un peu particulier
return fns // reduce uniquement sur le tableau moins son premier élément
.splice(1)
// "val" est l'accumulateur, et "f" la valeur courante
// fns[0](...args) est la valeur initiale
.reduce((val, f) => f(val), fns[0](...args));
}
}
};
Exercice 3 - Same, but different
Ecrivez une fonction compose
qui fonctionne comme suit :
const multiply = (x, y) => x * y;
const square = (x) => x * x;
const double = (y) => y * 2;
const addTen = (z) => z + 10;
// compose prend un nombre indéfini de fonctions en argument
// et retourne une nouvelle fonction
const composed1 = compose((x) => multiply(x, 3), square, double, addTen);
const composed2 = compose(square, double);
// la nouvelle fonction prend un nombre indéfini d'arguments
// et exécute les différentes fonctions comme une composition mathématique
const result1 = composed1(2);
const result2 = composed2(4);
console.log(result1); // affiche 1728
console.log(result2); // affiche 64
Indice : compose
est le pendant de pipe
: au lieu d’exécuter les fonctions dans l’ordre passé, elle les exécute dans l’ordre inverse (type f(g(x))
)
Correction :
function compose(...functions) {
const fns = filterNonFunctions(...functions);
return (...args) => {
if (fns.length > 0) {
// Pareil que pour "pipe", sauf qu'on "reverse" le tableau
return fns.reverse().splice(1).reduce((val, f) => f(val), fns[fns.length -1](...args));
}
}
}
Exercice 4 - Promises et Observables
4A
Allez lire la documentation sur les Promises avant de faire l’exercice.
Ecrivez une fonction delay
qui fonctionne comme suit :
const multiply = (x, y) => x * y;
// delay prend en paramètre une fonction et un timeout (millisecondes)
// et retourne une nouvelle fonction qui :
// - retournera une Promise
// - exécutera la fonction (ici multiply) après le timeout
const delayed = delay(multiply, 2000);
delayed(3, 4).then((res) => console.log(res)); // affichera 12 après 2s
Correction :
function delay(fn, timeout) {
return (...args) => {
// Cette fonction sera passée au constructeur de la Promise
// Elle prend deux paramètres, resolve et reject :
// https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Promise
const promiseCallback = (resolve, reject) => {
const timedOutFunction = () => {
let res;
// Le try...catch n'était pas demandé
// Je l'ai rajouté
try {
res = fn(...args);
resolve(res);
} catch (exception) {
reject(exception)
}
};
// On enclenche le setTimeout
setTimeout(timedOutFunction, timeout);
};
return new Promise(promiseCallback);
}
}
4B
On a la classe suivante :
class Observable {
// rest parameters
constructor(...args) {
// on ne fait pas "this.values"
// afin que le champ reste privé
let values = args;
// idem avec subscribers, qui sera un tableau de fonctions
const subscribers = [];
// la méthode "next" prend des rest parameters en entrée
this.next = (...vals) => {
// elle met à jours "values"
values = [...vals];
// et déclenche tous les traitements avec ces nouvelles valeurs
subscribers.forEach((f) => f(...values));
};
// "subscribe" enregistre simplement une nouvelle fonction
// qui sera exécutée à la prochaine valeur émise par "next"
this.subscribe = (f) => {
subscribers.push(f);
};
}
}
En vous aidant de cette classe, écrivez une fonction debounce
de sorte à ce que le code suivant fonctionne :
const square = (x) => x ** 2;
// "debounce" prend une fonction ainsi qu'un timeout en paramètre
// et retourne un Observable
const debounced = debounce(square, 1000);
const subscriber = (res) => console.log("res", res);
// (1) DEVRAIT exécuter square après 1s et afficher 9
debounced(3).subscribe(subscriber);
// (2) se déclenchera après 1.5s, exécutera square et affichera 4
setTimeout(() => debounced(2), 1500);
debounced(10); // (3) annulera l'appel (1) et relancera un nouveau timeout
Indices :
- vous pouvez vous aider de cet article, écrit par votre humble intervenant. Par contre l’implémentation qui s’y trouve n’est pas totalement celle demandée ici 🙃.
Correction :
function debounce(fn, timeout) {
let timer;
let obs = new Observable();
return (...args) => {
clearTimeout(timer);
// À la fin du timeout, on n'a plus qu'à donner la nouvelle valeur à l'Observable
timer = setTimeout(() => obs.next(fn(...args)), timeout)
// On retourne l'Observable
return obs;
}
}
4C
Reprenez la fonction delay
et, au lieu de retourner une Promise
, retournez un Observable
.
Correction :
function delay(fn, timeout) {
let obs = new Observable();
return (...args) => {
setTimeout(() => obs.next(fn(...args)), timeout);
return obs;
}
}
4D
Dans la classe Observable
, implémentez une méthode pipe
: contrairement à la première fonction codée dans les précédents exercices, celle-ci ne retournera pas une fonction mais exécutera directement les fonctions passées en paramètre et retournera un Observable
contenant le résultat.
Le code suivant doit fonctionner :
class Observable {
constructor(...args) {
let values = args;
const subscribers = [];
this.next = (...vals) => {
values = vals;
subscribers.forEach((f) => f(...values));
};
this.subscribe = (f) => {
subscribers.push(f);
f(...values);
};
this.pipe = (...functions) => {
// TODO: implémentez cette méthode
// Pensez à filtrer les arguments qui ne sont pas des fonctions
// Vous devez retourner un Observable comportant le résultat final
// Pour le reste, c'est sensiblement pareil que la précédente
// fonction pipe()
};
}
}
new Observable(4)
.pipe(
(x) => x ** 2,
(x) => ("" + x).split(""),
(numbers) => numbers.map((x) => +x),
(numbers) => numbers.reduce((acc, curr) => acc + curr, 0)
)
.subscribe((res) => console.log("resultat", res)); // 7
Correction :
class Observable {
constructor(...args) {
let values = args;
const subscribers = [];
this.next = (...vals) => {
values = vals;
subscribers.forEach((f) => f(...values));
};
this.subscribe = (f) => {
subscribers.push(f);
f(...values);
};
this.pipe = (...functions) => {
// on filtre les fonctions
const fns = filterNonFunctions(...functions);
const obs = new Observable();
// s'il en reste, alors on les exécute les unes à la suite des autres
if (fns.length > 0) {
const res = fns.slice(1).reduce((val, fn) => fn(val), fns[0](...values));
obs.next(res);
}
// on retourne l'Observable
return obs;
};
const filterNonFunctions = (...fns) => {
return fns.filter((f) => typeof f === 'function');
}
}
}