IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

ES3 en détail

Les constructeurs et les prototypes en JavaScript

Ce tutoriel va traiter de deux points importants de l'implémentation de la programmation orientée objet du point de vue de JavaScript, les fonctions constructeurs et la chaîne des prototypes.

15 commentaires Donner une note à l´article (5)

Article lu   fois.

L'auteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Le JavaScript est un langage de programmation orienté objet supportant l'héritage par délégation basé sur les prototypes. À ce titre donc, il existe des fonctions constructeurs en rapport avec l'utilisation du mot-clé new pour la création d'objets d'une part, et il existe d'autre part un mécanisme appelé chaîne des prototypes s'occupant de gérer l'héritage. Nous allons étudier ces deux aspects dans ce tutoriel. En complément nous en profiterons pour étudier les méthodes d'accès à un objet qui font appel à la chaîne des prototypes et d'où la notion d'héritage découle.

II. Constructeur

Les objets en JavaScript sont créés à l'aide de ce que l'on appelle : les constructeurs.

Un constructeur est une fonction qui crée et initialise l'objet nouvellement créé.

Pour la création (allocation de mémoire) de l'objet, une méthode interne [[Construct]] est utilisée. Le comportement de cette méthode interne est défini par l'implémentation. Tous les constructeurs de fonctions utilisent cette méthode pour allouer de la mémoire aux nouveaux objets.

Pour l'initialisation de l'objet, c'est cette fois la méthode interne [[Call]] qui s'en occupe en appelant une fonction dédiée dans le contexte de l'objet nouvellement créé.

Notez que d'un point de vue utilisateur, seule la phase d'initialisation est accessible et programmable. Cet objet nouvellement créé est accessible dans cette fonction d'initialisation via this. C'est cet objet this qui sera implicitement retourné. Nous pouvons, puisque nous avons la main sur cette phase d'initialisation, retourner autre chose que cet objet nouvellement créé, si l'envie nous en prend avec return :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
// Cette fonction est un constructeur...
function Character() {
    // mettre à jour l'objet nouvellement créé
    this.level = 10;
    //, mais retourner un objet différent
    return [1, 2, 3];
}
// ...si elle est appelée avec le mot-clé `new`
var tiz = new Character();
console.log(tiz.level, tiz); `undefined`, `[1, 2, 3]`

En faisant référence à l'algorithme de création de fonction dont nous avons discuté dans ce tutoriel, nous voyons que cette fonction est un objet natif se trouvant parmi d'autres propriétés internes, comme [[Construct]] et [[Call]] ou propriétés explicites comme prototype, la référence au prototype des futurs objets.

Pseudo-code
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
F = new NativeObject() // objet natif inaccessible

F.[[Class]] = "Function"

.... // autres propriétés

F.[[Call]] = <réference à la fonction> // la fonction elle-même

F.[[Construct]] = internalConstructor // constructeur interne général pour l'allocation mémoire

.... // autres propriétés

// prototype de l'objet créé par le constructeur de F
__objectPrototype = new Object()
__objectPrototype.constructor = F // `{DontEnum}`
F.prototype = __objectPrototype

Ainsi, un objet qui peut être activé par l'appel des parenthèses ( et ) est appelé une fonction, et possède donc cette propriété [[Call]]. Il y a également la propriété [[Class]] qui est responsable de la distinction entre un objet simple et un objet activable puisque, dans le cas d'une fonction, celui-ci vaut "Function". L'opérateur typeof, sur ces objets, retourne la valeur function. Cependant, cela est vrai pour des objets natifs. Dans le cas d'objets hôtes activables, l'opérateur typeof peut retourner d'autres valeurs. Exemple avec window.console.log(...) dans IE :

 
Sélectionnez
1.
2.
3.
// dans IE : "Object", "object", dans d'autres : "Function", "function"
console.log(Object.prototype.toString.call(window.console.log));
console.log(typeof window.console.log); // "Object"

La méthode interne [[Construct]] est activée avec l'opérateur new appliqué à la fonction dite constructeur. Comme nous l'avons vu, c'est cette méthode qui est responsable de l'allocation mémoire et de la création des objets. S'il n'y a aucun argument, l'appel entre parenthèses peut être omis :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
function Character(level) { // constructeur `Character`
    this.level = level || 10;
}

// sans arguments, l'appel
// avec les parenthèses peut être omis
var agnes = new Character; // ou `new Character()`;
console.log(agnes.level); // `10`

// passage explicite
// de la valeur de l'argument `level`
var edea = new Character(20);
console.log(edea.level); // `20`

Et comme nous le savons également la valeur de this à l'intérieur du constructeur (lors de la phase d'initialisation) est affectée d'un nouvel objet.

II-A. Algorithme de création d'objets

Le comportement de la méthode [[Construct]] peut être décrit ainsi :

Pseudo-code
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
O = new NativeObject()

// la propriété `[[Class]]` est mise à `"Object"`,
// c.-à-d. représente un simple objet
O.[[Class]] = "Object"

