Le modèle objet

Le JavaScript utilise un modèle objet un peu particulier puisqu’il se base sur le principe du prototype. Il diffère de la plupart des autres langages de programmation qui utilisent la notion de classe. Depuis ES6, le mot-clé classe a été introduit pour faciliter la compréhension du code.

Créer des objets

Il existe deux façons de créer des objets en JavaScript : la déclaration littérale et l’utilisation d’une fonction de construction (appelée plus simplement constructeur).

La déclaration littérale

La déclaration littérale consiste à utiliser des accolades :

let utilisateur = {
    prenom: "David",
    nom: "Gayerie"
}

console.log(utilisateur.prenom, utilisateur.nom);

Si on désire ajouter des méthodes à l’objet, on peut utiliser les fonctions anonymes :

let utilisateur = {
    prenom: "David",
    nom: "Gayerie",
    print: function() {
        console.log(this.nom, this.prenom);
    }
}

utilisateur.print();

Le mot-clé this permet d’accéder aux attributs et aux méthodes de l’objet à l’intérieur d’une méthode.

Note

Depuis ES6, il est possible de déclarer directement la fonction sans passer par une affectation d’une fonction anonyme :

let utilisateur = {
    prenom: "David",
    nom: "Gayerie",
    print() {
        console.log(this.nom, this.prenom);
    }
}

utilisateur.print();

Le constructeur

Un constructeur est une fonction qui permet de créer un objet. Par convention ces fonctions commencent par une majuscule :

function Utilisateur() {
    this.prenom = "David";
    this.nom = "Gayerie";

    this.print = function() {
        console.log(this.nom, this.prenom);
    }
}

Pour créer un objet, il faut utiliser le mot-clé new :

let u = new Utilisateur;
u.print();

Un des intérêts d’un constructeur est d’utiliser des paramètres pour initialiser l’état de l’objet :

function Utilisateur(prenom, nom) {
    this.prenom = prenom;
    this.nom = nom;

    this.print = function() {
        console.log(this.nom, this.prenom);
    }
}

let u = new Utilisateur("David", "Gayerie");
u.print();

Note

En utilisant le principe de la fermeture (closure) sur les fonctions, il est possible de créer un état interne et privé pour un objet :

function Utilisateur(prenom, nom) {
    let _prenom = prenom;
    let _nom = nom;

    this.print = function() {
        console.log(_nom, _prenom);
    }
}

let u = new Utilisateur("David", "Gayerie");
u.print();

Dans l’exemple précédent, les variables _prenom et _nom sont locales à la fonction et ne seront pas visibles à travers la variable u.

Le prototype

Un constructeur possède un attribut prototype qui permet de stocker les méthodes d’un objet. Plutôt que d’utiliser le mot-clé this pour affecter des méthodes à la construction, on ajoute les méthodes au prototype.

function Utilisateur(prenom, nom) {
    this.prenom = prenom;
    this.nom = nom;

}

Utilisateur.prototype.print = function() {
    console.log(this.nom, this.prenom);
}

let u = new Utilisateur("David", "Gayerie");
u.print();

Nous verrons dans la section suivante que l’utilisation du prototype optimise la gestion mémoire.

Note

Tous les objets créés possèdent un attribut constructor qui permet d’accéder à la fonction de construction :

function MaClasse() {
}

let o = new MaClasse;
console.log(o.constructor === MaClasse); // Affiche true

La notion de propriété

Une propriété est accessible comme un attribut sauf que sa lecture ou sa modification sont réalisées par l’appel à une fonction. Il s’agit la plupart du temps d’une valeur calculée ou dérivée d’autres attributs de l’objet.

Pour une déclaration littérale d’un objet, on utilise les mots-clés get et set :

let utilisateur = {
    prenom: "David",
    nom: "Gayerie",

    get nom_complet() {
        return this.prenom + " " + this.nom;
    },

    set nom_complet(v) {
        let t = v.split(" ");
        this.prenom = t[0];
        this.nom = t[1];
    }
}

console.log(utilisateur.nom_complet); // affiche David Gayerie

utilisateur.nom_complet = "John Doe";
console.log(utilisateur.prenom, utilisateur.nom); // affiche John Doe

Il est également possible de définir dynamiquement une propriété sur un objet grâce à la méthode Object.defineProperty.

