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.
Le JavaScript possède des objets natifs,
commeString
,Boolean
etArray
,
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 !
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 :
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.
À 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 ».
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 constructeurPerson
.
Si on utilisethis
au sein du
constructeurPerson
,this
pointe
versPerson
. Grâce
àthis
, nous allons pouvoir définir les propriétés de
l'objetPerson
:
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 dethis
vont rester présentes.
Autrement dit,this.nick
affecte une
propriéténick
à notre objet, tandis que le
paramètrenick
n'est qu'une simple variable qui sera détruite à la fin
de l'exécution du constructeur.
L'objetPerson
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'objetPerson
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'objetPerson
, 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
faisantseb.nick
.
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éécriremyArray
comme
contenant des éléments de typePerson
:
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
.
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éthodeaddFriend()
à
l'objetPerson
de manière à pouvoir ajouter un ami comme ceci :
seb.addFriend('Johann', 19, 'm', 'aîné', 'JavaScripteur aussi', []);
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,
commeString
ouArray
.
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 objetPerson
dans le tableau des amis.
N'aurions-nous pas pu
utilisernew this(/* ... */)
à la place
denew 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 constructeurPerson
. Si nous avions
faitnew 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.
Lorsque vous définissez un objet, il possède automatiquement un
sous-objet appeléprototype
.
Cet
objetprototype
va nous permettre d'ajouter des méthodes à un objet.
Voici comment ajouter une méthodeaddFriend()
à notre
objetPerson
:
Person.prototype.addFriend = function(nick, age, sex, parent, work, friends) {
this.friends.push(new Person(nick, age, sex, parent, work, friends));
}
Lethis
fait ici aussi référence à l'objet dans lequel il s'exécute,
c'est-à-dire l'objetPerson
.
L'ajout de méthodes parprototype
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).
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
objetprototype
autorisant donc la modification de leurs méthodes !
Ce n'est parfois pas facile de visualiser le contenu d'un tableau
avecalert()
. Pourquoi ne pas créer une méthode qui afficherait le
contenu d'un objet littéral viaalert()
, mais de façon plus élégante
(un peu comme la fonctionvar_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éthodedebug()
affichera ceci :
Comme il s'agit d'un objet, le type natif
estObject
. Comme vu précédemment, nous allons utiliser son sous-objetprototype
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
sii
est différent de'debug'
?
Parce qu'en ajoutant la
méthodedebug()
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 :
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
unfor 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 commedebug()
, 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 natifObject
, les autres objets
commeArray
,String
,
etc. ne sont pas concernés.
Comme vous avez dû le constater, quand nous
utilisonsprototype
, 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.
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
commeString
,Date
,Array
,Object
,Number
,Boolean
et
de nombreux autres, cela fonctionne moins bien avec les objets natifs liés au DOM
commeNode
,Element
ou
encoreHTMLElement
, en particulier dans Internet Explorer.
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…
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 fonctionmyBestFunction()
, 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 fonctionmyBestFunction()
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 !
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
appelermyBestFunction()
, il faut obligatoirement passer par
l'objetmyNamespace
, 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.
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
etcontacts
,
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.
this
Le mot-cléthis
s'utilise
ici exactement comme dans les objets vus précédemment. Mais attention, si vous
utilisezthis
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éthodeinit()
on lui demande
d'exécuterthis.test()
. Or,this
pointe
verssubNamespace
, et il n'existe aucune
méthodetest()
au sein desubNamespace
.
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();
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à !');
}
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 paswindow
). La
fonctionalert()
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éthodepush()
qui
est dépendante de l'objetArray
. 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éesapply()
etcall()
.
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éthodesapply()
etcall()
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éthodepush()
, 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 fonctionalert()
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éthodetoString()
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éthodetoString()
de
l'objetObject
sur un objetArray
,
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éthodesapply()
etcall()
.
Elles vont nous permettre de redéfinir le mot-cléthis
de la
méthodetoString()
. 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éthodecall()
.
Comment utiliser notre méthodecall()
? Tout simplement de la manière
suivante :
methode_a_modifier.call(objet_a_definir);
Dans notre exemple actuel, la méthode
à modifier esttoString()
de
l'objetObject
. 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éthodetoString()
deObject
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éthodesapply()
etcall()
.
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);
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éthodesaddTrailer()
etremoveTrailer()
.
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.