// Prend comme référence de prototype
// la valeur de `F.prototype` de la fonction
var __objectPrototype = F.prototype

// on associe le prototype `O.[[Prototype]]` de l'objet créé
if (isAnObject(__objectPrototype)) {
    O.[[Prototype]] = __objectPrototype
} else {
    O.[[Prototype]] = Object.prototype
}

// initialisation du nouvel objet créé
// utilisation de `F.[[Call]]`;
// affectation à la valeur de `this` de l'objet `O`
// `initialParameters` sont les arguments passés au constructeur
R = F.[[Call]].apply(O, initialParameters)

if (isAnObject(R)) {
    // on retourne ce que l'utilisateur demande avec `return`
    return R
} else {
    // sinon on retourne l'objet nouvellement créé
    return O
}

Notez deux fonctionnalités majeures :

- premièrement, le [[Prototype]] de l'objet créé est défini à partir de la propriété prototype d'une fonction au moment courant (cela signifie que le prototype de deux objets créés depuis un même constructeur peut varier si la propriété prototype de la fonction change ensuite) ;

- deuxièmement, comme nous l'avons mentionné plus haut, si lors de l'initialisation de l'objet, [[Call]] retourne un objet, c'est cet objet qui sera utilisé comme le résultat de l'expression avec mot clé new :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
function Character() {}
Character.prototype.level = 10;

var ringabel = new Character();
console.log(a.level); // `10` par délégation, depuis le prototype

// affectons a la propriété `prototype` un nouvel objet
// qui va explicitement définir la propriété `constructeur`
Character.prototype = {
    constructor: Character,
    hp: 100
};

var yew = new Character();
// objet `yew` a un nouveau prototype
console.log(yew.level); // `undefined`
console.log(yew.hp); // `100` par délégation, depuis le prototype

// cependant, le prototype de l'objet `ringabel`
// est toujours l'ancien (nous allons voir pourquoi plus bas)
console.log(ringabel.level); // `10` par délégation, depuis le prototype

function Asterisk() {
    this.power = 10;
    return new Array();
}

// si le constructeur de `Asterisk` n'a pas de `return`
// (ou retourne `this`), et bien l'objet `this`
// sera utilisé, sinon ça sera le tableau
var knight = new Asterisk();
console.log(knight.power); // `undefined`
console.log(Object.prototype.toString.call(knight)); // `[object Array]`

II-B. Résumons

Tout objet activable (appelable ou exécutable) est une fonction. Les fonctions activées avec le mot clé new sont dites des fonctions constructeurs (toute fonction est donc possiblement un constructeur dès lors qu'elle ne retourne rien, ou qu'elle retourne this). C'est ce mécanisme qui crée de nouveaux objets en mémoire basés sur un prototype.

Regardons maintenant ce prototype plus en détail.

III. Prototype

Tous les objets ont un prototype (exception faite de certains objets systèmes). La communication avec le prototype est organisée via la propriété interne, implicite et inaccessible [[Prototype]]. Un prototype peut être aussi bien un _objet que la valeur null.

III-A. Propriété constructor

Dans l'exemple ci-dessus, il y a deux points importants. Le premier concerne la propriété constructor de la propriété prototype.

Comme nous l'avons vu dans l'algorithme de la fonction de création d'objets, la propriété constructor est affectée à la propriété prototype lors de la phase de création de la fonction. La valeur de cette propriété est une référence circulaire à la fonction elle-même :

 
Sélectionnez
1.
2.
3.
4.
function Character() {}
var magnolia = new Character();
console.log(magnolia.constructor); // `function Character() {}` par délégation
console.log(magnolia.constructor === Character); // `true`

Souvent dans ce cas il y a un malentendu. La propriété constructor est incorrectement traitée comme une propriété appartenant à l'objet créé. Alors que, comme nous venons de le voir, cette propriété appartient au prototype de la fonction constructeur (ici Character) et est accessible par héritage.

Via la propriété constructor héritée, les objets créés peuvent indirectement obtenir une référence sur l'objet prototype du constructeur :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
function Character() {}
Character.prototype.level = new Number(10);

var janne = new Character();
console.log(janne.constructor.prototype); // `[object Object]`

console.log(janne.level); // `10`, par délégation
// la même chose que `janne.[[Prototype]].level`
console.log(janne.constructor.prototype.level); // `10`

console.log(janne.constructor.prototype.level === janne.level); // `true`

Notez que les propriétés constuctor et prototype peuvent être redéfinies après que l'objet soit créé. Dans ce cas l'objet perd la référence mise en place par le mécanisme ci-dessus.

Cependant, si nous changeons la propriété prototype de la fonction complètement (en assignant un nouvel objet), la référence au constructeur original (ainsi que le prototype original) sont perdu.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
function Character() {}
Character.prototype = {
    level: 10
};

var nikolai = new Character();
console.log(nikolai.level); // `10`
console.log(nikolai.constructor === Character); // `false` !

Et donc c'est pourquoi il est intéressant de restaurer la référence manuellement :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
function Character() {}
Character.prototype = {
    constructor: Character,
    level: 10
};

var nikolai = new Character();
console.log(nikolai.x); // `10`
console.log(nikolai.constructor === Character); // `true`

Notons cependant que la propriété constructor manuellement restaurée, par contraste avec l'originale perdue, n'a pas d'attribut {DontEnum} et, par conséquent, est énumérable dans une boucle for..in sur le Character.prototype.

ES5 introduit la possibilité de contrôler l'état de l'énumération des propriétés avec l'attribut [[Enumerable]].
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
var airy = { level: 10 };

Object.defineProperty(airy, "advice", {
    value: 20,
    enumerable: false // aka `{DontEnum} = true`
});

console.log(airy.level, airy.advice); // `10`, `20`

for (var k in airy) {
    console.log(k); // only `level`
}

var levelDesc = Object.getOwnPropertyDescriptor(airy, "level");
var adviceDesc = Object.getOwnPropertyDescriptor(airy, "advice");

console.log(
    levelDesc.enumerable, // `true`
    adviceDesc.enumerable  // `false`
);

III-B. Propriétés explicites prototype vs. propriétés implicite [[Prototype]]

Souvent, le prototype [[Prototype]] d'un objet, qui est interne à l'objet et inaccessible, est incorrectement confondu avec la référence explicite prototype de la fonction constructeur à ce prototype. Oui, effectivement, ils font référence au même objet, mais ce sont deux propriétés différentes :

 
Sélectionnez
1.
a.[[Prototype]] ----> Prototype <---- A.prototype

De plus, le [[Prototype]] d'un objet créé par un constructeur donne la valeur que possédait la propriété prototype du constructeur lors de la phase de création de l'objet.

Cependant, remplacer la propriété prototype du constructeur n'affecte pas la référence [[Prototype]] des objets déjà créés. Ce sera uniquement la propriété prototype du constructeur qui changera ! Cela signifie que des nouveaux objets auront ce nouveau prototype, mais les objets déjà créés (avant que la propriété prototype ne change), auront une référence vers le vieux prototype. Cette référence ne pourra plus être changée :

Pseudo-code
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
// Création de `anne`
anne = new Character

// État avant le changement de `A.prototype`
anne.[[Prototype]] // ----> Prototype
Character.prototype // ----> Prototype

// Changement de prototype
Character.prototype = newPrototype

// Création de `airy`
airy = new Character

// État après changement
Character.prototype ----> newPrototype
anne.[[Prototype]] ----> Prototype // les objets déjà créés ont une référence à l'ancien prototype
airy.prototype ----> newPrototype // les nouveaux objets auront une référence au nouveau prototype

Par exemple :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
function Character() {}
Character.prototype.level = 10;

var tiz = new Character();
console.log(tiz.level); // `10`

Character.prototype = {
    constructor: Character,
    level: 20,
    hp: 30
};

// l'objet `tiz` utilise l'ancien
// prototype via sa référence
// implicite `[[Prototype]]`
console.log(tiz.level); // `10`
console.log(tiz.hp) // `undefined`

var yew = new Character();

//, mais les nouveaux objets, à la création,
// ont bien une référence au nouveau prototype
console.log(yew.level); // `20`
console.log(yew.hp); // `30`

Parfois on peut lire des articles sur le JavaScript disant que « le changement dynamique de l'objet du prototype va affecter tous les objets qui auront ce nouveau prototype » ; cela est incorrect. Un nouveau prototype réaffecté sera utilisé uniquement sur les nouveaux objets créés après le changement.

La règle principale ici c'est : le prototype d'un objet est assigné au moment de la création et ne peut pas être réassigné par celui que les nouveaux objets auront. En utilisant la référence explicite prototype depuis le constructeur, il est uniquement possible de muter l'objet, c.-à-d. d'ajouter, de modifier ou de supprimer des propriétés existantes dans le prototype de l'objet afin de répercuter les changements dans les objets déjà créés.

III-C. La propriété non standard __proto__

Cependant, certaines implémentations, par exemple, SpiderMonkey, fournissent une référence explicite vers l'objet du prototype via la propriété non standard __proto__ :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
function Character() {}
Character.prototype.level = 10;

var agnes = new Character();
console.log(agnes.level); // `10`

var __newPrototype = {
    constructor: Character,
    level: 20,
    hp: 30
};

// référence au nouvel objet
Character.prototype = __newPrototype;

var ringabel = new Character();
console.log(ringabel.level); // `20`
console.log(ringabel.hp); // `30`

// `agnes` utilise toujours une référence
// sur l'ancien objet
console.log(agnes.level); // `10`
console.log(agnes.hp); // `undefined`

// changeons explicitement le prototype de `agnes`
agnes.__proto__ = __newPrototype;

// maintenant `agnes` fait également
// référence au nouvel objet
console.log(agnes.level); // `20`
console.log(agnes.hp); // `30`
Notez que ES5 a introduit la méthode Object.getPrototypeOf qui retourne directement la valeur de la propriété [[Prototype]] d'un objet, le prototype original de l'instance. Cependant, à la différence de __proto__, cela ne fournit qu'un accesseur, et ne permet en aucun cas de changer le prototype.
 
