Méthodes protégées dans JavaScript ES5

Beaucoup d'excellents articles ont été écrits sur le modèle objet en JavaScript. Et sur les différentes façons de créer des membres de classe privés sur Internet est plein de descriptions dignes. Mais à propos des méthodes protégées - il y a très peu de données. Je voudrais combler cette lacune et dire comment vous pouvez créer des méthodes protégées sans bibliothèques en pur JavaScript ECMAScript 5.

Dans cet article:


Lien vers le référentiel git-hub avec le code source et les tests.

Pourquoi les membres de classe protégés sont nécessaires


Bref, alors

  • il est plus facile de comprendre le fonctionnement de la classe et d'y trouver des erreurs. (Vous pouvez immédiatement voir dans quels cas les membres de la classe sont utilisés. S'il est privé, vous devez analyser uniquement cette classe, enfin, et s'il est protégé, alors uniquement cette classe et les classes dérivées.)
  • plus facile à gérer le changement. (Par exemple, vous pouvez supprimer des membres privés sans craindre que quelque chose en dehors de la classe modifiable se brise.)
  • le nombre d'applications dans le suivi des bogues est réduit, car les utilisateurs de la bibliothèque ou du contrôle peuvent "recoudre" nos membres "privés", que nous avons décidé de supprimer dans la nouvelle version de la classe, ou de changer la logique de leur travail.
  • Et en général, les membres de classe protégés sont un outil de conception. C'est bien de l'avoir à portée de main et bien testé.

Permettez-moi de vous rappeler que l'idée principale des membres protégés est de masquer les méthodes et les propriétés aux utilisateurs de l'instance de classe, mais en même temps de permettre aux classes dérivées d'y avoir accès.

L'utilisation de TypeScript ne permettra pas d'appeler des méthodes protégées, cependant, après la compilation en JavaScript, tous les membres privés et protégés deviennent publics. Par exemple, nous développons un contrôle ou une bibliothèque que les utilisateurs installeront sur leurs sites ou applications. Ces utilisateurs pourront faire tout ce qu'ils veulent avec des membres protégés, violant l'intégrité de la classe. En conséquence, notre traqueur de bogues regorge de plaintes selon lesquelles notre bibliothèque ou notre contrôle ne fonctionne pas correctement. Nous passons du temps et des efforts à le trier - "est-ce ainsi que l'objet était dans cet état du client, ce qui a conduit à une erreur?!" . Par conséquent, afin de faciliter la vie de tous, une telle protection est nécessaire qui ne permettra pas de changer le sens des membres du groupe privés et protégés.

Ce dont vous avez besoin pour comprendre la méthode en question


Pour comprendre la méthode de déclaration des membres de classe protégés, vous avez besoin de solides connaissances:

  • classes d'appareils et objets en JavaScript.
  • façons de créer des membres de classe privés (au moins par la fermeture).
  • méthodes Object.defineProperty et Object.getOwnPropertyDescriptor

À propos du modèle d'appareil en JavaScript, je peux recommander, par exemple, un excellent article d'Andrey Akinshin ( DreamWalker ) «Comprendre la POO dans JS [Part No. 1]» .
À propos des propriétés privées, il y a une bonne et, à mon avis, une description assez complète de pas moins de 4 façons différentes de créer des membres de classe privés sur le site Web de MDN.

Quant à la méthode Object.defineProperty, elle nous permettra de masquer les propriétés et les méthodes des boucles for-in et, par conséquent, des algorithmes de sérialisation:

function MyClass(){ Object.defineProperty(MyClass.prototype, 'protectedNumber', { value: 12, enumerable: false }); this.publicNumber = 25; }; var obj1 = new MyClass(); for(var prop in obj1){ console.log('property:' prop); //prop     'protectedNumber' } console.log(JSON.stringify(obj1)); //  { 'publicNumber': 25 } 

Une telle dissimulation doit être effectuée, mais cela, bien sûr, ne suffit pas car Il y a toujours la possibilité d'appeler directement la méthode / propriété:

  console.log(obj1.protectedNumber); //  12. 

Classe d'assistance ProtectedError


Tout d'abord, nous avons besoin de la classe ProtectedError, qui hérite d'Erreur, et qui sera levée s'il n'y a pas d'accès à la méthode ou à la propriété protégée.

 function ProtectedError(){ this.message = "Encapsulation error, the object member you are trying to address is protected."; } ProtectedError.prototype = new Error(); ProtectedError.prototype.constructor = ProtectedError; 

Implémentation de membres de classe protégés dans ES5


Maintenant que nous avons la classe ProtectedError et que nous comprenons ce que fait Object.defineProperty avec la valeur énumérable: false, analysons la création d'une classe de base qui souhaite partager la méthode protectedMethod avec toutes ses classes dérivées, mais la cacher à tout le monde:

 function BaseClass(){ if (!(this instanceof BaseClass)) return new BaseClass(); var _self = this; //   ,        /** @summary       */ function checkAccess() { if (!(this instanceof BaseClass)) throw new ProtectedError(); if (this.constructor === BaseClass) throw new ProtectedError() } Object.defineProperty(_self, 'protectedMethod', { enumerable: false, //    for-in  configurable:false, //     value: function(){ //   , ,          Base,     checkAccess.call(this); //  . protectedMethod(); } }); function protectedMethod(){ //       , //       this,   _self return 'example value'; } this.method = function (){ protectedMethod(); //          BaseClass //this.protectedMethod(); //   , ..      ProtectedError } } 

Description du constructeur de la classe BaseClass


Peut-être serez-vous dérouté par le chèque:

  if (!(this instanceof BaseClass)) return new BaseClass(); 
Ce test est un "amateur". Vous pouvez le supprimer, cela n'a rien à voir avec les méthodes protégées. Cependant, je le laisse personnellement dans mon code, car il est nécessaire pour les cas où l'instance de classe n'est pas créée correctement, c'est-à-dire sans le mot-clé new. Par exemple, comme ceci:

 var obj1 = BaseClass(); //  : var obj2 = BaseClass.call({}); 

Dans de tels cas, faites ce que vous voulez. Vous pouvez, par exemple, générer une erreur:

  if (!(this instanceof BaseClass)) throw new Error('Wrong instance creation. Maybe operator "new" was forgotten'); 

Ou vous pouvez simplement instancier correctement, comme cela est fait dans BaseClass.

Ensuite, nous enregistrons la nouvelle instance dans la variable _self (pourquoi je dois l'expliquer plus tard).

Description d'une propriété publique nommée protectedMethod


En entrant dans la méthode, nous appelons la vérification de contexte sur laquelle nous avons été appelés. Il est préférable de vérifier dans une méthode distincte, par exemple, checkAccess, car la même vérification sera nécessaire dans toutes les méthodes et propriétés protégées des classes. Donc, tout d'abord, vérifiez le type de contexte de l'appel à ceci. S'il s'agit d'un type autre que BaseClass, le type n'est ni BaseClass ni aucun de ses dérivés. Nous interdisons de tels appels.

 if(!(this instanceof BaseClass)) throw new ProtectedError(); 

Comment cela peut-il arriver? Par exemple, comme ceci:

 var b = new BaseClass(); var someObject = {}; b.protectedMethod.call(someObject); //   ,  protectedMethod this   someObject    , .. someObject instanceof BaseClass   

Dans le cas des classes dérivées, l'expression this instanceof BaseClass sera vraie. Mais pour les instances BaseClass, cette expression instanceof BaseClass sera vraie. Par conséquent, pour distinguer les instances de la classe BaseClass des instances de classes dérivées, nous vérifions le constructeur. Si le constructeur correspond à BaseClass, alors notre protectedMethod est appelée sur l'instance BaseClass, tout comme une méthode publique classique:

 var b = new BaseClass(); b.protectedMethod(); 

Nous interdisons de tels appels:

 if(this.constructor === BaseClass) throw new ProtectedError(); 

Vient ensuite l'appel de la méthode fermée protectedMethod, qui, en fait, est la méthode que nous protégeons. Dans la méthode, si vous devez faire référence aux membres de la classe BaseClass, vous pouvez le faire en utilisant l'instance stockée de _self. C'est exactement ce que _self a été créé pour avoir accès aux membres de la classe à partir de toutes les méthodes privées / privées. Par conséquent, si vous n'avez pas besoin d'accéder aux membres de classe dans votre méthode ou propriété protégée, vous ne pouvez pas créer la variable _self.

Appel d'une méthode protégée dans la classe BaseClass


À l'intérieur de la classe BaseClass, protectedMethod doit être accessible uniquement par son nom, pas par ce biais. Sinon, dans protectedMethod, nous ne pouvons pas distinguer si nous avons été appelés en tant que méthode publique ou à l'intérieur d'une classe. Dans ce cas, la fermeture nous sauve - protectedMethod se comporte comme une méthode privée classique, fermée à l'intérieur de la classe et visible uniquement dans le cadre de la fonction BaseClass.

Description de la classe dérivée DerivedClass


Examinons maintenant une classe dérivée et comment la rendre accessible à une méthode protégée d'une classe de base.

 function DerivedClass(){ var _base = { protectedMethod: this.protectedMethod.bind(this) }; /** @summary       */ function checkAccess() { if (this.constructor === DerivedClass) throw new ProtectedError(); } //     Object.defineProperty(this, 'protectedMethod', { enumerable: false, // ..       this configurable: false,//         for-in  //      value: function(){ checkAccess.call(_self); return _base.protectedMethod(); } }); //        this.someMethod = function(){ console.log(_base.protectedMethod()); } } DerivedClass.prototype = new BaseClass(); Object.defineProperty(DerivedClass.prototype, 'constructor', { value : DerivedClass, configurable: false }); 

Description du constructeur de classe dérivée


Dans la classe dérivée, nous créons un objet _base dans lequel nous plaçons une référence à la méthode protectedMethod de la classe de base, fermée au contexte de la classe dérivée via la méthode de liaison standard. Cela signifie que l'appel de _base.protectedMethod (); à l'intérieur de protectedMethod, il ne s'agit pas d'un objet _base, mais d'une instance de la classe DerivedClass.

Méthode ProtectedMethod Description À l'intérieur de DerivedClass


Dans la classe DerivedClass, il est nécessaire de déclarer la méthode publique protectedMethod de la même manière que nous l'avons fait dans la classe de base via Object.defineProperty et de vérifier son accès en appelant la méthode checkAccess ou en vérifiant directement dans la méthode:

  Object.defineProperty(DerivedClass.prototype, 'protectedMethod', { enumerable: false, configurable: false, value: function(){ if(this.constructor === DerivedClass) throw new ProtectedError() return _base.protectedMethod(); } }); 

Nous vérifions - "mais avons-nous été appelés comme une simple méthode publique?" Pour les instances de la classe DerivedClass, le constructeur sera égal à DerivedClass. Si c'est le cas, générez une erreur. Sinon, nous l'envoyons à la classe de base et il fera déjà tous les autres contrôles.

Donc, dans la classe dérivée, nous avons deux fonctions. L'un est déclaré via Object.defineProperty et est nécessaire pour les classes dérivées de DerivedClass. Il est public et dispose donc d'un chèque interdisant les appels publics. La deuxième méthode se trouve dans l'objet _base, qui est fermé à l'intérieur de la classe DerivedClass et n'est donc visible par personne de l'extérieur et elle est utilisée pour accéder à la méthode protégée à partir de toutes les méthodes DerivedClass.

Protection des biens


Avec les propriétés, le travail se déroule un peu différemment. Les propriétés dans BaseClass sont définies comme d'habitude via Object.defineProperty, uniquement dans les getters et setters, vous devez d'abord ajouter notre chèque, c'est-à-dire appelez checkAccess:

 function BaseClass(){ function checkAccess(){ ... } var _protectedProperty; Object.defineProperty(this, 'protectedProperty', { get: function () { checkAccess.call(this); return _protectedProperty; }, set: function (value) { checkAccess.call(this); _protectedProperty = value; }, enumerable: false, configurable: false }); } 

À l'intérieur de la classe BaseClass, la propriété protégée n'est pas accessible par ce biais, mais à la variable fermée _protectedProperty. S'il est important pour nous que le getter et le setter fonctionnent lors de l'utilisation de la propriété à l'intérieur de la classe BaseClass, nous devons créer des méthodes privées getProtectedPropety et setProtectedProperty, dans lesquelles il n'y aura pas de vérifications, et elles doivent déjà être appelées.

 function BaseClass(){ function checkAccess(){ ... } var _protectedProperty; Object.defineProperty(this, 'protectedProperty', { get: function () { checkAccess.call(this); return getProtectedProperty(); }, set: function (value) { checkAccess.call(this); setProtectedProperty(value); }, enumerable: false, configurable: false }); function getProtectedProperty(){ //    return _protectedProperty; } function setProtectedProperty(value){ //    _protectedProperty = value; } } 

Dans les classes dérivées, travailler avec des propriétés est un peu plus compliqué, car la propriété ne peut pas être remplacée par le contexte. Par conséquent, nous utiliserons la méthode Object.getOwnPropertyDescriptor standard pour obtenir le getter et le setter de la propriété de la classe de base en tant que fonctions qui peuvent déjà être utilisées pour changer le contexte d'appel:

 function DerivedClass(){ function checkAccess(){ ... } var _base = { protectedMethod: _self.protectedMethod.bind(_self), }; var _baseProtectedPropertyDescriptor = Object.getOwnPropertyDescriptor(_self, 'protectedProperty'); //      _base //    DerivedClass     Object.defineProperty(_base, 'protectedProperty', { get: function() { return _baseProtectedPropertyDescriptor.get.call(_self); }, set: function(value){ _baseProtectedPropertyDescriptor.set.call(_self, value); } }) //      ,      DerivedClass      . Object.defineProperty(_self, 'protectedProperty', { get: function () { checkAccess.call(_self); return base.protectedProperty; }, set: function (value) { checkAccess.call(_self); _base.protectedProperty = value; }, enumerable: false, configurable: false }); } 

Description de l'héritage


Et la dernière chose que je voudrais commenter est l'héritage de DerivedClass de BaseClass. Comme vous le savez peut-être, DerivedClass.prototype = new BaseClass (); crée non seulement un prototype, mais réécrit également sa propriété constructeur. De ce fait, pour chaque instance de DerivedClass, la propriété constructeur devient égale à BaseClass. Pour résoudre ce problème, généralement après avoir créé un prototype, réécrivez la propriété constructeur:

 DerivedClass.prototype = new BaseClass(); DerivedClass.prototype.constructor = DerivedClass; 

Cependant, afin que personne ne réécrive cette propriété après nous, nous utilisons le même Object.defineProperty. La propriété configurable: false empêche la propriété d'être à nouveau remplacée:

 DerivedClass.prototype = new BaseClass(); Object.defineProperty(DerivedClass.prototype, 'constructor', { value : DerivedClass, configurable: false }); 

Source: https://habr.com/ru/post/fr425521/


All Articles