Mis à jour le mardi 5 septembre 2017
  • 40 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Ce cours existe en eBook.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Les objets

Connectez-vous ou inscrivez-vous pour bénéficier de toutes les fonctionnalités de ce cours !

Nous avons vu dans la première partie du cours un chapitre sur les objets et les tableaux. Ce chapitre en est la suite et permet de mettre les pieds dans l'univers de la création et de la modification d'objets en JavaScript.

Au programme, vous découvrirez comment créer un objet de A à Z en lui définissant un constructeur, des propriétés et des méthodes ; vous saurez aussi comment modifier un objet natif. S'en suivra alors une manière d'exploiter les objets pour les utiliser en tant que namespaces et nous étudierons la modification du contexte d'exécution d'une méthode. Pour terminer, la notion d'héritage sera abordée.

Petite problématique

Le JavaScript possède des objets natifs, comme String, Boolean et Array, mais nous permet aussi de créer nos propres objets, avec leurs propres méthodes et propriétés.

Mais quel est l'intérêt ?

L'intérêt est généralement une propreté de code ainsi qu'une facilité de développement. Les objets sont là pour nous faciliter la vie, mais leur création peut prendre du temps.

Rappelez-vous l'exemple des tableaux avec les prénoms :

var myArray = ['Sébastien', 'Laurence', 'Ludovic', 'Pauline', 'Guillaume'];

Ce tableau sert juste à stocker les prénoms, rien de plus. Imaginons qu'il faille faire un tableau contenant énormément de données, et ce pour chaque personne. Par exemple, pour chaque personne, on aurait les données suivantes : prénom, âge, sexe, parenté, travail… Comment structurer tout cela ?

Avec une très grosse dose de motivation, il est possible de réaliser quelque chose comme ceci :

var myArray = [
    {
        nick: 'Sébastien',
        age: 23,
        sex: 'm',
        parent: 'aîné',
        work: 'JavaScripteur'
    },
    {
        nick: 'Laurence',
        age: 19,
        sex: 'f',
        parent: 'soeur',
        work: 'Sous-officier'
    },
    
    // et ainsi de suite…

];

Ce n'est pas encore trop compliqué car les données restent relativement simples. Maintenant, pour chaque personne, nous allons ajouter un tableau qui contiendra ses amis, et pour chaque ami, les mêmes données. Là, c'est déjà plus compliqué… Profitons de cette problématique pour étudier les objets !

Objet constructeur

Nous avons vu que le JavaScript nous permettait de créer des objets littéraux et nous allons voir maintenant comment créer de véritables objets qui possèdent des propriétés et des méthodes tout comme les objets natifs.

Un objet représente quelque chose, une idée ou un concept. Ici, suite à l'exemple de la famille, nous allons créer un objet appelé Person qui contiendra des données, à savoir le prénom, l'âge, le sexe, le lien de parenté, le travail et la liste des amis (qui sera un tableau).

L'utilisation de tels objets se fait en deux temps :

  1. On définit l'objet via un constructeur, cette étape permet de définir un objet qui pourra être réutilisé par la suite. Cet objet ne sera pas directement utilisé car nous en utiliserons une « copie » : on parle alors d'instance.

  2. À chaque fois que l'on a besoin d'utiliser notre objet, on crée une instance de celui-ci, c'est-à-dire qu'on le « copie ».

Définition via un constructeur