Sélectionnez
1.
2.
var luxendarc = {};
Object.getPrototypeOf(luxendarc) == Object.prototype; // `true`

III-D. L'objet est indépendant de son constructeur

Comme le prototype de l'objet créé est indépendant du constructeur et de la propriété prototype du constructeur, cela permet la chose suivante : l'objet du prototype de la phase de création peut être supprimé. Le prototype de l'objet créé va continuer d'exister, toujours référencé par la propriété [[Prototype]] :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
function Asterisk() {}
Asterisk.prototype.power = 10;

var whiteMage = new Asterisk();
console.log(whiteMage.power); // `10`

// mise explicite de la référence
// du constructeur `Asterisk` à `null`
Asterisk = null;

//, mais, il est toujours possible de créer
// des objets via la référence indirecte
// depuis les autres objets si
// la propriété `constructor` n'a pas été changée
var blackMage = new whiteMage.constructor();
console.log(blackMage.power); // `10`

// suppression de la référence implicite,
// après ça, `whiteMage.constructor` ainsi que `blackMage.constructor`
// feront référence à la fonction `Object`
// par défaut, mais plus à `Asterisk`
delete whiteMage.constructor.prototype.constructor;

// il ne sera plus possible de créer des objets
// du constructeur `Asterisk`
//, mais ces deux objets auront toujours
// une référence à leur prototype dans `[[Prototype]]`
console.log(whiteMage.power); // `10`
console.log(blackMage.power); // `10`

III-E. L'opérateur instanceof

Il y a un lien entre la référence explicite au prototype, via la propriété prototype du constructeur et l'opérateur instanceof.

Cet opérateur fonctionne de pair avec la chaîne des prototypes d'un objet et pas uniquement avec son constructeur lui-même. Prenez ça en compte, car il y a souvent des incompréhensions à ce niveau. Quand on fait cette vérification :

 
Sélectionnez
1.
2.
3.
if (tiz instanceof Character) {
    /* ... */
}

cela ne veut pas dire que l'objet tiz a été créé par le constructeur Character !

Tout ce que fait l'opérateur instanceof c'est de prendre la valeur de Character.prototype et vérifier sa présence dans la chaîne des prototypes de tiz, en commençant par tiz.[[Prototype]]. L'opérateur instanceof est activé par la méthode interne [[HasInstance]] du constructeur.

Regardons un exemple :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
function Character() {}
Character.prototype.level = 10;

var magnolia = new Character();
console.log(magnolia.level); // `10`

console.log(level instanceof Character); // `true`

// Si maintenant on met `Character.prototype`
// à `null`...
Character.prototype = null;

// ...et bien l'objet `magnolia` a
// toujours accès à son
// prototype, via `magnolia.[[Prototype]]`
console.log(magnolia.level); // `10`

// cependant, l'opérateur `instanceof`
// ne pourra plus fonctionner, car
// il commence son examen depuis la
// propriété `prototype` du constructeur.
console.log(magnolia instanceof Character); // `Character.prototype` n'est pas défini`

Il est également possible de créer soit même le constructeur d'un objet, et instanceof retournera true en vérifiant l'instance d'un autre objet. Tout ce qu'il faut faire c'est définir soi- même la propriété d'objet [[Prototype]] et la propriété prototype du constructeur avec le même objet :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
function Asterisk() {}
var thief = new Asterisk();

console.log(thief instanceof Asterisk); // `true`

function Weapon() {}

var __proto = {
    constructor: Weapon
};

Weapon.prototype = __proto;
thief.__proto__ = __proto;

console.log(thief instanceof Weapon); // `true`
console.log(thief instanceof Asterisk); // `false`

III-F. Stockage via prototype pour partager des méthodes et propriétés

L'application la plus utile des prototypes en JavaScript est le stockage des méthodes, des états par défaut et des propriétés partagées des objets.

En effet, les objets peuvent avoir leur propre état, mais les méthodes sont habituellement les mêmes. C'est pourquoi, pour une optimisation de la mémoire, les méthodes sont habituellement définies dans le prototype. Cela signifie que tous les objets créés par un constructeur partagent toujours les mêmes méthodes.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
function Character(stat) {
    this.stat = stat || 100;
}

Character.prototype = (function () {

    // initialisation du contexte,
    // utilisation d'un objet additionnel

    var sharedStat = 500;

    function helper() {
        console.log('stat partagée : ' + sharedStat);
    }

    function attack() {
        console.log('attaque : ' + this.stat);
    }

    function defence() {
        console.log('défense : ' + this.stat);
        helper();
    }

    // le prototype lui-même.
    return {
        constructor: Character,
        attack: attack,
        defence: defence
    };

})();

var tiz = new Character(10);
var agnes = new Character(20);

tiz.attack(); // `attaque : 10`
tiz.defence(); // `défense : 10`, `stat partagée : 500`

agnes.attack(); // `attaque : 20`
agnes.defence(); // `défense : 20`, `stat partagée : 500`

// les deux objets utilisent
// la même méthode
// le même prototype
console.log(tiz.attack === agnes.attack); // `true`
console.log(tiz.defence === agnes.defence); // `true`

IV. Lire et écrire des propriétés

Comme nous l'avons déjà mentionné, lire et écrire des propriétés se fait grâce à l'aide des méthodes internes [[Get]] et [[Put]]. La méthode est activée grâce à l'accesseur de propriété que ce soit via la notation par point ou par crochet droit :

 
Sélectionnez
1.
2.
3.
4.
5.
// écrire
yew.level = 10; // `[[Put]]` est appelée

console.log(yew.level); // `10`, `[[Get]]` est appelée
console.log(yew['level']); // la même chose

IV-A. La méthode [[Get]]

La méthode [[Get]] considère les propriétés venant de la chaîne des prototypes comme des objets aussi. Ainsi, les propriétés d'un prototype sont accessibles depuis l'objet lui-même. Ainsi pour O.[[Get]](P) avec O comme objet et P comme propriété réclamée, nous avons le mécanisme suivant :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
// si c'est sa propre propriété,
// on la retourne
if (O.hasOwnProperty(P)) {
    return O.P
}

// sinon, on analyse le prototype
var __proto = O.[[Prototype]]

// s'il n'y a pas de prototype (cela est possible dans le dernier maillon de la chaîne pour `Object.prototype.[[Prototype]]`,
// qui est égale à `null`),
// on retourne `undefined`
if (__proto === null) {
    return undefined
}

// sinon, on appelle la méthode `[[Get]]` récursivement
// maintenant pour le prochain prototype ; c.-à-d.
// que l'on traverse la chaîne des prototypes: et on essaye de trouver
// la propriété, puis ensuite dans le prototype de prototype
// et ainsi de suite jusqu'à ce que le prototype soit égal à `null`
return __proto.[[Get]](P)

Notez que, puisque la méthode [[Get]] dans un des cas retourne undefined, il est possible de vérifier la présence d'une variable comme ceci :

 
Sélectionnez
1.
2.
3.
if (window.someObject) {
    /* ... */
}

Ici, la propriété someObject n'est pas trouvée dans window, ni dans le prototype, ni dans le prototype du prototype, et l'algorithme retourne alors undefined.

Notez que c'est exactement le test de présence dont est responsable l'opérateur in. Il va également fouiller dans la chaîne des prototypes :

 
Sélectionnez
1.
2.
3.
if ('someObject' in window) {
    /* ... */
}

Cela aide a éviter les cas où, par exemple, someObject serait égale à false et ou le première vérification aurait échouée malgré l'existence de la propriété.

IV-B. La méthode [[Put]]

La méthode [[Put]] quand a elle, met à jour sa propre propriété d'objet et masque les propriétés du même nom venant d'un prototype plus haut. Voyons cela avec l'algorithme de O.[[Put]](P, V) ou O est l'objet, P la propriété et V la valeur.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
O.[[Put]](P, V):

// s'il n'est pas possible d'écrire
// dans cette propriété
// alors on ne fait rien.
if (!O.[[CanPut]](P)) {
    return
}

// si l'objet ne possède pas cette propriété,
// alors on la crée; tous les attributs
// de propriété étant placés à `false`.
if (!O.hasOwnProperty(P)) {
    createNewProperty(O, P, attributes: {
        ReadOnly: false,
        DontEnum: false,
        DontDelete: false,
        Internal: false
    })
}

// changer la valeur.
// si la propriété existait déjà, ses
// attributs restent inchangés, seule la valeur
// change
O.P = V

return

Par exemple :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
Object.prototype.level = 100;

var edea = {};
console.log(edea.level); // `100`, hérité

edea.level = 10; // `[[Put]]`
console.log(edea.level); // `10`, possédé

delete edea.level;
console.log(edea.level); // again `100`, hérité

Notez qu'il n'est pas possible de masquer des propriétés héritées en lecture seule. Le résultat de l'affectation est simplement ignoré. Ceci est contrôlé par la méthode interne [[CanPut]].

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
// Par exemple, la propriété `length`
// de l'objet `String` est en lecture seule ; faisons de
// `String` le prototype de notre objet et essayons
// de masquer la propriété `length`

function SuperString() {
    /* rien */
}

SuperString.prototype = new String("abc");

var luxendarc = new SuperString();

console.log(luxendarc.length); // `3`, la longueur de `abc`

// essayons de la masquer
luxendarc.length = 5;
console.log(luxendarc.length); // toujours `3`

En mode strict de ES5, tenter de modifier une propriété en lecture seule lève l'erreur TypeError.

IV-C. Accesseurs de propriété

Comme expliqué, les méthodes internes [[Get]] et [[Put]] sont activées par les accesseurs de propriété disponibles dans JavaScript via la notation avec point ou la notation avec _crochet droit. La notation avec point est utilisée quand le nom de propriété est un identifieur valide ou connu à l'avance, alors que la notation avec crochet droit permet l'utilisation de noms invalides ou dynamiques.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
var rpg = { testProperty: 10 };

