Geschützte Methoden in JavaScript ES5

    Es wurden viele großartige Artikel über das Objektmodell in JavaScript geschrieben. Und über die verschiedenen Möglichkeiten, private Klassenmitglieder im Internet zu erstellen, gibt es jede Menge wertvolle Beschreibungen. Aber über geschützte Methoden - es gibt nur sehr wenige Daten. Ich möchte diese Lücke füllen und erläutern, wie Sie geschützte Methoden ohne Bibliotheken in reinem JavaScript ECMAScript 5 erstellen können.

    In diesem Artikel:


    Link zum Git-Hub-Repository mit Quellcode und Tests.

    Warum geschützte Klassenmitglieder gebraucht werden


    Kurz gesagt

    • Es ist einfacher, die Funktionsweise der Klasse zu verstehen und Fehler darin zu finden. (Sie können sofort sehen, in welchem ​​Fall die Klassenmitglieder verwendet werden. Wenn privat, müssen Sie nur diese Klasse analysieren, und wenn geschützt, nur diese und abgeleitete Klassen.)
    • einfacher zu handhaben ändern. (Sie können beispielsweise private Mitglieder entfernen, ohne befürchten zu müssen, dass etwas außerhalb der bearbeitbaren Klasse beschädigt wird.)
    • Die Anzahl der Anwendungen im Bug-Tracker wird reduziert, da Benutzer der Bibliothek oder des Steuerelements können unsere "privaten" Mitglieder "vernähen", die wir in der neuen Version der Klasse entfernen oder die Logik ihrer Arbeit ändern möchten.
    • Und im Allgemeinen sind geschützte Klassenmitglieder ein Entwurfswerkzeug. Es ist gut, es griffbereit und gut getestet zu haben.

    Ich möchte Sie daran erinnern, dass die Hauptidee von geschützten Mitgliedern darin besteht, Methoden und Eigenschaften vor Benutzern der Klasseninstanz zu verbergen, aber gleichzeitig abgeleiteten Klassen den Zugriff darauf zu ermöglichen.

    Mit TypeScript können keine geschützten Methoden aufgerufen werden. Nach dem Kompilieren in JavaScript werden jedoch alle privaten und geschützten Mitglieder öffentlich. Beispielsweise entwickeln wir ein Steuerelement oder eine Bibliothek, die Benutzer auf ihren Sites oder Anwendungen installieren. Diese Benutzer können mit geschützten Mitgliedern alles tun, was sie wollen, wodurch die Integrität der Klasse verletzt wird. Infolgedessen wimmelt es in unserem Bug-Tracker nur so von Beschwerden, dass unsere Bibliothek oder unser Steuerelement nicht ordnungsgemäß funktioniert. Wir investieren viel Zeit und Mühe, um das Problem zu lösen. Befand sich das Objekt in diesem Zustand des Clients, der zu einem Fehler führte?. Um das Leben für alle zu erleichtern, ist daher ein solcher Schutz erforderlich, der es nicht ermöglicht, die Bedeutung privater und geschützter Klassenmitglieder zu ändern.

    Was ist erforderlich, um die fragliche Methode zu verstehen


    Um die Methode zum Deklarieren von geschützten Mitgliedern einer Klasse zu verstehen, benötigen Sie fundierte Kenntnisse:

    • Geräteklassen und Objekte in JavaScript.
    • Möglichkeiten, private Klassenmitglieder zu schaffen (zumindest durch Schließung).
    • Methoden Object.defineProperty und Object.getOwnPropertyDescriptor

    Über das Gerätemodell in JavaScript kann ich zum Beispiel einen ausgezeichneten Artikel von Andrey Akinshin ( DreamWalker ) „OOP in JS verstehen [Teil Nr. 1]“ empfehlen .
    Über private Objekte gibt es eine gute und meiner Meinung nach ziemlich vollständige Beschreibung von 4 verschiedenen Möglichkeiten, private Klassenmitglieder auf der MDN-Website zu erstellen .

    Mit der Object.defineProperty-Methode können wir Eigenschaften und Methoden vor for-in-Schleifen und folglich vor Serialisierungsalgorithmen verbergen:

    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 }
    

    Eine solche Verschleierung muss durchgeführt werden, aber das ist natürlich nicht genug, weil Es ist weiterhin möglich, die Methode / Eigenschaft direkt aufzurufen:

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

    Helper-Klasse ProtectedError


    Zunächst benötigen wir die ProtectedError-Klasse, die von Error erbt und ausgelöst wird, wenn kein Zugriff auf die protected-Methode oder -Eigenschaft besteht.

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

    Implementieren von geschützten Klassenmitgliedern in ES5


    Nachdem wir nun die ProtectedError-Klasse haben und verstehen, was Object.defineProperty mit dem Wert enumerable macht: false, analysieren wir die Erstellung einer Basisklasse, die die protectedMethod-Methode mit allen abgeleiteten Klassen teilen möchte, aber sie vor allen anderen Klassen verbergen möchte:

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

    Beschreibung des BaseClass-Klassenkonstruktors


    Vielleicht werden Sie durch die Prüfung verwirrt:

      if (!(this instanceof BaseClass))
         return new BaseClass(); 
    
    Dieser Test ist ein "Amateur". Sie können es entfernen, es hat nichts mit geschützten Methoden zu tun. Allerdings lasse ich es persönlich in meinem Code, weil Es wird für die Fälle benötigt, in denen die Klasseninstanz nicht korrekt erstellt wurde, d. h. ohne das Schlüsselwort neu. Zum Beispiel so:

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

    Machen Sie in solchen Fällen, was Sie möchten. Sie können beispielsweise einen Fehler erzeugen:

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

    Oder Sie können es einfach korrekt instanziieren, wie in BaseClass.

    Als nächstes speichern wir die neue Instanz in der Variablen _self (warum muss ich das später erklären).

    Beschreibung einer öffentlichen Eigenschaft mit dem Namen protectedMethod


    Bei der Eingabe der Methode rufen wir die Kontextprüfung auf, für die wir aufgerufen wurden. Es ist besser, in einer separaten Methode auszuchecken, zum Beispiel checkAccess, weil Dieselbe Prüfung ist für alle geschützten Methoden und Eigenschaften von Klassen erforderlich. Überprüfen Sie also zunächst den Kontexttyp des Aufrufs. Wenn es sich um einen anderen Typ als BaseClass handelt, ist der Typ weder BaseClass selbst noch eine seiner Ableitungen. Wir verbieten solche Anrufe.

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

    Wie kann das passieren? Zum Beispiel so:

    var b = new BaseClass(); 
    var someObject = {};
    b.protectedMethod.call(someObject); // В этом случае, внутри protectedMethod this будет равен someObject и мы это отловим, т.к. someObject instanceof BaseClass будет ложным
    

    Bei abgeleiteten Klassen ist der Ausdruck dieser Instanz von BaseClass wahr. Für BaseClass-Instanzen ist diese Instanz des BaseClass-Ausdrucks jedoch wahr. Daher überprüfen wir den Konstruktor, um Instanzen der BaseClass-Klasse von Instanzen abgeleiteter Klassen zu unterscheiden. Wenn der Konstruktor mit BaseClass übereinstimmt, wird unsere protectedMethod in der BaseClass-Instanz wie eine reguläre öffentliche Methode aufgerufen:

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

    Wir verbieten solche Anrufe:

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

    Als nächstes folgt der Aufruf der closed-Methode protectedMethod, die eigentlich die Methode ist, die wir schützen. Wenn Sie innerhalb der Methode auf die Member der BaseClass-Klasse verweisen müssen, können Sie dies mit der gespeicherten Instanz von _self tun. Dies ist genau das, was _self erstellt wurde, um Zugriff auf Klassenmitglieder aus allen privaten / privaten Methoden zu haben. Wenn Sie in Ihrer geschützten Methode oder Eigenschaft nicht auf Klassenmitglieder zugreifen müssen, können Sie die Variable _self daher nicht erstellen.

    Aufrufen einer geschützten Methode innerhalb der BaseClass-Klasse


    Auf protectedMethod in der BaseClass-Klasse darf nur über den Namen zugegriffen werden, nicht über diesen. Andernfalls können wir in protectedMethod nicht unterscheiden, ob wir als öffentliche Methode oder innerhalb einer Klasse aufgerufen wurden. In diesem Fall erspart uns der Abschluss - protectedMethod verhält sich wie eine reguläre private Methode, die innerhalb der Klasse geschlossen und nur im Rahmen der BaseClass-Funktion sichtbar ist.

    DerivedClass Beschreibung der abgeleiteten Klasse


    Schauen wir uns nun eine abgeleitete Klasse an und wie Sie sie für eine geschützte Methode einer Basisklasse zugänglich machen.

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

    Beschreibung des abgeleiteten Klassenkonstruktors


    In der abgeleiteten Klasse erstellen wir ein _base-Objekt, in das wir einen Verweis auf die protectedMethod-Methode der Basisklasse einfügen, die durch die Standardbindungsmethode für den Kontext der abgeleiteten Klasse geschlossen wird. Dies bedeutet, dass der Aufruf von _base.protectedMethod (); In protectedMethod ist dies kein _base-Objekt, sondern eine Instanz der DerivedClass-Klasse.

    ProtectedMethod-Methode Beschreibung Inside DerivedClass


    In der DerivedClass-Klasse müssen Sie die public-Methode protectedMethod genau wie in der Basisklasse über Object.defineProperty deklarieren und den Zugriff darauf überprüfen, indem Sie die checkAccess-Methode aufrufen oder direkt in der Methode einchecken:

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

    Wir prüfen: " Wurden wir aber als einfache öffentliche Methode aufgerufen?" Für Instanzen der DerivedClass-Klasse ist der Konstruktor gleich DerivedClass. Wenn ja, dann generieren Sie einen Fehler. Andernfalls senden wir es an die Basisklasse und es führt bereits alle anderen Überprüfungen durch.

    In der abgeleiteten Klasse haben wir also zwei Funktionen. Eine wird über Object.defineProperty deklariert und für von DerivedClass abgeleitete Klassen benötigt. Es ist öffentlich und daher mit einem Scheck versehen, der öffentliche Anrufe verbietet. Die zweite Methode befindet sich im _base-Objekt, das innerhalb der DerivedClass-Klasse geschlossen ist und daher für niemanden von außen sichtbar ist. Sie wird verwendet, um von allen DerivedClass-Methoden aus auf die geschützte Methode zuzugreifen.

    Eigentumsschutz


    Bei Eigenschaften läuft die Arbeit etwas anders ab. Eigenschaften in BaseClass werden wie gewohnt über Object.defineProperty definiert, nur bei Gettern und Setters müssen Sie zuerst unseren Check hinzufügen, d. H. checkAccess aufrufen:

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

    Innerhalb der BaseClass-Klasse wird auf die protected-Eigenschaft nicht über diese zugegriffen, sondern über die geschlossene Variable _protectedProperty. Wenn es für uns wichtig ist, dass der Getter und der Setter funktionieren, wenn die Eigenschaft in der BaseClass-Klasse verwendet wird, müssen private Methoden getProtectedPropety und setProtectedProperty erstellt werden, in denen keine Prüfungen stattfinden und die bereits aufgerufen werden sollten.

    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 abgeleiteten Klassen ist die Arbeit mit Eigenschaften etwas komplizierter, weil Eigenschaft kann nicht durch Kontext ersetzt werden. Daher verwenden wir die Standardmethode Object.getOwnPropertyDescriptor, um den Getter und den Setter aus der Eigenschaft der Basisklasse als Funktionen abzurufen, die bereits zum Ändern des Aufrufkontexts verwendet werden können:

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

    Vererbungsbeschreibung


    Und das Letzte, was ich kommentieren möchte, ist die Vererbung von DerivedClass von BaseClass. Wie Sie vielleicht wissen, ist DerivedClass.prototype = new BaseClass (); Erstellt nicht nur einen Prototyp, sondern schreibt auch seine Konstruktoreigenschaft neu. Aus diesem Grund wird die Konstruktoreigenschaft bei jeder Instanz von DerivedClass gleich BaseClass. Um dies zu beheben, schreiben Sie in der Regel nach dem Erstellen eines Prototyps die Konstruktoreigenschaft neu:

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

    Damit jedoch niemand diese Eigenschaft überschreibt, verwenden wir dieselbe Object.defineProperty. Die konfigurierbare Eigenschaft false verhindert, dass die Eigenschaft erneut überschrieben wird:

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

    Jetzt auch beliebt: