Data

EcmaScript 6 Part 3

Mar 7, 2015

Nicolas Bailly

Bonjour à tous ! Voici la troisième partie de ma série d’articles sur ECMAScript 6. Bon, quoi de beau au menu : en entrée et plat principal (très copieux ne vous inquiétez pas ^^) je vous propose les classes ! Hein ??? Des classes en JavaScript ? Oui oui vous avez bien entendu. Nous finirons par le dessert : les nouveautés pour le type object !

Introduction

La POO en JavaScript ? Ça n’existe pas ! Ce genre de discours est typique d’une personne qui dit avoir fait du JavaScript après avoir utilisé JQuery sur quelques-uns de ces projets et donc qui ne connais pas le langage. Si le mot-clé prototype ne vous parle pas, pas de panique ! Nous allons faire un rapide retour sur comment qu’on faisait avant pour mieux comprendre comment les classes ECMAScript 6 fonctionnent. Il ne s’agit ici que de sucre syntaxique permettant une approche plus intuitive (enfin presque). Dans la logique des choses, nous parlerons ensuite des nouvelles méthodes associées au type object.

Les classes

Bon comme d’habitude, on ne fonce pas tête baissé. Comme je vous l’ai précédemment précisé, les classes amènent une surcouche de syntaxe permettant de faire ce qu’il était déjà possible, mais de façon plus lisible (notamment pour développeur non JavaScript). Nous allons dans un premier temps nous pencher sur les objets et une propriété toute particulière : prototype. Elle nous permettra d’atteindre notre but ultime : faire de l’héritage.

A. Les objets

En JavaScript tout est objet ! Cela est assez déroutant lorsqu’on débarque d’un langage type Java, C#,… Avec ECMAScript 5, pas de classes, mais vous allez voir qu’il est possible de retrouver les mêmes possibilités. Bon premier petit exemple de constructeur :

function Nuggets(type, quantity) {
    this.type = type;
    this.quantity = quantity;
    this.fried = function() {
        console.log('Je suis frit !');
    }
}

var myNuggetsBox = new Nuggets('chicken', 20);
console.log(myNuggetsBox instanceof Nuggets); // true
console.log(myNuggetsBox.type); // chicken
console.log(myNuggetsBox.quantity); // 20
myNuggetsBox.fried(); // Je suis frit !

Plutôt classique, on voit que le constructeur est une fonction qui peut prendre des paramètres pour initialiser ses instances. On instancie un nouvel objet à l’aide du mot-clé new. On peut voir l’utilisation d’un autre mot-clé bien utile instanceof, qui permet de déterminer si l’objet à gauche de l’opérande à bien été construit par le constructeur précisé à droite. Repassons à prototype, pour bien comprendre l’utilité de ce mot-clé voyez l’exemple qui suit :

function Nuggets(type, quantity) {
    this.type = type;
    this.quantity = quantity;
    this.fried = function() {
        console.log('Je suis frit !');
    }
}

var myNuggetsBox = new Nuggets('chicken', 20);
var myNuggetsBox2 = new Nuggets('beef', 10);
console.log(myNuggetsBox instanceof Nuggets); // true
console.log(myNuggetsBox2 instanceof Nuggets); // true
console.log(myNuggetsBox2 == myNuggetsBox); // false
console.log(myNuggetsBox2 === myNuggetsBox); // false
console.log(myNuggetsBox2.fried == myNuggetsBox.fried); // false
console.log(myNuggetsBox2.fried === myNuggetsBox.fried); // false

myNuggetsBox.salt = function() {
    console.log('Je suis salé');
}

myNuggetsBox.salt(); // Je suis salé
myNuggetsBox2.salt(); // Throw TypeError