Le constructeur (ou objet constructeur ou constructeur d'objet) va contenir la structure de base de notre objet. Si vous avez déjà fait de la programmation orientée objet dans des langages tels que le C++, le C# ou le Java, sachez que ce constructeur ressemble, sur le principe, à une classe.

La syntaxe d'un constructeur est la même que celle d'une fonction :

function Person() {
   // Code du constructeur
}

Le code du constructeur va contenir une petite particularité : le mot-clé this. Ce mot-clé fait référence à l'objet dans lequel il est exécuté, c'est-à-dire ici le constructeur Person. Si on utilise this au sein du constructeur Person, this pointe vers Person. Grâce à this, nous allons pouvoir définir les propriétés de l'objet Person :

function Person(nick, age, sex, parent, work, friends) {
    this.nick = nick;
    this.age = age;
    this.sex = sex;
    this.parent = parent;
    this.work = work;
    this.friends = friends;
}

Les paramètres de notre constructeur (les paramètres de la fonction si vous préférez) vont être détruits à la fin de l'exécution de ce dernier, alors que les propriétés définies par le biais de this vont rester présentes. Autrement dit, this.nick affecte une propriété nick à notre objet, tandis que le paramètre nick n'est qu'une simple variable qui sera détruite à la fin de l'exécution du constructeur.

Utilisation de l'objet

L'objet Person a été défini grâce au constructeur qu'il ne nous reste plus qu'à utiliser :

// Définition de l'objet Person via un constructeur
function Person(nick, age, sex, parent, work, friends) {
    this.nick = nick;
    this.age = age;
    this.sex = sex;
    this.parent = parent;
    this.work = work;
    this.friends = friends;
}

// On crée des variables qui vont contenir une instance de l'objet Person :
var seb = new Person('Sébastien', 23, 'm', 'aîné', 'JavaScripteur', []);
var lau = new Person('Laurence', 19, 'f', 'soeur', 'Sous-officier', []);

alert(seb.nick); // Affiche : « Sébastien »
alert(lau.nick); // Affiche : « Laurence »

Que s'est-il passé ici ? L'objet Person a été défini comme nous l'avons vu plus haut. Pour pouvoir utiliser cet objet, on définit une variable qui va contenir une instance de l'objet Person, c'est-à-dire une copie. Pour indiquer au JavaScript qu'il faut utiliser une instance, on utilise le mot-clé new.

Retenez bien que ce mot-clé new ne signifie pas « créer un nouvel objet », mais signifie « créer une nouvelle instance de l'objet », ce qui est très différent puisque dans le deuxième cas on ne fait que créer une instance, une copie, de l'objet initial, ce qui nous permet de conserver l'objet en question.

Dans les paramètres de l'objet, on transmet les différentes informations pour la personne. Ainsi donc, en transmettant 'Sébastien' comme premier paramètre, celui-ci ira s'enregistrer dans la propriété this.nick, et il sera possible de le récupérer en faisant seb.nick.

Modifier les données

Une fois la variable définie, on peut modifier les propriétés, exactement comme s'il s'agissait d'un simple objet littéral comme vu dans la première partie du cours :

var seb = new Person('Sébastien', 23, 'm', 'aîné', 'JavaScripteur', []);

seb.nick = 'Bastien'; // On change le prénom
seb.age = 18;        // On change l'âge

alert(seb.nick + ' a ' + seb.age + 'ans'); // Affiche : « Bastien a 18 ans »

Au final, si on reprend la problématique du début de ce chapitre, on peut réécrire myArray comme contenant des éléments de type Person :

var myArray = [
    new Person('Sébastien', 23, 'm', 'aîné', 'JavaScripteur', []),
    new Person('Laurence', 19, 'f', 'soeur', 'Sous-officier', []),
    new Person('Ludovic', 9, 'm', 'frère', 'Etudiant', []),
    new Person('Pauline', 16, 'f', 'cousine', 'Etudiante', []),
    new Person('Guillaume', 16, 'm', 'cousin', 'Dessinateur', []),
];

Il sera ainsi possible d'accéder aux différents membres de la famille de cette manière pour récupérer le travail : myArray[i].work.

Ajouter des méthodes

L'objet vu précédemment est simple. Il y a moyen de l'améliorer en lui ajoutant des méthodes. Les méthodes, vous savez ce que c'est car vous en avez déjà croisé dans les chapitres sur les tableaux. Si nous reprenons l'exemple précédent et que l'on souhaite ajouter un ami, il faut faire comme ceci :

var seb = new Person('Sébastien', 23, 'm', 'aîné', 'JavaScripteur', []);

// On ajoute un ami dans le tableau « friends »
seb.friends.push(new Person('Johann', 19, 'm', 'aîné', 'JavaScripteur aussi', []));

alert(seb.friends[0].nick); // Affiche : « Johann »

Avec ça, on peut aussi ajouter un ami à Johann :

seb.friends[0].friends.push(new Person('Victor', 19, 'm', 'aîné', 'Internet Hero', []));

Les possibilités sont donc infinies !

Mais tout cela reste long à écrire. Pourquoi ne pas ajouter une méthode addFriend() à l'objet Person de manière à pouvoir ajouter un ami comme ceci :

seb.addFriend('Johann', 19, 'm', 'aîné', 'JavaScripteur aussi', []);

Ajouter une méthode

Il y a deux manières de définir une méthode pour un objet : dans le constructeur ou via prototype. Définir les méthodes directement dans le constructeur est facile puisque c'est nous qui créons le constructeur. La définition de méthodes via prototype est utile surtout si on n'a pas créé le constructeur : ce sera alors utile pour ajouter des méthodes à des objets natifs, comme String ou Array.

Définir une méthode dans le constructeur

Pour cela, rien de plus simple, on procède comme pour les propriétés, sauf qu'il s'agit d'une fonction :

function Person(nick, age, sex, parent, work, friends) {
    this.nick = nick;
    this.age = age;
    this.sex = sex;
    this.parent = parent;
    this.work = work;
    this.friends = friends;

    this.addFriend = function(nick, age, sex, parent, work, friends) {
        this.friends.push(new Person(nick, age, sex, parent, work, friends));
    };
}

Le code de cette méthode est simple : il ajoute un objet Person dans le tableau des amis.

N'aurions-nous pas pu utiliser new this(/* ... */) à la place de new Person(/* ... */) ?

Non, car, comme nous l'avons vu plus haut, this fait référence à l'objet dans lequel il est appelé, c'est-à-dire le constructeur Person. Si nous avions fait new this(/* ... */) cela aurait équivalu à insérer le constructeur dans lui-même. Mais de toute façon, en tentant de faire ça, vous obtenez une erreur du type « this n'est pas un constructeur », donc l'interpréteur JavaScript ne plante heureusement pas.

Ajouter une méthode via prototype

Lorsque vous définissez un objet, il possède automatiquement un sous-objet appelé prototype.

Cet objet prototype va nous permettre d'ajouter des méthodes à un objet. Voici comment ajouter une méthode addFriend() à notre objet Person :

Person.prototype.addFriend = function(nick, age, sex, parent, work, friends) {
    this.friends.push(new Person(nick, age, sex, parent, work, friends));
}

Le this fait ici aussi référence à l'objet dans lequel il s'exécute, c'est-à-dire l'objet Person.

L'ajout de méthodes par prototype a l'avantage d'être indépendant de l'objet, c'est-à-dire que vous pouvez définir votre objet dans un fichier, et ajouter des méthodes dans un autre fichier (pour autant que les deux fichiers soient inclus dans la même page Web).

Ajouter des méthodes aux objets natifs

Une grosse particularité du JavaScript est qu'il est orienté objet par prototype ce qui le dote de certaines caractéristiques que d'autres langages orientés objet ne possèdent pas. Avec le JavaScript, il est possible de modifier les objets natifs, comme c'est le cas en C# par exemple. En fait, les objets natifs possèdent eux aussi un objet prototype autorisant donc la modification de leurs méthodes !

Ajout de méthodes

Ce n'est parfois pas facile de visualiser le contenu d'un tableau avec alert(). Pourquoi ne pas créer une méthode qui afficherait le contenu d'un objet littéral via alert(), mais de façon plus élégante (un peu comme la fonction var_dump() du PHP si vous connaissez) ?

Voici le type d'objet à afficher proprement :

var family = {
    self: 'Sébastien',
    sister: 'Laurence',
    brother: 'Ludovic',
    cousin_1: 'Pauline',
    cousin_2: 'Guillaume'
};

family.debug(); // Nous allons créer cette méthode debug()

La méthode debug() affichera ceci :

Notre méthode devra afficher quelque chose de semblable
Notre méthode devra afficher quelque chose de semblable

Comme il s'agit d'un objet, le type natif est Object. Comme vu précédemment, nous allons utiliser son sous-objet prototype pour lui ajouter la méthode voulue :

// Testons si cette méthode n'existe pas déjà !
if (!Object.prototype.debug) {

    // Créons la méthode
    Object.prototype.debug = function() {
        var text = 'Object {\n';
        
        for (var i in this) {
            if (i !== 'debug') {   
                text += '    [' + i + '] => ' + this[i] + '\n';    
            }
        }
        
        alert(text + '}');
    }

}

Mais pourquoi tester si i est différent de 'debug' ?

Parce qu'en ajoutant la méthode debug() aux objets, elle s'ajoute même aux objets littéraux : autrement dit, debug() va se lister elle-même ce qui n'a pas beaucoup d'intérêt. Regardez donc le résultat en enlevant cette condition :

Le code de la méthode debug() est affiché
Le code de la méthode debug() est affiché

Nous avons ajouté une méthode à Object. Nous l'avons fait pour l'exemple, mais ceci ne doit jamais être reproduit dans vos scripts pour une raison très simple : après ajout d'une méthode ou d'une propriété à Object, celle-ci sera listée à chaque fois que vous utiliserez un for in. Par exemple, le code suivant ne devrait même pas afficher une seule alerte :

var myObject = {};

for (var i in myObject) {
    alert(i);
}

Et pourtant, après l'ajout d'une méthode comme debug(), votre boucle affichera cette méthode pour tout objet parcouru, ce qui n'est clairement pas conseillé. Cependant, notez bien que cette restriction s'applique uniquement à l'objet natif Object, les autres objets comme Array, String, etc. ne sont pas concernés.

Remplacer des méthodes

Comme vous avez dû le constater, quand nous utilisons prototype, nous affectons une fonction. Cela veut donc dire qu'il est possible de modifier les méthodes natives des objets en leur affectant une nouvelle méthode. Cela peut se révéler très utile dans certains cas, mais nous verrons cela plus tard dans le chapitre qui aborde l'usage des polyfills.

Limitations

Dans Internet Explorer

En théorie, chaque objet peut se voir attribuer des méthodes via prototype. Mais en pratique, si cela fonctionne avec les objets natifs génériques comme String, Date, Array, Object, Number, Boolean et de nombreux autres, cela fonctionne moins bien avec les objets natifs liés au DOM comme Node, Element ou encore HTMLElement, en particulier dans Internet Explorer.

Chez les éditeurs

Sachez également que si vos scripts sont destinés à être utilisés sous la forme d'extensions pour Firefox et que vous soumettez votre extension sur le site de Mozilla Addons, celle-ci sera refusée car Mozilla trouve qu'il est dangereux de modifier les objets natifs. C'est un peu vrai puisque si vous modifiez une méthode native, elle risque de ne pas fonctionner correctement pour une autre extension…

Les namespaces

En informatique, un namespace, ou « espace de nom » en français, est un ensemble fictif qui contient des informations, généralement des propriétés et des méthodes, ainsi que des sous-namespaces. Le but d'un namespace est de s'assurer de l'unicité des informations qu'il contient.

Par exemple, Sébastien et Johann habitent tous deux au numéro 42. Sébastien dans la rue de Belgique et Johann dans la rue de France. Les numéros de leurs maisons peuvent être confondus, puisqu'il s'agit du même. Ainsi, si Johann dit « J'habite au numéro 42 », c'est ambigu, car Sébastien aussi. Alors, pour différencier les deux numéros, nous allons toujours donner le nom de la rue. Ce nom de rue peut être vu comme un namespace : il permet de différencier deux données identiques.

En programmation, quelque chose d'analogue peut se produire : imaginez que vous développiez une fonction myBestFunction(), et vous trouvez un script tout fait pour réaliser un effet quelconque. Vous ajoutez alors ce script au vôtre. Problème : dans ce script tout fait, une fonction myBestFunction() est aussi présente… Votre fonction se retrouve écrasée par l'autre, et votre script ne fonctionnera plus correctement.

Pour éviter ce genre de désagrément, nous allons utiliser un namespace !

Définir un namespace

Un namespace est une sorte de catégorie : vous allez vous créer un namespace, et, au sein de celui-ci, vous allez placer vos fonctions. De cette manière, vos fonctions seront en quelque sorte préservées d'éventuels écrasements. Comme le JavaScript ne gère pas nativement les namespaces (comprenez : il n'y a pas de structure consacrée à cela), nous allons devoir nous débrouiller seuls, et utiliser un simple objet littéral. Premier exemple :