Note

Depuis ES6, il existe aussi la méthode Reflect.defineProperty.

L’héritage

En JavaScript, un objet peut être créé à partir d’une fonction de construction. Cette dernière est associé à un prototype qui est un objet représentant l’objet parent.

Par défaut, le prototype correspond à un objet qui ne contient que la propriété constructor qui désigne la fonction elle-même.

Pour créer un héritage, il suffit de modifier l’objet prototype pour lui assigner un objet parent.

function Parent() {
    this.nom = "parent";
}

function Enfant() {
}

Enfant.prototype = new Parent;

let e = new Enfant;
console.log(e.nom); // Affiche parent

Lorsqu’on accède à un attribut, une propriété ou une méthode, l’interpréteur commence par le rechercher dans l’objet et, s’il n’existe pas, il recherche récursivement dans l’objet prototype. Dans l’exemple précédent, on peut bien dire que l’attribut nom est hérité de l’objet prototype Parent.

Le mécanisme d’héritage est très souple en JavaScript puisqu’on peut accéder à l’objet prototype d’un objet avec la méthode Object.getPrototypeOf(o) mais on peut également changer dynamiquement le prototype d’un objet grâce à la méthode Object.setPrototypeOf(o, p).

La notion de classe depuis ES6

Depuis ES6, la notion de classe a été introduite car elle est plus largement utilisée dans les langages de programmation objet. Cependant, cela ne change rien dans le principe de programmation objet par prototype de JavaScript. Il s’agit plus d’un sucre syntaxique.

class Utilisateur {
    constructor(prenom, nom) {
        this.prenom = prenom;
        this.nom = nom;
    }

    print() {
        console.log(prenom, nom);
    }
}

let u = new Utilisateur("David", "Gayerie");
u.print();

La méthode nommée constructor correspond à la fonction de construction et toutes les autres méthodes déclarées dans la classe sont ajoutées au prototype de la fonction de construction.

Il est également possible de déclarer des propriétés grâce aux mots-clés get et set.

class Utilisateur {
    constructor(prenom, nom) {
        this.prenom = prenom;
        this.nom = nom;
    }

    get nom_complet() {
        return `${this.prenom} ${this.nom}`;
    }

    set nom_complet(v) {
        let t = v.split(" ");
        this.prenom = t[0];
        this.nom = t[1];
    }
}

La déclaration d’une méthode de générateur doit commencer par * :

class Compteur {
    * compter(max) {
        for (let c = 0; c <= max; ++c) {
            yield c;
        }
    }
}

let compteur = new Compteur();

for (let i of compteur.compter(10)) {
    console.log(i);
}

Une méthode avec le mot-clé static désigne une fonction qui est directement ajoutée à la fonction de construction et non au prototype. Il s’agit donc d’une fonction accessible à travers le nom de la classe et dans laquelle on ne peut pas utiliser le mot-clé this :

class Utilisateur {
    constructor(prenom, nom) {
        this.prenom = prenom;
        this.nom = nom;
    }

    static meme_famille(u1, u2) {
        return u1.nom === u2.nom;
    }

}

let u1 = new Utilisateur("David", "Gayerie");
let u2 = new Utilisateur("Eric", "Gayerie");

console.log(Utilisateur.meme_famille(u1, u2)); // Affiche true

L’héritage avec les classes

Pour préciser l’héritage, on utilise le mot-clé extends.

class Animal {

    mange(a) {
        console.log(`mange ${a}`);
    }

}

class Chien extends Animal {
}

let c = new Chien;
c.mange("un os"); // affiche mange un os

Utilisation du mot-clé super

Pour faciliter l’héritage, ES6 introduit le mot-clé super. Lorsqu’il est utilisé dans un constructeur, cela permet d’appeler le constructeur de la classe parente (en lui passant éventuellement des paramètres). Il peut également servir à appeler une méthode déclarée dans la superclasse.

class Personne {
    constructor(prenom, nom) {
        this.prenom = prenom;
        this.nom = nom;
    }

    print() {
        console.log(this.prenom, this.nom);
    }
}

class Utilisateur extends Personne {
    constructor(id, prenom, nom) {
        super(prenom, nom);
        this.id = id;
    }

    print() {
        console.log(id);
        super.print();
    }
}