Métodos protegidos no JavaScript ES5

Muitos ótimos artigos foram escritos sobre o modelo de objeto em JavaScript. E as várias maneiras de criar alunos particulares na Internet estão cheias de descrições dignas. Mas sobre métodos protegidos - há muito poucos dados. Gostaria de preencher essa lacuna e dizer como você pode criar métodos protegidos sem bibliotecas no JavaScript ECMAScript 5 puro.

Neste artigo:


Link para o repositório git-hub com código-fonte e testes.

Por que os membros da classe protegidos são necessários


Em suma, então

  • é mais fácil entender como a classe funciona e encontrar erros nela. (Você pode ver imediatamente em que caso os membros da classe são usados. Se privado, você precisará analisar apenas esta classe, bem e, se estiver protegido, somente esta e as classes derivadas.)
  • mais fácil de gerenciar mudanças. (Por exemplo, você pode remover membros privados sem medo de que algo fora da classe editável ocorra.)
  • o número de aplicativos no rastreador de erros é reduzido, porque os usuários da biblioteca ou controle podem "costurar" nossos membros "particulares", que decidimos remover na nova versão da classe ou alterar a lógica de seu trabalho.
  • E, em geral, os alunos protegidos são uma ferramenta de design. É bom tê-lo à mão e bem testado.

Deixe-me lembrá-lo de que a principal idéia dos membros protegidos é ocultar métodos e propriedades dos usuários da instância da classe, mas ao mesmo tempo permitir que as classes derivadas tenham acesso a eles.

O uso do TypeScript não permitirá a chamada de métodos protegidos; no entanto, após a compilação em JavaScript, todos os membros privados e protegidos se tornam públicos. Por exemplo, desenvolvemos um controle ou biblioteca que os usuários instalarão em seus sites ou aplicativos. Esses usuários poderão fazer o que quiserem com membros protegidos, violando a integridade da classe. Como resultado, nosso rastreador de erros está repleto de reclamações de que nossa biblioteca ou controle não está funcionando corretamente. Nós gastamos tempo e esforço para resolvê-lo - "é assim que o objeto estava naquele estado do cliente, o que levou a um erro?!" . Portanto, para facilitar a vida de todos, é necessária uma proteção que não permita alterar o significado dos membros da classe privada e protegida.

O que é necessário para entender o método em questão


Para entender o método de declarar membros da classe protegidos, você precisa de um forte conhecimento:

  • classes de dispositivo e objetos em JavaScript.
  • maneiras de criar membros da classe privada (pelo menos até o fechamento).
  • métodos Object.defineProperty e Object.getOwnPropertyDescriptor

Sobre o modelo de dispositivo em JavaScript, posso recomendar, por exemplo, um excelente artigo de Andrey Akinshin ( DreamWalker ) “Entendendo o POO em JS [Parte 1]” .
Sobre propriedades privadas, há uma descrição boa e, na minha opinião, bastante completa de até quatro maneiras diferentes de criar membros de classe privada no site da MDN.

Quanto ao método Object.defineProperty, ele permitirá ocultar propriedades e métodos dos loops for-in e, como resultado, dos algoritmos de serialização:

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 } 

Essa ocultação deve ser realizada, mas isso, é claro, não é suficiente porque Ainda existe a possibilidade de chamar o método / propriedade diretamente:

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

Classe Auxiliar ProtectedError


Primeiro, precisamos da classe ProtectedError, que herda de Error, e que será lançada se não houver acesso ao método ou propriedade protegida.

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

Implementando membros de classe protegidos no ES5


Agora que temos a classe ProtectedError e entendemos o que Object.defineProperty faz com o valor enumerável: false, vamos analisar a criação de uma classe base que deseja compartilhar o método protectedMethod com todas as suas classes derivadas, mas ocultá-lo de todos os outros:

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

Descrição do construtor da classe BaseClass


Talvez você fique confuso com a verificação:

  if (!(this instanceof BaseClass)) return new BaseClass(); 
Este teste é um "amador". Você pode removê-lo; ele não tem nada a ver com métodos protegidos. No entanto, eu pessoalmente o deixo no meu código, porque é necessário para os casos em que a instância da classe não é criada corretamente, ou seja, sem a palavra-chave new. Por exemplo, assim:

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

Nesses casos, faça o que desejar. Você pode, por exemplo, gerar um erro:

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

Ou você pode simplesmente instanciar corretamente, como feito em BaseClass.

Em seguida, salvamos a nova instância na variável _self (por que preciso explicar isso mais tarde).

Descrição de uma propriedade pública denominada protectedMethod


