Se han escrito muchos artículos excelentes sobre el modelo de objetos en JavaScript. Y sobre las diversas formas de crear miembros de clase privada en Internet está lleno de descripciones valiosas. Pero sobre los métodos protegidos, hay muy pocos datos. Me gustaría llenar este vacío y decirle cómo puede crear métodos protegidos sin bibliotecas en JavaScript puro ECMAScript 5.
En este articulo:
Enlace al repositorio de git-hub con código fuente y pruebas. ¿Por qué se necesitan miembros de la clase protegida?
En resumen, entonces
- es más fácil entender el funcionamiento de la clase y encontrar errores en ella. (Puede ver inmediatamente en qué caso se usan los miembros de la clase. Si es privado, entonces necesita analizar solo esta clase, bueno, y si está protegido, entonces solo esto y las clases derivadas).
- Cambio más fácil de gestionar. (Por ejemplo, puede eliminar miembros privados sin temor a que algo fuera de la clase editable se rompa).
- se reduce la cantidad de aplicaciones en el rastreador de errores, porque los usuarios de la biblioteca o el control pueden "coser" en nuestros miembros "privados", que decidimos eliminar en la nueva versión de la clase, o cambiar la lógica de su trabajo.
- Y, en general, los miembros de la clase protegida son una herramienta de diseño. Es bueno tenerlo a mano y bien probado.
Permítame recordarle que la idea principal de los miembros protegidos es ocultar métodos y propiedades de los usuarios de la instancia de clase, pero al mismo tiempo permitir que las clases derivadas tengan acceso a ellos.
El uso de TypeScript no permitirá llamar a métodos protegidos, sin embargo, después de la compilación en JavaScript, todos los miembros privados y protegidos se hacen públicos. Por ejemplo, desarrollamos un control o biblioteca que los usuarios instalarán en sus sitios o aplicaciones. Estos usuarios podrán hacer lo que quieran con miembros protegidos, violando la integridad de la clase. Como resultado, nuestro rastreador de errores está lleno de quejas de que nuestra biblioteca o control no funciona correctamente. Dedicamos tiempo y esfuerzo a resolverlo:
"¿es así como estaba el objeto en ese estado del cliente, lo que provocó un error?" . Por lo tanto, para facilitar la vida de todos, se necesita tal protección que no permita cambiar el significado de los miembros de la clase privados y protegidos.
Lo que necesitas para entender el método en cuestión
Para comprender el método de declarar miembros de clase protegidos, necesita un conocimiento sólido:
- clases de dispositivo y objetos en JavaScript.
- formas de crear miembros de la clase privada (al menos a través del cierre).
- métodos Object.defineProperty y Object.getOwnPropertyDescriptor
Sobre el modelo de dispositivo en JavaScript, puedo recomendar, por ejemplo, un excelente artículo de Andrey Akinshin (
DreamWalker )
“Comprender la POO en JS [Parte No. 1]” .
En cuanto a las propiedades privadas, hay una buena y, en mi opinión, una descripción bastante completa de
hasta 4 formas diferentes de crear miembros de clases privadas en el sitio web de MDN.
En cuanto al método Object.defineProperty, nos permitirá ocultar propiedades y métodos de los bucles for-in y, como resultado, de los algoritmos de serialización:
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);
Tal ocultamiento debe realizarse, pero esto, por supuesto, no es suficiente porque Todavía existe la posibilidad de llamar al método / propiedad directamente:
console.log(obj1.protectedNumber);
Helper Class ProtectedError
Primero, necesitamos la clase ProtectedError, que hereda de Error, y que se generará si no hay acceso al método o propiedad protegidos.
function ProtectedError(){ this.message = "Encapsulation error, the object member you are trying to address is protected."; } ProtectedError.prototype = new Error(); ProtectedError.prototype.constructor = ProtectedError;
Implementación de miembros de clase protegidos en ES5
Ahora que tenemos la clase ProtectedError y entendemos lo que hace Object.defineProperty con el valor enumerable: falso, analicemos la creación de una clase base que quiera compartir el método protectedMethod con todas sus clases derivadas, pero ocultámoslo a todos los demás:
function BaseClass(){ if (!(this instanceof BaseClass)) return new BaseClass(); var _self = this;
Descripción del constructor de la clase BaseClass
Quizás te confundirá el cheque:
if (!(this instanceof BaseClass)) return new BaseClass();
Esta prueba es un "aficionado". Puede eliminarlo; no tiene nada que ver con los métodos protegidos. Sin embargo, personalmente lo dejo en mi código, porque es necesario para aquellos casos en que la instancia de clase no se crea correctamente, es decir sin la palabra clave new. Por ejemplo, así:
var obj1 = BaseClass();
En tales casos, haga lo que desee. Puede, por ejemplo, generar un error:
if (!(this instanceof BaseClass)) throw new Error('Wrong instance creation. Maybe operator "new" was forgotten');
O simplemente puede crear instancias correctamente, como se hace en BaseClass.
A continuación, guardamos la nueva instancia en la variable _self (por qué necesito explicar esto más adelante).
Descripción de una propiedad pública llamada protectedMethod
Al ingresar el método, llamamos a la verificación de contexto en la que fuimos llamados. Es mejor verificar en un método separado, por ejemplo, checkAccess, porque se necesitará la misma verificación en todos los métodos y propiedades protegidos de las clases. Entonces, antes que nada, verifique el tipo de contexto de la llamada a esto. Si este tiene un tipo distinto de BaseClass, entonces el tipo no es BaseClass ni ninguno de sus derivados. Prohibimos tales llamadas.
if(!(this instanceof BaseClass)) throw new ProtectedError();
¿Cómo puede pasar esto? Por ejemplo, así:
var b = new BaseClass(); var someObject = {}; b.protectedMethod.call(someObject);
En el caso de las clases derivadas, la expresión esta instancia de BaseClass será verdadera. Pero para las instancias de BaseClass, esta instancia de la expresión de BaseClass será verdadera. Por lo tanto, para distinguir instancias de la clase BaseClass de instancias de clases derivadas, verificamos el constructor. Si el constructor coincide con BaseClass, se llama a nuestro método protegido en la instancia de BaseClass, al igual que un método público normal:
var b = new BaseClass(); b.protectedMethod();
Prohibimos tales llamadas:
if(this.constructor === BaseClass) throw new ProtectedError();
Luego viene la llamada del método cerrado protectedMethod, que, de hecho, es el método que protegemos. Dentro del método, si necesita referirse a los miembros de la clase BaseClass, puede hacerlo utilizando la instancia almacenada de _self. Esto es exactamente lo que _self fue creado para tener acceso a los miembros de la clase desde todos los métodos privados / privados. Por lo tanto, si no necesita acceder a los miembros de la clase en su método o propiedad protegidos, no puede crear la variable _self.
Llamar a un método protegido dentro de la clase BaseClass
Dentro de la clase BaseClass, se debe acceder a protectedMethod solo por nombre, no a través de esto. De lo contrario, dentro de protectedMethod no podemos distinguir si fuimos llamados como método público o desde dentro de una clase. En este caso, el cierre nos salva: protectedMethod se comporta como un método privado normal, cerrado dentro de la clase y visible solo dentro del alcance de la función BaseClass.
DerivedClass Derived Class Descripción
Ahora veamos una clase derivada y cómo hacerla accesible para un método protegido de una clase base.
function DerivedClass(){ var _base = { protectedMethod: this.protectedMethod.bind(this) }; function checkAccess() { if (this.constructor === DerivedClass) throw new ProtectedError(); }
Descripción del constructor de clase derivada
En la clase derivada, creamos un objeto _base en el que colocamos una referencia al método protectedMethod de la clase base, cerrado al contexto de la clase derivada a través del método de enlace estándar. Esto significa que llamar a _base.protectedMethod (); dentro de protectedMethod, este no es un objeto _base, sino una instancia de la clase DerivedClass.
Método protegido Método Descripción Dentro de DerivedClass
En la clase DerivedClass, es necesario declarar el método público protectedMethod de la misma manera que lo hicimos en la clase base a través de Object.defineProperty y verificar el acceso llamando al método checkAccess o ingresando directamente en el método:
Object.defineProperty(DerivedClass.prototype, 'protectedMethod', { enumerable: false, configurable: false, value: function(){ if(this.constructor === DerivedClass) throw new ProtectedError() return _base.protectedMethod(); } });
Verificamos:
"¿pero nos han llamado como un método público simple?" Para instancias de la clase DerivedClass, el constructor será igual a DerivedClass. Si es así, entonces genera un error. De lo contrario, lo enviamos a la clase base y ya realizará todas las demás comprobaciones.
Entonces, en la clase derivada, tenemos dos funciones. Uno se declara a través de Object.defineProperty y es necesario para las clases derivadas de DerivedClass. Es público y, por lo tanto, tiene un cheque que prohíbe las llamadas públicas. El segundo método se encuentra en el objeto _base, que está cerrado dentro de la clase DerivedClass y, por lo tanto, no es visible para nadie desde el exterior y se utiliza para acceder al método protegido desde todos los métodos DerivedClass.
Protección de la propiedad
Con las propiedades, el trabajo ocurre un poco diferente. Las propiedades en BaseClass se definen como de costumbre a través de Object.defineProperty, solo en getters y setters primero debe agregar nuestro cheque, es decir llamada 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 }); }
Dentro de la clase BaseClass, se accede a la propiedad protegida no a través de esto, sino a la variable cerrada _protectedProperty. Si es importante para nosotros que el getter y el setter funcionen al usar la propiedad dentro de la clase BaseClass, entonces debemos crear métodos privados getProtectedPropety y setProtectedProperty, dentro de los cuales no habrá comprobaciones, y ya deberían llamarse.
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(){
En las clases derivadas, trabajar con propiedades es un poco más complicado porque la propiedad no se puede reemplazar por contexto. Por lo tanto, usaremos el método estándar Object.getOwnPropertyDescriptor para obtener el captador y definidor de la propiedad de la clase base como funciones que ya pueden usarse para cambiar el contexto de la llamada:
function DerivedClass(){ function checkAccess(){ ... } var _base = { protectedMethod: _self.protectedMethod.bind(_self), }; var _baseProtectedPropertyDescriptor = Object.getOwnPropertyDescriptor(_self, 'protectedProperty');
Descripción de la herencia
Y lo último que me gustaría comentar es la herencia de DerivedClass de BaseClass. Como ya sabrás, DerivedClass.prototype = new BaseClass (); no solo crea un prototipo, sino que también reescribe su propiedad de constructor. Debido a esto, para cada instancia de DerivedClass, la propiedad del constructor se vuelve igual a BaseClass. Para solucionar esto, generalmente después de crear un prototipo, reescribe la propiedad del constructor:
DerivedClass.prototype = new BaseClass(); DerivedClass.prototype.constructor = DerivedClass;
Sin embargo, para que nadie reescriba esta propiedad después de nosotros, usamos el mismo Object.defineProperty. La propiedad configurable: false evita que la propiedad se anule nuevamente:
DerivedClass.prototype = new BaseClass(); Object.defineProperty(DerivedClass.prototype, 'constructor', { value : DerivedClass, configurable: false });