Fermer

avril 16, 2018

Une plongée profonde dans les classes ES6 –


Souvent, nous devons représenter une idée ou un concept dans nos programmes – peut-être un moteur de voiture, un fichier informatique, un routeur ou une lecture de température. La représentation directe de ces concepts dans le code se fait en deux parties: les données pour représenter l'état et les fonctions pour représenter le comportement. Les classes ES6 nous donnent une syntaxe pratique pour définir l'état et le comportement des objets qui représenteront nos concepts.

Les classes ES6 rendent notre code plus sûr en garantissant qu'une fonction d'initialisation sera appelée, et elles facilitent la définition d'un ensemble fixe des fonctions qui fonctionnent sur ces données et maintenir un état valide. Si vous pouvez penser à quelque chose comme une entité séparée, il est probable que vous deviez définir une classe pour représenter cette "chose" dans votre programme.

Considérez ce code hors classe. Combien d'erreurs pouvez-vous trouver? Comment les répareriez-vous?

 // fixé aujourd'hui au 24 décembre
const aujourd'hui = {
  mois: 24,
  jour: 12,
}

const demain = {
  année: aujourd'hui.année,
  mois: aujourd'hui.mois,
  jour: aujourd'hui.jour + 1,
}

const dayAfterTomorrow = {
  année: demain.année,
  mois: demain.month,
  jour: demain.jour + 1 <= 31? demain.jour + 1: 1,
}

La date aujourd'hui n'est pas valide: il n'y a pas de mois 24. De plus, aujourd'hui n'est pas complètement initialisé: il manque l'année. Ce serait mieux si nous avions une fonction d'initialisation qui ne pouvait pas être oubliée. Notez également que, lors de l'ajout d'un jour, nous avons vérifié dans un endroit si nous sommes allés au-delà de 31, mais a raté cette vérification dans un autre endroit. Il serait préférable que nous n'interagissions avec les données qu'à travers un petit ensemble fixe de fonctions qui maintiennent chacune un état valide.

Voici la version corrigée qui utilise les classes.

 class SimpleDate {
  constructeur (année, mois, jour) {
    // Vérifie que (année, mois, jour) est une date valide
    // ...

    // Si c'est le cas, utilisez-le pour initialiser "this" date
    this._year = année;
    this._month = mois;
    this._day = jour;
  }

  addDays (nDays) {
    // Augmente "this" date de n jours
    // ...
  }

  getDay () {
    renvoyez this._day;
  }
}

// "today" est garanti valide et entièrement initialisé
const aujourd'hui = nouveau SimpleDate (2000, 2, 28);

// La manipulation des données uniquement à travers un ensemble fixe de fonctions garantit que nous maintenons un état valide
aujourd'hui.addDays (1);
JARGON TIP:

  • Lorsqu'une fonction est associée à une classe ou un objet, nous l'appelons
  • Quand un objet est créé à partir d'une classe, cet objet est dit

qualité, une introduction en profondeur à ES6, vous ne pouvez pas passer devant le développeur canadien full-stack Wes Bos. Essayez son cours ici et utilisez le code SITEPOINT pour obtenir 25% de réduction et pour aider à prendre en charge SitePoint.


Constructeurs

La méthode du constructeur est spéciale et résout le problème. premier problème. Son travail consiste à initialiser une instance à un état valide, et il sera appelé automatiquement afin que nous ne puissions pas oublier d'initialiser nos objets.

Keep Data Private

Nous essayons de concevoir nos classes pour que leur état soit garanti pour être valable. Nous fournissons un constructeur qui ne crée que des valeurs valides, et nous concevons des méthodes qui ne laissent toujours que des valeurs valides. Mais tant que nous laissons les données de nos classes accessibles à tout le monde, quelqu'un va les gâcher. Nous protégeons contre cela en gardant les données inaccessibles sauf par les fonctions que nous fournissons.

JARGON TIP: Garder les données privées pour les protéger s'appelle encapsulation .

Confidentialité avec Conventions

