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,
- easier to understand the work of the class and find errors in it. (You can immediately see in which cases the class members are used. If the private ones, then only the given class should be analyzed, well, and if the protected ones, then only the given and derived classes.)
- easier to manage change. (For example, you can remove private members without fear of breaking something outside of the class being edited.)
- decreases the number of requests in the bug-tracker, because Library or control users can “bind” to our “private” members, who in the new version of the class we decided to remove or change the logic of their work.
- And in general, protected class members are a design tool. It’s good to have it handy and well-tested.
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
- device classes and objects in javascript.
- ways to create private members of a class (at least through a closure).
- Object.defineProperty and Object.getOwnPropertyDescriptor methods
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);
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);
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;
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();
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);
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) }; function checkAccess() { if (this.constructor === DerivedClass) throw new ProtectedError(); }
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(){
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');
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 });