⬆️ ⬇️

Protected methods in javascript ES5

A lot of great articles have been written about the object model in JavaScript. Yes, and about the various ways to create private class members on the Internet is full of decent descriptions. But about protected methods - there is very little data. I would like to fill this gap and tell you how to create protected methods without libraries on pure JavaScript ECMAScript 5.



In this article:





Link to git-hub repository with source code and tests.

')

Why do we need protected members of the class



In short,





Recall that the main idea of ​​protected members is to hide methods and properties from users of an instance of a class, but still allow derived classes to have access to them.



Using TypeScript will not allow you to call protected methods, however, after compilation in JavaScript, all private and protected members become public. For example, we are developing a control or library that users will install on their sites or applications. These users will be able to do whatever they want with the protected members, violating the integrity of the class. As a result, our bug tracker breaks down from complaints that our library or controls are not working properly. We spend time and effort on sorting out - “this is how the object turned out to be in that state at the client, which led to an error ?!” . Therefore, in order to make life easier for all, we need such protection that will not give the opportunity to change the meaning of private and protected members of a class.



What you need to understand the method under consideration



To understand the method of declaring the protected members of a class, you need to know for sure





About the object model device in JavaScript, I can recommend, for example, an excellent article by Andrey Akinshin ( DreamWalker ) “Understanding the OOP in JS [part №1]” .

About private properties there is a good and, in my opinion, fairly complete description of as many as 4 different ways of creating private class members on the MDN website.



As for the Object.defineProperty method, it will allow us to hide properties and methods from for-in loops, and, as a result, from serialization algorithms:



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 } 


Such a concealment is necessary, but this, of course, is not enough. it is still possible to call the method / property directly:



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


Helper class ProtectedError



To begin, we need the ProtectedError class, which inherits from Error, and which will be thrown out if there is no access to a protected method or property.



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


Implementing protected class members in ES5



Now that we have a ProtectedError class and we understand what Object.defineProperty does with enumerable: false, let's look at creating a base class that wants to share the protectedMethod method with all its derived classes, but hide it from everyone else:



 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 of the BaseClass class constructor



You might be confused by the check:



  if (!(this instanceof BaseClass)) return new BaseClass(); 
This check is "an amateur." You can remove it, it is not related to protected methods. However, I personally leave it in my code, since it is needed for those cases when an instance of the class is created incorrectly, i.e. without the keyword new. For example, like this:



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


In such cases, do what you want. You can, for example, generate an error:



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


Or you can simply create an instance correctly, as is done in BaseClass.



Next, we save the new instance in the _self variable (why I need to explain it a little later).



A description of a public property named protectedMethod



Entering the method, we call the context check on which we were called. It is better to put the check into a separate method, for example, checkAccess, since the same check will be needed in all protected methods and class properties. So, first of all, we check the context type of the “this” call. If this has a type other than BaseClass, then the type is neither BaseClass itself, nor any of its derivatives. We prohibit such calls.



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


How can this happen? For example:



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


In the case of derived classes, the expression this instanceof BaseClass will be true. But for BaseClass instances, the expression of this instanceof BaseClass will be true. Therefore, to distinguish instances of the BaseClass class from instances of derived classes, check the constructor. If the constructor matches BaseClass, then our protectedMethod is called on the BaseClass instance, as is the usual public method:



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


We prohibit such calls:



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


Next comes the call to the protectedMethod closed method, which, in fact, is the method we are protecting. Inside the method, if there is a need to refer to the members of the BaseClass class, you can do this using a saved instance of _self. It was for this reason that _self was created to have access to class members from all closed / private methods. Therefore, if in your protected method or property you do not need to access the members of the class, then you can not create the _self variable.



Calling a protected method inside the BaseClass class



Inside the BaseClass class, a protectedMethod should be accessed only by name, and not through this. Otherwise, inside protectedMethod we will not be able to distinguish whether we were called as a public method or from inside the class. In this case, the closure saves us - protectedMethod behaves like a normal private method, closed inside a class and visible only inside the scope of the BaseClass function.



DerivedClass Derived Class Description



Now let's look at the derived class and how to make it access to the protected method of the base class.



 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 }); 


Derived Class Constructor Description



In a derived class, we create an _base object in which we place a reference to the protectedMethod method of the base class, closed to the context of the derived class via the standard bind method. This means that the _base.protectedMethod () call; inside protectedMethod this is not an _base object, but an instance of the DerivedClass class.



Description of the protectedMethod method inside the DerivedClass class



In the DerivedClass class, you must declare the protectedMethod public method in the same way as we did in the base class via Object.defineProperty and check access in it by calling the checkAccess method or performing the check directly in the method:



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


We check - “didn’t they call us as a simple public method?” Instances of the class DerivedClass constructor will be equal to DerivedClass. If so, then we generate an error. Otherwise, we send it to the base class and it will already do all the other checks.



So, in the derived class, we have two functions. One is declared via Object.defineProperty and is needed for classes derived from DerivedClass. It is public and therefore there is a check prohibiting public calls. The second method is in the _base object, which is closed inside the DerivedClass class and therefore not visible to anyone from the outside and it is used to access the protected method from all the DerivedClass methods.



Property Protection



With properties, the work happens a little differently. The properties in BaseClass are defined as usual via Object.defineProperty, only in getters and setters you need to first add our check, i.e. call 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 }); } 


Inside the BaseClass class, we access the protected property not through this, but to the closed variable _protectedProperty. In case it is important for us to work the getter and setter when using the property inside the BaseClass class, then we need to create private getProtectedPropety and setProtectedProperty methods, inside which there will be no checks, and already call them.



 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; } } 


In derived classes, working with properties is a bit more complicated, since property cannot be replaced by context. Therefore, we will use the standard Object.getOwnPropertyDescriptor method to get the getter and setter as a function from the base class property, which can already change the calling context:



 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 }); } 


Inheritance description



And the last thing I would like to comment on is the inheritance of DerivedClass from BaseClass. As you probably know, DerivedClass.prototype = new BaseClass (); not only creates a prototype, but also rewrites its constructor property. Because of this, for each instance of DerivedClass, the constructor property becomes equal to BaseClass. To fix this, usually, after prototyping, the constructor property is rewritten:



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


However, so that no one rewrites this property after us, use the same Object.defineProperty. The configurable property: false prevents the property from being redefined again:



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

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



All Articles