Présentation
Dans cet article, nous examinons divers aspects de la programmation orientée objet dans ECMAScript (bien que ce sujet ait déjà été abordé dans de nombreux articles). Nous examinerons ces questions davantage d’un point de vue théorique. En particulier, nous examinerons les algorithmes de création d'objets, la façon dont les objets sont liés (y compris les relations de base - héritage), qui peuvent également être utilisés dans les discussions (ce qui, je l'espère, dissipera certaines ambiguïtés conceptuelles précédentes sur la POO en JavaScript).
Texte original en anglais :http://dmitrysoshnikov.com/ecmascript/chapter-7-1-oop-general-theory/
Introduction, paradigme et idées
Avant de procéder à l'analyse technique de la POO dans ECMAScript, il nous est nécessaire de maîtriser certaines caractéristiques de base de la POO et de clarifier les principaux concepts dans l'introduction.
ECMAScript prend en charge une variété de méthodes de programmation, notamment structurée, orientée objet, fonctionnelle, impérative, etc. Dans certains cas, il prend également en charge la programmation orientée aspect, mais cet article traite de la programmation orientée objet, voici donc la programmation orientée objet ; programmation orientée en ECMAScript Définition :
ECMAScript est un langage de programmation orienté objet basé sur l'implémentation d'un prototype.
Il existe de nombreuses différences entre les approches POO basées sur des prototypes et basées sur des classes statiques. Jetons un coup d’œil à leurs différences en détail immédiat.
Basé sur les attributs de classe et basé sur un prototype
Notez qu'un point important a été souligné dans la phrase précédente - entièrement basé sur des classes statiques. Avec le mot « statique », nous entendons les objets statiques et les classes statiques, fortement typés (bien que non obligatoires).
Concernant cette situation, de nombreux documents sur le forum ont souligné que c'est la principale raison pour laquelle ils s'opposent à la comparaison des "classes avec des prototypes" en JavaScript, bien que leurs implémentations soient différentes (comme celles basées sur des classes dynamiques Python et Ruby) ne sont pas trop opposés au focus (certaines conditions sont écrites, bien qu'il existe certaines différences de pensée, JavaScript n'est pas devenu si alternatif), mais le focus de leur opposition est les classes statiques contre les prototypes dynamiques ), pour être précis, le mécanisme d'une classe statique (par exemple : C, JAVA) et de ses subordonnés et définitions de méthodes nous permet de voir la différence exacte entre celle-ci et une implémentation basée sur un prototype.
Mais listons-les un par un. Considérons les principes généraux et les principaux concepts de ces paradigmes.
Basé sur une classe statique
Dans le modèle basé sur les classes, il existe un concept de classes et d'instances. Les instances de classes sont également souvent nommées objets ou instances.
Classes et objets
Une classe représente une abstraction d'une instance (c'est-à-dire un objet). C'est un peu comme les mathématiques à cet égard, mais nous appelons cela un type ou une classification.
Par exemple (les exemples ici et ci-dessous sont des pseudocodes) :
Héritage hiérarchique
Pour améliorer la réutilisation du code, les classes peuvent être étendues de l'une à l'autre, en ajoutant des informations supplémentaires. Ce mécanisme est appelé héritage (hiérarchique).
Lorsque vous appelez une méthode sur une instance d'une classe, vous rechercherez généralement la méthode dans la classe native. Si elle n'est pas trouvée, accédez à la classe parent directe pour rechercher. Si elle n'est pas encore trouvée, accédez à la classe parent de. la classe parent à rechercher (par exemple, dans une chaîne d'héritage stricte), si le haut de l'héritage est trouvé mais pas encore trouvé, le résultat est : l'objet n'a pas de comportement similaire et il n'y a aucun moyen d'obtenir le résultat.
Concepts clés basés sur les cours
Nous avons ainsi les concepts clés suivants :1. Avant de créer un objet, la classe doit être déclarée. Il faut d'abord définir sa classe
2. Par conséquent, l'objet sera créé à partir de la classe abstraite dans son propre « iconogramme et similarité » (structure et comportement)
3. Les méthodes sont traitées via une chaîne d'héritage stricte, directe et immuable
4. La sous-classe contient tous les attributs de la chaîne d'héritage (même si certains attributs ne sont pas nécessaires à la sous-classe
) ;
5. Créez une instance de classe. Une classe ne peut pas (en raison du modèle statique) modifier les caractéristiques (propriétés ou méthodes) de son instance
;
6. Les instances (en raison du modèle statique strict) ne peuvent pas avoir de comportements ou d'attributs supplémentaires autres que ceux déclarés dans la classe correspondant à l'instance.
Basé sur un prototype
Le concept de base ici est celui des objets mutables dynamiques. Les transformations (transformations complètes, incluant non seulement des valeurs mais aussi des attributs) sont directement liées aux langages dynamiques. Les objets comme ceux-ci peuvent stocker toutes leurs propriétés (propriétés, méthodes) indépendamment sans avoir besoin d'une classe.
Dans ce cas, la réutilisation du code ne se fait pas par extension de classe, (veuillez noter que nous n'avons pas dit que la classe ne peut pas être modifiée, car il n'y a pas de notion de classe ici), mais par prototype.
Un prototype est un objet qui est utilisé comme copie primitive d'autres objets, ou si certains objets n'ont pas les propriétés nécessaires qui leur sont propres, le prototype peut être utilisé comme délégué pour ces objets et servir d'objet auxiliaire .
Basé par délégation
N'importe quel objet peut être utilisé comme objet prototype pour un autre objet, car un objet peut facilement changer son prototype de manière dynamique au moment de l'exécution.Notez que nous envisageons actuellement une vue d'ensemble plutôt qu'une implémentation spécifique. Lorsque nous discuterons d'implémentations spécifiques dans ECMAScript, nous verrons certaines de leurs propres caractéristiques.
Exemple (pseudocode) :
Cet exemple montre la fonction et le mécanisme importants du prototype en tant qu'attribut d'objet auxiliaire, tout comme demander ses propres attributs, par rapport à ses propres attributs, ces attributs sont des attributs délégués. Ce mécanisme est appelé délégué, et un modèle prototype basé sur celui-ci est un prototype délégué (ou prototype basé sur un délégué). Le mécanisme de référence ici est appelé envoi d'un message à un objet. Si l'objet n'obtient pas de réponse, il sera délégué au prototype pour le trouver (l'obligeant à essayer de répondre au message).
La réutilisation du code dans ce cas est appelée héritage basé sur les délégués ou héritage basé sur les prototypes. Puisque n’importe quel objet peut être utilisé comme prototype, cela signifie qu’un prototype peut également avoir son propre prototype. Ces prototypes sont reliés entre eux pour former ce qu'on appelle une chaîne de prototypes. Les chaînes sont également hiérarchiques comme les classes statiques, mais elles peuvent être facilement réorganisées pour modifier la hiérarchie et la structure.
Si un objet et sa chaîne prototype ne peuvent pas répondre à l'envoi de message, l'objet peut activer le signal système correspondant, éventuellement géré par d'autres délégués sur la chaîne prototype.
Ce signal système est disponible dans de nombreuses implémentations, y compris des systèmes basés sur des classes dynamiques entre crochets : #doesNotUnderstand dans Smalltalk, method_missing dans Ruby ; __getattr__ en Python, __call en PHP et __noSuchMethod__ implémentation dans ECMAScript, etc.
Exemple (implémentation ECMAScript de SpiderMonkey) :
En d'autres termes, si l'implémentation basée sur la classe statique ne peut pas répondre au message, la conclusion est que l'objet actuel n'a pas les caractéristiques requises, mais si vous essayez de l'obtenir à partir de la chaîne de prototypes, vous pouvez toujours obtenir le résultat, ou l'objet possède cette caractéristique après une série de changements.
Concernant ECMAScript, l'implémentation spécifique est : l'utilisation de prototypes basés sur des délégués. Cependant, comme nous le verrons dans la spécification et la mise en œuvre, ils ont également leurs propres caractéristiques.
Modèle concaténatif
Honnêtement, il faut dire quelque chose sur une autre situation (dès qu'elle n'est pas utilisée dans ECMASCript) : la situation où le prototype remplace l'objet natif d'autres objets. Dans ce cas, la réutilisation du code est une copie fidèle (clone) d'un objet pendant la phase de création de l'objet plutôt qu'une délégation. Ce type de prototype est appelé prototype concaténatif. La copie de toutes les propriétés du prototype d'un objet peut en outre modifier complètement ses propriétés et ses méthodes, et le prototype peut également se modifier lui-même (dans un modèle basé sur des délégués, cette modification ne modifiera pas le comportement de l'objet existant, mais modifiera ses propriétés du prototype). L’avantage de cette méthode est qu’elle peut réduire le temps de planification et de délégation, mais l’inconvénient est que l’utilisation de la mémoire est élevée.
Type Canard
Renvoyer des objets qui modifient dynamiquement les types faibles. Par rapport aux modèles basés sur des classes statiques, tester s'il peut faire ces choses n'a rien à voir avec le type (classe) de l'objet, mais s'il peut répondre au message (qui c'est-à-dire, après avoir vérifié si la capacité de le faire est indispensable).
Par exemple :
C'est ce qu'on appelle le type Dock. Autrement dit, les objets peuvent être identifiés par leurs propres caractéristiques lors de la vérification, plutôt que par leur position dans la hiérarchie ou leur appartenance à un type spécifique.
Concepts clés basés sur des prototypes
Regardons les principales caractéristiques de cette approche :
1. Le concept de base est objet
2. L'objet est complètement dynamique et variable (théoriquement il peut être converti d'un type à un autre)
3. Les objets n'ont pas de classes strictes qui décrivent leur propre structure et leur propre comportement. Les objets n'ont pas besoin de classes
.
4. Les objets n'ont pas de classes mais peuvent avoir des prototypes. S'ils ne peuvent pas répondre aux messages, ils peuvent être délégués au prototype
.
5. Le prototype de l'objet peut être modifié à tout moment pendant l'exécution ;
6. Dans le modèle basé sur les délégués, la modification des caractéristiques du prototype affectera tous les objets liés au prototype ;
7. Dans le modèle de prototype concaténatif, le prototype est une copie originale clonée à partir d'autres objets, et devient en outre une copie originale complètement indépendante. La transformation des caractéristiques du prototype n'affectera pas les objets clonés à partir de celui-ci
.
8. S'il est impossible de répondre au message, son appelant peut prendre des mesures supplémentaires (par exemple, modifier la planification)
9. La défaillance des objets ne peut pas être déterminée par leur niveau et à quelle classe ils appartiennent, mais par les caractéristiques actuelles
Cependant, il existe un autre modèle que nous devrions également considérer.
Basé sur des classes dynamiques
Nous pensons que la distinction "classe VS prototype" montrée dans l'exemple ci-dessus n'est pas si importante dans ce modèle basé sur des classes dynamiques, (surtout si la chaîne de prototypes est immuable, pour une distinction plus précise, il faut quand même considérons une classe statique). A titre d'exemple, il pourrait également utiliser Python ou Ruby (ou d'autres langages similaires). Ces langages utilisent tous un paradigme dynamique basé sur les classes. Cependant, sous certains aspects, nous pouvons voir certaines fonctionnalités implémentées sur la base du prototype.
Dans l'exemple suivant, nous pouvons voir que sur la base uniquement de la délégation, nous pouvons agrandir une classe (prototype), affectant ainsi tous les objets liés à cette classe. Nous pouvons également modifier dynamiquement cet objet au moment de l'exécution (en fournissant une nouvelle classe. objet pour le délégué) et ainsi de suite.
L'implémentation dans Ruby est similaire : des classes entièrement dynamiques sont également utilisées (d'ailleurs dans la version actuelle de Python, contrairement à Ruby et ECMAScript, l'agrandissement des classes (prototypes) ne fonctionne pas), on peut complètement changer l'objet (ou de classe) (ajout de méthodes/propriétés à la classe, et ces changements affecteront les objets existants), cependant, il ne peut pas changer dynamiquement la classe d'un objet.
Cependant, cet article ne concerne pas spécifiquement Python et Ruby, nous n'en dirons donc pas plus et continuons à discuter d'ECMAScript lui-même.
Mais avant cela, nous devons jeter un autre regard sur le « sucre syntaxique » que l'on trouve dans certaines POO, car de nombreux articles précédents sur JavaScript abordent souvent ces questions.
La seule phrase incorrecte à noter dans cette section est : "JavaScript n'est pas une classe, il possède des prototypes, qui peuvent remplacer les classes." Il est important de savoir que toutes les implémentations basées sur les classes ne sont pas complètement différentes. Même si l'on peut dire que "JavaScript est différent", il faut également considérer que (en plus du concept de "classes") il existe d'autres caractéristiques connexes. .
Autres fonctionnalités de diverses implémentations de POO
Dans cette section, nous présentons brièvement d'autres fonctionnalités et méthodes de réutilisation de code dans diverses implémentations de POO, y compris les implémentations de POO dans ECMAScript. La raison en est qu'il existe certaines restrictions de pensée habituelles sur l'implémentation de la POO en JavaScript. La seule exigence principale est que cela soit prouvé techniquement et idéologiquement. On ne peut pas dire que nous n'avons pas découvert la fonction sucre syntaxique dans d'autres implémentations de POO, et nous avons hâtivement supposé que JavaScript n'était pas un langage POO pur. C'est faux.
Polymorphe
Les objets ont plusieurs significations de polymorphisme dans ECMAScript.
Par exemple, une fonction peut être appliquée à différents objets, tout comme les propriétés de l'objet natif (car la valeur est déterminée lors de la saisie du contexte d'exécution) :
Le soi-disant polymorphisme des paramètres lors de la définition d'une fonction est équivalent à tous les types de données, sauf qu'il accepte les paramètres polymorphes (comme la méthode de tri .sort du tableau et ses paramètres - fonction de tri polymorphe). À propos, l’exemple ci-dessus peut également être considéré comme une sorte de polymorphisme paramétrique.
Les méthodes du prototype peuvent être définies comme vides, et tous les objets créés doivent redéfinir (implémenter) cette méthode (c'est-à-dire "une interface (signature), plusieurs implémentations").
Le polymorphisme est lié au type Canard que nous avons mentionné ci-dessus : c'est-à-dire que le type et la position de l'objet dans la hiérarchie ne sont pas si importants, mais s'il possède toutes les caractéristiques nécessaires, il peut être facilement accepté (c'est-à-dire que les interfaces communes sont importantes , les implémentations peuvent être diverses).
Encapsulation
Il existe souvent des idées fausses sur l'encapsulation. Dans cette section, nous discutons de certains sucres syntaxiques dans les implémentations de la POO - également connus sous le nom de modificateurs : Dans ce cas, nous discuterons de certains "sucres" pratiques dans les implémentations de la POO - des modificateurs bien connus : privé, protégé et public (également connu sous le nom d'accès à un objet niveau ou modificateur d’accès).
Ici, je voudrais vous rappeler l'objectif principal de l'encapsulation : l'encapsulation est un ajout abstrait, pas un "hacker malveillant" caché qui écrit quelque chose directement dans votre classe.
C'est une grosse erreur : utilisez hide pour vous cacher.
Des niveaux d'accès (privé, protégé et public) ont été implémentés dans de nombreux programmes orientés objet pour faciliter la programmation (sucre de syntaxe vraiment très pratique), décrivant et construisant des systèmes de manière plus abstraite.
Cela peut être vu dans certaines implémentations (telles que Python et Ruby déjà mentionnées). D'une part (en Python), ces attributs __private_protected (nommés via la convention underscore) ne sont pas accessibles de l'extérieur. Python, en revanche, est accessible de l'extérieur avec des règles spéciales (_ClassName__field_name).
Dans Ruby : D'une part, il a la capacité de définir des caractéristiques privées et protégées. D'autre part, il existe également des méthodes spéciales (telles que instance_variable_get, instance_variable_set, send, etc.) pour obtenir des données encapsulées.
La raison principale est que le programmeur lui-même souhaite obtenir les données encapsulées (notez que je n'utilise spécifiquement pas de données "cachées"). Si ces données changent de manière incorrecte d'une manière ou d'une autre ou comportent des erreurs, l'entière responsabilité incombe au programmeur, mais pas simplement aux « erreurs de frappe » ou à la « simple modification de certains champs ». Mais si cela se produit fréquemment, il s'agit d'une très mauvaise habitude et d'un très mauvais style de programmation, car il vaut généralement la peine d'utiliser l'API publique pour « parler » à l'objet.
Je le répète, l'objectif fondamental de l'encapsulation est de faire abstraction de l'utilisateur des données auxiliaires, et non d'empêcher les pirates de cacher les données. Plus sérieusement, l'encapsulation n'utilise pas le privé pour modifier les données afin d'assurer la sécurité du logiciel.
Encapsuler les objets auxiliaires (partiels). Nous utilisons un coût minimal, une localisation et des changements prédictifs pour assurer la faisabilité des changements de comportement dans les interfaces publiques.
De plus, l'objectif important de la méthode setter est d'abstraire des calculs complexes. Par exemple, le setter element.innerHTML - l'instruction abstraite - "Le HTML à l'intérieur de cet élément est maintenant le contenu suivant", et la fonction setter dans la propriété innerHTML sera difficile à calculer et à vérifier. Dans ce cas, le problème concerne principalement l’abstraction, mais l’encapsulation se produit également.
Le concept d'encapsulation n'est pas seulement lié à la POO. Par exemple, il peut s'agir d'une fonction simple qui n'encapsule que divers calculs, la rendant abstraite (l'utilisateur n'a pas besoin de savoir, par exemple, comment la fonction Math.round(...) est implémentée, l'utilisateur appelle simplement il). C'est une sorte d'encapsulation. Notez que je n'ai pas dit que c'est "privé, protégé et public".
La version actuelle de la spécification ECMAScript ne définit pas les modificateurs private, protected et public.
Cependant, dans la pratique, il est possible de voir quelque chose nommé "Mock JS Encapsulation". Généralement ce contexte est destiné à être utilisé (en règle générale, le constructeur lui-même). Malheureusement, ce "mimétisme" est souvent mis en œuvre et les programmeurs peuvent produire des "méthodes getter/setter" de paramétrage d'entité pseudo-absolument non abstraites (je le répète, c'est faux) :
Donc, tout le monde comprend que pour chaque objet créé, les méthodes getA/setA sont également créées, ce qui est aussi la raison de l'augmentation de la mémoire (par rapport à la définition du prototype). Cependant, en théorie, l'objet peut être optimisé dans le premier cas.
De plus, certains articles JavaScript mentionnent souvent la notion de « méthodes privées ». Remarque : La norme ECMA-262-3 ne définit aucune notion de « méthodes privées ».
Cependant, dans certains cas, il peut être créé dans le constructeur, car JS est un langage idéologique - les objets sont complètement mutables et ont des caractéristiques uniques (sous certaines conditions dans le constructeur, certains objets peuvent obtenir des méthodes supplémentaires tandis que d'autres non ).
De plus, en JavaScript, si l'encapsulation est encore interprétée à tort comme une compréhension qui empêche les pirates malveillants d'écrire automatiquement certaines valeursau lieu d'utiliser la méthode setter, alors ce qu'on appelle « caché » et « « privé » n'est en fait pas très "caché", certaines implémentations peuvent obtenir la valeur sur la chaîne de portée appropriée (et tous les objets variables correspondants) en appelant le contexte à la fonction eval (peut être testé sur SpiderMonkey1.7).
Alternativement, l'implémentation permet un accès direct à l'objet actif (comme Rhino), et la valeur de la variable interne peut être modifiée en accédant à la propriété correspondante de l'objet :
var _myPrivateData = 'testString';
Il est souvent utilisé pour mettre le contexte d'exécution entre parenthèses, mais pour les données auxiliaires réelles, il n'est pas directement lié à l'objet, mais est simplement pratique pour faire abstraction d'une API externe :
Héritage multiple
L'héritage multiple est un sucre syntaxique très pratique pour améliorer la réutilisation du code (si nous pouvons hériter d'une classe à la fois, pourquoi ne pouvons-nous pas en hériter 10 à la fois ?). Cependant, en raison de certaines lacunes de l’héritage multiple, sa mise en œuvre n’est pas devenue populaire.
ECMAScript ne prend pas en charge l'héritage multiple (c'est-à-dire qu'un seul objet peut être utilisé comme prototype direct), bien que son langage de programmation ancêtre ait une telle capacité. Mais dans certaines implémentations (telles que SpiderMonkey), l'utilisation de __noSuchMethod__ peut être utilisée pour gérer la planification et la délégation au lieu de la chaîne de prototypes.
Mixins
Les mixins sont un moyen pratique de réutiliser du code. Les mixins ont été suggérés comme alternatives à l'héritage multiple. Chacun de ces éléments individuels peut être mélangé avec n'importe quel objet pour étendre ses fonctionnalités (les objets peuvent donc également être mélangés avec plusieurs Mixins). La spécification ECMA-262-3 ne définit pas le concept de « Mixins », mais selon la définition des Mixins et ECMAScript a des objets dynamiquement mutables, il n'y a aucun obstacle à simplement étendre les fonctionnalités à l'aide de Mixins.
Exemple typique :
Veuillez noter que j'utilise ces définitions ("mixin", "mix") entre guillemets mentionnées dans ECMA-262-3. Il n'y a pas de tel concept dans la spécification, et il ne s'agit pas de mix mais couramment utilisé pour étendre des objets. avec de nouvelles fonctionnalités. (Le concept de mixins dans Ruby est officiellement défini. Les mixins créent une référence à un module conteneur au lieu de simplement copier toutes les propriétés du module dans un autre module - en fait : créer un objet supplémentaire (prototype) pour le délégué. ).
Caractéristiques
Les traits sont similaires dans leur concept aux mixins, mais ils ont beaucoup de fonctionnalités (par définition, puisque les mixins peuvent être appliqués, ils ne peuvent pas contenir d'état, car cela peut provoquer des conflits de noms). Selon ECMAScript, les Traits et les mixins suivent les mêmes principes, donc la spécification ne définit pas le concept de « Traits ».
Interface
Les interfaces implémentées dans certaines POO sont similaires aux mixins et aux traits. Cependant, contrairement aux mixins et aux traits, les interfaces obligent les classes d'implémentation à implémenter le comportement de leurs signatures de méthode.
Les interfaces peuvent être entièrement considérées comme des classes abstraites. Cependant, par rapport aux classes abstraites (les méthodes des classes abstraites ne peuvent implémenter qu'une partie de la méthode, et l'autre partie est toujours définie comme une signature), l'héritage ne peut hériter que d'une seule classe de base, mais peut hériter de plusieurs interfaces. les interfaces (multiples mixtes) peuvent être considérées comme une alternative à l’héritage multiple.
La norme ECMA-262-3 ne définit ni la notion d'« interface » ni la notion de « classe abstraite ». Cependant, à titre d'imitation, il est possible d'implémenter un objet avec une méthode "vide" (ou une exception levée dans une méthode vide pour indiquer au développeur que cette méthode doit être implémentée).
Combinaison d'objets
La composition d'objets est également l'une des technologies de réutilisation dynamique du code. La composition d'objets diffère de l'héritage très flexible dans la mesure où elle implémente un délégué dynamiquement modifiable. Et cela repose également sur des prototypes commandés. En plus des prototypes dynamiquement modifiables, l'objet peut regrouper des objets pour les délégués (créer ainsi une combinaison - une agrégation) et envoyer en outre des messages aux objets qui délèguent au délégué. Cela peut être fait avec plus de deux délégués, car sa nature dynamique signifie qu'elle peut changer au moment de l'exécution.
L'exemple __noSuchMethod__ déjà mentionné fait cela, mais montrons également comment utiliser explicitement les délégués :
Par exemple :
Cette relation d'objet est appelée "has-a", et l'intégration est une relation "is-a".
En raison du manque de composition explicite (flexibilité par rapport à l'héritage), l'ajout de code intermédiaire est également acceptable.
Fonctionnalités AOP
En tant que fonction orientée aspect, vous pouvez utiliser des décorateurs de fonctions. La spécification ECMA-262-3 ne définit pas clairement la notion de « décorateurs de fonctions » (contrairement à Python, où ce terme est officiellement défini). Cependant, les fonctions avec des paramètres fonctionnels peuvent être décorées et activées sous certains aspects (en appliquant ce que l'on appelle des suggestions) :
L'exemple de décorateur le plus simple :
Conclusion
Dans cet article, nous avons clarifié l'introduction de la POO (j'espère que ces informations vous ont été utiles), et dans le prochain chapitre nous continuerons avec l'implémentation d'ECMAScript pour la programmation orientée objet.