Entendendo POO em JavaScript

trinity_

Ivan Trindade

Posted on January 17, 2023

Entendendo POO em JavaScript

Objetos estão em toda parte em JavaScript. Funções são objetos, arrays são objetos. Inferno, até as classes são objetos, bem, não exatamente. Classes são funções, mas, novamente, funções são objetos.

Bem-vindo á gerança prototípica:

JavaScript não funciona da maneira tradicional de usar classes como plantas para criar objetos, mas sim objetos como plantas. Basta dizer que isso feriu os sentimentos de muitos. Mas como isso funciona?

Todo objeto em JavaScript tem uma cadeia de Prototype anexada, que é um conjunto de objetos vinculados á propriedade oculta interna [[Prototype]]. Toda vez que uma pesquisa de propriedade ou método é feita, toda a cadeia de Prototype é vasculhada de cima para baixo, para encontrar o valor que é essencialmente a Herança Prototypal.

Essa cadeia de protótipos pode ser acessada usando a propriedade __proto__, que é um getter para a propriedade oculta [[Prototype]]. O __proto__ geralmente não é usado, pois existe apenas por motivos legados. Se você deseja acessar o protótipo em seu código, a melhor alternativa é Object.getPrototypeOf.

Cada novo objeto criado possui Object.prototype em sua cadeia protótipo, e cada array possui Array.prototype em sua cadeia protótipo, que permite herdar métodos como map(), filter() etc:

Gráfico de proto

Também podemos adicionar a esta cadeia usando o método Object.create(), que nos permite criar um novo objeto com seu
conjunto [[Prototype]] como o objeto passado para o método no primeiro @parameter:

const person={

run(){ return `${this.name} is running.`},

sleep(){return `${this.name} is sleeping.`}

}

let woman=Object.assign( Object.create(person) , {name:'She'} ); /* @returns {
                                                                            name: "She"
                                                                          __proto__: Object <person>
                                                                          }
                                                               */

let man=Object.assign( Object.create(person) , {name:'He'} ); /* @returns {
                                                                            name: "He"
                                                                          __proto__: Object <person>
                                                                          }
                                                               */
woman.run(); // @returns "She is running."
man.sleep(); // @returns "He is sleeping."
Enter fullscreen mode Exit fullscreen mode

Embora Object.create() seja muito legal, seria mais legal se uma função pudesse fazer isso, dando-nos mais flexibilidade na definição do objeto que está sendo criado e é aí que as funções do construtor entram em cena.

Toda função definida em JavaScript com a palavra-chave function, possui uma propriedade chamada prototype. Esta propriedade refere-se ao objeto container que contém a função definida como sua constructor:

function(){}).prototype
@returns {
  constructor: ƒ () //function on which prototype property was accessed.
  __proto__: Object
}
Enter fullscreen mode Exit fullscreen mode

Essas funções podem ser chamadas com uma palavra-chave new que faz o seguinte:

  • Um novo objeto é criado com seu [[Prototype]] atribuído ao objeto protótipo da função do construtor.
  • A palavra-chave this dentro da função, aponta para o novo objeto que está sendo criado.
  • O objeto criado é retornado automaticamente pela função, embora possa ser substituído por um arquivo return.

O exemplo acima pode ser recriado assim:


const Person=function(name){
  this.name=name;
}

Person.prototype.run=function (){ return `${this.name} is running.`};

Person.prototype.sleep=function (){return `${this.name} is sleeping.`};

let woman=new Person('She'); 
let man=new Person('He');

woman.run(); // @returns "She is running."
man.sleep(); // @returns "He is sleeping."
Enter fullscreen mode Exit fullscreen mode

Classes em JavaScript:

Uma classe é uma versão muito mais padronizada do construtor Functions. Ele oferece recursos adicionais como:

  • Ela só pode ser chamada com uma palavra-chave new, caso contrário, gera um erro.
  • constructor e outros métodos podem ser adicionados diretamente por meio de declarações de classe constructor.prototype.
  • Métodos e propriedades estáticos podem ser adicionados à palavra-chave constructor usando static. Podemos usar a palavra-chave extends para estender a cadeia de protótipos.
  • Atualizações recentes também permitem que propriedades/métodos privados sejam adicionados.
  • Ela é executada em modo estrito.