Malheureusement, les propriétés d'objet privé n'existent pas en JavaScript. Nous devons les imiter. La façon la plus courante de le faire est d'adhérer à une convention simple: si un nom de propriété est préfixé par un trait de soulignement (ou, moins souvent, suffixé par un trait de soulignement), il doit être traité comme non public. Nous avons utilisé cette approche dans l'exemple de code précédent. Généralement cette convention simple fonctionne, mais les données sont techniquement toujours accessibles à tout le monde, donc nous devons compter sur notre propre discipline pour faire la bonne chose.

Confidentialité avec des méthodes privilégiées

La manière la plus courante de truquer un objet privé properties consiste à utiliser des variables ordinaires dans le constructeur et à les capturer dans des fermetures. Cette astuce nous donne des données vraiment privées qui sont inaccessibles à l'extérieur. Mais pour que cela fonctionne, les méthodes de notre classe devraient elles-mêmes être définies dans le constructeur et attachées à l'instance:

 class SimpleDate {
  constructeur (année, mois, jour) {
    // Vérifie que (année, mois, jour) est une date valide
    // ...

    // Si c'est le cas, utilisez-le pour initialiser les variables ordinaires de cette date
    let _year = année;
    laissez _month = mois;
    let _day = jour;

    // Les méthodes définies dans le constructeur capturent les variables dans une fermeture
    this.addDays = function (nDays) {
      // Augmente "this" date de n jours
      // ...
    }

    this.getDay = function () {
      retour _day;
    }
  }
}

Confidentialité avec symboles

Les symboles sont une nouvelle fonctionnalité de JavaScript à partir de ES6, et ils nous donnent une autre façon de fausser les propriétés des objets privés. Au lieu de souligner les noms de propriété, nous pourrions utiliser des clés d'objet symbole uniques, et notre classe peut capturer ces clés dans une fermeture. Mais il y a une fuite. Une autre nouvelle fonctionnalité de JavaScript est Object.getOwnPropertySymbols et permet à l'extérieur d'accéder aux clés de symboles que nous avons essayé de garder privées:

 const SimpleDate = (function () {
  const _yearKey = Symbole ();
  const _monthKey = Symbole ();
  const _dayKey = Symbole ();

  class SimpleDate {
    constructeur (année, mois, jour) {
      // Vérifie que (année, mois, jour) est une date valide
      // ...

      // Si c'est le cas, utilisez-le pour initialiser "this" date
      ceci [_yearKey] = année;
      ceci [_monthKey] = mois;
      ceci [_dayKey] = jour;
     }

    addDays (nDays) {
      // Augmente "this" date de n jours
      // ...
    }

    getDay () {
      renvoie ceci [_dayKey];
    }
  }

  return SimpleDate;
} ());

Vie privée avec cartes faibles

Les cartes faibles sont aussi une nouvelle fonctionnalité de JavaScript. Nous pouvons stocker des propriétés d'objet privé dans des paires clé / valeur en utilisant notre instance comme clé, et notre classe peut capturer ces cartes clé / valeur dans une fermeture:

 const SimpleDate = (function () {
  const _years = new WeakMap ();
  const _months = new WeakMap ();
  const _days = new WeakMap ();

  class SimpleDate {
    constructeur (année, mois, jour) {
      // Vérifie que (année, mois, jour) est une date valide
      // ...

      // Si c'est le cas, utilisez-le pour initialiser "this" date
      _years.set (cette année);
      _months.set (ceci, mois);
      _days.set (ce jour-là);
    }

    addDays (nDays) {
      // Augmente "this" date de n jours
      // ...
    }

    getDay () {
      retourne _days.get (this);
    }
  }

  return SimpleDate;
} ());

Autres modificateurs d'accès

Il existe d'autres niveaux de visibilité que «privé» que vous trouverez dans d'autres langues, telles que «protégé», «interne», «paquet privé» ou «ami». JavaScript ne nous permet toujours pas d'imposer ces autres niveaux de visibilité. Si vous en avez besoin, vous devrez compter sur les conventions et l'autodiscipline.