var myNamespace = {
    myBestFunction: function() {
        alert('Ma meilleure fonction !');
    }
};

// On exécute la fonction :
myNamespace.myBestFunction();

On commence par créer un objet littéral appelé myNamespace. Ensuite on définit une méthode : myBestFunction(). Souvenez-vous, dans le chapitre sur les objets littéraux, nous avions vu que nous pouvions définir des propriétés, et il est aussi possible de définir des méthodes, en utilisant la même syntaxe.

Pour appeler myBestFunction(), il faut obligatoirement passer par l'objet myNamespace, ce qui limite très fortement la probabilité de voir votre fonction écrasée par une autre. Bien évidemment, votre namespace doit être original pour être certain qu'un autre développeur n'utilise pas le même… Cette technique n'est donc pas infaillible, mais réduit considérablement les problèmes.

Un style de code

Utiliser un namespace est aussi élégant, car cela permet d'avoir un code visuellement propre et structuré. Une grande majorité des « gros » scripts sont organisés via un namespace, notamment car il est possible de décomposer le script en catégories. Par exemple, vous faites un script qui gère un webmail (comme Hotmail ou GMail) :

var thundersebWebMail = {
    // Propriétés
    version: 1.42,
    lang: 'english',

    // Initialisation
    init: function() { /* initialisation */ },

    // Gestion des mails
    mails: {
        list: function() { /* affiche la liste des mails */ },
        show: function() { /* affiche un mail */ },
        trash: function() { /* supprime un mail */ },
        // et cætera…
    },

    // Gestion des contacts
    contacts: {
        list: function() { /* affiche la liste des contacts */ },
        edit: function() { /* édite un contact */ },
        // et cætera…
    }
};

