Les cours sont l'un des moyens les plus populaires de structurer des projets logiciels de nos jours. Cette approche de programmation est également utilisée en JavaScript. Aujourd'hui, nous publions une traduction de la partie 15 de la série d'écosystèmes JS. Cet article abordera différentes approches pour implémenter des classes en JavaScript, les mécanismes d'héritage et la transpiration. Nous allons commencer par vous expliquer le fonctionnement des prototypes et analyser différentes façons de simuler l'héritage basé sur les classes dans les bibliothèques populaires. Ensuite, nous verrons comment, grâce à la transpilation, vous pouvez écrire des programmes JS qui utilisent des fonctionnalités qui ne sont pas disponibles dans la langue ou, bien qu'ils existent sous la forme de nouvelles normes ou propositions qui sont à différents stades d'approbation, ne sont pas encore implémentés dans JS- moteurs. En particulier, nous parlerons de Babel et TypeScript et des classes ECMAScript 2015. Après cela, nous examinerons quelques exemples qui démontrent les caractéristiques de l'implémentation interne des classes dans le moteur J8 V8.

[Conseiller la lecture] Les 19 autres parties du cycle Revue
En JavaScript, nous sommes constamment confrontés à des objets, même s'il semblerait que nous travaillons avec des types de données primitifs. Par exemple, créez un littéral de chaîne:
const name = "SessionStack";
Après cela, nous pouvons immédiatement nous tourner vers
name
pour appeler diverses méthodes d'un objet de type
String
, dans lesquelles le littéral de chaîne que nous avons créé sera automatiquement converti.
console.log(name.repeat(2)); // SessionStackSessionStack console.log(name.toLowerCase()); // sessionstack
Contrairement à d'autres langages, en JavaScript, après avoir créé une variable contenant par exemple une chaîne ou un nombre, on peut, sans effectuer de conversion explicite, travailler avec cette variable comme si elle avait été initialement créée à l'aide du
new
mot-clé et du constructeur correspondant. Par conséquent, en raison de la création automatique d'objets encapsulant des valeurs primitives, vous pouvez travailler avec de telles valeurs comme s'il s'agissait d'objets, en particulier, faire référence à leurs méthodes et propriétés.
Un autre fait notable concernant le système de type JavaScript est que, par exemple, les tableaux sont également des objets. Si vous regardez la sortie de la commande
typeof
pour le tableau, vous pouvez voir qu'elle signale que l'entité sous enquête a le type de données
object
. En conséquence, il s'avère que les indices des éléments du tableau ne sont que des propriétés d'un objet particulier. Par conséquent, lorsque nous accédons à un élément d'un tableau par index, cela revient à travailler avec une propriété d'un objet de type
Array
et à obtenir la valeur de cette propriété. Si nous parlons de la façon dont les données sont stockées à l'intérieur d'objets et de tableaux ordinaires, les deux constructions suivantes conduisent à la création de structures de données presque identiques:
let names = ["SessionStack"]; let names = { "0": "SessionStack", "length": 1 }
En conséquence, l'accès aux éléments du tableau et aux propriétés de l'objet s'effectue à la même vitesse. L'auteur de cet article dit qu'il l'a découvert en résolvant un problème complexe. À savoir, une fois qu'il avait besoin de procéder à une optimisation sérieuse d'un morceau de code très important dans le projet. Après avoir essayé de nombreuses approches simples, il a décidé de remplacer tous les objets utilisés dans ce code par des tableaux. En théorie, l'accès aux éléments du tableau est plus rapide que de travailler avec des clés de table de hachage. À sa grande surprise, ce remplacement n'a eu aucune incidence sur les performances, car travailler avec des tableaux et travailler avec des objets en JavaScript revient à interagir avec les clés de la table de hachage, ce qui, dans les deux cas, nécessite le même temps.
Simuler des classes à l'aide de prototypes
Quand on pense aux objets, la première chose qui me vient à l'esprit, ce sont les classes. Peut-être que chacun de ceux qui se consacrent à la programmation aujourd'hui a créé des applications dont la structure est basée sur les classes et sur les relations entre elles. Bien que les objets en JavaScript puissent être trouvés littéralement partout, le langage n'utilise pas un système d'héritage traditionnel basé sur les classes. JavaScript utilise des
prototypes pour résoudre des problèmes similaires.
Objet et son prototypeEn JavaScript, chaque objet est associé à un autre objet - avec son propre prototype. Lorsque vous essayez d'accéder à une propriété ou une méthode d'un objet, la recherche de ce dont vous avez besoin est d'abord effectuée dans l'objet lui-même. Si la recherche échoue, elle se poursuit dans le prototype de l'objet.
Prenons un exemple simple qui décrit une fonction constructeur pour la classe de base
Component
:
function Component(content) { this.content = content; } Component.prototype.render = function() { console.log(this.content); }
Ici, nous affectons la fonction
render()
à la méthode prototype, car nous avons besoin de chaque instance de la classe
Component
pour utiliser cette méthode. Lorsque, dans n'importe quelle instance de
Component
, la méthode de
render
est appelée, sa recherche commence dans l'objet lui-même pour lequel elle est appelée. Ensuite, la recherche se poursuit dans le prototype, où le système trouve cette méthode.
Prototype et deux instances de la classe ComponentEssayons maintenant d'étendre la classe
Component
. Créons un constructeur pour une nouvelle classe -
InputField
:
function InputField(value) { this.content = `<input type="text" value="${value}" />`; }
Si nous avons besoin de la classe
InputField
étendre les fonctionnalités de la classe
Component
et pouvoir appeler sa méthode de
render
, nous devons changer son prototype. Lorsqu'une méthode est appelée sur une instance d'une classe enfant, cela n'a aucun sens de la rechercher dans un prototype vide. Nous devons, dans la recherche de cette méthode, être trouvés dans la classe
Component
. Par conséquent, nous devons procéder comme suit:
InputField.prototype = Object.create(new Component());
Désormais, lorsque vous travaillez avec une instance de la classe
InputField
et que vous appelez la méthode de la classe
Component
, cette méthode se trouve dans le prototype de la classe
Component
. Pour implémenter le système d'héritage, vous devez connecter le prototype
InputField
à une instance de la classe
Component
. De nombreuses bibliothèques utilisent
Object.setPrototypeOf () pour résoudre ce problème.
Extension de la classe de composants avec la classe InputFieldCependant, les actions ci-dessus ne suffisent pas à mettre en œuvre un mécanisme similaire à l'héritage traditionnel. Chaque fois que nous étendons la classe, nous devons effectuer les actions suivantes:
- Faites du prototype de la classe descendante une instance de la classe parente.
- Appelez, dans le constructeur de la classe descendante, le constructeur de la classe parente pour vous assurer que la classe parente est correctement initialisée.
- Fournir un mécanisme pour appeler les méthodes de la classe parent dans les situations où la classe descendante remplace la méthode parent, mais il est nécessaire d'appeler l'implémentation d'origine de cette méthode à partir de la classe parent.
Comme vous pouvez le voir, si un développeur JS souhaite utiliser les capacités de l'héritage basé sur une classe, il devra constamment effectuer les étapes ci-dessus. Dans le cas où vous avez besoin de créer de nombreuses classes, tout cela peut être réalisé sous forme de fonctions adaptées à la réutilisation.
En fait, la tâche d'organiser l'héritage en fonction des classes a été initialement résolue dans la pratique du développement JS de cette manière. En particulier, en utilisant diverses bibliothèques. Ces solutions sont devenues très populaires, ce qui indique clairement que quelque chose manquait clairement dans JavaScript. C'est pourquoi ECMAScript 2015 a introduit de nouvelles constructions syntaxiques visant à soutenir le travail avec les classes et à implémenter les mécanismes d'héritage correspondants.
Transpilation en classe
Après que les nouvelles fonctionnalités d'ECMAScript 2015 (ES6) ont été proposées, la communauté JS a voulu en profiter dès que possible, sans attendre l'achèvement du long processus d'ajout de la prise en charge de ces fonctionnalités dans les moteurs et navigateurs JS. Pour résoudre de tels problèmes, la transpilation est bonne. Dans ce cas, la compilation se réduit à transformer le code JS écrit selon les règles d'ES6 en une vue compréhensible pour les navigateurs qui jusqu'à présent ne prennent pas en charge les capacités ES6. Par conséquent, par exemple, il devient possible de déclarer des classes et d'implémenter des mécanismes d'héritage basés sur les classes selon les règles ES6 et de convertir ces constructions en code qui fonctionne dans n'importe quel navigateur. Schématiquement, ce processus, en utilisant l'exemple du traitement d'une fonction flèche par un transpilateur (une autre nouvelle fonctionnalité de langage qui a besoin de temps pour être prise en charge), peut être représenté comme illustré dans la figure ci-dessous.