Entrando no método, chamamos a verificação de contexto na qual fomos chamados. É melhor fazer check-out em um método separado, por exemplo, checkAccess, porque a mesma verificação será necessária em todos os métodos e propriedades protegidos das classes. Portanto, antes de tudo, verifique o tipo de contexto da chamada para isso. Se este tiver um tipo diferente de BaseClass, o tipo não será nem a própria BaseClass nem nenhum de seus derivados. Proibimos tais chamadas.

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

Como isso pode acontecer? Por exemplo, assim:

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

No caso de classes derivadas, a expressão nesta instância da BaseClass será verdadeira. Mas para instâncias de BaseClass, essa expressão de instância de BaseClass será verdadeira. Portanto, para distinguir instâncias da classe BaseClass de instâncias de classes derivadas, verificamos o construtor. Se o construtor corresponder a BaseClass, o protectedMethod será chamado na instância BaseClass, assim como um método público comum:

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

Proibimos tais chamadas:

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

Em seguida, vem a chamada do método closed protectedMethod, que, de fato, é o método que protegemos. Dentro do método, se você precisar se referir aos membros da classe BaseClass, poderá fazer isso usando a instância armazenada de _self. Foi exatamente isso que _self foi criado para ter acesso aos membros da classe de todos os métodos particulares / particulares. Portanto, se você não precisar acessar membros da classe em seu método ou propriedade protegida, não poderá criar a variável _self.

Chamando um método protegido dentro da classe BaseClass


Dentro da classe BaseClass, protectedMethod deve ser acessado apenas por nome, não por isso. Caso contrário, no protectedMethod, não podemos distinguir se fomos chamados como método público ou dentro de uma classe. Nesse caso, o fechamento nos salva - protectedMethod se comporta como um método privado comum, fechado dentro da classe e visível apenas dentro do escopo da função BaseClass.

Classe derivada DerivedClass Descrição


Agora vamos ver uma classe derivada e como torná-la acessível a um método protegido de uma classe base.

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

Descrição do construtor da classe derivada


Na classe derivada, criamos um objeto _base no qual colocamos uma referência ao método protectedMethod da classe base, fechado ao contexto da classe derivada através do método de ligação padrão. Isso significa que chamar _base.protectedMethod (); inside protectedMethod, este não é um objeto _base, mas uma instância da classe DerivedClass.

ProtectedMethod Descrição do método Inside DerivedClass


Na classe DerivedClass, é necessário declarar o método público protectedMethod da mesma maneira que fizemos na classe base via Object.defineProperty e verificar o acesso chamando o método checkAccess ou verificando diretamente no método:

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

Verificamos - "mas fomos chamados como um método público simples?" Para instâncias da classe DerivedClass, o construtor será igual a DerivedClass. Nesse caso, gere um erro. Caso contrário, enviamos para a classe base e ele já fará todas as outras verificações.

Portanto, na classe derivada, temos duas funções. Um é declarado via Object.defineProperty e é necessário para as classes derivadas de DerivedClass. É público e, portanto, possui um cheque que proíbe chamadas públicas. O segundo método está localizado no objeto _base, que é fechado dentro da classe DerivedClass e, portanto, não é visível para ninguém de fora, e é usado para acessar o método protegido de todos os métodos DerivedClass.

Proteção de Propriedade


Com propriedades, o trabalho acontece um pouco diferente. As propriedades no BaseClass são definidas como de costume no Object.defineProperty, somente nos getters e setters, você primeiro precisa adicionar nossa verificação, por exemplo chamada 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 da classe BaseClass, a propriedade protegida é acessada não por meio disso, mas à variável fechada _protectedProperty. Se é importante para nós que o getter e o setter funcionem ao usar a propriedade dentro da classe BaseClass, precisamos criar métodos privados getProtectedPropety e setProtectedProperty, nos quais não haverá verificações e eles já devem ser chamados.

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

Nas classes derivadas, trabalhar com propriedades é um pouco mais complicado, porque A propriedade não pode ser substituída pelo contexto. Portanto, usaremos o método Object.getOwnPropertyDescriptor padrão para obter o getter e o setter da propriedade da classe base como funções que já podem alterar o contexto da chamada:

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

Descrição da herança


E a última coisa que gostaria de comentar é a herança do DerivedClass do BaseClass. Como você deve saber, DerivedClass.prototype = new BaseClass (); não apenas cria um protótipo, mas também reescreve sua propriedade construtora. Por esse motivo, para cada instância de DerivedClass, a propriedade construtor se torna igual a BaseClass. Para corrigir isso, geralmente depois de criar um protótipo, reescreva a propriedade construtor:

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

No entanto, para que ninguém reescreva essa propriedade depois de nós, usamos o mesmo Object.defineProperty. A propriedade configurable: false impede que a propriedade seja substituída novamente:

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

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


All Articles