En se référant à l'objet courant

Regardez encore à getDay () . Il ne spécifie aucun paramètre, alors comment sait-il l'objet pour lequel il a été appelé? Lorsqu'une fonction est appelée comme méthode en utilisant la notation object.function il y a un argument implicite qu'elle utilise pour identifier l'objet, et cet argument implicite est assigné à un paramètre implicite nommé this Pour illustrer, voici comment nous enverrions l'argument object explicitement plutôt qu'implicitement:

 // Obtenir une référence à la fonction "getDay"
const getDay = SimpleDate.prototype.getDay;

getDay.call (aujourd'hui); // "ceci" sera "aujourd'hui"
getDay.call (demain); // "ceci" sera "demain"

tomorrow.getDay (); // identique à la dernière ligne, mais "demain" est passé implicitement

Propriétés et méthodes statiques

Nous avons la possibilité de définir des données et des fonctions qui font partie de la classe mais qui ne font partie d'aucune instance de cette classe. Nous appelons ces propriétés statiques et méthodes statiques, respectivement. Il n'y aura qu'une seule copie d'une propriété statique plutôt qu'une nouvelle copie par instance:

 class SimpleDate {
  static setDefaultDate (année, mois, jour) {
    // Une propriété statique peut être référée sans mentionner une instance
    // Au lieu de cela, il est défini sur la classe
    SimpleDate._defaultDate = nouveau SimpleDate (année, mois, jour);
  }

  constructeur (année, mois, jour) {
    // Si vous construisez sans arguments,
    // puis initialise la date "this" en copiant la date par défaut statique
    if (arguments.length === 0) {
      this._year = SimpleDate._defaultDate._year;
      this._month = SimpleDate._defaultDate._month;
      this._day = SimpleDate._defaultDate._day;

      revenir;
    }

    // Vérifie que (année, mois, jour) est une date valide
    // ...

    // Si c'est le cas, utilisez-le pour initialiser "this" date
    this._year = année;
    this._month = mois;
    this._day = jour;
  }

  addDays (nDays) {
    // Augmente "this" date de n jours
    // ...
  }

  getDay () {
    renvoyez this._day;
  }
}

SimpleDate.setDefaultDate (1970, 1, 1);
const defaultDate = new SimpleDate ();

Sous-classes

Souvent, nous trouvons des points communs entre nos classes – du code répété que nous aimerions consolider. Les sous-classes nous permettent d'incorporer l'état et le comportement d'une autre classe dans la nôtre. Ce processus est souvent appelé héritage et notre sous-classe est dit "hériter" d'une classe parente, également appelée superclasse . L'héritage peut éviter la duplication et simplifier l'implémentation d'une classe qui a besoin des mêmes données et fonctions qu'une autre classe. L'héritage nous permet également de substituer des sous-classes, en ne s'appuyant que sur l'interface fournie par une superclasse commune.

Hériter d'éviter la duplication

Considérons ce code de non-héritage:

 class Employee {
  constructeur (prénom, nom de famille) {
    this._firstName = firstName;
    this._familyName = nomFamille;
  }

  getFullName () {
    renvoie `$ {this._firstName} $ {this._familyName}`;
  }
}

gestionnaire de classe {
  constructeur (prénom, nom de famille) {
    this._firstName = firstName;
    this._familyName = nomFamille;
    this._managedEmployees = [];
  }

  getFullName () {
    renvoie `$ {this._firstName} $ {this._familyName}`;
  }

  addEmployee (employé) {
    this._managedEmployees.push (employé);
  }
}

Les propriétés de données _firstName et _familyName et la méthode getFullName sont répétées entre nos classes. Nous pourrions éliminer cette répétition en faisant hériter notre classe Manager de la classe Employee . Quand nous le ferons, l'état et le comportement de la classe Employee – ses données et ses fonctions – seront incorporés dans notre classe Manager .

Voici une version qui utilise l'héritage. Notez l'utilisation de super :

 // Manager fonctionne toujours comme avant mais sans code répété
Le gestionnaire de classe étend Employee {
  constructeur (prénom, nom de famille) {
    super (prénom, nom de famille);
    this._managedEmployees = [];
  }

  addEmployee (employé) {
    this._managedEmployees.push (employé);
  }
}

IS-A et WORKS-LIKE-A

Il existe des principes de conception pour vous aider à décider quand l'héritage est approprié. L'héritage doit toujours modéliser une relation IS-A et WORKS-LIKE-A. C'est-à-dire, un gestionnaire "est un" et "fonctionne comme un" type spécifique d'employé, de sorte que n'importe où nous opérons sur une instance de superclasse, nous devrions pouvoir substituer dans une instance de sous-classe, et tout devrait fonctionner. La différence entre violer et adhérer à ce principe peut parfois être subtile. Un exemple classique de violation subtile est une superclasse Rectangle et une sous-classe Square :

 class Rectangle {
  définir la largeur (w) {
    this._width = w;
  }

  get width () {
    renvoie this._width;
  }

  définir la hauteur (h) {
    this._height = h;
  }

  get height () {
    retourne ceci.
  }
}

// Une fonction qui fonctionne sur une instance de Rectangle
fonction f (rectangle) {
  rectangle.width = 5;
  rectangle.height = 4;

  // Vérifier le résultat attendu
  if (rectangle.width * rectangle.height! == 20) {
    throw new Error ("Attendu que la surface du rectangle (largeur * hauteur) est de 20");
  }
}

// Un carré IS-A rectangle ... non?
classe Square extends Rectangle {
  définir la largeur (w) {
    super.width = w;

    // Maintenir carré-ness
    super.height = w;
  }

  définir la hauteur (h) {
    super.height = h;

    // Maintenir carré-ness
    super.width = h;
  }
}

// Mais un rectangle peut-il être remplacé par un carré?
f (nouveau carré ()); // Erreur

Un carré peut être un rectangle mathématiquement mais un carré ne fonctionne pas comme un rectangle comportementalement.

Cette règle que toute utilisation d'une instance de superclasse devrait être substituable par une instance de sous-classe s'appelle le principe de substitution de Liskov et c'est une partie importante de la conception de classe orientée objet

Méfiez-vous de la surutilisation

Il est facile de trouver partout classe qui offre des fonctionnalités complètes peut être séduisante, même pour les développeurs expérimentés. Mais il y a aussi des inconvénients à l'héritage. Rappelons que nous assurons un état valide en manipulant les données uniquement à travers un petit ensemble fixe de fonctions. Mais quand nous héritons, nous augmentons la liste des fonctions qui peuvent directement manipuler les données, et ces fonctions supplémentaires sont alors également responsables du maintien de l'état valide. Si trop de fonctions peuvent manipuler directement les données, ces données deviennent presque aussi mauvaises que les variables globales. Trop d'héritage crée des classes monolithiques qui diluent l'encapsulation, sont plus difficiles à corriger et plus difficiles à réutiliser. Au lieu de cela, préférez concevoir des classes minimales qui n'incorporent qu'un seul concept.

Revoyons le problème de la duplication de code. Pouvons-nous le résoudre sans héritage? Une approche alternative consiste à connecter des objets à travers des références pour représenter une relation partie-entière. Nous appelons cela composition .

Voici une version de la relation manager-employé utilisant la composition plutôt que l'héritage:

 class Employee {
  constructeur (prénom, nom de famille) {
    this._firstName = firstName;
    this._familyName = nomFamille;
  }

  getFullName () {
    renvoie `$ {this._firstName} $ {this._familyName}`;
  }
}

Classe Group {
  constructeur (manager / *: Employee * /) {
    this._manager = gestionnaire;
    this._managedEmployees = [];
  }

  addEmployee (employé) {
    this._managedEmployees.push (employé);
  }
}

Ici, un gestionnaire n'est pas une classe distincte. Au lieu de cela, un gestionnaire est une instance ordinaire Employee à laquelle appartient une instance Group . Si l'héritage modélise la relation IS-A, alors la composition modélise la relation HAS-A. C'est-à-dire qu'un gestionnaire de groupe a un gestionnaire.

Si l'héritage ou la composition peut raisonnablement exprimer nos concepts et relations de programme, alors préférer la composition

Hériter des sous-classes de substitution

Héritage permet également d'utiliser différentes sous-classes interchangeable via l'interface fournie par une superclasse commune. Une fonction qui attend une instance de superclasse en tant qu'argument peut également être passée à une instance de sous-classe sans que la fonction n'ait besoin de connaître l'une des sous-classes. Substituer des classes qui ont une super-classe commune est souvent appelé polymorphisme :

 // Ce sera notre superclasse commune
class Cache {
  get (clé, defaultValue) {
    const valeur = this._doGet (clé);
    if (value === undefined || value === null) {
      retourne defaultValue;
    }

    valeur de retour;
  }

  set (clé, valeur) {
    if (clé === undefined || clé === null) {
      throw new Error ('Argument invalide');
    }

    this._doSet (clé, valeur);
  }

  // Doit être remplacé
  // _doGet ()
  // _doSet ()
}

// Les sous-classes ne définissent aucune nouvelle méthode publique
// L'interface publique est entièrement définie dans la superclasse
classe ArrayCache étend Cache {
  _doGet () {
    // ...
  }

  _doSet () {
    // ...
  }
}

class LocalStorageCache étend Cache {
  _doGet () {
    // ...
  }

  _doSet () {
    // ...
  }
}

// Les fonctions peuvent fonctionner de manière polymorphique sur n'importe quel cache en interagissant à travers l'interface de la superclasse
fonction calcul (cache) {
  const cached = cache.get ('result');
  if (! caché) {
    résultat de const = // ...
    cache.set ('résultat', résultat);
  }

  // ...
}

calcul (nouveau ArrayCache ()); // utilise le cache de tableau via l'interface de la superclasse
calculer (new LocalStorageCache ()); // utilise le cache de stockage local via l'interface de la superclasse

Plus que du sucre

La syntaxe de la classe JavaScript est souvent considérée comme du sucre syntaxique, et à bien des égards, mais il existe aussi de réelles différences – ce que nous pouvons faire avec les classes ES6 ES5.

Les propriétés statiques sont héritées

ES5 ne nous a pas laissé créer l'héritage vrai entre les fonctions de constructeur. Object.create pourrait créer un objet ordinaire mais pas un objet fonction. Nous avons truqué l'héritage des propriétés statiques en les copiant manuellement. Maintenant, avec les classes ES6, nous obtenons un réel lien prototype entre une fonction de constructeur de sous-classe et le constructeur de la superclasse:

 // ES5
fonction B () {}
B.f = fonction () {};

fonction D () {}
D.prototype = Object.create (B.prototype);

D.f (); // Erreur
 // ES6
classe B {
  statique f () {}
}

la classe D étend B {}

D.f (); // D'accord

Les constructeurs intégrés peuvent être sous-classés

Certains objets sont "exotiques" et ne se comportent pas comme des objets ordinaires. Les tableaux, par exemple, ajustent leur propriété length pour qu'elle soit supérieure à l'index entier le plus grand. Dans ES5, lorsque nous essayions de sous-classer Array l'opérateur new allouait un objet ordinaire pour notre sous-classe, pas l'objet exotique de notre superclasse:

 // ES5
la fonction D () {
  Array.apply (this, arguments);
}
D.prototype = Object.create (Array.prototype);

var d = nouveau D ();
d [0] = 42;

d.length; // 0 - mauvais, aucun comportement exotique de tableau

Les classes ES6 ont corrigé ceci en changeant quand et par qui les objets sont alloués. Dans ES5, les objets étaient alloués avant d'appeler le constructeur de la sous-classe, et la sous-classe passait cet objet au constructeur de la superclasse. Maintenant, avec les classes ES6, les objets sont alloués avant d'appeler le constructeur superclasse et la superclasse rend cet objet disponible pour le constructeur de la sous-classe. Cela permet à Array d'allouer un objet exotique même lorsque nous invoquons new sur notre sous-classe.

 // ES6
classe D étend Array {}

let d = nouveau D ();
d [0] = 42;

d.length; // 1 - bon, tableau comportement exotique

Divers

Il y a un petit assortiment d'autres différences probablement moins importantes. Les constructeurs de classe ne peuvent pas être appelés par des fonctions. Cela protège contre l'oubli d'invoquer des constructeurs avec new . De plus, la propriété du prototype d'un constructeur de classe ne peut pas être réaffectée. Cela peut aider les moteurs JavaScript à optimiser les objets de classe. Enfin, les méthodes de classe n'ont pas de propriété prototype . Utilisation de nouvelles fonctionnalités dans Imaginative Ways

La plupart des fonctionnalités décrites ici et dans d'autres articles de SitePoint sont nouvelles pour JavaScript, et la communauté expérimente en ce moment pour utiliser ces fonctionnalités dans de nouvelles fonctionnalités. et méthodes imaginatives.

Héritage multiple avec des proxies

Une de ces expériences utilise des proxies une nouvelle fonctionnalité de JavaScript pour l'implémentation de l'héritage multiple. La chaîne de prototypes de JavaScript n'autorise qu'un seul héritage. Les objets peuvent déléguer à un seul autre objet. Les proxies nous permettent de déléguer des accès aux propriétés à plusieurs autres objets:

 const transmitter = {
  transmit () {}
}

const récepteur = {
  recevoir() {}
}

// Crée un objet proxy qui intercepte les accès de propriété et les transmet à chaque parent,
// retourne la première valeur définie trouvée
const inheritsFromMultiple = nouveau proxy ([transmitter, receiver]{
  get: function (proxyTarget, propriétéKey) {
    const foundParent = proxyTarget.find (parent => parent [propertyKey]! == indéfini);
    return foundParent && foundParent [propertyKey];
  }
});

inheritsFromMultiple.transmit (); // travaux
inheritsFromMultiple.receive (); // travaux

Pouvons-nous étendre ceci pour travailler avec les classes ES6? Le prototype d'une classe pourrait être un proxy qui transfère l'accès à la propriété à plusieurs autres prototypes. La communauté JavaScript travaille sur ce sujet maintenant. Pouvez-vous le comprendre? Participez à la discussion et partagez vos idées

Héritage multiple avec les usines de classes

Une autre approche que la communauté JavaScript a expérimentée consiste à générer des classes à la demande qui étendent une superclasse variable. Chaque classe n'a encore qu'un seul parent, mais nous pouvons enchaîner ces parents de manière intéressante:

 function makeTransmitterClass (Superclass = Object) {
  Classe de retour Transmitter extends Superclass {
    transmit () {}
  }
}

function makeReceiverClass (Superclass = Object) {
  classe de retour Receiver étend Superclass
    recevoir() {}
  }
}

La classe InheritsFromMultiple étend makeTransmitterClass (makeReceiverClass ()) {}

const inheritsFromMultiple = nouveau InheritsFromMultiple ();

inheritsFromMultiple.transmit (); // travaux
inheritsFromMultiple.receive (); // travaux

Existe-t-il d'autres façons imaginatives d'utiliser ces fonctionnalités? Il est temps de laisser votre empreinte dans le monde JavaScript

Conclusion

Comme le montre le graphique ci-dessous, le support pour les classes est assez bon

Puis-je utiliser es6-class? ] Données sur la prise en charge de la fonctionnalité es6-class dans les principaux navigateurs de caniuse.com.

J'espère que cet article vous a donné un aperçu de la façon dont les classes fonctionnent dans ES6 et a démystifié une partie du jargon qui les entoure.

Cet article a été révisé par Nilson Jacques and ] Tim Severien . Merci à tous les pairs évaluateurs de SitePoint pour avoir rendu le contenu de SitePoint le meilleur possible




Source link