TranspilationBabel.js est l'un des transpilers JavaScript les plus populaires. Voyons comment cela fonctionne en effectuant une compilation du code de déclaration de classe
Component
, dont nous avons parlé ci-dessus. Voici donc le code ES6:
class Component { constructor(content) { this.content = content; } render() { console.log(this.content) } } const component = new Component('SessionStack'); component.render();
Et voici ce que ce code se transforme après la transpilation:
var Component = function () { function Component(content) { _classCallCheck(this, Component); this.content = content; } _createClass(Component, [{ key: 'render', value: function render() { console.log(this.content); } }]); return Component; }();
Comme vous pouvez le voir, le code ECMAScript 5 est obtenu à la sortie du transpilateur, qui peut être exécuté dans n'importe quel environnement. De plus, les appels à certaines fonctions qui font partie de la bibliothèque standard Babel sont ajoutés ici.
Nous parlons des fonctions
_classCallCheck()
et
_createClass()
incluses dans le code transpilé. La première fonction,
_classCallCheck()
, est conçue pour empêcher la fonction constructeur d'être appelée comme une fonction régulière. Pour ce faire, il vérifie si le contexte dans lequel la fonction est appelée est le contexte d'instance de la classe
Component
. Le code vérifie si le mot-clé this pointe vers une instance similaire. La deuxième fonction,
_createClass()
, crée des propriétés d'objet qui lui sont transmises sous la forme d'un tableau d'objets contenant des clés et leurs valeurs.
Afin de comprendre le fonctionnement de l'héritage, nous analysons la classe
InputField
, qui est la descendante de la classe
Component
. Voici comment les relations de classe se rejoignent dans ES6:
class InputField extends Component { constructor(value) { const content = `<input type="text" value="${value}" />`; super(content); } }
Voici le résultat de la transposition de ce code à l'aide de Babel:
var InputField = function (_Component) { _inherits(InputField, _Component); function InputField(value) { _classCallCheck(this, InputField); var content = '<input type="text" value="' + value + '" />'; return _possibleConstructorReturn(this, (InputField.__proto__ || Object.getPrototypeOf(InputField)).call(this, content)); } return InputField; }(Component);
Dans cet exemple, la logique des mécanismes d'héritage est encapsulée dans un appel à la fonction
_inherits()
. Il effectue les mêmes actions que celles décrites ci-dessus, associées notamment à l'écriture dans le prototype de la classe descendante d'une instance de la classe parente.
Afin de transposer le code, Babel effectue plusieurs de ses transformations. Premièrement, le code ES6 est analysé et converti en une représentation intermédiaire appelée
arbre de syntaxe abstraite . L'arbre de syntaxe abstraite résultant est ensuite converti en un autre arbre, dont chaque nœud est transformé en son équivalent ES5. En conséquence, cet arbre est converti en code JS.
Arbre de syntaxe abstraite à Babel
Un arbre de syntaxe abstraite contient des nœuds, dont chacun n'a qu'un seul nœud parent. Babel a un type de base pour les nœuds. Il contient des informations sur ce qu'est le nœud et où il peut être trouvé dans le code. Il existe différents types de nœuds, par exemple, des nœuds pour représenter des littéraux, tels que des chaînes, des nombres,
null
valeurs
null
, etc. De plus, il existe des nœuds pour représenter les expressions utilisées pour contrôler le flux d'exécution du programme (
if
construction) et des nœuds pour les boucles (
for
,
while
). Il existe également un type spécial de nœud pour représenter les classes. Il s'agit d'un descendant de la classe de base
Node
. Il étend cette classe en ajoutant des champs pour stocker des références à la classe de base et au corps de la classe en tant que nœud distinct.
Convertissez le fragment de code suivant en une arborescence de syntaxe abstraite:
class Component { constructor(content) { this.content = content; } render() { console.log(this.content) } }
Voici à quoi ressemblera sa représentation schématique.
Arbre de syntaxe abstraiteAprès avoir créé une arborescence, chacun de ses nœuds est transformé en son nœud ES5 correspondant, après quoi cette nouvelle arborescence est convertie en code conforme à la norme ECMAScript 5. Au cours du processus de conversion, recherchez d'abord le nœud le plus éloigné du nœud racine, après quoi ce nœud est converti en code à l'aide d'extraits générés pour chaque nœud. Après cela, le processus est répété. Cette technique est appelée
recherche approfondie .
Dans l'exemple ci-dessus, le code pour les deux nœuds
MethodDefinition
sera généré en premier, après quoi le code pour le nœud
ClassBody
sera généré, et enfin, le code pour le nœud
ClassDeclaration
.
Transpilation TypeScript
Un autre système populaire qui utilise la transpilation est TypeScript. Il s'agit d'un langage de programmation dont le code est transformé en code ECMAScript 5 compréhensible par tout moteur JS. Il offre une nouvelle syntaxe pour l'écriture d'applications JS. Voici comment implémenter la classe
Component
sur TypeScript:
class Component { content: string; constructor(content: string) { this.content = content; } render() { console.log(this.content) } }
Voici l'arbre de syntaxe abstrait de ce code.
Arbre de syntaxe abstraiteTypeScript prend en charge l'héritage.
class InputField extends Component { constructor(value: string) { const content = `<input type="text" value="${value}" />`; super(content); } }
Voici le résultat de la transpilation de ce code:
var InputField = (function (_super) { __extends(InputField, _super); function InputField(value) { var _this = this; var content = "<input type=\"text\" value=\"" + value + "\" />"; _this = _super.call(this, content) || this; return _this; } return InputField; }(Component));
Comme vous pouvez le voir, il s'agit là encore d'un code ES5, dans lequel, en plus des constructions standard, il y a des appels à certaines fonctions de la bibliothèque TypeScript. Les capacités de la fonction
__extends()
similaires à celles dont nous avons parlé au tout début de ce document.
Grâce à l'adoption généralisée de Babel et de TypeScript, les mécanismes de déclaration des classes et d'organisation de l'héritage basé sur les classes sont devenus des outils standard pour structurer les applications JS. Cela a contribué à l'ajout de la prise en charge de ces mécanismes dans les navigateurs.
Prise en charge des classes de navigateur
La prise en
charge des cours est apparue dans le navigateur Chrome en 2014. Cela permet au navigateur de travailler avec les déclarations de classe sans utiliser de transpilation ou de bibliothèques auxiliaires.
Travailler avec des classes dans la console Chrome JSEn fait, la prise en charge du navigateur pour ces mécanismes n'est rien d'autre que du sucre syntaxique. Ces constructions sont converties dans les mêmes structures de base qui sont déjà prises en charge par le langage. Par conséquent, même si vous utilisez la nouvelle syntaxe, à un niveau inférieur, tout ressemblera à la création de constructeurs et à la manipulation de prototypes d'objets:
Le support de classe est le sucre syntaxiqueSupport de classe dans V8
Parlons du fonctionnement de la prise en charge de la classe ES6 dans le moteur V8 JS. Dans le
matériel précédent consacré aux arbres de syntaxe abstraite, nous avons parlé du fait que lors de la préparation du code JS pour l'exécution, le système le parse et forme un arbre de syntaxe abstraite sur sa base. Lors de l'analyse des constructions de déclarations de classe, les nœuds de type
ClassLiteral tombent dans l'arbre de syntaxe abstrait.
Ces nœuds stockent quelques éléments intéressants. Premièrement, c'est un constructeur en tant que fonction distincte, et deuxièmement, c'est une liste de propriétés de classe. Il peut s'agir de méthodes, de getters, de setters, de domaines publics ou privés. Un tel nœud, en outre, stocke une référence à la classe parent, qui étend la classe pour laquelle le nœud est formé, qui, encore une fois, stocke le constructeur, la liste des propriétés et un lien vers sa propre classe parent.
Une fois le nouveau nœud
ClassLiteral
transformé en code , il est converti en constructions composées de fonctions et de prototypes.
Résumé
L'auteur de ce document dit que
SessionStack s'efforce d'optimiser le code de sa bibliothèque aussi complètement que possible, car il doit résoudre des tâches difficiles de collecte d'informations sur tout ce qui se passe sur les pages Web. Au cours de la résolution de ces problèmes, la bibliothèque ne doit pas ralentir le travail de la page analysée. L'optimisation de ce niveau nécessite de prendre en compte les moindres détails de l'écosystème JavaScript qui affectent les performances, en particulier, en tenant compte des caractéristiques de la façon dont les classes et les mécanismes d'héritage sont organisés dans ES6.
Chers lecteurs! Utilisez-vous des constructions de syntaxe ES6 pour travailler avec des classes en JavaScript?
