Skip to content

Instantly share code, notes, and snippets.

@lukkaslt
Created August 7, 2019 21:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lukkaslt/099d31f723b0fbe816825d632e4ae054 to your computer and use it in GitHub Desktop.
Save lukkaslt/099d31f723b0fbe816825d632e4ae054 to your computer and use it in GitHub Desktop.
Herança

Inheriting and Creating Mixins

Neste capítulo ver-se-á em profundidade a herança prototipada e suas diferenças com a herança clássica; e também os principais padrões usados para implementar: overriding, member protection e extension prevention. Tudo isso, além da herança múltipla e dos mixins, será discutido e implementado em ES6.

Abordar-se-á neste capítulo:

  • Prototypal inheritance
  • Overriding methods and properties
  • Implementing protected members
  • Controlling object extension
  • Multiple inheritance and mixins

Why inheritance?

A herança é um dos princípios fundamentais da Orientação a Objetos. Definem-na como sendo um relacionamento do tipo "is-a" [é-um(a)] entre objetos(ou classes, se a linguagem permitir). Consideremos, por exemplo, uma pessoa e um estudante genéricos. Pode-se dizer que o estudante é uma pessoa que herda todas as "características" (features) de uma pessoa genérica. Contudo, o estudante possui um "perfil especializado" (specialized profile | qualidade particular) - ele estuda. Vale também para cantor, professor, advogado, etc.

O "is-a" é uma relação, uma herança. Pode-se dizer, de certo modo, que se um objeto A herda de um objeto B, então A é B, ou seja, A é uma versão especializada de B. Às vezes, diz-se que A é derivado de B, enquanto B é o objeto base ou pai.

Em que é útil a herança na programação orientada a objetos? Como todo objeto derivado possui todos os recursos do objeto base, a herança ajuda a reduzir código redundante entre objetos semelhantes, os que compartilham recursos entre si. O objeto base contém os "recursos(características) base" (common features), que são compartilhados com os objetos derivados. Ademais, torna o código sustentável, já que se pode alterar um recurso apenas no objeto base e compartilhá-lo com todos os objetos derivados.

Objects and prototypes

Nós já vimos duas maneiras de criar objetos. A primeira e mais simples usa a notação literal:

let obra = {
  data: new Date(),
  autor: "Luís de Camões"
};

A segunda e mais flexível é a que usa uma função construtora:

function Obra(data, autor) {
  this.data = data;
  this.autor = autor.
}

let obra = new Obra( new Date(), "Luis de Camões" )

Não há diferença quanto aos resultados e, talvez, por isso tendemos a acreditar que criamos dois objetos do zero. Não é verdade. Nos dois casos criamos um objeto derivado de uma instância do construtor Object(). É um construtor que nos permite criar o objeto base de todos os objetos Javascript: o objeto vazio {}. Todo objeto criado literalmente ou via construtor herda todas as propriedades e métodos de uma instância do objeto vazio.

O método toString() nos ajuda a verificar:

let obra = {
  data: new Date(),
  autor: "Luís de Camões"
};

obra.toString();  // resultado: "[object Object]"

Não definimos o método toString(), contudo, a qualquer objeto criado via notação literal ou função construtora é anexado esse e mais alguns métodos herdados do objeto vazio. Além disso, haverá um construtor assossiado ao "objeto literal". Verifiquemos isso acessando a propriedade herdada pelo construtor:

obra.constructor.autor;   // resultado: "Object"

Também podemos verificar se nosso objeto é uma instância do "Object constructor" usando o operador instanceof:

console.log( obra instanceof Object );  // resultado: true

Até agora não sabemos como criar objetos do zero, apenas os que derivam de uma instância vazia do "Object() constructor".

Em JavaScript diz-se que o objeto vazio é o "prototype" dos seus demais objetos.

What is a prototype?

O "prototype" é um objeto que serve de modelo para outro objeto. Não há classes em JavaScript, logo, os recursos compartilhados entre objetos são tomados dos objetos referência. Em outras palavras, se precisarmos de um objeto que seja semelhante a um objeto existente A, criamos um novo objeto B dizendo que seu prototype é o objeto existente A. Esse é o mecanismo básico de herança da JavaScript (linguagem)'.

Todos os objetos JavaScript pussuem prototype, incluindo funções. O prototype de um objeto literal é o objeto vazio {}. Quando criamos um objeto via função construtora, o prototype do novo objeto é o prototype da função construtora.

