Bases de programmation illustrées en javascript,
fonctions d'ordre supérieur, modèle d’exécution
Olivier Pons
(olivier.pons@lecnam.net)
2022
> s.split(" ") // coupe par rapport a " ", sépare les mots
[ 'JAVASCRIPT', 'est', 'un', 'langage' ] //le résultat est un tableau de mot
> s.split("a") // coupe par rapport a "a"
[ 'JAVASCRIPT est un l', 'ng', 'ge' ]
> s.split("") // coupe par rapport a ""
[ // donne un tableau de lettre
'J', 'A', 'V', 'A', 'S', 'C',
'R', 'I', 'P', 'T', ' ', 'e',
's', 't', ' ', 'u', 'n', ' ',
'l', 'a', 'n', 'g', 'a', 'g',
'e'
]
>
> t1.indexOf(5) //indice de 5 dans le tableau
2
> t1.indexOf(9) // -1 car 9 n'y est pas et n'a donc pas d'indice
-1 // attention 0 est l'indice du premier élément
> t1.join("#") // recolle les élément du tableau pour former une chaîne et les séparant
'5#3#1' // par la chaîne en argument
> t1.join(",") // recoller en séparant par des virgules permet
'5,3,1' // de produit des fichier au format cvs d'excel
> t1.join("----")
'5----3----1'
>
>
> "1 6 7 9 3 6 2 7 9 8 33 1".split(" ").sort()
[
'1', '1', '2', '3',
'33', '6', '6', '7',
'7', '8', '9', '9'
]
>
let nombre="un deux trois quatre cinq six sept huit neuf"
let tab=nombre.split(" ");
tab.slice(2,6)
> tab.slice(2,6)
[ 'trois', 'quatre', 'cinq', 'six' ] //de l'indice 2 a l'indice 5
> tab // tab n'a pas changé
[
'un', 'deux',
'trois', 'quatre',
'cinq', 'six',
'sept', 'huit',
'neuf'
]
>
> tab.splice(2,6,"tree","four")
[ 'trois', 'quatre', 'cinq', 'six', 'sept', 'huit' ]
> tab
[ 'un', 'deux', 'tree', 'four', 'neuf' ]
>
slice
ne modifie pas le tableau et ses arguments sont
des indices de debut et de fin, il renvoie un nouveau tableaux.
splice
modifie le tableau ses argument sont l'indice de
debut, le nombre d'argument a supprimer et les éventuels remplaçants. Il
renvoie le tableau des éléments supprimés.
let t1= [1,3,5]
> t1.pop() // renvoi l'element le plus à droite
5
> t1
[ 1, 3 ]
3
> t1.push(99) // ajoute a la fin
3
> t1
[ 1, 3, 99 ]
>
> t1.shift() // renvoi l'element le plus à gauche
1
> t1
[ 3, 99 ]
> t1.unshift(10) //ajoute en tête
3
> t1
[ 10, 3, 99 ]
>
On en verra plein d'autre par la suite.
Les fonctions peuvent s'écrire avec deux syntaxes:
ou
La premiere est la syntaxe historique. Les deux se comportent
différemment vis à vis de this
.
Dans la seconde this
est toujours global
(window
dans le navigateur).
On ne doit a-priori pas l'utiliser dans les méthodes (sauf si pas de
this
dans la méthode) . Dans la premiere si la fonction est
une methode d'un objet, l'objet appelant est lié à this
au
moment de l'appel.
> let o1={v:3,a:function (x){return this.v+x}}
undefined
> o1.a(6) //this est lié a o1
9
> let o2={v:3,a:(x)=>this.v+x}
undefined
> o2.a(6) //this est toujours global
NaN
On peut définir et/ou appliquer des fonctions anonymes
Dont l’intérêt va apparaître juste après
Rappelons qu'en javascript une fonction est une valeur comme une autre (on parle d'objet ou citoyen de premiere classe).
On peut définir :
Les fonctions que nous avons vu jusque là prenaient en argument, des valeurs de base des tableaux ou des objects mais jamais des fonctions.
En fait, les fonctions peuvent aussi être passées en argument ou être renvoyées par une fonction, on parle alors de fonction d'ordre supérieur
Certaines fonctions prennent naturellement des fonctions en arguments. Cela est fréquent en Mathématique ou l'on a des notations telles que la sommation
\[\Sigma_{i=a}^bf(i)\]
Cela se traduisent immédiatement en javascript si l’on peut utiliser des arguments fonctionnels
on peut alors définir la somme des entiers de 1 à 10
ou la somme des carrés de 1 à 10
ou le centième terme de la série mathématique \(\Sigma_{x=0}^{\infty} \frac{1}{2^x}\) qui converge assez rapidement vers 2.
On dit que l'on a abstrait la fonction `f` en en faisant un paramètre (comme le sont déjà les bornes de sommation).
On peut continuer à abstraire.
Si l'on veut la somme des carrés des nombres pairs. On peut introduire en paramètre une fonction `suivant`
```js
function somme (f,a,b,suivant){
if (a>b) {return 0}
else {return f(a) + somme (f,suivant(a),b,suivant)}
}
Si on instancie suivant
avec x=>x+1
on
retombe dans le cas précédent
Pour se limiter aux nombres pairs, il suffit de commencé sur un pair
(par exemple 0) et d'instancier suivant
avec
x=>x+2
Notons qui bien que les fonctions précédentes soient définies de
façons récursive, rien ne nous empêche de définir notre fonction
somme
avec une boucle:
On peut aussi se servir de notre fonction somme
pour des
choses « un peu plus compliquées » comme approximer \(\Pi\).
La formule de Leibniz
permet de calculer \(Pi/4\) est
\[\Pi/4 = 1/1 - 1/3 + 1/5 -1/7 + 1/9 -1/11
\dots \frac{(-1)^n}{(2n+1)} \dots\\
\ \ \ \ \ =\Sigma_{n=0}^\infty \frac{(-1)^n}{(2n+1)}
\]
mais en réunissant chaque terme positif avec le terme consécutif
négatif on arrive facilement à
\[
\Pi/8= 1/1*3 + 1/5*7 + 1/9*11 +... \frac{1}{n*(n+2)}
\]
qui se traduit immédiatement en js
function appPi (a,b){
return 8*somme((x)=>(1/(x*(x+2))),a,b,(x)=>x+4);
}
> Math.PI-appPi(1,10000)
0.00019999999800024426
>
On pourrait continuer en abstrayant aussi l'opérateur +
,
la valeur du cas non récursif etc.
Mais le principe est là, pour gagner en généralité, on abstrait en passant des fonctions en argument.
Avant de voir comment abstraire dans les parcours de tableaux, on peut donner un dernier exemple de math.
Si \(f\) est une fonction continue et monotone, on peut trouver un zero de \(f\) sur un intervalle \([a, b]\) par la méthode dichotomique quand f(a) et f(b) sont de signes opposés.
l’algorithme est le suivant:
\(\epsilon\) est la précision
souhaitée.
function dichotomie (f,a,b,epsilon){
if (Math.abs(b - a) < epsilon) {return a}
else{
let c = (a+b) / 2.0
let [na,nb] = (f(a)*f(c)>0)?[c,b]:[a,c]
return dichotomie (f,na,nb,epsilon)
}
}
On peut utiliser cette fonction pour trouver une nouvelle
approximation de \(\pi\) en le
calculant comme zero de la fonction x=>cos(x/2)
Là encore on peut préférer écrire une fonction itérative avec une boucle:
function map(t,f){
let res=[];
for (let i=0;i<t.length;i++){
res.push(f(t[i]))
}
return res;
}
> map([3,4,5],(x)=>x*3)
[ 9, 12, 15 ]
> map([{nom:"Diderot",prenom:"Denis"},
{nom:"Kant",prenom:"Emmanuel"},{nom:"Descartes",prenom:"René"}],
x=>x.nom)
[ 'Diderot', 'Kant', 'Descartes' ]
Cette fonction existe c'est une méthode du type Array:
Il existe de nombreuse fonctions de ce type, définies comme méthode des tableaux.
Nous définirons nos propres version en exercice.
Dans les exemple precedent on avait défini une fonction
suivant
qui donné le pas de progression (x=>x+1 ,
x=>x+4,....)
L'opérateur +
attend 2 arguments, on peut définir une
fonction plus
permettant de fixer un des 2 arguments.
plus(2)
renvoie une fonction qui ajoutera 2 a son
argument:
L'exemple canonique est la composition de fonctions. C'est-à-dire la
création d'une fonction compose
qui combine 2 fonctions
passées en argument pour former une nouvelle fonction.
// on compose les fonction f et g pour
// former une nouvelle fonction.
function compose(f,g){
return (x)=>f(g(x))
}
> let fcomp=compose(x=>x+2,x=>x*3)
undefined
> typeof fcomp
'function'
> fcomp(5)
17
//on peut évidemment l'appliquer directement
> compose(x=>x+2,x=>x*3)(7)
23
Si on suppose qu'on dispose d'une fonction mère qui associe sa mère à
un individu. On peut définir alors définir une fonction
grandmère
(la mère de la mère).
Si on cherche l'entier max des element pairs d'un tableau
let maxPairs=
compose(x=>Math.max(...x),
x=>x.filter(x=>x%2===0))
> maxPairs([1,9,2,8,3,7,5])
8
//le chiffre le plus grand dans une chaine de chiffre
let res=(y)=>compose((x)=>Math.max(...x),
(x)=>x.split(""),y)
qres("458537")
8
Coté Math, on peut calculer de façon approximative la dérivée \(f'\) d’une fonction \(f\) avec un petit intervalle \(dx\) de la manière suivante :
(si besoin page 1 de l'excellent)
function derive (f,dx) {
return function (x){
return (f(x + dx) - f(x))/ dx
}
}
let df=derive ((x)=>x*x,1e-10)
df(1)
On peut verifier que la dérivé de x*x
est
2*x
qui en 1 vaut bien 2, à l'approximation près.
Un timer (anglicisme pour minuteur ) permet de déclencher une fonction apres un certain délais.
Javascript en propose 2. - setTimeOut (fct, delay)
:
applique la fonction fct
après delay
millisecondes. - setInterval(fct,intervale)
: applique la
fonction fct
tous les intervale
millisecondes.
clearTimeout
permet de stopper l'execution d'un
setInterval
(ou d'un setTimeout
si le delay
n'est pas écoulé)
La fonction passée en argument s'appel une fonction de rappel (callback).
//définissons une fonction pour avoir l'h au format h:m:s
function h(){
let d = new Date();
let h = d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds();
return h
}
//une fonction qui affiche `tic` suivi de l'heure
function tic(){
console.log("tic",h())
}
//test setTimeout
console.log("avant",h());
let t=setTimeout(tic,3000); //delais de 3 seconde
console.log("après",h());
qui affichera :
avant 15:27:33
après 15:27:33
tic 15:27:36 //affichage du tic 3 secondes APRÈS
Exemple avec un setInterval
. On déclenche le tic toute
les secondes. On utilise setTimeout
pour arreter le
setInterval
par clearTimeout
au bout de 10
secondes 10 millièmes
Les choses se complique si le calcul en cours est plus long que le
délais. Dans l'exemple ci-dessous on affiche l'heure, on planifie le
lancement de tic
dans 3 seconde avec
setTimeout
puis on lance un calcul qui prend plus de 3
secondes...
console.log("avant",h());
t=setTimeout(tic,3000); //délais de 3 seconde
for(let i=0;i<10000000000;i++){Math.sin(Math.pow(5555,2))}
console.log("après",h());
Voila le résultat...
```js
avant 15:33:20
après 15:33:30
tic 15:33:30 //le tic apparaît a 10 s
le tic
n'est affiché qu'a la fin de l’exécution du code
en cours, à savoir la boucle for
qui prend environ 10
seconde () sur ma machine si la votre est plus rapide ajouter un 0...),
puis le second console.log
affiche l'heure puis lorsqu’il
n'y a plus de code en cours, tic
est exécuté.
Donc attention le délais des timers est un temps minimal qui peut augmenter en fonction du temps d’exécution du code en cours.
Javascript est un langage « mono-thread ». Il exécute une instruction à la fois dans l'ordre ou elles arrivent. Mais il tourne dans un environnement hôte (le navigateur, nodejs, cordova...).
Lorsque
Compliquons un peu l'exemple précédent on crée un fichier "ex_concurrence1.js" avec le contenu suivant:
function h(){
let d = new Date();
let h = d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds();
return h
}
function longue(){
for(let i=0;i<10000000000;i++){Math.sin(Math.pow(5555,2))}
}
function message(m){
console.log(h()+" : "+m)
}
function un(m){
message("debut un "+m)
deux()
message("fin un "+m )
}
function deux(){
message("debut de deux")
longue();
message("fin de deux")
}
console.log("avant")
setTimeout(()=>{message("debut timer 1");longue();message("fin timer 1")},10);"";
setTimeout(()=>{message(" timer 2")},1);"";
un("premier")
setTimeout(()=>{message(" timer 3")},10);""
un("second")
produit
bash-3.2$ node ex_concurrence1.js
avant
23:51:33 : debut un premier
23:51:33 : debut de deux
23:51:43 : fin de deux
23:51:43 : fin un premier
23:51:43 : debut un second
23:51:43 : debut de deux
23:51:53 : fin de deux
23:51:53 : fin un second
23:51:53 : timer 2
23:51:53 : debut timer 1
23:52:27 : fin timer 1
23:52:27 : timer 3
En fait l’environnent d'execution se compose de plusieurs chose.
une pile d'appel (des fonctions en cours d'execution), et une file d'attente des fonctions de rappel (callback queue) une **boucle d'événement et l'API de l’environnement hôte (web , node...).
au lancement du programme (qui est assimilé à une fonction principale
main
qui est empilée) - le console.log
de la
ligne 28 est placé sur le haut de la pille d'appel , s'execute, affiche
avant
et est dépilé. - puis le setTimeout
de
la ligne 29 est empilé, execute le premier setTimeout qui passe le
relais a l'API, l’environnement gère le timing et à la fin du délais
ajoutera la fonction de rappel dans à la fin la file
d'attente;
l'instruction est dépilé - puis le setTimeout
de la ligne
30 est empilé,execute le second setTimeout qui passe le relais a l'API,
l’environnement gère le timing et à la fin du délais ajoute la fonction
de rappel dans à la fin la file d'attente;
l'instruction est dépilé - puis l'appel à la fonction un
est placé dans la pile d'appel et commence à être exécuté + puis l'appel
à la fonction message
est placé dans la pile d'appel et
exécuté,
le message 23:51:33 : debut un premier
est affiché;
l'appel à la fonction message
est dépilé.
deux
(qui est
appelé dans un
ligne 17) est placé dans la pile d'appel
s'execute est placé dans la pile d'appel. a ce moment là la pile
contient* puis l'appel à la fonction `message` (dans `deux`) est empilé, puis exécuté,puis dépilée.
* puis l'appel à la fonction `longue` (dans `deux`) est empilé, puis exécuté,puis dépilée.
* puis l'appel à la seconde fonction `message` (dans `deux`) est empilé, puis exécuté,puis dépilée.
* `deux`a finie de s'executer est est dépilé, on peut continuer l'execution de `un`.
Le second appel a message
dans un
est
empilé, exécuté puis dépilé. un
a finie de s'executer est
est dépilée
puis le setTimeout
de la ligne 32 est empilé,execute
le second setTimeout qui passe le relais a l'API, l’environnement gère
le timing et à la fin du délais ajoute la fonction de rappel dans à la
fin la file d'attente;
puis l'appel à la fonction un
est placé dans la pile
d'appel et commence à être exécuté
puis l'appel à la fonction message
est placé dans la
pile d'appel et exécuté,
le message 23:51:33 : debut un premier
est affiché;
l'appel à la fonction message
est dépilé.
puis l'appel à la fonction à la fonction deux
(qui
est appelé dans un
ligne 17) est placé dans la pile d'appel
s'execute est placé dans la pile d'appel.
message
(dans
deux
) est empilé, puis exécuté,puis dépilé.longue
(dans
deux
) est empilé, puis exécuté,puis dépilé.message
(dans
deux
) est empilé, puis exécuté, puis dépilé.deux
a finie de s'executer est est dépilée, on peut
continuer l'execution de un
.Le second appel a message
dans un
est
empilé, exécuté puis dépilé. un
a finie de s'executer est
est dépilée
main
a fini de s’exécute et est dépilé.
Les différentes fonctions de rappel des setTimeout
, ont
été placées dans la file d'attente au fur et à mesure de leur
déclenchement par l’environnement. Comme le second
setTimeout
(avec le message timer 2
) avait un
délais plus court, il est en tête de la file, les deux autres ayant le
meme délais, timer 1
sera logiquement devant
timer 3
.
Comme la pile d'appel est vide, la boucle d’événements copie la tête
de la file sur la pile d'appel.
message(" timer 2")
est donc empilée, puis exécutée, le
message timer 2
s'affiche; puis l'instruction est
dépilée.
Comme la pile d'appel est vide, la boucle d’événements copie la tête
de la file sur la pile d'appel.
la fonction (anonyme)
()=>{message("debut timer 1");longue();message("fin timer 1")},10);"
est
donc empilée, puis commence à s’exécuter.
message("debut timer 1")
est
empilé, puis exécuté, affichant debut timer 1
, puis
dépilée.longue()
est empilée, puis
exécuté,puis dépilé.message("fin timer 1")
est
empilée, puis exécuté, affichant fin timer 1
puis dépilé.
l'execution de la fonction anonyme est finie; elle est dépiléeComme la pile d'appel est vide, la boucle d’événements copie la tête
de la file sur la pile d'appel.
message(" timer 3")
est donc empilée, puis exécutée, le
message timer 3
s'affiche; puis l'instruction est
dépilée
La pile et la file d'attente sont vide. L'api n'a plus rien en attente. Le programme est terminé.
Ce mécanisme à base de fonction de rappel est fondamentale pour la gestion des événements que nous verrons au prochaine chapitre dans le Navigateur