console.log(rpg.testProperty); // `10`, notation avec point
console.log(rpg['testProperty']); // `10`, notation avec crochet droit

var propertyName = 'Propriété';
console.log(rpg['test' + propertyName]); // `10`, notation dynamique avec crochet

Il y a encore une fonctionnalité importante : les accesseurs appellent toujours la conversion ToObject pour les objets placés sur la partie gauche de la propriété accession. Et du fait de cette conversion implicite, il est possible de dire « tout en JavaScript est un objet » (cependant comme nous le savons déjà, bien entendu, tout n'est pas objet, il y a également des valeurs primitives).

Si nous utilisons des accesseurs de propriété sur des valeurs primitives, nous créons juste un objet encadrant immédiat correspondant à cette valeur. Une fois le travail terminé, cet objet encadrant est supprimé.

Exemple :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
var level = 10; // valeur primitive

//, mais si on le demande, il y aura
// accès à la méthode, comme si c'était un objet.
console.log(level.toString()); // `"10"`

// nous pouvons également
// (tenter de) créer une nouvelle
// propriété dans la primitive `level` en appelant `[[Put]]`
level.test = 100; // et cela semble fonctionner.

//, mais, `[[Get]]` ne retourne
// pas la valeur de cette propriété, et
// l'algorithme retourne `undefined`
console.log(level.test); // `undefined`

Alors, pourquoi, dans cet exemple, la valeur « primitive » de level à accès à la méthode toString, mais n'a pas accès à la propriété nouvellement créée test ?

La réponse est simple :

en premier lieu, comme déjà dit, après que l'accesseur de propriété ait été appliqué, on ne manipule pas une primitive, mais un objet intermédiaire. Dans ce cas, new Number(level) est utilisé, et par délégation la méthode toString de la chaîne du prototype :

Pseudo-code
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
// Algorithme d'évaluation de `level.toString()`

// 1.
wrapper = new Number(level)
// 2.
wrapper.toString() // `"10"`
// 3.
delete wrapper

Maintenant, la méthode [[Put]] crée également son propre objet intermédiaire englobant quand la propriété test est évaluée :

Pseudo-code
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
// Algorithme d'évaluation de `level.test = 100`

// 1.
wrapper = new Number(level)
// 2.
wrapper.test = 100
// 3.
delete wrapper

Nous voyons à l'étape 3 que l'objet encadrant est supprimé et que la propriété test nouvellement créée a été supprimée elle aussi à la suppression de l'objet lui-même.

Quand [[Get]] est utilisé de nouveau sur l'accesseur de propriété créé, il crée encore une fois un nouvel objet encadrant qui lui, ne sait rien à propos d'une quelconque propriété test :

Pseudo-code
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
// Algorithme d'évaluation de `level.test`

// 1.
wrapper = new Number(level)
// 2.
wrapper.test // `undefined`
// 3.
delete wrapper

Donc faire référence à une propriété ou méthode depuis une valeur primitive n'a de sens que pour la lecture des propriétés. Aussi, quand une valeur primitive accède souvent à des propriétés, pour économiser du temps de ressources, cela peut avoir du sens de directement la remplacer par sa représentation objet. Et, au contraire, si la valeur n'est utilisée que pour de petits calculs qui ne dépendent d'aucune propriété d'accès, il sera plus performant d'utiliser une valeur primitive à la place.

V. Héritage

Comme nous le savons, le JavaScript utilise l'héritage par délégation basé sur les prototypes.

Chaînage et prototype sont également souvent mentionnés en tant que chaîne de prototype.

En fait, l'intégralité de l'implémentation et l'analyse de la délégation se réduit au travail effectué par [[Get]] et déjà mentionné précédemment.

Si vous comprenez intégralement ce simple algorithme de la méthode [[Get]], la question de l'héritage en JavaScript disparaît d'elle-même et la réponse devient claire.

Souvent sur les forums, quand les discussions se tournent vers l'héritage en JavaScript, je montre, en tant qu'exemple, seulement une ligne de code JavaScript qui représente exactement la définition d'une structure d'objet du langage et montre la délégation basée sur l'héritage. La ligne de code est vraiment simple :

 
Sélectionnez
1.
console.log(1..toString()); // `"1"`

Maintenant, comme nous connaissons l'algorithme de la méthode [[Get]] et les accesseurs de propriétés, nous pouvons voir ce qu'il se passe ici :

  1. Depuis la valeur primitive 1, un objet encadrant équivalent à new Number(1) est créé ;
  2. La méthode toString héritée est appelée depuis cet objet encadrant.

Pourquoi héritée ? Car les objets JavaScript peuvent avoir leurs propres propriétés, et que l'objet encadrant créé dans ce cas n'a pas sa propre méthode toString. Cependant, il en hérite par délégation via son prototype, c’est-à-dire utilise Number.prototype.

Notez la subtilité de la syntaxe. Deux points dans l'exemple précédent n'est pas une erreur. Le premier point est utilisé pour la partie fractionnée du nombre, et le second point est quant à lui l'accesseur de propriété :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
1.toString(); // `SyntaxError` !

(1).toString(); // OK

1 .toString(); // OK (espace après 1)

1..toString(); // OK

1['toString'](); // OK

V-A. Chaîne de prototype

Montrons comment créer cette chaîne avec des objets définis par les utilisateurs. C'est très simple :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
function Monster() {
    console.log('Monster.[[Call]] activé');
    this.attack = 10;
}
Monster.prototype.power = 20;

var monster = new Monster();
console.log([monster.attack, monster.power]); // `10` (possédé), `20` (délégué)

function Humanoid() {}

// la variante la plus simple du chaînage de prototype
// est de chaîner la valeur d'un enfant prototype
// a un nouvel objet créé,
// avec le constructeur du parent.
Humanoid.prototype = new Monster();

// fixons la propriété `constructor`, sinon elle vaudra `Monster`
Humanoid.prototype.constructor = Humanoid;

var goblin = new Humanoid();
console.log([goblin.attack, goblin.power]); // `10`, `20`, les deux sont délégués

// `[[Get]] goblin.attack` :
// `goblin.attack` (pas trouvé) -->
// `goblin.[[Prototype]].attack` (trouvé) - `10`

// `[[Get]] goblin.power` :
// `goblin.power` (pas trouvé) -->
// `goblin.[[Prototype]].power` (pas trouvé) -->
// `goblin.[[Prototype]].[[Prototype]].power` (trouvé) - `20`

// où `goblin.[[Prototype]] === Humanoid.prototype`,
// et `goblin.[[Prototype]].[[Prototype]] === Monster.prototype`

Cette approche a deux fonctionnalités.

La première, Humanoid.prototype va contenir la propriété attack. Mais cela n'est pas correct puisque la propriété attack est définie dans Monster lui-même et que même s'il pourrait être attendu que le constructeur Humanoid le possède aussi, ce n'est pas le cas.

Dans le cas d'une traversée d'héritage prototypal normal, jusqu'à l'objet descendant, personne ne possède sa propre propriété déléguée d'un prototype. L'idée derrière ça, c'est que les objets créés par le constructeur Humanoid n'_ont _pas besoin de la propriété attack. Ce qui n'est pas le cas des modèles basés sur les classes, où toute propriété est copiée dans la classe descendante.

Cependant, s'il est nécessaire que la propriété attack soit propre aux objets créés par le constructeur Humanoid, il existe certaines techniques pour cela (émulation d'une approche basée sur la classe), dont nous allons parler ci-dessous.