On voit bien que les deux objets myNuggetsBox et myNuggetsBox2 sont effectivement des instances de Nuggets. Première chose étrange, les deux instances ne sont pas égales. Elles viennent du même constructeur pourtant ? Oui mais l’opérateur de comparaison (== et ===) à un fonctionnement bien particulier lorsqu’il s’agit de comparer des objets. Les types primitifs (String, Number,…) sont comparés par valeur. Ici ce sont les références qui sont comparées, il vérifie donc si les objets occupent le même espace mémoire. Ce qui n’est évidemment pas le cas. Cela explique également le second cas : la méthode fried des deux objets n’est pas équivalente. Elle n’occupe pas le même espace mémoire. On peut donc facilement conclure que lors de l’instanciation d’un objet, l’ensemble des propriétés et méthodes liées à l’instance sont dupliquées. Étendre les fonctionnalités des objets en ajoutant de nouvelles méthodes dans le constructeur n’est donc pas très performant, puisque aucune propriété ou méthode n’est partagée. Bon, la suite de l’exemple est assez logique, on ajoute une méthode à notre premier objet ce qui n’affecte évidemment pas le second. Maintenant la question qui est censée vous brûler les lèvres : comment qu’on créer des méthodes partagées par toutes les instances ? Un seul mot : prototype !

B. Prototype

Enfin nous y voici. Voyons tout de suite un premier petit exemple illustrant, comment ajouter une méthode partagée par toutes les instances à l’aide du mot-clé prototype :

function Nuggets(type, quantity) {
    this.type = type;
    this.quantity = quantity;
    this.fried = function() {
        console.log('Je suis frit !');
    }
}

console.log(Nuggets.prototype); // Nuggets {}

Nuggets.prototype.salt = function() {
    console.log('Je suis salé');
}

var myNuggetsBox = new Nuggets('chicken', 20);
var myNuggetsBox2 = new Nuggets('beef', 10);
console.log(myNuggetsBox.salt == myNuggetsBox2.salt); // true
console.log(myNuggetsBox.salt === myNuggetsBox2.salt); // true
console.log(myNuggetsBox); // Voir image ci-dessous
console.log(Nuggets.prototype); // Voir image ci-dessous
myNuggetsBox.salt(); // Je suis salé
myNuggetsBox2.salt(); // Je suis salé

Ecmascript_prototype

Bon analysons un peu tout ça. On voit que juste après avoir défini notre constructeur, le prototype de Nuggets existe, c’est un objet vide. On ajoute une nouvelle méthode au prototype de Nuggets (salt). Grâce à l’opérateur de comparaison, on peut voir que ce coup-ci la méthode salt est partagée par les instances du constructeur Nuggets. Notre objectif est atteint ! Mais là ou cela deviens encore plus intéressant, c’est lorsque l’on observe l’imprime écran de la sortie console de l’objet myNuggetsBox. On voit clairement que c’est un objet instancié via le constructeur Nuggets, on voit ses propriétés et ses méthodes, mais surtout on voit sa propriété _proto_ ! Qui comme vous vous en douter contient une référence qui pointe sur le prototype Nuggets. Mais si l’on regarde la sortie console du prototype Nuggets, on voit qu’il a une méthode salt et surtout une propriété _proto_, qui point sur Object.prototype ! Hein ? Eh oui, en JavaScript tout objet dérive du prototype Object. Cela explique pourquoi après avoir créé mon objet myNuggetsBox, je peux utiliser les méthodes .valueOf(), .toString() ou encore .toLocaleString(). Ces méthodes proviennent du prototype Object et son partagées par toutes les instances. Pour ce qui est des variables de types primitifs, elles n’héritent pas directement d’un prototype, mais les wrapper object ( String, Number, …) eux héritent bien du prototype associé à leur type (pour plus de détail sur les wrapper object c’est ici). En JavaScript quand on dit que tout est objet, on ne blague pas ! Car les prototypes sont eux-mêmes des objets (oui ça fait un peu mal à la tête au début). Bon, revenons à nos moutons, on voulait faire de l’héritage. Notre but est avant tout de définir un constructeur parent, ajouter une propriété au prototype puis de créer un constructeur fils qui héritera des propriétés et des méthodes du parent. Pour cela il y a plusieurs façons de faire :

//Définition premier constructeur
function Nuggets(type) {
    this.type = type;
}

Nuggets.prototype.salt = function() {
    console.log('Je suis salé');
};

//Définition deuxième constructeur
function NuggetsSpicy(type) {
    this.type = type;
}