O exemplo acima pode ser escrito como uma classe como essa:

class Person{

constructor(name){
 this.name=name;
};

run(){ return `${this.name} is running.`};

sleep(){return `${this.name} is sleeping.`};

}

let woman = new Person('She'); 
let man = new Person('He');

woman.run(); // @returns "She is running."
man.sleep(); // @returns "He is sleeping."
Enter fullscreen mode Exit fullscreen mode

Métodos e propriedades estáticos:

Métodos e propriedades static são realmente adicionados à própria função do construtor, pois a função é um objeto e os objetos podem ter suas próprias propriedades.

Dessa forma, esses métodos e propriedades só podem ser acessados ​​por meio da função construtora e não podem ter acesso direto aos métodos e propriedades da instância:

/** Constructor Functions **/
function Person(name){
  this.name=name;
}
Person.isNamed=function(instance){return !!instance.name}

var you=new Person(); /** @returns 
                          Person { name: undefined
                                 __proto__: constructor: ƒ Person{
                                                                    isNamed: ƒ (instance)
                                                                }  
                                  } 
                       **/                         
Person.isNamed(you); //@returns false;

/** class Constructor **/
class Person{

constructor(name){
  this.name=name;
  }
static isNamed(instance){
      return !!instance.name;
  }
}
Enter fullscreen mode Exit fullscreen mode

Estendendo uma classe:

Uma extensão de classe é basicamente estender a cadeia de protótipos. Sem usar uma classe, isso pode ser feito por:

  • Criando uma função construtora e modificando seu objeto prototype para herdar do pai constructor.prototype para criar a cadeia de protótipos.
  • Chamando a função do construtor pai dentro da nova função do construtor com a referência this certa para que a nova instância também seja instanciada pelo construtor pai.
  • Adicionando a função de construtor pai à cadeia de protótipo da nova função de construtor para garantir que todas as propriedades/métodos estáticos sejam herdados. Isso também garante a referência correta de super como veremos.
function Life(medium){
    this.medium=medium;
}
Life.prototype.eat=function(){return `get food from ${this.medium}`};


function Human(identity,medium){
     Object.setPrototypeOf(Human,Life);  
     Life.call(this,medium);
     this.identity=identity;  
}

Human.prototype=Object.assign(Object.create(Life.prototype),{constructor:Human});

var developer=new Human('coder','soil');
Enter fullscreen mode Exit fullscreen mode

Isso na classe se tornaria:

class Life{
  constructor(medium){
    this.medium=medium;
   }

  eat(){return `get food from ${this.medium}`};
}

class Human extends Life{
  constructor(identity,medium){
    super(medium);
    this.identity=identity;
    }
}


var developer=new Human('coder','soil');
Enter fullscreen mode Exit fullscreen mode

super()

A palavra-chave super é uma palavra-chave especial que só pode ser usada dentro de declarações de classe e literais de objeto. A razão é que ele depende de uma propriedade interna [[HomeObject]], que aponta para o objeto pai de um método durante a declaração do método.

O [[HomeObject]] pode nem sempre ser o mesmo que this está apotando em um método, como chamar o método de um objeto diferente ou usar function.bind(). Assim, o uso de tais métodos como o super, os torna dependentes do objeto pai.

A palavra-chave super, portanto, refere-se ao [[Prototype]] de [[HomeObject]] em um método:

const one={
    who(){return 'I am the one'}
}

const two={

    getRoots(){
      return super.who();
    }

}

Object.setPrototypeOf(two,one);
Enter fullscreen mode Exit fullscreen mode

Aqui, o método getRoots() tem seu [[HomeObject]] apontamento para two e o super refere-se ao [[Prototype]] qual two é definido como one.

O construtor super interno chamado when refere-se ao construtor pai, no entanto, a palavra-chave super também pode ser usada para acessar métodos na cadeia de protótipos.