La seconde n'est pas vraiment une fonctionnalité, mais un désavantage. Le code du constructeur est aussi exécuté quand le descendant du prototype est créé. Nous pouvons voir ça grâce au message "Monster.[[Call]] activé" qui apparaît deux fois, quand l'objet est créé par le constructeur Monster qui est utilisé par Humanoid.prototype et lors de la création de l'objet monster lui-même !

Un exemple plus critique est une exception lancée dans le constructeur parent : peut-être que pour un objet réellement créé par ce constructeur, une vérification est nécessaire, mais le même cas est totalement inacceptable avec l'utilisation de cet objet comme prototype parent :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
function Monster(param) {
    if (!param) {
        throw 'Paramètre requis';
    }
    this.param = param;
}
Monster.prototype.attack = 10;

var monster = new Monster(20);
console.log([monster.attack, monster.param]); // `10`, `20`

function Humanoid() {}
Humanoid.prototype = new Monster(); // `Erreur`

En outre, des calculs lourds dans le constructeur parent peuvent également être considérés comme un désavantage avec cette approche.

Pour résoudre le problème de cette « fonctionnalité », les programmeurs actuels utilisent un motif standard pour chaîner les prototypes, comme nous allons le voir plus bas. Le principal objectif de cette astuce consiste à créer un objet constructeur encadrant intermédiaire qui chaîne les prototypes souhaités.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
function Monster() {
    console.log('Monster.[[Call]] activé');
    this.attack = 10;
}
Monster.prototype.defence = 20;

var monster = new Monster();
console.log([monster.attack, monster.defence]); // `10` (possédé), `20` (hérité)

function Humanoid() {
    // Ou simplement `Monster.apply(this, arguments)`
    Humanoid.superproto.constructor.apply(this, arguments);
}

// héritage : chaînage de prototypes
// en créant un constructeur intermédiaire vide.
var F = function () {};
F.prototype = Monster.prototype; // référence
Humanoid.prototype = new F();
Humanoid.superproto = Monster.prototype; // référence explicite au prototype ancêtre, « sucre »

// fixons la propriété `constructor, sinon elle vaudra `Monster`
Humanoid.prototype.constructor = Humanoid;

var goblin = new Humanoid();
console.log([goblin.attack, goblin.defence]); // `10` (propre), `20` (hérité)