//NON ! Le fils obtient une référence pointant sur le prototype parent et peut donc le modifier !
NuggetsSpicy.prototype = Nuggets.prototype;

NuggetsSpicy.prototype.spicy = function() {
    console.log('Je suis épicé');
};

var myNuggetsBox = new Nuggets('chicken');
var myNuggetsBox2 = new NuggetsSpicy('beef');
console.log(myNuggetsBox instanceof Nuggets); // true
console.log(myNuggetsBox2 instanceof Nuggets); // true
console.log(myNuggetsBox instanceof NuggetsSpicy); // true
console.log(myNuggetsBox2 instanceof NuggetsSpicy); // true
myNuggetsBox.spicy(); // Je suis épicé
myNuggetsBox2.spicy(); // Je suis épicé

Ce premier exemple, c’est justement la chose à ne pas faire, car on ne fait pas de l’héritage à proprement parlé. Toutes modifications sur le prototype fils modifiera le prototype parent étant donné qu’il pointe sur le même objet ! Cela explique également le fait que myNuggetsBox instanceof NuggetsSpicy retourne true. En effet l’opérateur instanceof ne check pas juste le constructeur. Il remonte la chaîne des prototypes afin de déterminer si l’opérande de droite est une propriété prototype d’un des constructeur parent. Ne vous inquiétez pas, nous reparlerons de la chaîne des prototypes plus en détail. Allez, on passe à un vrai exemple d’héritage :

//Définition premier constructeur
function Nuggets(type) {
    this.type = type;
}

Nuggets.prototype.salt = function () {
    console.log('Je suis salé');
};

//Définition deuxième constructeur
function NuggetsSpicy(type, spiceLevel) {
    this.type = type;
    this.spiceLevel = spiceLevel;
}

//On modifie la chaîne de prototype
NuggetsSpicy.prototype.__proto__ = Nuggets.prototype;

NuggetsSpicy.prototype.spicy = function () {
    console.log('Je suis épicé');
};

var myNuggetsBox = new Nuggets('chicken');
var myNuggetsBox2 = new NuggetsSpicy('beef', 'HARD');
console.log(myNuggetsBox instanceof Nuggets); // true
console.log(myNuggetsBox2 instanceof Nuggets); // true
console.log(myNuggetsBox instanceof NuggetsSpicy); // false
console.log(myNuggetsBox2 instanceof NuggetsSpicy); // true
myNuggetsBox.salt(); // Je suis salé
myNuggetsBox2.salt(); // Je suis salé
myNuggetsBox2.spicy(); // Je suis épicé
myNuggetsBox.spicy(); // Throw error

Pas mal nan ? Seul petit bémol, notre constructeur fils n’hérite pas du constructeur parent. Ici on modifie seulement la chaîne des prototypes. Ça fait deux fois que t’en parle de cette chaîne ! Explique-nous ! Eh bien, lorsqu’on utilise la méthode ou la propriété d’un objet, la chaîne des prototypes est utilisée pour résoudre cette donnée. J’utilise par exemple la méthode .toString() sur mon objet myNuggetsBox, le moteur JavaScript (V8, Rhino, …) va tout d’abord rechercher la méthode dans l’objet lui-même. Il ne la trouve pas et donc il va la rechercher dans le prototype de l’objet (dans notre cas Nuggets.prototype). Il ne la trouve toujours pas, donc il va la rechercher dans le prototype du prototype de notre objet (Object.prototype), et la bingo !
Il y a un deuxième souci dans l’exemple ci-dessus, la propriété __proto__ ! Elle est d’abord apparue sous FireFox puis est rapidement devenue populaire. On la trouve maintenant sous Chrome, Safari,… Mais elle n’est pas disponible sur tous les navigateurs et de plus elle n’est pas spécifiée par ECMAScript 5 et donc non standardisée. Mais ne vous inquiétez pas elle fera partie d’ECMASCript 6 ! Vous pouvez donc être sûr que tous les navigateurs modernes implémenteront cette propriété même si son utilisation est controversée.