Ce code fictif comprend une méthode d'initialisation et deux sous-namespaces : mails et contacts, servant respectivement à gérer les e-mails et les contacts. Chacun de ces sous-namespaces contient les méthodes qui lui sont propres.

Structurer son code de cette manière est propre et lisible, ce qui n'est pas toujours le cas d'une succession de fonctions. Voici l'exemple que nous venons de voir mais cette fois-ci sans namespaces :

var webmailVersion = 1.42,
    webmailLang = 'english';

function webmailInit() { /* initialisation */ }

function webmailMailsList() { /* affiche la liste des mails */ }
function webmailMailsShow() { /* affiche un mail */ }
function webmailMailsTrash() { /* supprime un mail */ }

function webmailContactsList() { /* affiche la liste des contacts */ }
function webmailContactsEdit() { /* édite un contact */ }

C'est tout de suite plus confus, il n'y a pas de hiérarchie, c'est brouillon. Bien évidemment, cela dépend du codeur : un code en namespace peut être brouillon, alors qu'un code « normal » peut être très propre ; mais de manière générale, un code en namespace est accessible, plus lisible et plus compréhensible. C'est évidemment une question d'habitude.

L'emploi de this

Le mot-clé this s'utilise ici exactement comme dans les objets vus précédemment. Mais attention, si vous utilisez this dans un sous-namespace, celui-ci pointera vers ce sous-namespace, et non vers le namespace parent. Ainsi, l'exemple suivant ne fonctionnera pas correctement, car en appelant la méthode init() on lui demande d'exécuter this.test(). Or, this pointe vers subNamespace, et il n'existe aucune méthode test() au sein de subNamespace.