Há duas maneiras de acessar o prototype de um objeto.

A primeira é usar a propriedade prototype do construtor. Por exemplo, podemos criar um objeto com o construtor de Obra() e colocar o prototype do objeto em uma variável da seguinte forma:

let protoOfObra = obra.constructor.prototype;

Vale também para o objeto literal, visto ter um método construtor, o Object().

A segunda é usar o método Object.getPrototypeOf():

let protoOfObra = Object.getPrototypeOf( obra );

Em ambos os casos obtemos o objeto modelo do objeto Obra.

Podemos verificar se um objeto é prototype de outro usando o método isPrototypeOf():

let l = Object.getPrototypeOf( obra );

console.log( l.isPrototypeOf( obra ) );  // resultado: true
console.log( obra.isPrototypeOf( l ) );  // resultado: false

Com esse exemplo podemos notar que a relação prototype entre objetos não é simétrica, ou seja, se o objeto A é prototype de B, então B não é prototype de A.

Creating objects

Ao discutirmos objetos e prototypes entendemos que criar objetos está intimamente ligado à herança. Duas são as maneiras de criar objetos: notação literal e método construtor, e, ambas criam-nos com um "prototype built-in" (default). Pergunto:

  • 1º. Podemos criar objetos sem prototype?
  • 2º. Podemos selecionar um prototype específico?

Sim, podemos. O Object.create() tem esse poder. Ele permite que se crie objetos de modo funcional, e ser funcional é ser flexível.

Podemos criar um objeto sem prototype da seguinte maneira:

let obra = Object.create( null )

Aí está. A variável obra contém um objeto sem prototype. Ele não herda os recursos do objeto vazio, ou seja, não possui o método toString() e os demais que vimos anteriormente. Obteremos o seguinte ao verificar o prototype:

console.log( Object.getPrototypeOf( obra ) );   //resultado: null

O prototype do objeto é null. É o valor que demos ao método Object.create(). Para criarmos um objeto cujo prototype seja outro objeto, especificamo-lo como argumento do método. Vejamos:

let obra = {
  data: new Date(),
  autor: "Luís de Camões"
}

let lusiadas = Object.create( livro )

Agora, lusiadas tem obra como prototype - Os Lusíadas é uma obra.

O método Object.create() permite-nos especificar novas propriedades para o novo objeto. Passamo-las como segundo argumento (opcional) do método. Vejamos:

let obra = {
  data: new Date(),
  autor: ""
};

let livro = Object.create(obra, {
  nome: { writable: true, configurable: true }
})

Adicionamos a propriedade nome ao objeto livro via descritor de propriedades[1]. Com isso, o objeto livro herda as propriedades do prototype e a nova propiedade definida no momento da criação.

Podemos associar um prototype a um objeto na criação - via Object.create(), e também após criarmos o objeto. E o método que se usa para tal é o Object.setPrototypeOf(). Vejamos:

let obra = { 
  data: new Date(),
  autor: "Luís de Camões"
}
let lusiadas = {
  nome: "Os Lusíadas"
}

Object.setPrototypeOf( lusiadas, obra )

Agora, o objeto obra é prototype do objeto lusiadas, logo, lusiadas herda os membros data e autor de obra:

console.log( lusiadas.data )   // resultado: "2019-07-05T16:08:19.392Z"
console.log( lusiadas.autor )  // resultado: "Luís de Camões"

Acabamos de combinar objetos após a criação dos mesmos. Na verdade, o método Object.setPrototypeOf() permite-nos alterar o prototype de um objeto, modificar os seus recursos (comportamento em OO), em tempo de execução (run-time). Em nosso caso, o prototype inicial de ambos os objetos, obra e lusiadas, era o do objeto vazio {}. Mas, ao executarmos o método Object.setPrototypeOf() o prototype de lusiadas passa a ser outro.

Apesar de o método Object.setPrototypeOf() ser interessante, deve-se evitá-lo, pois o seu modo dinâmico e em tempo de execução de trabalhar com o prototype produz impactos negativos no desempenho da aplicação. O Object.create(), ao contrário, permite que o mecanismo JavaScript analize e otimize o seu código de modo estático.

Prototype chaining