Bon tu nous le montre ton exemple d’héritage en ECMAScript 5 ! Oui oui j’y viens. Depuis ECMAScript 5, il existe un mot-clé qui permet de créer un objet, qui héritera d’un autre objet : Object.Create(). À l’aide de ce mot-clé je vais enfin vous montrer un héritage ES5 en bon et du forme :

//Définition premier constructeur
function Nuggets(type) {
    this.type = type;
}

Nuggets.prototype.salt = function () {
    console.log('Je suis salé');
};

//Définition deuxième constructeur
function NuggetsSpicy(type, spiceLevel) {
    //Appel du constructeur parent
    Nuggets.call(this, type);
    this.spiceLevel = spiceLevel;
}

//On créer un objet qui hérite du prototype de Nuggets
NuggetsSpicy.prototype = Object.create(Nuggets.prototype);
//On ré-affecte le constructeur
NuggetsSpicy.prototype.constructor = NuggetsSpicy;

NuggetsSpicy.prototype.spicy = function () {
    console.log('Je suis épicé');
};

var myNuggetsBox = new Nuggets('chicken');
var myNuggetsBox2 = new NuggetsSpicy('beef', 'HARD');
console.log(myNuggetsBox instanceof Nuggets); // true
console.log(myNuggetsBox2 instanceof Nuggets); // true
console.log(myNuggetsBox instanceof NuggetsSpicy); // false
console.log(myNuggetsBox2 instanceof NuggetsSpicy); // true
myNuggetsBox.salt(); // Je suis salé
myNuggetsBox2.salt(); // Je suis salé
myNuggetsBox2.spicy(); // Je suis épicé
myNuggetsBox.spicy(); // Throw error

Ayer ! On y est enfin arrivé. Dans ce cas, on utilise bien le constructeur parent et on hérite du prototype parent et donc des ses méthodes. Bon il faut l’avouer, quand on n’est pas habitué à la syntaxe, cela donne envie de fuir et de ne jamais se retourner ! Mais ne partez pas tout de suite ! On vient de voir ce qu’il y avait sous le capot et voyons maintenant la carrosserie flambant neuve offerte par ECMAScript 6.

C. Classes ECMAScript 6

Pour toute suite rentrée dans le sujet, je vous propose de refaire le dernier exemple, mais en utilisant la syntaxe proposée par ECMAScript 6 :

//Définition de notre première classe
class Nuggets {

    constructor(type) {
        this.type = type;
    }

    salt() {
        console.log('Je suis salé');
    }
}

//Définition de la classe fille
class NuggetsSpicy extends Nuggets {

    constructor(type, spiceLevel) {
        //Appel du constructeur parent
        super(type); // Équivalent à super.constructor()
        this.spiceLevel = spiceLevel;
    }

    spicy() {
        console.log('Je suis épicé');
    }

}

var myNuggetsBox = new Nuggets('chicken');
var myNuggetsBox2 = new NuggetsSpicy('beef', 'HARD');
myNuggetsBox.salt(); // Je suis salé
myNuggetsBox2.salt(); // Je suis salé
myNuggetsBox2.spicy(); // Je suis épicé
myNuggetsBox.spicy(); // Throw error

Comme vous pouvez le voir, c’est beaucoup plus intuitif ! Pourtant, c’est exactement le même code que l’exemple en ECMAScript 5, mais écrit différemment. Il est déjà possible d’utiliser les classes en utilisant des compilateurs ES6->ES5, des langages compilés en JavaScript tel que TypeScript, CoffeeScript,… En début d’article, je vous avais dit que c’était presque que du sucre syntaxique, et bien pas tout à fait, car les classes possèdent leur lot de petites particularités. Premièrement, la définition d’une classe n’est pas hoisted ! La position de la déclaration est donc très importante :

var myNuggetsBox = new Nuggets('chicken'); // Throw error
class Nuggets {}
var myNuggetsBox2 = new Nuggets('chicken'); // Ça marche !

Deuxièmement, que se passe-t-il lorsqu’on ne définit pas de constructeur ? Eh bien un constructeur par défaut est créé, ayant la définition suivante :

 constructor(...args) {
        super(...args);
    }

Troisièmement, sachez que tout comme les fonctions, il existe deux manières de déclarer une classe (déclaration et expression) :