Mas aqui está o problema, em uma classe, se super for chamado a partir de métodos estáticos, o [[HomeObject]] aponta para a função construtora e, portanto, super aponta para a função construtora pai conforme ela é definida na cadeia de protótipos por classe. No entanto, para métodos normais, o [[HomeObject]] aponta para constructor.prototypee, portanto, super refere-se ao constructor.prototype pai:

class Home{

sleep(){ console.log(`${this.name} is sleeping.`) }

resting(){console.log(`${this.name} is resting`)}

static atHome(){console.log('I am at Home.')}

}

class HomeWork extends Home{

constructor(name){
  super();
  this.name=name;
  super.resting();
}
sleep(){
    super.sleep();
    console.log('no work.')
}

static workingAt(){super.atHome();}

}

var lad=new HomeWork('lad'); //@logs "lad is resting"
lad.sleep(); //@logs "lad is sleeping." & "no work."
HomeWork.workingAt(); //@logs "I am at Home."
Enter fullscreen mode Exit fullscreen mode

O importante a observar aqui é que super.atHome() não pode ser acessado pelo método sleep() e super.sleep() não pode ser acessado pelo método workingAt().

Propriedades privadas

O encapsulamento tem sido um problema de longa data em JavaScript, pois não há campos privados ou protegidos. As soluções alternativas geralmente têm sido com variáveis ​​sublinhadas, símbolos ou fechamentos poderosos.

As atualizações recentes, no entanto, têm a proposta de avançar para campos de classe privada que podem ser usados ​​assim:

class MI{

#secretcode=123;

getAccess(value){
  if(value===this.#secretcode)return 'granted access.';
  else return 'access denied.'
}

}

let agent=new MI();
agent.#secretcode=0; //@throws Uncaught SyntaxError: Private field '#secretcode' must be declared in an enclosing class.
agent.getAccess(0); //@returns "access denied."
Enter fullscreen mode Exit fullscreen mode

No entanto, se os encerramentos tiverem que ser usados ​​para obter o encapsulamento, a única maneira de fazer isso é:

const Matrix=(id)=>{

    const STATE={
        human_id:id,
        current:'IDLING',
        loaders:[]
    }

    const drive=()=>{
            STATE.loaders.push('driving');
            STATE.current='DRIVING';
    }

    const work=()=>{
           STATE.loaders.push('working'); 
           STATE.current='WORKING' 
    }

    const getActivity=()=>(STATE);

    return {work, drive, getActivity}

}

const getHumanConstructor=()=>{

  let id=0;  
  let matrix={};  

  return class Human{

      constructor(name){
          this.name=name; 
          Object.defineProperty(this,'id',{
                value:++id,
                writable:false,
                configurable:false,
                enumerable:false,    
            })   
          matrix[this.id]=Matrix(this.id);
          } 
      drive(){
          matrix[this.id].drive();
          console.log(`${this.name} is driving.`)
          }
      work(){
          matrix[this.id].work();
          console.log(`${this.name} is working.`)
          }

      __checkActivity(){return matrix[this.id].getActivity()}

  }

}

const Human=getHumanConstructor();

var Neo=new Human('Neo');
Neo.drive(); //@logs "Neo is driving."
Neo.work(); //@logs "Neo is working."
Neo.__checkActivity(); //@returns {human_id: 1, current: "WORKING", loaders: ["driving", "working"]}

var Trinity=new Human('Trinity');
Trinity.drive(); //@logs "Trinity is driving."
Trinity.__checkActivity(); //@returns {human_id: 2, current: "DRIVING", loaders: ["driving"]}

Enter fullscreen mode Exit fullscreen mode

Conclusão:

POO é um poderoso paradigma de programação que pode ser usado para criar sistemas independentes e a linguagem nos fornece ferramentas suficientes para implementar com sucesso tais sistemas. Tudo o que pude fazer foi dar uma pequena olhada no mesmo. Espero que você tenha gostado. Obrigado por ler.

💖 💪 🙅 🚩
trinity_
Ivan Trindade

Posted on January 17, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related