Entendendo POO em JavaScript
Ivan Trindade
Posted on January 17, 2023
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:
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."
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
}
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."
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-chaveextends
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."
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;
}
}
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');
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');
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);
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."
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."
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"]}
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.
Posted on January 17, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.