//Déclaration
 class X {
    // ...
 }
//Expression
var z = class Z {
    // ...
}

Il y a également quelques petites choses à savoir sur l’héritage. Lorsque vous définissez un constructeur qui dérive d’un autre constructeur, vous devez impérativement faire appel au constructeur parent avant d’avoir accès au contexte this. De même si vous définissez un constructeur vide dans une classe fille, vous aurez une erreur :

 class Nuggets {}
    
    class NuggetsSpicy extends Nuggets {
        constructor(type, spiceLevel) {
            this.type= type; // Throw error
            super();
            this.type= type; // OK
        }
    }

    class Nuggets {}
    
    class NuggetsSpicy extends Nuggets {
        constructor() {
        }
    }
    
    var myNuggetsBox = new NuggetsSpicy(); // Throw error

Qui dit classes dit propriété statique ! On y passe à un moment ou un autre. Voyons donc ce petit exemple :

 class Nuggets {
     constructor(type) {
         this.type = type;
     }
     static pepper() {
         console.log('Je poivre tout');
     }
     salt() {
          console.log('Je suis salé');
     }
 }
Nuggets.pepper();// Je poivre tout
var myNuggetsBox = new Nuggets('chicken');
myNuggetsBox.salt(); // Je suis salé

Je l’ai déjà dit et répété en JavaScript tout est objet et donc le constructeur et lui-même est un objet. Les méthodes statiques sont des propriétés de l’objet constructeur, tandis que les autres méthodes sont attachées au prototype. En reprenant l’exemple, voyons comment les méthodes sont réellement définies (sans le sucre syntaxique) :

//Méthode static
Nuggets.pepper = function() {
     console.log('Je poivre tout');
}
//Autre méthode
Nuggets.prototype.salt = function() {
    console.log('Je suis salé');
}

Passons maintenant à une autre grosse nouveauté qu’apportent les classes : la possibilité d’étendre les types déjà existant (natif) ! Alors oui, c’était déjà possible mais très compliqué, notamment pour les types Array, Date,… Je vous renvoie sur ces deux très bons articles qui expliquent le problème et ses solutions en ECMASCript 5 : le premier et le second. Avec ECMAScript 6 cela donne tout simplement :

 class MySuperArray extends Array {
       // ...
 }

Facile nan ? Une dernière petite chose. L’exemple montré ci-dessus ne fonctionnerait pas si les classes ECMAScript 6 fonctionnaient pareil qu’en ECMAScript 5. Le secret ? L’objet n’est pas instancié au même moment :

  • ECMAScript 5 : l’objet est créer dans le premier constructeur de la chaîne des prototypes.
  • ECMAScript 6 : l’objet est créer dans le dernier constructeur de la chaîne des prototypes.

C’est cette petite particularité qui permet d’étendre si facilement les types renvoyant des instances dites ‘exotic’ (terme utilisé dans le brouillon ECMAScript 6).

Bon je vais m’arrêter ici pour les classes. Si vous voulez en savoir plus je vous renvoie directement sur le brouillon de la norme ECMAScript 6 : ici. Nous allons maintenant parler des nouvelles propriétés liés au type object.

Nouveautés Object

Je vous l’avais dit l’apport en nouveauté d’ECMAScript 6 est énorme ! On attaque tout de suite avec la déclaration d’objet littéral. Notation raccourcie des méthodes grâce à la syntaxe des classes et notations raccourcie lors de l’affectation, ceux-ci grâce à la déstructuration (vu dans le premier article). Tout de suite un petit exemple qui illustre le tout :

//Raccourci définition de méthode
var myNuggetsBox = {
    //Avant
    salt: function() {
    },
    //Après
    pepper() {
    }
}
//Raccourci affectation
var x = 4;
var y = 6;
//Avant
var myNuggetsBox2 = { x : x, y : y};
//Après
var myNuggetsBox3 = {x, y};

