Programmation Javascript (NFA041)

Bases de programmation illustrées en javascript,

Les Objets ( partie 1 )

Olivier Pons
(olivier.pons@lecnam.net)

2022

Les objects

Le type objet est LE type fondamental de javascript :

On a des :

Un objet c'est:

Une valeur composées qui regroupe plusieurs valeurs (primitive ou autres objets)

Une collection non ordonnée de propriétés

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.

Creation d'objets 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,
         }
       };

Remarque:

Ceux qui connaissent auront remarqué la similitude avec le format d'échange de données JSON qui est inspiré des objets JS.

Accès aux propriétés:

On a 2 façons d'acceder aux propriétés d'un objet:

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 :

let a= point1.x;
console.log(a);
let b=point1["y"];
console.log(b)
point1.x=5;
console.log(point) //affichage par default de l'objet liste ses propriété 
                   // on verra que certaines peuvent être cachées
                   // on ne pas apparaître

Lister les propriétés et leur valeurs

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 ...)

> for (var i in livre1){
  console.log("clef :"+i+ " , valeur"+ livre1[i]);
}
clef :titre , valeurjavascript
clef :sous titre , valeurfacile !
clef :auteur , valeur[object Object]
undefined
> 

Les objets sont dynamiques

On peut ajouter ou supprimer des propriétés.

point1.z=2;
console.log(point1)
delete point1.z;
console.log(point1)

Remarque

Bien que typeof null réponde object, il représente l'absence de valeur d'un objet. C'est une valeur primitive de javascript.

Les propriété peuvent être des fonctions:

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

let personne1={
  nom:"olivier",
  age:"20",
  grandir: function(){this.age++} 
}

Attention

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) .

> let personne1_pb={
...   nom:"olivier",
...   age:"20",
...   grandir: function(){age++}
... }
undefined
> personne1_pb.grandir()
Uncaught ReferenceError: age is not defined
    at Object.grandir (REPL5:4:23)
> 

Object et mémoire (1)

Comme pour les tableaux (qui sont des objects) on manipule des références !

let origine={x:0,y:0}

let cercle1={
  centre:origine,
  rayon:7;
}

représentation memoire

Exemple d'objet (un peu) plus complexe

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} /
  }

insuffisance de cette declaration

  1. On a envie de dire que centre est un Point dans les 2 cas.
  2. on n'a pas envie de recopier le code de circonférence dans tout les objets représentant un cercle.

Pour cela in faut un moyen de

  1. avoir un moyen de créer facilement des objets « de la même forme » (fonctions , constructeur , classes ...)
  2. avoir un moyen de « partager du code » (prototype, classes(qui masquent le prototype)... )

Créer des objects (1)

Exemple des Points:

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 p1et 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'
]
> 

representation mémoire

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)

> Object.getPrototypeOf(p1)===prototypeDePoint
true
> 

représentation memoire

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.

> p1.distance(p2)
5

On peut dynamiquement modifier le prototype (cela modifie aussi le comportement des objets définis précédemment).

prototypeDePoint.estOrigine=function (){return this.x===0 && this.y === 0}

Maintenant on peut appeler cette méthode sur p1 ou p2

> p2.estOrigine()
false
> 

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.

représentation mémoire

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.

  1. Ce que nous venons de faire manuellement peut se faire en javascript grace aux constructeur

  2. Pour se rapprocher de la programmation objet traditionnelle (comme en java, C++ ...) javascript a récemment introduit les classes

Constructeur

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);
> p5
Point { x: 2, y: 3 }
> 

Cela ressemble beaucoup a ce qu'on avait précédemment sauf

  1. on utilise le mot clef new
  2. il se passe des choses sous le capot.
    Le prototype est défini.

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:

  1. il creer un object
  2. il récupère la propriété nommée prototype du constructeur et la positionne comme prototype de l'objet
  3. il lie this a l'objet crée
  4. il applique la fonction (constructeur) avec ses arguments
  5. il renvoie l'objet (ou le résultat de la fonction constructeur si elle renvoie quelque chose)

Exercice

On 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

Object natif

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                 
...
          

Les classes

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.

Comparaison d'objet

let o1={x:0,y:0}
let o2={x:0,y:0}

> o1===o2
false

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.

let cercle1={origine:o1,rayon:1}
let cercle2={origine:o2,rayon:1}

function estEgaleCercle(c1,c2){
  return estEgalePoint(o1,o2) && c1.rayon===c2.rayon;
}

> estEgaleCercle(cercle1,cercle2)
true
> >

Un point complet sur la comparaison d'objet (en anglais)

Sérialisation

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

  1. JSON.stringify: qui transforme l'objet en chaîne de caractères
  2. JSON.parse:qui transforme la chaîne en objet
> JSON.stringify(cercle1)
'{"origine":{"x":0,"y":0},"rayon":1}'
> let chaine=JSON.stringify(cercle1)
undefined
> let cercle3=JSON.parse(chaine)
undefined
> cercle3
{ origine: { x: 0, y: 0 }, rayon: 1 }
 
> cercle3===cercle1
false
> estEgaleCercle(cercle3,cercle1)
true
> 

Les exceptions

Il 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.

function premierEl(t){
  if(t.length<1){throw new Error("tableau vide");}
  return t[0];
}

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
>