- Les fichiers de démonstration sont dans une archive téléchargeable [ ici ]
- Dans les premières versions de javascript, la notion de variable locale à un bloc de code (donc entre { } ) n'existait pas.
- Il n'y avait que la possibilité de déclarer une variable globale au fichier ou bien à la fonction dans laquelle elle est déclarée.
- Maintenant, il existe 2 mots clés différents :
- var : déclare la variable comme globale au fichier/fonction.
- let : déclare la variable locale au bloc.
- A ceux-là s'ajoutent const, qui permet de définir une constante. Attention, :
- il faut déclarer et définir dans une même instruction,
- il n'y aura pas forcément d'erreur si on essaie de changer sa valeur. Pourtant, elle restera bien constante.
Exemple : td1-1.html (à ouvrir dans un navigateur avec l'inspecteur ouvert)
<script>
var a;
function showMult() {
for(let i=0;i<2;i++) {
console.log(a);
for(let j=0;j<2;j++) {
console.log( (i+1)*(j+1) );
}
}
}
showMult()
const b = 66;
//b += 1; // si on decommente : erreur
console.log(b);
a = 99;
console.log(a);
console.log(i);
</script>
Résultat :
undefined
1
2
undefined
2
4
66
99
Uncaught ReferenceError: i is not defined
- On remarque que console.log(a) dans la boucle for fonctionne correctement : a est déclarée globale au fichier mais n'a pas encore reçu de valeur au moment de l'appel à showMult(). En fait, elle en a quand même une qui est undefined.
- b est bien constante car si on décommente la ligne b+=1, on obtient une erreur
- En revanche, essayer d'utiliser i est interdit : la variable est locale à la boucle for(let i=...).
1.2°/ Le "hoisting"
- Il existe une "liberté" avec les déclarations variables. Le mécanisme de "hoisting" permet d'écrire du code "bizarre" car il autorise la déclaration absolument n'importe où dans le code. En fait, ce mécanisme s'applique même aux déclarations de fonction.
- On peut donc définir la valeur d'une variable ou bien appeler une fonction avant même leur déclaration/définition. Le compilateur JS va faire comme s'il "remontait" les déclarations au début du fichier.
- Cependant, il existe pire : on peut ne pas déclarer une variable avec var et de la définir directement avec =. Dans ce cas, on obtient une variable globale au fichier. Et pour compliquer les choses, ce type de variable n'est pas prise en compte par le hoisting !
Exemple : td1-2.html (à ouvrir dans un navigateur avec l'inspecteur ouvert)
<script>
a=5;
console.log(a+" "+b)
showMult();
function showMult() {
b = 666;
for(let i=0;i<2;i++) {
console.log(a+" "+b)
for(let j=0;j<2;j++) {
console.log((i+1)*(j+1));
}
}
}
var a;
var b;
</script>
Résultat :
5 undefined
5 666
1
2
5 666
2
4
- Dans cet exemple, on remarque que les variables a et b sont déclarées bien après leur définition, tout comme showMult() qui est appelée AVANT de définir son code.
- Pourtant le code s'exécute sans erreur car les déclarations de a et b, et la définition de showMult() sont "remontées" en début de fichier.
- Les problèmes apparaissent lorsque l'on oublie la déclaration.
- Par exemple, si on commente la dernière ligne var b; , on obtient :
js: uncaught JavaScript runtime exception: ReferenceError: "b" n'est pas défini
- C'est normal car le premier console.log() essaie d'accéder à b qui n'est ni déclarée (via hoisting) ni définie.
- MAIS, si on commente ce console.log(), l'exécution redevient correcte ... Argh !
- Pour éviter ces ambiguïtés, on essaie généralement d'exécuter le code en mode strict, ce qui provoque des erreurs en cas de non déclaration d'une variable.
- Attention cependant :
- le mode strict n'empêche pas le hoisting,
- les navigateurs ne passent pas forcément en mode strict automatiquement et il faut mettre "use strict"; en début de fichier pour s'en assurer.
Exemple : si on modifie td1-2.html comme suivant (à ouvrir dans un navigateur avec l'inspecteur ouvert)
<script>
"use strict";
a=5;
//console.log(a+" "+b)
showMult();
function showMult() {
b = 666;
for(let i=0;i<2;i++) {
console.log(a+" "+b)
for(let j=0;j<2;j++) {
console.log((i+1)*(j+1));
}
}
}
var a;
//var b;
</script>
Résultat dans la console :
td1-2.html:7 Uncaught ReferenceError: b is not defined
at showMult (td1-2.html:7)
at td1-2.html:5
- On voit bien qu'il n'y a un problème qu'avec b.
- L'appel à showMult() et l'affectation de a ne provoquent pas d'erreur : le hoisting joue son rôle.
- D'ailleurs, si on décommente la dernière ligne, il n'y a plus d'erreur.
- Les bizarreries liées au hoisting ne s'arrêtent pas là. Les variables let et const ne sont pas vraiment remontées. La façon de déclarer une fonction va aussi jouer sur le fait que sa définition est remontée ou pas.
- En conclusion, sauf contrainte, écrivez du code qui ne compte pas sur le hoisting pour fonctionner !
- JS n'est pas un langage objet au même titre que le Java ou le C++.
- En effet, on peut créer des objets sans utiliser de classe, simplement en utilisant la syntaxe JSON pour définir les champs (= membres en POO) de l'objet
Exemple : td1-3.html (à ouvrir dans un navigateur)
<script>
"use strict";
let b = {
type:"banane",
color:"jaune",
setColor: function(newColor) {
console.log(this);
this.color = newColor;
},
}
console.log(b.type+" "+b.color);
b.setColor("verte");
console.log(b.type+" "+b.color);
</script>
Remarques :
- Quand on crée une fonction (avec le mot-clé function), celle-ci possède automatiquement une variable this qui référence l'objet courant au moment de l'exécution. S'il n'y pas pas d'objet englobant, this prend undefined comme valeur (en mode strict).
- Dans le cas présent, quand on appelle b.setColor(), l'objet courant est b, donc this vaut b pour setColor(). C'est ce qui lui permet d'accéder au champ color.
- On a bien créé un objet b, avec 2 attributs et une méthode, auxquels on peut accéder comme on le ferait pour un objet en Java.
- Le problème est que si l'on veut créer un orange avec les mêmes attributs et méthode, il va falloir copier/coller ce code, en changeant juste les valeurs des attributs.
- Bref, cette façon de faire n'est pas très pratique pour créer des objets de la même "classe".
2.2°/ Les classes en Javascript
- Fort heureusement, JS propose un vrai mécanisme d'instanciation, avec l'opérateur new.
- Mais contrairement à Java/C++, JS ne se base pas sur un type classe mais sur la notion de prototype objet.
- Un prototype objet est un objet qui contient un ensemble de fonctions (et éventuellement d'attributs), dont une appelée constructeur que l'on va appeler avec new.
- Un peu comme en Java, il existe un prototype primordial nommé Object contenant quelques méthodes.
- Dès que l'on crée un nouveau prototype objet, il est relié à Object soit directement, soit via d'autres prototypes. On a donc un chaînage des prototypes, qui reproduit un héritage.
- En apparence, c'est donc la même chose qu'avec les classes. Cependant, il y a des différences fondamentales :
- un objet qui est construit en appelant le constructeur du prototype est simplement relié à ce dernier. L'objet ne contient pas les membres du prototype.
- un objet peut avoir lui-même d'autres attributs et méthodes qui ne sont pas dans le prototype.
- Il existe plusieurs façons pour créer un prototype objet, mais les plus utilisées sont :
- en créant d'abord la fonction de construction puis en "remplissant" l'attribut prototype de cette fonction.
- avec le mot-clé class.
2.1.1°/ constructeur + prototype
- En JS, une fonction est aussi un type d'objet mais un peu particulier : lors de sa définition, il reçoit automatiquement un attribut nommé prototype.
- prototype est lui-même un objet contenant au départ un seul attribut, constructor, qui référence le code de la fonction.
- Mais à quoi ça sert ??
- En fait, on peut ajouter des fonctions dans prototype, comme on le ferait dans une classe,
- Quant à constructor, il permet tout simplement d'utiliser l'opérateur new pour créer un nouvel objet, plutôt que de l'écrire en JSON directement.
- Avantage considérable : ce nouvel objet va être automatiquement relié au prototype et pourra donc appeler directement ses méthodes.
Exemple : td1-4.html (à ouvrir dans un navigateur)
<script>
"use strict";
function Fruit(type, color) {
this.type = type;
this.color = color;
}
Fruit.prototype.setColor(color) {
this.color = color;
}
var b = new Fruit("banane", "jaune");
console.log(b);
b.setColor("verte");
console.log(b);
b.shape = "sphere";
b.setType = function(type) { this.type = type};
b.setType("orange");
console.log(b);
</script>
Résultat :
Fruit {type: 'banane', color: 'jaune'}
Fruit {type: 'banane', color: 'verte'}
Fruit {type: 'orange', color: 'verte', shape: 'sphere', setType: ƒ}
Manipulations dans la console :
- taper Fruit; Cela affiche bien le contenu, donc le code de la fonction.
- taper Fruit.prototype; Cela affiche { setColor: f, constructor: f } mais avec une flèche à gauche.
- En cliquant sur la flèche, on affiche le contenu détaillé. On voit un champ "caché" nommé [[ Prototype ]]. Si on clique sur sa flèche, on déroule un niveau de la chaîne d'héritage : on obtient le prototype de Object dont hérite Fruit.prototype. ce qui permet d'accéder aux fonctions définies dans celui-ci : valueOf(), isPrototypeOf(), ...
- taper Fruit.prototype.constructor; Cela affiche bien le code de Fruit.
- taper b; Cela affiche bien l'objet créé. En cliquant sur les flèches, on déroule la chaîne d'héritage des prototypes.
- taper b.__proto__; Cela affiche directement le prototype hérité de Fruit. On remarque que l'ajout de shape et setType() comme membres de b n'a eu aucun effet sur le prototype de b,
- taper Fruit("a","b"); On obtient une erreur. C'est normal car en appelant cette méthode directement, il n'y a pas d'objet courant donc this vaut undefined.
2.1.2°/ avec class & extends
- La méthode précédente n'est pas top du point de vue lisibilité et clarté, et c'est encore plus vrai lorsque l'on veut créer des prototypes qui héritent d'autres prototypes (cf. l'exemple https://developer.mozilla.org/fr/docs/Learn/JavaScript/Objects/Inheritance)
- C'est pourquoi, il est conseillé d'utiliser les mot-clé class et extends qui ne changent rien au modèle d'héritage par prototype mais qui permet d'avoir un code lisible.
- Il existe plusieurs utilisations de ces mot-clés mais la plus "Java-like" est la déclaration de classe
Exemple : td1-5.html (à ouvrir dans un navigateur)
<script>
"use strict";
class Rectangle {
constructor(largeur, hauteur, z) {
this.largeur = largeur;
this.hauteur = hauteur;
this.z = z;
}
perimetre() {
return 2*(this.largeur + this.hauteur);
}
get area() {
return this.largeur*this.hauteur;
}
set up(inc) {
this.z += inc;
}
set down(dec) {
this.z -= dec;
}
}
class Carre extends Rectangle {
constructor(cote,z) {
super(cote,cote,z);
}
perimetre() {
return 5*this.largeur; // calcul faux pour vérifier la redef.
}
}
var r = new Rectangle(5,10,0);
var c = new Carre(4,3);
</script>
Manipulations dans la console :
- taper c.perimetre(); Cela affiche bien 20, car on appelle la méthode redéfinie dans Carre.
- taper r.area; La méthode est un accesseur donc on utilise directement son nom, sans parenthèses (NB : si on tape r.area() on obtient une erreur)
- taper c.area; Cela affiche bien 16, signe qu'on a hérité de l'accesseur.
- taper c.up = 5; puis c.z; Cela affiche 8, signe que le mutateur up() a bien incrémenté l'attribut z.
Remarques :
- Il ne doit y avoir qu'une seule méthode constructor : pas de surcharge possible.
- La possibilité de déclarer des attributs public/privé est pour l'instant "expérimentale" et n'est donc pas supportée par tous les navigateurs.
- Au sein de la classe, les attributs et méthodes doivent obligatoirement être précédés de this.
- Comme en Java, on peut utiliser static pour créer une "fonction de classe". On ne pourra donc l'appeler qu'avec NomClasse.NomFct()
- On peut hériter d'une seule autre classe.
- On peut redéfinir des méthodes dans les sous-classes.
- On peut utiliser les accesseurs get et set.
3°/ Syntaxes spéciales
- l'opérateur ... permet de de décomposer un tableau en une liste d'éléments, par exemple lorsque l'on veut incorporer un tableau dans un autre, pour les appels de fonctions, ou encore lors d'une affectation par décomposition.
- il est possible de décomposer les éléments d'un tableau ou d'un objet pour les affecter à plusieurs variables, le tout en une seule instruction. Pour cela, il suffit d'utiliser les [ ] (pour les tableaux) ou les { } (pour les objets) à gauche d'un =.
Exemple : td1-6.html (à ouvrir dans un navigateur)
<script>
"use strict";
function sum3(x,y,z) {
return x+y+z
}
let tab1 = [ 2,3,4 ];
let tab2 = [ 1, ...tab1, 5, 6 ];
console.log(tab2);
console.log(sum3(...tab1));
let obj = {a:"12",b:"allo",c:true,d:[2,7]};
// decomposition tableau
let [x,y] = tab2;
console.log(x+" "+y);
let [,t,,u,...right] = tab2
console.log(t+" "+u);
console.log(right);
// decomposition objet
// on extrait a et c séparement, et le reste format un objet reste
let {a,c, ...reste} = obj;
console.log(a+" "+c);
console.log(reste);
</script>
4°/ Les fonctions fléchées
4.1°/ contexte
- Comme bon nombre d'autres langages, JS a intégré des concepts de programmation fonctionnelle.
- Par exemple, on peut avoir des fonctions :
- qui prennent en paramètre des fonctions,
- qui renvoient une fonction
Exemple : td1-7.html (à ouvrir dans un navigateur)
<script>
"use strict";
function fun1(msg) {
console.log("fun1 dit: "+msg);
}
function fun2() { // fun2 renvoie un fonction anonyme
return function(msg) {
console.log("anon fun dit: "+msg);
}
}
function fun3(f,msg) { // fun3 prend en paramètre une fonction f
f(msg)
}
fun1("hello"); // appel direct a fun1
fun2()("salut"); // appel a fun2() qui renvoie la fonction anonyme,
// que l'on appelle avec salut en paramètre
fun3(fun1,"bonjour"); // idem en passant par fun3
fun3(fun2(),"hi"); // appel fun2() qui renvoie une fonction, qui sert dans fun3
</script>
- Ces possibilités sont d'autant plus pratiques que l'on peut définir ces fonctions paramètre/retour grâce à des lambda-expression, ou en jargon JS, des fonctions fléchées.
4.2°/ syntaxe
- La syntaxe est similaire a celle de Java, excepté qu'il n'y a jamais de type indiqué.
- de base, une fonction fléchée est définit par : (param1, param2, ...) => { instructions }
- comme en Java, il y a les raccourcis suivants :
- si un seul paramètre, pas besoin de parenthèses : param => { instructions }
- si une seul instruction, pas besoin d'accolade + return automatiquement ajouté devant l'instruction : (param1, param2, ...) => instruction.
- contrairement à Java, il n'y a pas de concept d'interface fonctionnelle. Une fonction fléchée est une fonction, donc un objet que l'on peut associer à une variable. Généralement, on déclare une telle variable constante.
- cependant, on utilise souvent directement une fonction fléchée comme paramètre, sans passer par une variable.
Exemple : td1-8.html (version fléchée de l'exemple précédent)
<script>
"use strict";
const fun1 = msg => console.log("fun1 dit: "+msg)
const fun2 = () => msg => console.log("anon fun dit: "+msg)
/* NB pour être moins déroutant, on pourrait écrire
const fun2 = () => { return msg => console.log("anon fun dit: "+msg) }
*/
const fun3 = (f, msg) => f(msg)
fun1("hello");
fun2()("salut");
fun3(fun1,"bonjour");
fun3(fun2(),"hi");
fun3(s => { // on définit le code de la fonction paramètre de fun3
// s est le paramètre de cette fonction
s = s.slice(3); // on enlève les 3 premiers caractères de s
fun2()(s); // on appelle fun2(), qui retourne une fonction,
// que l'on appelle avec s modifié en paramètre !!
},"au revoir")
</script>
4.3°/ Cas d'utilisation sur les tableaux
- La classe Array contient beaucoup de fonctions qui prennent en paramètre une fonction.
- Cela permet notamment de trouver, filtrer, ordonner, traiter ... selon des critères variables.
Exemple : td1-9.html (à ouvrir dans un navigateur)
<script>
"use strict";
const log = obj => console.log(JSON.stringify(obj)); // ma propre fct de log pour les objets
class Rectangle {
constructor(largeur,hauteur) {this.largeur = largeur; this.hauteur = hauteur}
perimetre() { return 2*(this.hauteur+this.largeur) }
}
const tab1 = [1,5,3,10,6,9]; // array d'int
const tab2 = [];
tab2.push(new Rectangle(2,3)); tab2.push(new Rectangle(4,2)); tab2.push(new Rectangle(5,6));
console.log(tab1.find(e => e > 6)); // premier trouvé dans tab1 > 6
log(tab2.filter(r => r.perimetre() > 10)); // tous ceux dont le perimetre > 10
tab2.forEach(r => {
if (r.largeur > r.hauteur) console.log("plat");
else console.log("élevé");
}
);
log(tab1.sort()); // sans fct de comparaison => ATTENTION, ordre lexicographique !!
log(tab2.sort((a,b) => a.hauteur - b.hauteur));
</script>
4.4°/ Pièges avec les fonctions fléchées.
- Le piège le plus important est qu'une fonction fléchée n'a pas d'attribut this propre. Pourtant, on peut utiliser this dans une fonction fléchée.
- Dans ce cas, JS essaie de trouver this dans la fonction où est définie la fonction fléchée. S'il n'y a pas de this, alors JS essaie de trouver dans la fonction au-dessus, etc.
- S'il ne trouve rien, alors this vaut undefined pour la fonction fléchée.
- C'est pour cela que l'on ne définit jamais une méthode de prototype avec une fonction fléchée.
- IMPORTANT : une fois this déterminé, il ne changera plus jamais de valeur, même si on change de contexte d'exécution. Cela peut être à la fois une contrainte, comme un avantage, notamment lorsque l'on retourne une fonction fléchée qui contient un this (cf. exemple ci-dessous)
- Il est impossible d'interrompre une boucle faite avec un forEach(e => { ... } )
- En effet, si on fait un return/break dans la fonction fléchée, cela va juste interrompre la fonction fléchée.
- Le seul moyen d'arrêter un parcours de tableau, c'est de le faire avec une boucle for classique.
Exemple : td1-10.html
<script>
"use strict";
const log = obj => console.log(JSON.stringify(obj)); // ma propre fct de log pour les objets
class Polygone {
constructor(points) { this.points = points;}
badFirstCloseTo(point, eps) {
this.points.forEach(p => {
if (((p.x-point.x)**2 + (p.y-point.y)**2) < eps) return p;
});
}
goodFirstCloseTo(point, eps) {
for(let i=0; i<this.points.length;i++) {
let p = this.points[i];
if (((p.x-point.x)**2 + (p.y-point.y)**2) < eps) return p;
};
}
goodIndirectPrint() {
return () => log(this.points); // this sera trouvé dans indirectPrint() et fixé une fois pour toute
}
badIndirectPrint() {
return function() { log(this.points);}
}
}
// définition d'une méthode de proto. avec une fonciton fléchée = NOT GOOD !
Polygone.prototype.badPrint = () => log(this.points)
let p = new Polygone([{x:1,y:1},{x:5,y:1},{x:5,y:2}])
log( p.goodFirstCloseTo({x:1.1,y:1.1},1) ) // affiche bien {x:1,y:1}
log( p.badFirstCloseTo({x:1.1,y:1.1},1) ) // affiche undefined
p.badPrint(); // affiche undefined : this n'est pas trouvé car aucune méthode englobante.
p.goodIndirectPrint()(); // ok : p.goodIndirectPrint() renvoie une fonction fléchée dont this a déjà été fixé.
p.badIndirectPrint()(); // erreur : p.badIndirectPrint() renvoie une fonction normale qui n'a pas d'objet englobant
</script>