Cela n’a rien de très compliqué, je passe donc tout de suite à une autre nouveauté également très sympa : nom de méthode calculée. En JavaScript il possible d’accéder à une propriété d’un objet principalement de deux façons : myObjet.myProp ou myObjet['myProp']. En ECMAScript 6 il est désormais possible de définir une méthode à l’aide de la seconde notation et ainsi déterminer le nom à partir de variable. Voilà ce que ça donne :

var myPorpName = 'type';
var x = 'computed';
var y = 'name';
var myNuggetsBox = {
    [myPorpName] : 'chicken',
    [x+y] : 'chicken'
}

Parlons maintenant des nouvelles méthodes du type Object. La première méthode sur laquelle on va se pencher est Object.is(value1,value2). Elle permet de comparer deux valeurs et détermine si ce sont les mêmes. Vous devez vous dire, mais les opérateurs == et === ne seront plus utilisés ? Bien sur que si, mais dans certains cas ces opérateurs non pas le comportement voulut. Voyez ce petit exemple pour mieux comprendre les cas d’applications :

console.log(NaN==NaN); // false
console.log(NaN===NaN); // false
console.log(Object.is(NaN, NaN)); // true

console.log(-0==+0); // true
console.log(-0===+0); // true
console.log(Object.is(-0, +0)); // false

Il s’agit vraiment de cas particulier et cette propriété n’a pas pour but de remplacer les opérateurs de comparaison déjà existant, je ne m’attarderai pas plus dessus.

Passons à une autre nouvelle méthode qui pour le coup vous sera bien utile : Object.setPrototypeOf(obj, prototype). Comme son nom l’indique elle va vous permettre de remplacer le prototype d’un objet par un autre ! Il s’agit ici d’une façon plus ‘classe’ de setter un prototype par rapport à l’utilisation de la variable __proto__ qui est assez controversée (voir article). La méthode setPrototypeOf() est un setter de la propriété prototype tandis que la méthode getPrototypeOf() (standardisée depuis ECMAScript 5) est un getter. La variable __proto__, standardisée dans d’ECMAScript 6, passera par ces deux méthodes lors de la récupération et l’affectation d’un prototype. Pourquoi donc garder ces deux façons de faire me direz vous ? Eh bien tout simplement pour assurer la rétrocompatibilité, car __proto__ est déjà implémenté par tous les navigateurs modernes et utilisé massivement. Je ne vous montre pas d’exemple étend donnée que je vous ai déjà bien parlé des prototypes.

Encore une nouvelle propriété et pas des moindres : Object.assign(target, ...sources). Bon, le nom et la signature sont légèrement moins explicite que la précédente méthode. Déjà existante dans des librairies tel que UnderscoreJs (sous le nom extendOwn) ou encore Lodash, elle permet de merger les propriétés des objets sources dans un objet cible. Voyons ce que ça donne :

var human = {
    healt: 100;
};

var armor = {
    armor: 100;
};

Object.assign(human, armor, {weapon : 'sword'});

console.log(human);
// {healt: 100,  armor: 100, weapon : 'sword'}

Pratique ! Elle peut aussi vous servir à cloner un objet. Là aussi quelques particularités à connaître. Premièrement, seules les propriétés ‘enumerable’ peuvent être copiées. Une propriété d’un objet est enumerable si elle apparaît lors de l’énumération des propriétés de l’objet correspondant. La méthode .propertyIsEnumerable(prop) du prototype Object vous permet de savoir si une propriété est enumerable, et peut donc être copiée à l’aide de la méthode .assign().

Conclusion

C’est déjà fini ? Eh oui toutes les bonnes choses ont une fin ^^ Bon petit récap de ce que l’on a pu voir dans cette troisième partie sur ECMAScript 6. Introduction à l’approche objet en Javascript ! On a pu voir l’utilité de la propriété prototype, comment faire de l’héritage sous la norme ECMAScript 5 et vu l’approche ECMASCript 6. On a terminé par les nouveautés sur le type Object. Dans la prochaine partie (qui finalement ne sera pas la dernière) nous ferons un tour du côté des tableaux avec notamment les nouveautés sur le type Array. Sera ensuite le tour des types Map et Set puis on passera aux itérateurs et générateurs ! À très bientôt.

0 commentaires

Soumettre un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Découvrez nos autres articles

Aller au contenu principal