Bases de programmation illustrées en javascript,
Les Objets ( partie 1 )
Olivier Pons
(olivier.pons@lecnam.net)
2022
On a des :
les types de base: number, string, booléen, undefined ,
Symbol
qui ont tous, sauf undefined
, des « Wrapper » objet :
Number
, String
, Boolean
les objets natifs : Array
, Function
,
Regexp
, Math, Error, JSON, Date ...
les objets définis par l'environnement (hôtes), (c'est à dire par
le Navigateur
(exemple document
) ou par nodejs (exemple
global
)
les objects définis par l’utilisateur (ou des bibliothèques)
chacune a un nom
et une valeur
. On a
des donc des couples qui associent des chaines de caractères (les noms)
a des valeurs.
Les noms sont appelés les clefs (keys).
Une telle collection de paires s'appelle aussi dictionnaire, tableau associatif, hashtable ...
JavaScript propose de nombreuses façons différentes de créer et d'utiliser des objets et il y beaucoup d'ajout récents.
Nous allons pour l'instant la creation d'object littéraux.
let vide = {} // un object sans propriété
let point1 = {x:0, y:0}; // ici on 2 propriétés x et y de type number
// et de valeur 0
let personne1={
nom : "olivier", //3 propriétés de type different
age : 20,
francophone : true;
}
let livre1 = {
"titre" : "javascript", // on peut mettre des guillemets autour des clefs
"sous titre" : "facile !", // on doit s' il y a un espace
auteur : { // la valeur d'une propriété peut être un objet
nom : "Dupond",
prenom : "D." //pas de , après le dernier,
}
};
Ceux qui connaissent auront remarqué la similitude avec le format d'échange de données JSON qui est inspiré des objets JS.
On a 2 façons d'acceder aux propriétés d'un objet:
monObjet.nomDuChamp
monObjet["nomDuChamp"]
La premiere est la plus courante, mais la seconde est obligatoire si le nom de champs contient des espaces ou si la clef est composée de chiffre(s).
Cela permet de :
A la différence des tableaux ou les clefs sont des entiers, ici, on
ne connaît pas les clefs, on ne peut donc faire une boucle
for
(ou while
) classique.
On va utiliser la boucles for (var ... in ...)
On peut ajouter ou supprimer des propriétés.
Bien que typeof null
réponde object
, il
représente l'absence de valeur d'un objet. C'est une valeur primitive de
javascript.
let milou= {
//on associe a aboyer une valeur qui est une fonction anonyme
aboyer:function (){console.log("Wouaf Wouaf !!! ")}
}
milou.aboyer()
C'est plus intéressant si la fonction peut faire référence à d'autres propriétés de l'objet
le mot clef this
est fondamental, il
représente l'objet;
donc ici this.age
sera personne1.age
.
Plus précisément c'est au moment de l'appel de
fonction (ici grandir
) que this
est
lié à l'objet appelant
On peut tester grandir
:
> console.log(personne1.age)
20
undefined
> personne1.grandir() // personne1 est liés a this
undefined // donc le this.age de la fonction
> console.log(personne1.age) // fait reference à l'ge de personne1
21
undefined
Si on omet le this on peut définir l'objet mais lors
de l'appel de grandir
on aura une erreur disant que
age
n'est pas défini (sauf si age
a été défini
comme variable globale a l’extérieur de la fonction) .
Comme pour les tableaux (qui sont des objects) on manipule des références !
Autres exemples d'objets avec des propriétés dont une valeur est une fonction.
Définissons 2 cercles
let cercle1 ={
centre :{
x:0,
y:0
},
rayon:3,
circonference:function (){return 2* Math.PI * this.rayon}
}
et
let cercle2 ={
centre :{
x:1,
y:2
},
rayon:4,
//
circonference:function (){return 2*Math.PI* this.rayon} /
}
centre
est un Point dans les 2
cas.circonférence
dans tout les objets représentant un cercle.Pour cela in faut un moyen de
On définie une représentation des points 2D.
function creerPoint(x,y){
return {
x:x,
y:y,
distance : function (p){return Math.sqrt(
(this.x - p.x)*(this.x - p.x) +
(this.y- p.y)*(this.y - p.y))
}
}
}
> let p1=creerPoint(0,0)
undefined
> let p2=creerPoint(3,4)
undefined
> p1.distance(p2)
5
>
Mais distance est dupliquée dans p1
et
p2
.
Pour éviter cela, l'idée est de mettre la propriété
distance
dans un object particulier auquel
p1
et p2
délégueront quand on
leur demandera la propriété distance
.
Cela se fait à travers la notion de prototype.
Tout objet a un object prototype (ou un prototype
null
si on le met explicitement). Les objets littéraux ont
par default le même prototype (Object.prototype
)
On peut le voir comme une propriété cachée.
Quand on essaye d'acceder à une propriété d'un objet, s'il ne l'a pas il la demande à son object prototype. si le prototype l'a il la fournit, sinon il la demande à son propre prototype et ainsi de suite...
Pour partager le code, l'idée est donc de mettre les propriétés qui sont des fonctions et qu'on appellera alors des méthodes, dans le prototype.
Ainsi tout les objets qui auront le même prototype partageront toutes les propriétés (notamment les méthodes) contenues sur ce prototype.
On peut (sous certaines conditions!) acceder au prototype lui même
grace à la propriété (en fait c'est un accesseur pas une propriété mais
c'est sans importance ici) __proto__
. Mais il est plus
propre de le faire avec Object.getPrototypeOf
et
Object.setPrototypeOf
. On notera que modifier le prototype
ailleurs qu'a la création d'objet est coûteux et déconseillé dans du «
vrai code »
function creerPoint(x,y){
return {
x:x,
y:y
}
}
let p3=creerPoint(6,7);
> p3
{ x: 6, y: 7 } // les propriete de p3 :x et y
> p3.toString() //on peut appeler toString bien qu'on ne l'ai pas défini sur p3
'[object Object]' // car elle existe sur son prototype
> p3.toto() // appeler toto fait une erreur car il n'existe pas
// sur le prototype
Uncaught TypeError: p3.toto is not a function
> let prototypeDeP3=Object.getPrototypeOf(p3) // on récupère le prototype de p3
undefined
> prototypeDeP3
[Object: null prototype] {}
> prototypeDeP3===p3.__proto__ // on a la meme chose avec __proto__
true
> Object.getOwnPropertyNames(p3) // on demande la liste des propriétés de p3
[ 'x', 'y' ] // cachées (non-enumerable) ou visibles
> Object.getOwnPropertyNames(prototypeDeP3) // idem sur le prototype
[
'constructor',
'__defineGetter__',
'__defineSetter__',
'hasOwnProperty',
'__lookupGetter__',
'__lookupSetter__',
'isPrototypeOf',
'propertyIsEnumerable',
'toString', // le prototype a bien une méthode toString
'valueOf',
'__proto__',
'toLocaleString'
]
>
Mais tous les objets littéraux partagent le meme prototype;
C'est celui de la propriété Object.prototype
> Object.getPrototypeOf(p3)===Object.prototype
true
> Object.getPrototypeOf({})===Object.prototype
true
> Object.getPrototypeOf({a:2})===Object.prototype
true
>
Si on rajoute des méthodes pour les points, on les rajoute sur tout les objets littéraux.
On voudrait un prototype spécifique pour les Points.
On peut créer un objet avec un prototype particulier grace à
Object.create
;
let prototypeDePoint={
distance : function (p){return Math.sqrt(
(this.x - p.x)*(this.x - p.x) +
(this.y- p.y)*(this.y - p.y))
}
}
function creerPoint(x,y){
let o=Object.create(prototypeDePoint); // on creer un object dont le prototype est prototypeDePoint
o.x=x;
o.y=y
return o;
}
let p1=creerPoint(0,0);
let p2=creerPoint(3,4);
On peur verifier que prototypeDePoint
est bien le
prototype de p1
(et p2
)
Maintenant si on demande la méthode distance
qui n'est
pas sur l'objet p1
il va la chercher sur son prototype,
prototypeDePoint
et à l'application de la fonction,
this
est lié a l'objet appelant donc ci-dessous
p1
.
On peut dynamiquement modifier le prototype (cela modifie aussi le comportement des objets définis précédemment).
Maintenant on peut appeler cette méthode sur p1
ou
p2
On peut creer de nouveaux objets, points
particuliers
comme des points colorés
function creerPointColore(x,y,c){
let o=Object.create(creerPoint(x,y));
o.couleur=c;
return o;
}
> let prouge=creerPointColore(4,5,"rouge")
undefined
> prouge.x
4
> prouge.distance(p1)
6.4031242374328485
>
On dit que PointColore
hérite de Point
.
Tout PointColore
est aussi un Point
mais plus
spécifique.
Ce mécanisme délégation est très puissant mais parfois déroutant pour le débutant ou pour le programmeur venant d'autre langage notamment java.
Ce que nous venons de faire manuellement peut se faire en javascript grace aux constructeur
Pour se rapprocher de la programmation objet traditionnelle (comme en java, C++ ...) javascript a récemment introduit les classes
Un constructeur est une fonction qui (utilisée avec
le mot clef new
) va nous permettre (entre autre) de créer
des objets avec les mêmes propriétés.
//constructeur de Point (par convention ils commence par une Majuscule !)
function Point(x, y) {
this.x = x;
this.y = y;
}
//creation de 2 Object point
p5 =new Point(2,3);
p6 =new Point(4,3);
Cela ressemble beaucoup a ce qu'on avait précédemment sauf
new
Remarquons d'abord que les fonctions sont des objets et ont toutes
une propriété nommée prototype
qui contient un objet (vide
par défaut).
> Point.prototype
{}
// attention ce n'est pas le prototype de la fonction
> Point.prototype ===Object.getPrototypeOf(Point)
false
Ce champs contient un object qui servira de prototype aux objets qui seront crées par le constructeur.
//avec notre premier fonction creerPoint
//on avait un point p1
Object.getPrototypeOf(p1)
[Object: null prototype] {}
//ici
> Object.getPrototypeOf(p5)
{}
si on veut ajouter une propriété à tout les points, il faut l'ajouter
à la propriété prototype
du constructeur (la fonction)
>Point.prototype.estOrigine=function(){return (this.x === 0 && this.y ===0)}
// on peut maintenant appliqué estOrigine à p5
> p5.estOrigine()
false
>
// la propriété prototype de Point a changé
> Point.prototype
{ estOrigine: [Function (anonymous)] }
// le Prototype de Point n'a pas changé
> Object.getPrototypeOf(Point)
{}
>
>Point.prototype === Object.getPrototypeOf(p5)
true
> Object.getPrototypeOf(p6)
{ estOrigine: [Function (anonymous)] }
>
Que fait news:
prototype
du
constructeur et la positionne comme prototype de l'objetOn peut à titre titre d'exercice définir notre propre new sous forme de fonction.
function monNews(constructeur,...arguments){
//1 et 2
let o=Object.create(constructeur.prototype);
//3 on lie this a o
let constructeurLie=constructeur.bind(o);
//... pour transformer le tableau en list d'argument
//4
let res=constructeurLie(...arguments);
return (undefined!=res)?res:o
}
On aurait pu faire 3 et 4 en utilisant la propriété
apply
du constructeur
il y a aussi des constructeurs pour les objets natifs:
let a= new Object(); // equivalent a: let a={}
let b= new Array(2,5,4); // equivalent a: let b=[2,5,4]
let c= new Number(10); // attention c'est un objet c'est different de
//let d=Number(10) qui est un number
//tester avec typeof !!
// on a c==10 mais pas c===10
let d=new Date() // Attention c'est un objet , different de
// Date() qui renvoi une chaine !
// la encore tester avec typeof
...
Pour se rapprocher de la programmation objet classique,
ECMAScript 2015 a introduit les classes.
Le mécanismes sous le capot est toujours à base de prototype mais cela
est masqué à l'utilisateur qui retrouve les notions de
classe et d'héritage de la programmation
object traditionnelle (en java, c++...)
//on définie la classe point
class Point{
//on declare les propriétés de la classe
x
y
//on définit le constructeur qui s’appelle toujours constructor
constructor(x,y){
this.x=x;
this.y=y;
}
//on définit une méthode distance
distance(p){return Math.sqrt(
(this.x - p.x)*(this.x - p.x) +
(this.y- p.y)*(this.y - p.y))
}
}
// on va étendre la classe Point pour pourvoir creer des points plus spécifiques,
// les points colorés. Il auront toutes les propriétés des Points (sauf si on les redéfinies)
// plus des propriétés spécifiques aux points colorés
class PointColore extends Point{
couleur
constructor(x,y,c){
super(x,y);
this.couleur=c
}
changerCouleur(c){this.couleur=c}
}
// on creer un point
let p9=new Point(0,0);
p9
// Point { x: 0, y: 0 }
// on creer un point coloré
let pc2=new PointColore(3,4,"rouge")
pc2
// PointColore { x: 3, y: 4, couleur: 'rouge'}
// un point coloré est un point particulier
// il a donc la ma méthode distance
pc2.distance(p9)
// 5
pc2.changerCouleur("vert")
pc2
// PointColore { x: 3, y: 4, couleur: 'vert' }
//Mais on ne peut pas appeler changerCouleur sur un point normal
p9.changerCouleur("vert")
//Uncaught TypeError: p9.changerCouleur is not a function
ECMAScript 2019 a aussi introduits les champs privés que nous détaillons pas ici.
les objets manipulent des références.
o1
et o2
font référencent à deux zones mémoire
différentes
ils ne sont donc pas égaux
On peut définir nous même la comparaison, en comparant les propriétés
function estEgalePoint(ob1,ob2){
return (ob1.x === ob2.x && ob1.y === ob2.y);
}
undefined
> estEgalePoint(o1,o2)
true
>
Mais si les propriétés font référence à des objets, on ne peut
utiliser ===
dans notre fonction de comparaison, il faut
avoir défini la comparaison sur ces objets avant.
L'idée est de pouvoir stoker et échanger des objects.
Pour cela, on leur associe une représentation
textuelle.
Attention elle ne prend pas en compte les fonctions !
On dispose de 2 méthodes pour passer d'une chaîne à un objet et réciproquement; Les chaines de caractère impliqué sont au format JSON
JSON.stringify
:
qui transforme l'objet en chaîne de caractèresJSON.parse
:qui
transforme la chaîne en objetIl y a des situations ou une fonction ne peut pas renvoyer le
résultat attendu.
Plutôt que de renvoyer une valeur erronée, elle peut alors lever
une exception pour signaler son problème.
L'exemple canonique est la fonction premierEl
qui
renvoie le premier élément d'un tableau.
Que faire si le tableau est vide.
Par default en javascript elle renverra undefined
mais cela
risque de fausser le résultat des calculs des fonctions appelantes.
function premierEl(t){
return t[0];
}
function sommePremier(tableauDeTableau){
let res=0;
for(let i=0;i<tableauDeTableau.length;i++){
res+=premierEl(tableauDeTableau[i])
}
return res;
}
function afficheSommePremier(tableauDeTableau){
console.log(sommePremier(tableauDeTableau));
}
afficheSommePremier([[1,2,3],[4,5,6,7],[8]]);
// 13
afficheSommePremier([[1,2,3],[],[4,5,6,7],[8]]);
// NaN
Le dernier appel à afficheSommePremier
produit
NaN
;
Normal car il y a un tableau vide qui n'a donc pas de premier
élément;
Plutôt que de propager le problème et d'avoir du mal à en
identifier la source.
On veut que premierEl
puisse signaler cette « situation
exceptionnelle »
On dispose du mot clef throw
qui permet de lever une exception
Les exceptions sont des objets. et on peut construire ses propres exceptions.
Maintenant dans un appel à afficheSommePremier
, si un
tableau vide est passé, une exception est levée, signalant un problème,
et renvoyant la pile d'appel qui permet d'en connaître la source.
> afficheSommePremier([[1,2,3],[],[4,5,6,7],[8]]);
Uncaught Error: tableau vide
at premierEl (REPL42:2:24)
at sommePremier (REPL12:4:10)
at afficheSommePremier (REPL19:2:15)
>
A chaque niveau on peut prévoir de rattraper l'exception pour la traiter ou de la laisser continuer vers le niveau suivant.
Ici on la traite au dernier niveau,
function afficheSommePremier(tableauDeTableau){
try{
console.log(sommePremier(tableauDeTableau));
}
catch(e){
console.log("attention pas de tableau vide" )
}
}
> afficheSommePremier([[1,2,3],[],[4,5,6,7],[8]]);
attention pas de tableau vide
undefined
>
Mais si on la rattrape avant par exemple dans
sommePremier
, on peut la traiter « plus efficacement».
function sommePremier(tableauDeTableau){
let res=0;
for(let i=0;i<tableauDeTableau.length;i++){
try{
res+=premierEl(tableauDeTableau[i])
}
catch(e){
//on ignore le tableau vide
}
}
return res;
}
> afficheSommePremier([[1,2,3],[],[4,5,6,7],[8]]);
13
undefined
>
Enfin on peut avoir plusieurs types d'exceptions et les traiter
différemment dans le bloc catch
, en orientant le choix avec
un if
function genereExp(n){
if(n===0){throw new Error("ZERO") }
if(n===1){throw new Error("UN") }
if(n===2){throw "DEUX" } //pas de constructeur
if(n==="ZZ"){throw new TypeError("ZZ") } //Autre constructeur
return n;
}
function rattrappe(n){
try{
console.log(genereExp(n))
}
catch(e){
if (e.message==="ZERO"){console.log("traitement du cas 0")}
if (e.message==="UN"){console.log("traitement du cas 1")}
if (e==="DEUX") {console.log("traitement du cas DEUX")}
if (e.name==="TypeError") {console.log("erreur de type")}
}
}
> rattrappe(5)
5
undefined
> rattrappe(1)
traitement du cas 1
undefined
> rattrappe(0)
traitement du cas 0
undefined
> rattrappe("ZZ")
erreur de type
undefined
> rattrappe(2)
traitement du cas DEUX
undefined
>