var myNamespace = {

    test: function() {
        alert('Test');
    },

    subNamespace: {
        init: function() {
            this.test();
        }
    }

};

myNamespace.subNamespace.init();

Pour accéder à l'objet parent, il n'y a malheureusement pas de solution si ce n'est écrire son nom entièrement :

var myNamespace = {

    test: function() {
        alert('Test');
    },

    subNamespace: {
        init: function() {
            myNamespace.test();
        }
    }

};

myNamespace.subNamespace.init();

Vérifier l'unicité du namespace

Une sécurité supplémentaire est de vérifier l'existence du namespace : s'il n'existe pas, on le définit et dans le cas contraire, on ne fait rien pour ne pas risquer d'écraser une version déjà existante, tout en retournant un message d'erreur.

// On vérifie l'existence de l'objet myNamespace
if (typeof myNamespace === 'undefined') {

    var myNamespace = {
        // Tout le code
    };

} else {
    alert('myNamespace existe déjà !');
}

Modifier le contexte d'une méthode

Pour finir ce chapitre, nous allons ici aborder un sujet assez avancé : la modification du contexte d'une méthode. Dans l'immédiat, cela ne signifie sûrement rien pour vous, nous allons donc expliquer le concept. Commençons par un petit rappel. Connaissez-vous la différence entre une fonction et une méthode ?

La première est indépendante et ne fait partie d'aucun objet (ou presque, n'oublions pas window). La fonction alert() est dans cette catégorie, car vous pouvez l'appeler sans la faire précéder du nom d'un objet :

alert('Test !'); // Aucun objet nécessaire !