Ao criarmos um objeto podemos especificar o seu prototype graças ao método Object.create() e, como bem sabemos, o objeto herdará os recursos do prototype. O prototype de um objeto criado da maneira "tradicional", por sua vez, possui seu próprio prototype. Observemos o código a seguir:

let obra = {
  data: new Date(),
  autor: "Luís de Camões"
}

let lusiadas = Object.create(obra, {
  nome: { writable: true, configurable: true }
});

Ao usar o objeto lusiadas podemos acessar a propriedade nome, adicionada no momento em que criamos o objeto, bem como data e autor, herdadas do seu prototype, o objeto obra. Contudo, o objeto obra possui o seu próprio prototype, pois quando se cria um objeto usando a notação literal, repito, o seu prototype é o objeto vazio {}. Sendo assim, o objeto obra herda os membros - por exemplo, o método toString() - do objeto "default". Logo, o objeto lusiadas possui tanto o prototype de obra quanto o do objeto vazio {}. Vejamos:

console.log( lusiadas.toString() );   // "[object Object]"

Conclui-se que a herança é um relacionamento entre vários objetos e não apenas entre dois. Um objeto herda todos os membros de seu prototype e os do prototype do prototype, e por aí vai. Esse encadeamento de prototypes é comumente chamado de prototype chain.

Que realmente significa ser membro (herdeiro|herdado) do prototype?

O mecanismo de herança é extremamente simples. Ao se tentar acessar um membro qualquer, o sistema, a princípio, procura-o no objeto mesmo [em si]. Se não o encontra, [o sistema] procura-o no prototype do objeto. E se novamente não o encontra, percorre-se a cadeia de prototypes até que, ou o membro ou o valor null seja encontrado. No primeiro caso: retorna-se o membro; no segundo: retorna-se undefined. Veja a representação gráfica do percurso que o sistema faz na cadeia de prototypes em busca do método toString():

+----------------------------------------------------------------------------+
|              +----+                                                        |
|     +--------| {} |--------+                                               |
|     |        +----+        |                                               |
|  prototype                 |                                               |
|     |     . toString()     |                                               |
|     |                      |                                               |
|     +----------------------+                                               |
|                                                                           |                                                                    |                 |                                                          |
|                 |           +------+                                       |
|                 |  +--------| obra |--------+                              |
|                 |  |        +------+        |                              |
|             prototype       . data          |                              |
|                    |        . autor         |                              |
|                    |                        |                              |
|                    +------------------------+                              |
|                                                                           |
|                                 |           +----------+                   |
|                                 |  +--------| lusiadas |--------+          |
|                                 |  |        +----------+        |          |
|                             prototype                           |          |
|                                    |          . nome            |          |
|                                    |                            |          |
|                                    +----------------------------+          |
+----------------------------------------------------------------------------+

O mecanismo de prototypes é flexível e consome pouca memória. Contudo, se se tem uma cadeia de prototypes muito longa, corre-se o risco de se ter uma aplicação pouco performática. Logo, devemos limitar a cadeia de prototypes para reduzir os problemas de performance em nossos aplicativos.

Inheritance and constructors

Pode-se gerenciar a herança mediante "object constructors" (construtores de objeto). Em linguagem simples: pode-se criar objetos mediante uma função construtora que herdam "características / recursos" de outros objetos. Ou seja, uma relação de tipo "is-a" entre construtores.

Vejamos o construtor de objetos do tipo Pessoa:

function Pessoa(nome, sobreNome) {
  this.nome = nome;
  this.sobreNome = sobreNome;
}

Vejamos o construtor de objetos do tipo desenvolvedor que herdam recursos de objetos criados pelo construtor Pessoa():

function Desenvolvedor(nome, sobreNome, linguagem_favorita) {
  Pessoa.apply(this, arguments);
  this.linguagem_favorita = linguagem_favorita;
}

Observemos que no "standard constructor" se usa o método apply do construtor Pessoa. O método apply executa o método (ou função) Pessoa no contexto e com os argumentos do Desenvolvedor. Ou seja, cria-se as propriedades da função Pessoa na instância do Desenvolvedor.

Ao se criar uma instância do Desenvolvedor, obtém-se as propriedades: nome, sobreNome e linguagem_favorita:

let desenvolvedor_01 = new Desenvolvedor("Lucas", "Lopes", "Haskell");