Notez comment nous créons notre propre propriété attack sur l'instance de defence : nous appelons la référence au constructeur parent via Humanoid.superproto.constructor dans le contexte nouvellement créé.

Nous fixons également le problème vis-à-vis de la non-nécessité d'appeler le constructeur parent pour créer le prototype descendant. Mantenant le message "Monster.[[Call]] activé" n'est affiché que si nécessaire.

Et pour ne pas avoir à répéter chaque fois les mêmes actions lors du chaînage de prototype (création d'un objet constructeur intermédiaire, créer un sucre superproto, restaurer la propriété constructor originale, etc.), ce modèle peut être encapsulé dans une fonction utilitaire, dont le but est de chaîner les prototypes indépendamment du nom concret de leurs constructeurs :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
function inherit(child, parent) {
    var F = function () {};
    F.prototype = parent.prototype
    child.prototype = new F();
    child.prototype.constructor = child;
    child.superproto = parent.prototype;
    return child;
}

Et l'héritage pourra se faire ainsi :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
function Monster() {}
Monster.prototype.attack = 10;

function Humanoid() {}
inherit(Humanoid, Monster); // chaînage de prototype

var goblin = new Humanoid();
console.log(goblin.attack); // `10`, trouver dans le `Monster.prototype`

Il y a beaucoup de variations de cet objet encadrant (au regard de la syntaxe), cependant, elles se résument toutes à effectuer les actions ci-dessus.

Par exemple, nous pouvons optimiser l'objet encadrant précédent en mettant l'objet encadrant intermédiaire à l'extérieur du constructeur (comme cela, seulement une fonction sera créée), pour ensuite la réutiliser :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
var inherit = (function(){
    function F() {}
    return function (child, parent) {
        F.prototype = parent.prototype;
        child.prototype = new F;
        child.prototype.constructor = child;
        child.superproto = parent.prototype;
        return child;
    };
})();

Et puisque le vrai prototype d'un objet est la propriété [[Prototype]], cela signifie que F.prototype peut facilement être changé et réutilisé, car child.prototype, qui a été créé via new F, va être dans [[Prototype]] comme la valeur courante de child.prototype :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
function Monster() {}
Monster.prototype.attack = 10;

function Humanoid() {}
inherit(Humanoid, Monster);

Humanoid.prototype.y = 20;

Humanoid.prototype.name = function () {
    console.log("Humanoid#name");
};

var goblin = new Humanoid();
console.log(goblin.attack); // `10`, est trouvé dans le `Monster.prototype`

function Goblin() {}
inherit(Goblin, Humanoid);

// en utilisant notre sucre « superproto »
// nous pouvons appeler la méthode parente avec le même nom.

Goblin.prototype.name = function () {
    Goblin.superproto.name.call(this);
    console.log("Goblin#name");
};

var goblinSlasher = new Goblin();
console.log([goblinSlasher.attack, goblinSlasher.defence]); // `10`, `20`

goblinSlasher.foo(); // `"Humanoid#foo"`, `"Goblin#foo"`

Notez qu'en ES5 cette fonctionnalité a été standardisée pour des meilleurs chaînages de prototype. C'est la méthode Object.create.Une version simplifiée en tant que fonction de substitution ES3 s'implémenterait de cette manière :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
Object.create ||
Object.create = function (parent, properties) {
    function F() {}
    F.prototype = parent;
    var child = new F;
    for (var k in properties) {
        child[k] = properties[k].value;
    }
    return child;
}

pour être utilisés ainsi  :

 
Sélectionnez
1.
2.
3.
var monster = { attack: 10 };
var kobold = Object.create(monster, { defence: { value: 20 } });
console.log(kobold.attack, kobold.attack); // `10`, `20`

De manière générale, toutes les limitations de « l'héritage classique en JavaScript » sont basées sur ce principe. Maintenant, nous voyons qu'en fait même si ça ressemble à « une imitation des classes basée sur l'héritage », c'est surtout une manière simple de réutiliser du code pour le chaînage de prototypes.

Notez qu'en ES6, le concept de « class » a été standardisé, et son implémentation est exactement un « sucre syntaxique » par-dessus les fonctions constructeurs décrites plus haut. De ce point de vue, le chaînage de prototype devient un détail d'implémentation de l'héritage basé sur les classes :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
// ES6
class Monster {
    constructor(name) {
       this._name = name;
    }

    getName() {
        return this._name;
    }
}

class Humanoid extends Monster {
    getName() {
        return super.getName() + ' Archer';
    }
}

var goblin = new Humanoid('Goblin');
console.log(goblin.getName()); // `"Goblin Archer"`

VI. Conclusion

Ce tutoriel n'a pas été avare de détails. Il pourra vous servir de référence globale pour lister la majorité des mécanismes JavaScript et retrouver rapidement des détails de fonctionnement.

VII. Remerciements

Nous remercions Bruno Lesieur qui nous a autorisés à publier ce tutoriel.

Nous remercions également Laethy pour la mise au gabarit et jacques_jean pour la correction orthographique.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2019 Bruno Lesieur. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.