Une méthode, en revanche, est dépendante d'un objet. C'est le cas par exemple de la méthode push() qui est dépendante de l'objet Array. Le fait qu'elle soit dépendante est à la fois un avantage et un inconvénient :

  • Un avantage car vous n'avez pas à spécifier quel objet la méthode doit modifier ;

  • Un inconvénient car cette méthode ne pourra fonctionner que sur l'objet dont elle est dépendante !

Cet inconvénient peut être résolu grâce à deux méthodes nommées apply() et call().

Comme vous le savez, une méthode utilise généralement le mot-clé this pour savoir à quel objet elle appartient, c'est ce qui fait qu'elle est dépendante. Les deux méthodes apply() et call() existent pour permettre de rediriger la référence du mot-clé this vers un autre objet !

Nous n'allons pas faire de cas pratique avec la méthode push(), car son fonctionnement est spécifique aux tableaux (il serait difficile de lui demander d'ajouter une donnée sur un objet dont la structure est totalement différente). En revanche, il existe une méthode que tout objet possède : toString() ! Cette méthode a pour but de fournir une représentation d'un objet sous forme de chaîne de caractères, c'est elle qui est appelée par la fonction alert() lorsque vous lui passez un objet en paramètre. Elle possède cependant un fonctionnement différent selon l'objet sur lequel elle est utilisée :

alert(['test']); // Affiche : « test »
alert({0:'test'}); // Affiche : « [object Object] »

Comme vous avez pu le constater, la méthode toString() renvoie un résultat radicalement différent selon l'objet. Dans le cas d'un tableau, elle retourne son contenu, mais quand il s'agit d'un objet, elle retourne son type converti en chaîne de caractères.

Notre objectif maintenant va être de faire en sorte d'appliquer la méthode toString() de l'objet Object sur un objet Array, et ce afin d'obtenir sous forme de chaîne de caractères le type de notre tableau au lieu d'obtenir son contenu.

C'est là qu'entrent en jeu nos deux méthodes apply() et call(). Elles vont nous permettre de redéfinir le mot-clé this de la méthode toString(). Ces deux méthodes fonctionnent quasiment de la même manière, elles prennent toutes les deux en paramètre un premier argument obligatoire qui est l'objet vers lequel va pointer le mot-clé this. Nos deux méthodes se différencient sur les arguments facultatifs, mais nous en reparlerons plus tard. En attendant, nous allons nous servir de la méthode call().

Comment utiliser notre méthode call() ? Tout simplement de la manière suivante :

methode_a_modifier.call(objet_a_definir);

Dans notre exemple actuel, la méthode à modifier est toString() de l'objet Object. En sachant cela il ne nous reste plus qu'à faire ceci :

var result = Object.prototype.toString.call(['test']);

alert(result); // Affiche : « [object Array] »

Nous y voilà ! La méthode toString() de Object a bien été appliquée à notre tableau, nous obtenons donc son type et non pas son contenu.

Revenons maintenant sur les arguments facultatifs de nos deux méthodes apply() et call(). La première prend en paramètre facultatif un tableau de valeurs, tandis que la deuxième prend une infinité de valeurs en paramètres. Ces arguments facultatifs servent à la même chose : ils seront passés en paramètres à la méthode souhaitée.

Ainsi, si nous écrivons :

var myArray = [];

myArray.push.apply(myArray, [1, 2, 3]);

Cela revient au même que si nous avions écrit :

var myArray = [];

myArray.push(1, 2, 3);

De même, si nous écrivons :

var myArray = [];

myArray.push.call(myArray, 1, 2, 3);

Cela revient à écrire :

var myArray = [];

myArray.push(1, 2, 3);

L'héritage

Tout comme beaucoup d'autres langages, il est possible, en JavaScript, d'appliquer le concept d'héritage à nos objets. Prenons l'exemple d'une voiture et d'un camion, vous voulez créer un objet constructeur pour chacun de ces deux véhicules cependant vous vous rendez alors compte que vous allez probablement devoir dupliquer votre code car ces deux véhicules ont tous deux la capacité de rouler, possèdent une plaque d'immatriculation et un réservoir d'essence. Arrêtons-nous là pour les similitudes, cela sera amplement suffisant.

Plutôt que dupliquer votre code entre les deux véhicules, nous allons faire appel à la notion d'héritage en créant un objet constructeur parent qui va rassembler ces caractéristiques communes :