console.log( desenvolvedor_01.nome )                  // Lucas
console.log( desenvolvedor_01.sobreNome )             // Lopes
console.log( desenvolvedor_01.linguagem_favorita )    // Haskell

Essa abordagem permite que se herde membros de Pessoa em objetos Desenvolvedor. Contudo, observemos como ela quebra a consistência do operador instanceof na cadeia de prototypes:

let desenvolvedor_01 = new Desenvolvedor("Lucas", "Lopes", "Haskell");

desenvolvedor_01 instanceof Desenvolvedor;    // true
desenvolvedor_01 instanceof Pessoa;           // false
desenvolvedor_01 instanceof Object;           // true

Nota-se que uma instância do Desenvolvedor não é considerada também como uma instância de Pessoa. Deve-se corrigir esse problema atribuindo explícitamente um prototype ao construtor Desenvolvedor:

Desenvolvedor.prototype = Object.create(Pessoa.prototype);
Desenvolvedor.prototype.constructor = Desenvolvedor;

// OU
function extendss(derivaded, base) {
  derivaded.prototype = Object.create(base.prototype);
  derivaded.prototype.constructor = derivaded;
}

extendss(Desenvolvedor, Pessoa);

ES6 inheritance

A "palavra-chave" (construção) class exposta pela especificação ECMAScript6 permite-nos trabalhar com herança de forma simples. Com sintaxe semelhante à de linguagens OO clássicas, pode-se fazer com uma classe herde de outra. Reescravamos em sintaxe ES6 o exemplo anterior:

class Pessoa {
  constructor(nome, sobreNome) {
    this.nome = nome;
    this.sobreNome = sobreNome;
  }
}

class Desenvolvedor extends Pessoa {                      // Atenção
  constructor(nome, sobreNome, linguagem_favorita) {
    super(nome, sobreNome);                               // Atenção
    this.linguagem_favorita = linguagem_favorita;
  }
}

As linhas importantes desse exemplo contêm o comentário Atenção. Nota-se que a classe Desenvolvedor herda da classe Pessoa mediante a palavra-chave extends. Ademais, a classe Developer, em seu construtor, chama a sua classe pai (ou base) mediante a palavra-chave super. O exemplo acima é apenas uma maneira mais compacta e legível de se obter o mesmo resultado do código:

function Pessoa(nome, sobreNome) {
  this.nome = nome;
  this.sobreNome = sobreNome;
}

function Desenvolvedor(nome, sobreNome, linguagem_favorita) {
  Pessoa.apply(this, arguments);
  this.linguagem_favorita = linguagem_favorita;
}

function extendss(derivaded, base) {
  derivaded.prototype = Object.create(base.prototype);
  derivaded.prototype.constructor = derivaded;
}

extendss(Desenvolvedor, Pessoa);

Pode-se usar extends de duas maneiras:

  • Num método construtor de classe (class constructor method) para chamar o construtor base.
  • Dentro do método de uma classe para usar os métodos da classe base

No primeiro caso podemos usá-lo como se usa uma função passando quaisquer parâmetros, como vimos ao definir a classe Desenvolvedor. No segundo podemos usá-lo como um "objeto que expõe métodos", assim:

class Pessoa {
  constructor(nome, sobreNome) {
    this.nome = nome;
    this.sobreNome = sobreNome;
  }

  obterNomeCompleto() {
    return `${this.nome} ${this.sobreNome}`; 
  }
}

class Desenvolvedor extends Pessoa {
  constructor(nome, sobreNome, linguagem_favorita) {
    super(nome, sobreNome);
    this.linguagem_favorita = linguagem_favorita;
  }

  exibirCompetencia() {
    console.log( `${super.obterNomeCompleto()} é competente em: ${this.linguagem_favorita}` )  // Atenção
  }
}

Classes e funções construtoras podem coexistir numa aplicação. Pode-se definir uma classe que herda de uma função construtora:

function Pessoa(nome, sobreNome) {
  this.nome = nome;
  this.sobreNome = sobreNome;
}

class Desenvolvedor extends Pessoa {
  constructor(nome, sobreNome, linguagem_favorita) {
    super(nome, sobreNome);
    this.linguagem_favorita = linguagem_favorita;
  }
}

Contudo, não se pode definir uma função construtora que herde de uma classe.

Controlling inheritance

Overriding methods


[1] https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment