Dans Javascript un objet est une collection non ordonnée de couples clé/valeur, une valeur pouvant être une donnée classique ou une fonction. En fait un objet est une table de hachage puisqu’on a un tableau associatif. On accède à une valeur particulière à l’aide de sa clé.
Création d’un objet
Classiquement on crée un objet ainsi :
const identite = new Object() identite.nom = 'Dupont' identite.prenom = 'Jacques' identite.nationalite = 'française' identite.writeNom = function() { console.log(this.nom + ' ' + this.prenom) } console.log(identite.nom) // "Dupont" console.log(identite['prenom']) // "jacques" identite.writeNom() // "Dupont Jacques"
Ici on crée l’objet identite qui a 3 propriétés et une méthode. C’est une syntaxe lourde qui a sévi très longtemps. Maintenant on peut utiliser la syntaxe littérale :
const identite = { nom:'Dupont', prenom: 'Jacques', nationalite: 'française', writeNom() { console.log(this.nom + ' ' + this.prenom) } } identite.writeNom() // "Dupont Jacques"
C’est plus léger et explicite avec le même résultat !
Les propriétés des valeurs
Les propriétés sont par défaut énumérables et modifiables mais il peut arriver que l’on désire qu’une propriété particulière soit par exemple en lecture seule. On utilise alors defineProperty :
const identite = {} Object.defineProperty(identite, 'nom', { value: 'Dupont', writable: false }) console.log(identite.nom) // "Dupont" identite.nom = 'Durand' console.log(identite.nom) // "Dupont"
Ici la propriété nom est en lecture seule.
Si plusieurs propriétés sont concernées on utilise defineProperties :
const identite = {} Object.defineProperties(identite, { nom: { value: 'Dupont', writable: false }, prenom: { value: 'Denis', writable: false } }) console.log(identite.nom) // "Dupont" identite.nom = 'Durand' console.log(identite.nom) // "Dupont" console.log(identite.prenom) // "Denis" identite.prenom = 'Pierre' console.log(identite.prenom) // "Denis"
Ici on a les deux propriétés nom et prenom en lecture seule.
Les accesseurs
Avec la syntaxe vue ci-dessus on accède directement aux valeurs des propriétés, ce qui n’est pas toujours souhaitable. Il est possible de définir une méthode get pour récupérer cette valeur. Lorsque cette méthode est définie dès qu’on veut lire la valeur d’une propriété on passe obligatoirement par elle. Considérons un objet simple :
const voiture = { marque: 'Peugeot', kms: 112000 } console.log(voiture.kms)
On va rendre la propriété kms inaccessible directement mais plutôt mettre en place un accesseur :
const voiture = { marque: 'Peugeot', kms_: 112000, get kms() { return this.kms_ + " kilomètres" } } console.log(voiture.kms) // "112000 kilomètres"
Classiquement on termine le nom d’une propriété non accessible par un caractère de soulignement. Une autre syntaxe possible est avec defineProperty :
const voiture = { marque: 'Peugeot', kms_: 112000 } Object.defineProperty(voiture, 'kms', { get() { return this.kms_ + " kilomètres" } }) console.log(voiture.kms) // "112000 kilomètres"
Les mutateurs
Ce que j’ai dit ci-dessus pour l’accès aux propriétés est aussi valable pour leur écriture. Mais cette fois on définit une méthode set :
const voiture = { marque: 'Peugeot', kms_: 112000, get kms() { return this.kms_ + " kilomètres" }, set kms(value) { this.kms_ = parseInt(value) } } console.log(voiture.kms) // "112000 kilomètres" voiture.kms = '98000 kms' console.log(voiture.kms) // "98000 kilomètres"
Ici aussi on peut utiliser defineProperty :
const voiture = { marque: 'Peugeot', kms_: 112000, get kms() { return this.kms_ + " kilomètres" } } Object.defineProperty(voiture, 'kms', { set (value) { this.kms_ = parseInt(value) } }) console.log(voiture.kms) // "112000 kilomètres" voiture.kms = '98000 kms' console.log(voiture.kms) // "98000 kilomètres"
Améliorations avec ES6
ES6 a apporté des améliorations plutôt utiles. Par exemple on peut initialiser les propriétés avec des variables existantes :
const marque = 'Peugeot' const voiture = { marque, kms: 112000 } console.log(voiture.marque) // "Peugeot"
On peut aussi avoir un nom de propriété déterminé par une expression. Il suffit d’utiliser des crochets :
const base = 'moteur' const voiture = { [base + 'Cylindree']: 1500 } console.log(voiture.moteurCylindree) // 1500
On peut aussi fusionner des objets avec Object.assign :
const voiture1 = { marque: 'Renault', kms: 150000 } const voiture2 = { marque: 'Peugeot', places: 5 } const result = {} Object.assign(result, voiture1, voiture2); console.log(result) // { // kms: 150000, // marque: "Peugeot", // places: 5 // }
Le modèle objet
Le modèle objet de Javascript ne ressemble pas aux modèles classiques. Avec Javascript on utilise des fonctions spécialisées pour créer des objets : les constructeurs. Ils permettent d’instancier des objets. Cette instanciation est un peu particulière puisque les fonctions ne sont pas copiées dans l’instance. Voici un exemple de constructeur :
function Identite(nom, prenom, naissance) { this.nom = nom this.prenom = prenom this.naissance = new Date(...naissance) this.nomComplet = function() { console.log(this.nom + ' ' + this.prenom) } }; const identite1 = new Identite('Foster', 'Robert', [1985, 6, 23]) const identite2 = new Identite('Wolpor', 'Simon', [1990, 10, 7]) console.log(identite1 instanceof Object); // true console.log(identite1 instanceof Identite); // true console.log(identite2 instanceof Object); // true console.log(identite2 instanceof Identite); // true
Par convention le nom d’un constructeur commence par une majuscule.
Donc avec Javascript les classes sont des fonctions, et on sait que les fonctions sont des objets puisque tout est objet.
Une notion pas trop évidente avec Javascript est celle de prototype, elle est pourtant essentielle pour bien comprendre ce modèle objet. Avec le code vu ci-dessus on peut utiliser la méthode nomComplet sur une instance :
identite1.nomComplet() // "Foster Robert"
Là on fait appel en fait à une méthode du prototype. Mais qu’est-ce que ça veut dire ?
Une fonction en Javascript a une propriété particulière nommée prototype. Pour les fonctions classique ça ne sert à rien mais pour un constructeur ça devient très utile. Quand on utilise une méthode sur un instance en fait cette méthode se trouve dans le prototype du constructeur. L’instance a accès à cette méthode.
Analysons une de ces instances :
On voit que la l’instance comporte une propriété __proto__. C’est là que se situe l’accès au prototype. On y trouve le constructeur Identite. Si on regarde encore on voit que le constructeur a lui aussi une propriété __proto__ parce qu’il découle d’Objet. On a ainsi une chaîne de prototypes. Lorsque qu’on appelle une méthode on remonte la chaîne de prototypes jusqu’à trouver cette méthode.
Si on se concentre sur la méthode nomComplet :
On voit bien qu’elle se situe dans le constructeur. et qu’on y accède par le prototype. Évidemment si on définit dans l’instance une méthode avec le même nom elle surcharge celle du prototype.
Le modèle objet revisité
Avec ES6 on dispose d’une syntaxe qui se rapproche de celle en général utilisée mais il ne faut jamais oublier que sous le capot c’est toujours les prototypes qui sont utilisés. On peut désormais créer une classe avec le mot clé Class :
class Identite { constructor(nom, prenom, naissance) { this.nom = nom this.prenom = prenom this.naissance = new Date(...naissance) } get nomComplet() { return this.nom + ' ' + this.prenom } set nomComplet(value) { let s = value.split(' ') this.nom = s[0] this.prenom = s[1] } } const identite1 = new Identite('Foster', 'Robert', [1985, 6, 23]) console.log(identite1.nomComplet) // "Foster Robert" identite1.nomComplet = "Durand Albert" console.log(identite1.nomComplet) // "Durand Albert"
On voit qu’on peut mettre en place des accesseurs et des mutateurs.
On peut hériter d’une classe avec extends :
class Identite { constructor(nom, prenom, naissance) { this.nom = nom this.prenom = prenom this.naissance = new Date(...naissance) } } class Employe extends Identite { constructor(nom, prenom, naissance, emploi) { super(nom, prenom, naissance) this.emploi = emploi } } const employe1 = new Employe('Foster', 'Robert', [1985, 6, 23], 'electricien') console.log(employe1.emploi) // "electricien" console.log(employe1.nom) // "Foster"
Le mot clé super invoque le constructeur de la classe mère.
On ne peut pas hériter de plusieurs classes comme dans certains langages et on ne dispose pas non plus d’interfaces. Toutefois Javascript est un langage suffisamment souple pour inventer des solutions :
class Identite { constructor(nom, prenom) { this.nom = nom this.prenom = prenom } } function makeEmploye(i) { i.addEmploi = function(emploi) { this.emploi = emploi } } const identite1 = new Identite('Foster', 'Robert') makeEmploye(identite1) identite1.addEmploi('garagiste') console.log(identite1.emploi) // "garagiste"
Conclusion
On a vu que le modèle objet de Javascript est un peu exotique pour ceux qui sont habitués à des langages plus classiques. Il permet toutefois des choses que ne permettent pas les autres langage grâce à sa souplesse.