Vous ne pouvez pas aller très loin en JavaScript sans traiter avec des objets. Ils sont à la base de presque tous les aspects du langage de programmation JavaScript. En fait, apprendre à créer des objets est probablement l’une des premières choses que vous avez étudiées lorsque vous avez commencé. Ceci étant dit, afin d’apprendre le plus efficacement possible les prototypes en JavaScript, nous allons canaliser notre développeur junior intérieur et revenir aux bases.
Les objets sont des paires clé/valeur. La façon la plus courante de créer un objet est avec des accolades {}
et vous ajoutez des propriétés et des méthodes à un objet en utilisant la notation par points.
let animal = {}animal.name = 'Leo'animal.energy = 10animal.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}animal.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}animal.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}
Simple. Maintenant, il y a de fortes chances que dans notre application, nous ayons besoin de créer plus d’un animal. Naturellement, la prochaine étape pour cela serait d’encapsuler cette logique à l’intérieur d’une fonction que nous pouvons invoquer chaque fois que nous avons besoin de créer un nouvel animal. Nous appellerons ce modèle Functional Instantiation
et nous appellerons la fonction elle-même une « fonction constructeur » puisqu’elle est responsable de la « construction » d’un nouvel objet.
Instauration fonctionnelle
function Animal (name, energy) { let animal = {} animal.name = name animal.energy = energy animal.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount } animal.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length } animal.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length } return animal}const leo = Animal('Leo', 7)const snoop = Animal('Snoop', 10)
"I thought this was an Advanced JavaScript course...?" - Your brain
C’est le cas. Nous y arriverons.
Maintenant, chaque fois que nous voulons créer un nouvel animal (ou plus largement une nouvelle » instance « ), il suffit d’invoquer notre fonction Animal
, en lui passant le niveau de l’animal name
et energy
. Cela fonctionne très bien et c’est incroyablement simple. Cependant, pouvez-vous déceler des faiblesses dans ce modèle ? La plus grande et celle que nous allons tenter de résoudre concerne les trois méthodes – eat
sleep
, et play
. Chacune de ces méthodes n’est pas seulement dynamique, mais elle est aussi complètement générique. Cela signifie qu’il n’y a aucune raison de recréer ces méthodes comme nous le faisons actuellement à chaque fois que nous créons un nouvel animal. Nous ne faisons que gaspiller de la mémoire et rendre chaque objet animal plus grand qu’il ne doit l’être. Pouvez-vous penser à une solution ? Et si, au lieu de recréer ces méthodes à chaque fois que nous créons un nouvel animal, nous les déplacions vers leur propre objet ; ensuite, nous pouvons faire en sorte que chaque animal fasse référence à cet objet ? Nous pouvons appeler ce pattern Functional Instantiation with Shared Methods
, verbeux mais descriptif.
Instanciation fonctionnelle avec des méthodes partagées
const animalMethods = { eat(amount) { console.log(`${this.name} is eating.`) this.energy += amount }, sleep(length) { console.log(`${this.name} is sleeping.`) this.energy += length }, play(length) { console.log(`${this.name} is playing.`) this.energy -= length }}function Animal (name, energy) { let animal = {} animal.name = name animal.energy = energy animal.eat = animalMethods.eat animal.sleep = animalMethods.sleep animal.play = animalMethods.play return animal}const leo = Animal('Leo', 7)const snoop = Animal('Snoop', 10)
En déplaçant les méthodes partagées vers leur propre objet et en référençant cet objet à l’intérieur de notre Animal
fonction, nous avons maintenant résolu le problème du gaspillage de mémoire et des objets animaux trop grands.
Object.create
Améliorons encore une fois notre exemple en utilisant Object.create
. En termes simples, Object.create vous permet de créer un objet qui déléguera à un autre objet en cas d’échec des recherches. Autrement dit, Object.create vous permet de créer un objet qui, en cas d’échec de la recherche d’une propriété sur cet objet, peut consulter un autre objet pour voir si celui-ci possède la propriété. C’était beaucoup de mots. Voyons un peu de code.
const parent = { name: 'Stacey', age: 35, heritage: 'Irish'}const child = Object.create(parent)child.name = 'Ryan'child.age = 7console.log(child.name) // Ryanconsole.log(child.age) // 7console.log(child.heritage) // Irish
Donc dans l’exemple ci-dessus, parce que child
a été créé avec Object.create(parent)
, chaque fois qu’il y a un échec de recherche de propriété sur child
, JavaScript déléguera cette recherche à l’objet parent
. Cela signifie que même si child
n’a pas de propriété heritage
parent
le fait, donc lorsque vous enregistrez child.heritage
, vous obtiendrez l’héritage de parent
qui était Irish
.
Maintenant avec Object.create
dans notre boîte à outils, comment pouvons-nous l’utiliser afin de simplifier notre code Animal
de tout à l’heure ? Eh bien, au lieu d’ajouter toutes les méthodes partagées à l’animal une par une comme nous le faisons maintenant, nous pouvons utiliser Object.create pour déléguer à l’objet animalMethods
à la place. Pour paraître vraiment intelligent, appelons celui-ci Functional Instantiation with Shared Methods and Object.create
🙃
Instauration fonctionnelle avec les méthodes partagées et Object.create
const animalMethods = { eat(amount) { console.log(`${this.name} is eating.`) this.energy += amount }, sleep(length) { console.log(`${this.name} is sleeping.`) this.energy += length }, play(length) { console.log(`${this.name} is playing.`) this.energy -= length }}function Animal (name, energy) { let animal = Object.create(animalMethods) animal.name = name animal.energy = energy return animal}const leo = Animal('Leo', 7)const snoop = Animal('Snoop', 10)leo.eat(10)snoop.play(5)
📈 Donc maintenant, lorsque nous appelons leo.eat
, JavaScript va chercher la méthode eat
sur l’objet leo
. Cette recherche échouera, puis, à cause de Object.create, il déléguera à l’objet animalMethods
qui est l’endroit où il trouvera eat
.
Pour l’instant, tout va bien. Il y a encore quelques améliorations que nous pouvons faire cependant. Il semble juste un peu « hacky » de devoir gérer un objet séparé (animalMethods
) afin de partager des méthodes entre les instances. Cela semble être une fonctionnalité commune que vous voudriez voir implémentée dans le langage lui-même. Il s’avère que c’est le cas et c’est la raison pour laquelle vous êtes ici – prototype
.
Alors, qu’est-ce que prototype
en JavaScript ? Eh bien, pour faire simple, chaque fonction en JavaScript possède une propriété prototype
qui fait référence à un objet. Anticlimatique, non ? Testez-le par vous-même.
function doThing () {}console.log(doThing.prototype) // {}
Et si, au lieu de créer un objet distinct pour gérer nos méthodes (comme nous le faisons avec animalMethods
), nous mettions simplement chacune de ces méthodes sur le prototype de la fonction Animal
? Ensuite, tout ce que nous aurions à faire, c’est qu’au lieu d’utiliser Object.create pour déléguer à animalMethods
, nous pourrions l’utiliser pour déléguer à Animal.prototype
. Nous appellerons ce pattern Prototypal Instantiation
.
Implantation prototypique
function Animal (name, energy) { let animal = Object.create(Animal.prototype) animal.name = name animal.energy = energy return animal}Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}const leo = Animal('Leo', 7)const snoop = Animal('Snoop', 10)leo.eat(10)snoop.play(5)
👏👏👏👏 Avec un peu de chance, vous venez d’avoir un grand moment « aha ». Encore une fois, prototype
est juste une propriété que chaque fonction en JavaScript possède et, comme nous l’avons vu ci-dessus, elle nous permet de partager des méthodes à travers toutes les instances d’une fonction. Toutes nos fonctionnalités sont toujours les mêmes, mais maintenant, au lieu de devoir gérer un objet séparé pour toutes les méthodes, nous pouvons simplement utiliser un autre objet qui vient intégré dans la Animal
fonction elle-même, Animal.prototype
.
Let’s. Allons. Deeper.
À ce stade, nous savons trois choses :
- Comment créer une fonction constructeur.
- Comment ajouter des méthodes au prototype de la fonction constructeur.
- Comment utiliser Object.create pour déléguer les recherches ratées au prototype de la fonction.
Ces trois tâches semblent assez fondamentales pour tout langage de programmation. JavaScript est-il vraiment si mauvais qu’il n’existe pas de moyen plus facile, » intégré « , d’accomplir la même chose ? Comme vous pouvez probablement le deviner à ce stade, il y en a un, et c’est en utilisant le mot-clé new
.
Ce qui est bien dans l’approche lente et méthodique que nous avons adoptée pour arriver ici, c’est que vous aurez maintenant une compréhension profonde de ce que fait exactement le mot-clé new
en JavaScript sous le capot.
En regardant de nouveau notre Animal
constructeur, les deux parties les plus importantes étaient la création de l’objet et son retour. Sans la création de l’objet avec Object.create
, nous ne serions pas en mesure de déléguer au prototype de la fonction sur les recherches échouées. Sans l’instruction return
, nous ne récupérerions jamais l’objet créé.
function Animal (name, energy) { let animal = Object.create(Animal.prototype) animal.name = name animal.energy = energy return animal}
Voici ce qui est cool avec new
– lorsque vous invoquez une fonction en utilisant le mot clé new
, ces deux lignes sont faites pour vous implicitement (« sous le capot ») et l’objet qui est créé s’appelle this
.
En utilisant les commentaires pour montrer ce qui se passe sous le capot et en supposant que le constructeur Animal
est appelé avec le mot-clé new
, on peut le réécrire comme ceci .
function Animal (name, energy) { // const this = Object.create(Animal.prototype) this.name = name this.energy = energy // return this}const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)
Et sans les commentaires « sous le capot »
function Animal (name, energy) { this.name = name this.energy = energy}Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)
Encore la raison pour laquelle ceci fonctionne et que l’objet this
est créé pour nous est parce que nous avons appelé la fonction constructeur avec le mot-clé new
. Si vous omettez new
lorsque vous invoquez la fonction, cet this
objet n’est jamais créé et n’est pas non plus implicitement renvoyé. Nous pouvons voir le problème que cela pose dans l’exemple ci-dessous.
function Animal (name, energy) { this.name = name this.energy = energy}const leo = Animal('Leo', 7)console.log(leo) // undefined
Le nom de ce motif est Pseudoclassical Instantiation
.
Si JavaScript n’est pas votre premier langage de programmation, vous commencez peut-être à vous agiter un peu.
« WTF ce mec vient de recréer une version plus merdique d’une Class » – Vous
Pour ceux qui ne sont pas familiers, une Class permet de créer un blueprint pour un objet. Ensuite, chaque fois que vous créez une instance de cette classe, vous obtenez un objet avec les propriétés et les méthodes définies dans le blueprint.
Ça vous dit quelque chose ? C’est essentiellement ce que nous avons fait avec notre fonction constructeur Animal
ci-dessus. Cependant, au lieu d’utiliser le mot-clé class
, nous avons simplement utilisé une vieille fonction JavaScript ordinaire pour recréer la même fonctionnalité. Certes, cela a demandé un peu de travail supplémentaire ainsi qu’une certaine connaissance de ce qui se passe » sous le capot » de JavaScript, mais les résultats sont les mêmes.
Voici la bonne nouvelle. JavaScript n’est pas un langage mort. Il est constamment amélioré et complété par le comité TC-39. Ce que cela signifie, c’est que même si la version initiale de JavaScript ne prenait pas en charge les classes, il n’y a aucune raison pour qu’elles ne puissent pas être ajoutées à la spécification officielle. En fait, c’est exactement ce que le comité TC-39 a fait. En 2015, EcmaScript (la spécification officielle de JavaScript) 6 a été publié avec la prise en charge des classes et du mot-clé class
. Voyons à quoi ressemblerait notre fonction constructeur Animal
ci-dessus avec la nouvelle syntaxe des classes.
class Animal { constructor(name, energy) { this.name = name this.energy = energy } eat(amount) { console.log(`${this.name} is eating.`) this.energy += amount } sleep(length) { console.log(`${this.name} is sleeping.`) this.energy += length } play(length) { console.log(`${this.name} is playing.`) this.energy -= length }}const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)
Pretty clean, right?
So if this is the new way to create classes, why did we spend so much time going over the old way ? La raison en est que la nouvelle façon (avec le mot-clé class
) n’est principalement qu’un » sucre syntaxique » par rapport à la façon existante que nous avons appelée le motif pseudo-classique. Afin de comprendre pleinement la syntaxe pratique des classes ES6, vous devez d’abord comprendre le motif pseudo-classique.
À ce stade, nous avons couvert les principes fondamentaux du prototype de JavaScript. Le reste de ce billet sera consacré à la compréhension d’autres sujets » bons à savoir » qui lui sont liés. Dans un autre post, nous verrons comment nous pouvons prendre ces fondamentaux et les utiliser pour comprendre le fonctionnement de l’héritage en JavaScript.
Méthodes de tableau
Nous avons parlé en profondeur ci-dessus de la façon dont si vous voulez partager des méthodes entre les instances d’une classe, vous devez coller ces méthodes sur le prototype de la classe (ou de la fonction). Nous pouvons voir ce même modèle démontré si nous regardons la classe Array
. Historiquement, vous avez probablement créé vos tableaux comme ceci
const friends =
Il s’avère que c’est juste du sucre sur la création d’une new
instance de la classe Array
.
const friendsWithSugar = const friendsWithoutSugar = new Array()
Une chose à laquelle vous n’avez peut-être jamais pensé est de savoir comment chaque instance d’un tableau dispose de toutes ces méthodes intégrées (splice
slice
pop
, etc) ?
Eh bien, comme vous le savez maintenant, c’est parce que ces méthodes vivent sur Array.prototype
et que lorsque vous créez une nouvelle instance de Array
, vous utilisez le mot-clé new
qui met en place cette délégation vers Array.prototype
en cas d’échec des recherches.
Nous pouvons voir toutes les méthodes du tableau en enregistrant simplement Array.prototype
.
console.log(Array.prototype)/* concat: ƒn concat() constructor: ƒn Array() copyWithin: ƒn copyWithin() entries: ƒn entries() every: ƒn every() fill: ƒn fill() filter: ƒn filter() find: ƒn find() findIndex: ƒn findIndex() forEach: ƒn forEach() includes: ƒn includes() indexOf: ƒn indexOf() join: ƒn join() keys: ƒn keys() lastIndexOf: ƒn lastIndexOf() length: 0n map: ƒn map() pop: ƒn pop() push: ƒn push() reduce: ƒn reduce() reduceRight: ƒn reduceRight() reverse: ƒn reverse() shift: ƒn shift() slice: ƒn slice() some: ƒn some() sort: ƒn sort() splice: ƒn splice() toLocaleString: ƒn toLocaleString() toString: ƒn toString() unshift: ƒn unshift() values: ƒn values()*/
La même logique exacte existe pour les objets également. Tous les objets délégueront à Object.prototype
en cas d’échec des recherches, ce qui explique pourquoi tous les objets ont des méthodes comme toString
et hasOwnProperty
.
Méthodes statiques
Jusqu’à ce point, nous avons couvert le pourquoi et le comment du partage des méthodes entre les instances d’une classe. Cependant, que se passe-t-il si nous avons une méthode qui est importante pour la Classe, mais qui n’a pas besoin d’être partagée entre les instances ? Par exemple, si nous avions une fonction qui prenait un tableau d’instances Animal
et déterminait laquelle devait être alimentée ensuite ? Nous l’appellerons nextToEat
.
function nextToEat (animals) { const sortedByLeastEnergy = animals.sort((a,b) => { return a.energy - b.energy }) return sortedByLeastEnergy.name}
Il n’est pas logique que nextToEat
vive sur Animal.prototype
puisque nous ne voulons pas la partager entre toutes les instances. Au lieu de cela, nous pouvons penser qu’il s’agit plutôt d’une méthode d’aide. Donc, si nextToEat
ne devrait pas vivre sur Animal.prototype
, où devrions-nous le mettre ? Eh bien, la réponse évidente est que nous pourrions simplement coller nextToEat
dans la même portée que notre classe Animal
puis la référencer quand nous en avons besoin comme nous le ferions normalement.
class Animal { constructor(name, energy) { this.name = name this.energy = energy } eat(amount) { console.log(`${this.name} is eating.`) this.energy += amount } sleep(length) { console.log(`${this.name} is sleeping.`) this.energy += length } play(length) { console.log(`${this.name} is playing.`) this.energy -= length }}function nextToEat (animals) { const sortedByLeastEnergy = animals.sort((a,b) => { return a.energy - b.energy }) return sortedByLeastEnergy.name}const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)console.log(nextToEat()) // Leo
Maintenant, cela fonctionne, mais il y a une meilleure façon.
Quand vous avez une méthode qui est spécifique à une classe elle-même mais qui n’a pas besoin d’être partagée entre les instances de cette classe, vous pouvez l’ajouter comme une
static
propriété de la classe.
class Animal { constructor(name, energy) { this.name = name this.energy = energy } eat(amount) { console.log(`${this.name} is eating.`) this.energy += amount } sleep(length) { console.log(`${this.name} is sleeping.`) this.energy += length } play(length) { console.log(`${this.name} is playing.`) this.energy -= length } static nextToEat(animals) { const sortedByLeastEnergy = animals.sort((a,b) => { return a.energy - b.energy }) return sortedByLeastEnergy.name }}
Maintenant, parce que nous avons ajouté nextToEat
comme une static
propriété de la classe, elle vit sur la classe Animal
elle-même (pas son prototype) et on peut y accéder en utilisant Animal.nextToEat
.
const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)console.log(Animal.nextToEat()) // Leo
Parce que nous avons suivi un schéma similaire tout au long de ce post, regardons comment nous pourrions accomplir cette même chose en utilisant ES5. Dans l’exemple ci-dessus, nous avons vu comment l’utilisation du mot-clé static
permettait de placer la méthode directement sur la classe elle-même. Avec ES5, ce même motif est aussi simple que d’ajouter manuellement la méthode à l’objet fonction.
function Animal (name, energy) { this.name = name this.energy = energy}Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}Animal.nextToEat = function (animals) { const sortedByLeastEnergy = animals.sort((a,b) => { return a.energy - b.energy }) return sortedByLeastEnergy.name}const leo = new Animal('Leo', 7)const snoop = new Animal('Snoop', 10)console.log(Animal.nextToEat()) // Leo
Avoir le prototype d’un objet
Quel que soit le motif que vous avez utilisé pour créer un objet, obtenir le prototype de cet objet peut être accompli en utilisant la méthode Object.getPrototypeOf
.
function Animal (name, energy) { this.name = name this.energy = energy}Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}const leo = new Animal('Leo', 7)const prototype = Object.getPrototypeOf(leo)console.log(prototype)// {constructor: ƒ, eat: ƒ, sleep: ƒ, play: ƒ}prototype === Animal.prototype // true
Il y a deux points importants à retenir du code ci-dessus.
Premièrement, vous remarquerez que proto
est un objet avec 4 méthodes, constructor
eat
sleep
, et play
. C’est logique. Nous avons utilisé getPrototypeOf
en passant dans l’instance, leo
en récupérant le prototype de cette instance, qui est l’endroit où toutes nos méthodes vivent. Cela nous apprend encore une chose sur prototype
dont nous n’avons pas encore parlé. Par défaut, l’objet prototype
aura une propriété constructor
qui pointe vers la fonction originale ou la classe à partir de laquelle l’instance a été créée. Ce que cela signifie également, c’est que parce que JavaScript met une propriété constructor
sur le prototype par défaut, toutes les instances pourront accéder à leur constructeur via instance.constructor
.
Le deuxième élément important à retenir de ce qui précède est que Object.getPrototypeOf(leo) === Animal.prototype
. Cela a également du sens. La fonction constructeur Animal
possède une propriété prototype où nous pouvons partager des méthodes entre toutes les instances et getPrototypeOf
nous permet de voir le prototype de l’instance elle-même.
function Animal (name, energy) { this.name = name this.energy = energy}const leo = new Animal('Leo', 7)console.log(leo.constructor) // Logs the constructor function
Pour lier ce dont nous avons parlé précédemment avec Object.create
, la raison pour laquelle cela fonctionne est que toutes les instances de Animal
vont déléguer à Animal.prototype
sur les recherches échouées. Ainsi, lorsque vous essayez d’accéder à leo.constructor
leo
n’a pas de propriété constructor
et va donc déléguer cette recherche à Animal.prototype
qui a effectivement une propriété constructor
. Si ce paragraphe n’a pas eu de sens, retournez lire le paragraphe Object.create
ci-dessus.
Vous avez peut-être déjà vu __proto__ utilisé pour obtenir le prototype d’une instance. C’est une relique du passé. Au lieu de cela, utilisez Object.getPrototypeOf(instance) comme nous l’avons vu ci-dessus.
Déterminer si une propriété vit sur le prototype
Il existe certains cas où vous devez savoir si une propriété vit sur l’instance elle-même ou si elle vit sur le prototype auquel l’objet délègue. Nous pouvons voir cela en action en bouclant sur notre objet leo
que nous avons créé. Disons que l’objectif était de boucler sur leo
et d’enregistrer toutes ses clés et valeurs. En utilisant une boucle for in
, cela ressemblerait probablement à ceci.
function Animal (name, energy) { this.name = name this.energy = energy}Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}const leo = new Animal('Leo', 7)for(let key in leo) { console.log(`Key: ${key}. Value: ${leo}`)}
Que vous attendriez-vous à voir ? Très probablement, c’était quelque chose comme ceci –
Key: name. Value: LeoKey: energy. Value: 7
Pourtant, ce que vous avez vu en exécutant le code était ceci –
Key: name. Value: LeoKey: energy. Value: 7Key: eat. Value: function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Key: sleep. Value: function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Key: play. Value: function (length) { console.log(`${this.name} is playing.`) this.energy -= length}
Pourquoi cela ? Eh bien, une boucle for in
va boucler sur toutes les propriétés énumérables à la fois sur l’objet lui-même ainsi que sur le prototype auquel il délègue. Parce que par défaut, toute propriété que vous ajoutez au prototype de la fonction est énumérable, nous voyons non seulement name
et energy
, mais nous voyons également toutes les méthodes du prototype – eat
sleep
, et play
. Pour résoudre ce problème, nous devons soit spécifier que toutes les méthodes du prototype sont non énumérables, soit trouver un moyen de ne consoler que si la propriété est sur l’objet leo
lui-même et non sur le prototype auquel leo
délègue en cas d’échec de la recherche. C’est là que hasOwnProperty
peut nous aider.
hasOwnProperty
est une propriété sur chaque objet qui renvoie un booléen indiquant si l’objet a la propriété spécifiée comme sa propre propriété plutôt que sur le prototype auquel l’objet délègue. C’est exactement ce dont nous avons besoin. Maintenant, avec ces nouvelles connaissances, nous pouvons modifier notre code pour tirer parti de hasOwnProperty
à l’intérieur de notre boucle for in
.
...const leo = new Animal('Leo', 7)for(let key in leo) { if (leo.hasOwnProperty(key)) { console.log(`Key: ${key}. Value: ${leo}`) }}
Et maintenant, ce que nous voyons sont seulement les propriétés qui sont sur l’objet leo
lui-même plutôt que sur le prototype leo
délégué à aussi.
Key: name. Value: LeoKey: energy. Value: 7
Si vous êtes encore un peu confus au sujet de hasOwnProperty
, voici un code qui pourrait vous éclairer.
function Animal (name, energy) { this.name = name this.energy = energy}Animal.prototype.eat = function (amount) { console.log(`${this.name} is eating.`) this.energy += amount}Animal.prototype.sleep = function (length) { console.log(`${this.name} is sleeping.`) this.energy += length}Animal.prototype.play = function (length) { console.log(`${this.name} is playing.`) this.energy -= length}const leo = new Animal('Leo', 7)leo.hasOwnProperty('name') // trueleo.hasOwnProperty('energy') // trueleo.hasOwnProperty('eat') // falseleo.hasOwnProperty('sleep') // falseleo.hasOwnProperty('play') // false
Vérifier si un objet est une instance d’une classe
Parfois, vous voulez savoir si un objet est une instance d’une classe spécifique. Pour ce faire, vous pouvez utiliser l’opérateur instanceof
. Le cas d’utilisation est assez simple, mais la syntaxe réelle est un peu bizarre si vous ne l’avez jamais vue auparavant. Cela fonctionne comme suit
object instanceof Class
L’instruction ci-dessus retournera true si object
est une instance de Class
et false si ce n’est pas le cas. Pour revenir à notre exemple Animal
, nous aurions quelque chose comme ceci.
function Animal (name, energy) { this.name = name this.energy = energy}function User () {}const leo = new Animal('Leo', 7)leo instanceof Animal // trueleo instanceof User // false
Le fonctionnement de instanceof
est qu’il vérifie la présence de constructor.prototype
dans la chaîne de prototypes de l’objet. Dans l’exemple ci-dessus, leo instanceof Animal
est true
car Object.getPrototypeOf(leo) === Animal.prototype
. De plus, leo instanceof User
est false
car Object.getPrototypeOf(leo) !== User.prototype
.
Création de nouvelles fonctions constructrices agnostiques
Pouvez-vous repérer l’erreur dans le code ci-dessous ?
function Animal (name, energy) { this.name = name this.energy = energy}const leo = Animal('Leo', 7)
Même les développeurs JavaScript chevronnés vont parfois trébucher sur l’exemple ci-dessus. Parce que nous utilisons le pseudoclassical pattern
que nous avons appris précédemment, lorsque la fonction constructeur Animal
est invoquée, nous devons nous assurer de l’invoquer avec le mot-clé new
. Si nous ne le faisons pas, alors le mot-clé this
ne sera pas créé et il ne sera pas non plus implicitement renvoyé.
Pour rafraîchir la mémoire, les lignes commentées sont ce qui se passe dans les coulisses lorsque vous utilisez le mot-clé new
sur une fonction.
function Animal (name, energy) { // const this = Object.create(Animal.prototype) this.name = name this.energy = energy // return this}
Cela semble être un détail trop important pour laisser aux autres développeurs le soin de s’en souvenir. En supposant que nous travaillons en équipe avec d’autres développeurs, existe-t-il un moyen de nous assurer que notre Animal
constructeur est toujours invoqué avec le new
mot-clé ? Il s’avère qu’il y en a un et c’est en utilisant l’opérateur instanceof
que nous avons appris précédemment.
Si le constructeur a été appelé avec le mot-clé new
, alors this
à l’intérieur du corps du constructeur sera un instanceof
la fonction du constructeur elle-même. C’était beaucoup de grands mots. Voici un peu de code.
function Animal (name, energy) { if (this instanceof Animal === false) { console.warn('Forgot to call Animal with the new keyword') } this.name = name this.energy = energy}
Maintenant, au lieu de simplement consigner un avertissement au consommateur de la fonction, que se passe-t-il si nous ré-invoquons la fonction mais avec le mot-clé new
cette fois-ci ?
function Animal (name, energy) { if (this instanceof Animal === false) { return new Animal(name, energy) } this.name = name this.energy = energy}
Maintenant, peu importe si Animal
est invoquée avec le mot-clé new
, elle fonctionnera toujours correctement.
Recréer Object.create
Tout au long de ce post, nous nous sommes largement appuyés sur Object.create
afin de créer des objets qui délèguent au prototype de la fonction constructeur. À ce stade, vous devriez savoir comment utiliser Object.create
à l’intérieur de votre code, mais une chose à laquelle vous n’avez peut-être pas pensé est la façon dont Object.create
fonctionne réellement sous le capot. Pour que vous puissiez vraiment comprendre comment Object.create
fonctionne, nous allons le recréer nous-mêmes. Tout d’abord, que savons-nous sur le fonctionnement de Object.create
?
- Il prend un argument qui est un objet.
- Il crée un objet qui délègue à l’objet de l’argument sur les recherches échouées.
- Il retourne le nouvel objet créé.
Démarrons avec le #1.
Object.create = function (objToDelegateTo) {}
C’est assez simple.
Maintenant le #2 – nous devons créer un objet qui déléguera à l’objet argument sur les recherches échouées. Celui-ci est un peu plus délicat. Pour ce faire, nous allons utiliser nos connaissances sur la façon dont le mot-clé new
et les prototypes fonctionnent en JavaScript. Tout d’abord, dans le corps de notre implémentation Object.create
, nous allons créer une fonction vide. Ensuite, nous allons définir le prototype de cette fonction vide comme étant égal à l’objet argument. Ensuite, afin de créer un nouvel objet, nous invoquerons notre fonction vide en utilisant le mot-clé new
. Si nous retournons cet objet nouvellement créé, cela terminera également le #3.
Object.create = function (objToDelegateTo) { function Fn(){} Fn.prototype = objToDelegateTo return new Fn()}
Wild. Parcourons-le.
Lorsque nous créons une nouvelle fonction, Fn
dans le code ci-dessus, elle est accompagnée d’une propriété prototype
. Lorsque nous l’invoquons avec le mot-clé new
, nous savons que ce que nous obtiendrons en retour est un objet qui déléguera au prototype de la fonction en cas d’échec des recherches. Si nous surchargeons le prototype de la fonction, nous pouvons alors décider de l’objet à déléguer en cas d’échec de la recherche. Ainsi, dans notre exemple ci-dessus, nous surchargeons le prototype de Fn
avec l’objet qui a été transmis lorsque Object.create
a été invoqué, que nous appelons objToDelegateTo
.
Notez que nous ne supportons qu’un seul argument à Object.create. L’implémentation officielle prend également en charge un deuxième argument, facultatif, qui vous permet d’ajouter davantage de propriétés à l’objet créé.
Fonctions flèches
Les fonctions flèches ne disposent pas de leur propre this
mot-clé. Par conséquent, les fonctions flèches ne peuvent pas être des fonctions constructeurs et si vous essayez d’invoquer une fonction flèche avec le mot-clé new
, une erreur sera levée.
const Animal = () => {}const leo = new Animal() // Error: Animal is not a constructor
Egalement, parce que nous avons démontré ci-dessus que le motif pseudo-classique ne peut pas être utilisé avec les fonctions flèches, les fonctions flèches n’ont pas non plus de prototype
propriété.
const Animal = () => {}console.log(Animal.prototype) // undefined
.