Protected Methods in JavaScript ES5

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

    In this article:


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

    Why protected class members are needed


    In short, then

    • it’s easier to understand the operation of the class and find errors in it. (You can immediately see in which case the class members are used. If private, then you need to analyze only this class, well, and if protected, then only this and derived classes.)
    • easier to manage change. (For example, you can remove private members without fear that something outside the editable class will break.)
    • the number of applications in the bug tracker is reduced, because users of the library or control can "sew up" on our "private" members, which we decided to remove in the new version of the class, 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.

    Let me remind you that the main idea of ​​protected members is to hide methods and properties from users of the class instance, but at the same time allow derived classes to have access to them.

    Using TypeScript will not allow calling protected methods, however, after compilation in JavaScript, all private and protected members become public. For example, we develop a control or library that users will install on their sites or applications. These users will be able to do whatever they want with protected members, violating the integrity of the class. As a result, our bug tracker is bursting with complaints that our library or control is not working properly. We spend time and effort to sort it out - “is this how the object was in that state of the client, which led to an error ?!”. Therefore, in order to make life easier for everyone, such protection is needed that will not make it possible to change the meaning of private and protected class members.

    What is needed to understand the method in question


    To understand the method of declaring protected class members, you need strong knowledge:

    • device classes and objects in JavaScript.
    • ways to create private class members (at least through closure).
    • methods Object.defineProperty and Object.getOwnPropertyDescriptor

    About the device model in JavaScript, I can recommend, for example, an excellent article by Andrey Akinshin ( DreamWalker ) “Understanding OOP in JS [Part No. 1]” .
    About private properties there is a good and, in my opinion, a fairly complete description of as many as 4 different ways to create 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 concealment must be performed, but this, of course, is not enough because There is still the possibility of calling the method / property directly:

        console.log(obj1.protectedNumber); // Выведет 12.
    

    Helper Class ProtectedError


    First, we need the ProtectedError class, which inherits from Error, and which will be thrown if there is no access to the 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 the ProtectedError class and we understand what Object.defineProperty does with the value enumerable: false, let's analyze the creation of 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


    Perhaps you will be confused by the check:

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

    var obj1 = BaseClass();
    // или так:
    var obj2 = BaseClass.call({});
    

    In such cases, do as you wish. 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 instantiate correctly, as done in BaseClass.

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

    Description of a public property named protectedMethod


    Entering the method, we call the context check on which we were called. It is better to check out in a separate method, for example, checkAccess, because the same check will be needed in all protected methods and properties of classes. So, first of all, check the context type of the call to this. 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, like this:

    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, this instanceof BaseClass expression will be true. Therefore, to distinguish instances of the BaseClass class from instances of derived classes, we check the constructor. If the constructor matches BaseClass, then our protectedMethod is called on the BaseClass instance, just like a regular public method:

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

    We prohibit such calls:

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

    Next comes the call of the protectedMethod closed method, which, in fact, is the method we protect. Inside the method, if you need to refer to the members of the BaseClass class, you can do this using the stored instance of _self. This is exactly what _self was created to have access to class members from all private / private methods. Therefore, if you do not need to access class members in your protected method or property, then you can not create the _self variable.

    Calling a protected method inside the BaseClass class


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

    DerivedClass Derived Class Description


    Now let's look at a derived class and how to make it accessible to a protected method of a 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 the 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 through the standard bind method. This means that calling _base.protectedMethod (); inside protectedMethod this is not a _base object, but an instance of the DerivedClass class.

    ProtectedMethod Method Description Inside DerivedClass


    In the DerivedClass class, it is necessary to 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 by checking 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 - “but have we been called as a simple public method?” For instances of the DerivedClass class, the constructor will be equal to DerivedClass. If so, then 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 DerivedClass-derived classes. It is public and therefore it has a check prohibiting public calls. The second method is located in the _base object, which is closed inside the DerivedClass class and therefore is not visible to anyone from outside and it is used to access the protected method from all DerivedClass methods.

    Property Protection


    With properties, work happens a little differently. Properties in BaseClass are defined as usual through Object.defineProperty, only in getters and setters you first need to 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, the protected property is accessed not through this, but to the closed variable _protectedProperty. If it is important for us that the getter and setter work when using the property inside the BaseClass class, then we need to create private methods getProtectedPropety and setProtectedProperty, inside which there will be no checks, and they should already be called.

    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, because property cannot be replaced by context. Therefore, we will use the standard Object.getOwnPropertyDescriptor method to get the getter and setter from the property of the base class as functions that can already be used to change the call 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 may 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 creating a prototype, rewrite the constructor property:

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

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

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

    Also popular now: