Javascript Inner Workings

-

Photo by Adrien Olichon on Unsplash

Il n'y a pas de classe en javascript. Il n'y a que des objets et des fonctions. Si l'on veut que plusieurs objets partagent des fonctionnalités communes, le seul moyen possible est de définir ces fonctionnalités communes dans un objet, et que cet objet soit référencé par tous les objets intéressés d'y accéder. Cet objet partagé est appelé un prototype.

Esquisse générale : stocker des fonctionnalités communes dans un objet partagé

Le fonctionnement esquissé est ainsi le suivant

  1. Créer le prototype qui contient les fonctionnalités partagées

  2. Lier le prototype à l'objet intéressé via une référence

  3. Accéder aux fonctionnalités partagées via la référence

Un prototype pour tous les objets : le prototype universel

Certaines fonctionnalités sont utiles à tout objet. On décide de regrouper ces fonctionnalités dans un prototype qui sera accessible à tous les objets. Cela signifie que tout objet pourra par exemple accéder à la méthode toString. On décide de stocker ce prototype dans Object.prototype.

Comment faire pour que ce prototype soit automatiquement référencé par tous les autres objets? La première solution est la suivante: Tout objet créé via un littéral se voit donner une référence vers Object.prototype. Javascript créé la référence au moment de la création de l'objet. Ainsi, john peut tout de suite accéder aux fonctionnalités partagées

let john = {name:"John"} john.__proto__ === Object.prototype // true john.toString()

Ainsi, la référence vers le prototype est nommée __proto__, mais on se dispense de la mentionner et on accède aux fonctionnalités partagées directement :

note: Pour comprendre comment on accède directement à toString() sans écrire la référence vers le prototype, voir la note plus bas "Note : Accès facilité au prototype"

Cependant, certains objets ne sont pas créés via un littéral. Certains objets sont créés via un constructeur spécifique afin d'avoir accès à un prototype spécifique. On verra d'abord pourquoi il existe des prototypes spécifiques, et ensuite comment on garde un lien vers le prototype universel.

Définir un prototype pour certains objets seulement : un prototype spécifique

On veut pouvoir créer des objets d'un certain type qui partage des fonctionnalités. On veut créer notre propre prototype partagé. Comme faire lorsqu'un objet doit accéder à ce prototype partagé personnalisé, mais en même temps doit continuer à pouvoir accéder au prototype partagé par tous les objets, à savoir Object.prototype. La solution est d'avoir des prototypes en chaine, où la chain remonte jusqu'à Object.prototype. Le prototype personnalisé, qui contient des fonctionnalités personnalisées, conserve une référence vers le prototype universel pour ne pas perdre les fonctionnalités de Object.prototype.

Ne pas briser la chaîne des prototypes

Parfois, on veut plusieurs niveaux de prototypes personnalisés. Ainsi, notre premier prototype personnalisé peut être très général, tandis qu'un deuxième prototype personnalisé sera réservé à une sous catégorie particulière. La sous catégorie doit pouvoir accéder aux fonctionnalités générales, aux fonctionnalités particulières, et aux fonctionnalités universelles. La solution est que le prototype particulier se réfère au prototype général, et le prototype général se réfère au prototype universel.

Comment faire pour que le prototype personnalisé soit référencé automatiquement par tous les objets intéressés, sans avoir à assigner manuellement la référence vers le prototype partagé? Il faut que cette référence soit créée automatiquement à la création de l'objet. Ainsi, lorsqu'un objet personnalisé est créé, il se voit automatiquement assigné une référence vers le prototype partagé :

function Person(){} Person.prototype = {// shared functionality} let john = new Person() // object creation john.__proto__ === Person.prototype // true

De son côté, le prototype personnalisé partagé référence le prototype universel :

Person.prototype.__proto__ === Object.prototype // true

Pour créer une catégorie particulière de personne,

function French(){}

on doit créer un prototype partagé particulier. Pour ne pas briser la chaîne, ce prototype particulier doit référencer le prototype général. On peut assigner la référence manuellement, afin d'illustrer le fonctionnement très clairement

French.prototype.__proto__ = Person.prototype

On établit un lien entre le prototype particulier (French.prototype) et le prototype plus général (Person.prototype)

Résultat : Accès à plusieurs prototypes

Désormais, on peut créer un objet qui a accès a trois prototypes à la chaîne

let paul = new French() paul.__proto__ === French.prototype paul.__proto__.__proto__ === Person.prototype paul.__proto__.__proto__.__proto__ === Object.prototype

Désormais, l'objet paul a accès aux fonctionnalités partagées des 3 prototypes. Ces 3 prototypes répondent à 3 de ses besoins. Le prototype défini au niveau de French est le prototype particulier, le prototype défini au niveau de Person est le prototype plus général, et le prototype défini au niveau de Object lui donne les fonctionnalités que tout objet doit avoir.

Grâce à l'enchainement de prototypes, paul peut accéder directement aux fonctionnalités du prototype universel, qui est à 3 niveaux de lui, sans avoir à enchainer les __proto__

paul.toString()

Paul peut accéder à toString() directement.

Bilan

Le problème a ainsi été résolu: les objets peuvent accéder à des fonctionnalités partagées définies à plusieurs de la hiérarchie. L'accès aux fonctionnalités se fait sans fournir le chemin absolu. La fonctionnalité est cherchée de manière récursive dans les prototypes, en partant du prototype particulier vers le prototype général. Le référencement des prototype se fait automatiquement, au moment de la création de l'objet, via l'opérateur new. Le prototype référencé par le nouvel objet est le prototype qui a été défini au niveau de la fonction constructrice.

Note: Accès facilité au prototype

Depuis l'objet, l'accès à une fonctionnalité définie au niveau du prototype nécessite en principe d'écrire le chemin complet, qui fait intervenir la référence vers le prototype.

object.__proto__.sharedMethod()

Cependant, c'est un peu pénible de préciser à chaque fois le chemin complet. Si cela a le mérite d'être très clair, c'est un peu long à écrire. Javascript nous donne la permission de ne pas écrire le chemin complet: il ira chercher lui même dans le prototype la propriété ou la méthode invoquée si elle n'est pas présente au niveau de l'objet lui même

object.sharedMethod() // chemin implicite

Ainsi, on pourrait croire en apparence qu'il possède cette méthode. En réalité, il ne la possède pas et se contente d'accéder à la méthode partagée.

Note sur les fonctions constructrices

Pour créer un objet, il faut donc d'abord créer une fonction constructrice et définir le prototype partagé au niveau de la fonction constructrice. Toute fonction est potentiellement constructrice, sauf les arrow fonctions.

function f(){} f.prototype = {//fonctionalité partagée}

Les fonctions ont un rôle important en javascript: c'est à leur niveau qu'on va définir les prototypes partagés, et c'est elles qu'on va appeler pour construire l'objet et établir automatiquement la référence vers le prototype.

let newObject = new f() newObject.__proto__ === f.prototype

Note sur le keyword class de ES6

C'est ainsi que fonctionne javascript, même lorsqu'on utilise le "faux" keyword class. En effet, ce qu'on définit avec le keyword n'est autre qu'une fonction constructrice

class Person{} typeof Person === function // true