function Vehicle(licensePlate, tankSize) {
    this.engineStarted = false; // Notre véhicule est-il démarré ?
    this.licensePlate = licensePlate; // La plaque d'immatriculation de notre véhicule.
    this.tankSize = tankSize; // La taille de notre réservoir en litres.
}

// Permet de démarrer notre véhicule.
Vehicle.prototype.start = function() {
    this.engineStarted = true;
};

// Permet d'arrêter notre véhicule.
Vehicle.prototype.stop = function() {
    this.engineStarted = false;
};

Maintenant que notre objet Vehicle  est prêt, nous pouvons l'exploiter. L'héritage va ici consister à créer deux objets constructeurs (un pour notre voiture ainsi qu'un pour notre camion) qui vont tous deux hériter de Vehicle . Concrètement, cela signifie que nos deux objets constructeurs vont bénéficier des mêmes propriétés et méthodes que leur parent et vont pouvoir ajouter à cela leurs propres propriétés et méthodes.

Commençons par la voiture qui va ajouter quelques fonctionnalités concernant son coffre :

function Car(licensePlate, tankSize, trunkSize) {
    // On appelle le constructeur de « Vehicle » par le biais de la méthode
    // call() afin qu'il affecte de nouvelles propriétés à « Car ».
    Vehicle.call(this, licensePlate, tankSize);

    // Une fois le constructeur parent appelé, l'initialisation de notre objet peut continuer.
    this.trunkOpened = false; // Notre coffre est-il ouvert ?
    this.trunkSize = trunkSize; // La taille de notre coffre en mètres cube.
}

// L'objet prototype de « Vehicle » doit être copié au sein du prototype
// de « Car » afin que ce dernier puisse bénéficier des mêmes méthodes.
Car.prototype = Object.create(Vehicle.prototype, {
    // Le prototype copié possède une référence vers son constructeur, actuellement
    // défini à « Vehicle », nous devons changer sa référence pour « Car »
    // tout en conservant sa particularité d'être une propriété non-énumerable.
    constructor: {
        value: Car,
        enumerable: false,
        writable: true,
        configurable: true
    }
});

// Il est bien évidemment possible d'ajouter de nouvelles méthodes.
Car.prototype.openTrunk = function() {
    this.trunkOpened = true;
};

Car.prototype.closeTrunk = function() {
    this.trunkOpened = false;
};

Il est maintenant possible d'instancier une nouvelle voiture de manière tout à fait classique :

var myCar = new Car('AA-555-AA', 70, 2.5);

Cette voiture est maintenant capable de démarrer et arrêter son moteur, ouvrir et fermer son coffre, et nous connaissons sa plaque d'immatriculation ainsi que la taille de son réservoir et de son coffre.

Afin que vous soyez en mesure de comprendre pleinement l'intérêt de l'héritage, occupons-nous de notre camion qui, lui, possèdera une gestion de ses remorques :

function Truck(licensePlate, tankSize, trailersNumber) {
    Vehicle.call(this, licensePlate, tankSize);

    this.trailersNumber = trailersNumber; // Le nombre de remorques attachées à notre camion.
}

Truck.prototype = Object.create(Vehicle.prototype, {
    constructor: {
        value: Truck,
        enumerable: false,
        writable: true,
        configurable: true
    }
});

Truck.prototype.addTrailer = function() {
    this.trailersNumber++;
};

Truck.prototype.removeTrailer = function() {
    this.trailersNumber--;
};

Comme vous pouvez le constater, le camion possède un nombre de remorques et il est possible de lui en ajouter ou retirer par le biais des méthodes addTrailer()  et removeTrailer() .

Nous avons donc créé deux objets constructeurs qui possèdent des caractéristiques communes regroupées au sein d'un objet constructeur parent. Ainsi, chaque objet enfant n'a qu'à prendre en charge ses propres particularités, les aspects communs seront gérés par l'objet parent, évitant ainsi de dupliquer votre code. Dans le cas où votre code serait relativement complexe, il est bien évidemment possible de faire hériter un objet d'un parent qui hérite lui-même d'un autre parent et ainsi de suite...

Dans la pratique, il est probable que l'héritage vous soit assez peu utile en JavaScript mais cette notion reste cependant utile lorsque votre code finit par se complexifier car il vous permettra de mieux segmenter votre code.

Exemple de certificat de réussite
Exemple de certificat de réussite