Herança de classes em JavaScript
Classes podem herdar de outras classes. A herança permite reduzir a quantidade de código nos classes derivadas. Por exemplo, considere as seguintes classes:
class Person {
name;
age;
print() {
console.log(`Name: ${this.name} Age: ${this.age}`);
}
}
class Employee {
name;
age;
company;
print() {
console.log(`Name: ${this.name} Age: ${this.age}`);
}
work() {
console.log(`${this.name} works in ${this.company}`);
}
}
const tom = new Person();
tom.name = "Tom";
tom.age = 34;
const bob = new Employee();
bob.name = "Bob";
bob.age = 36;
bob.company = "Google";
tom.print(); // Name: Tom Age: 34
bob.print(); // Name: Bob Age: 36
bob.work(); // Bob works in Google
Aqui, temos duas classes: Person
, que representa uma pessoa, e Employee
, que representa um funcionário de uma empresa. Ambas as classes funcionam bem, podemos criar objetos delas, mas também vemos que a classe Employee
repete funcionalidades da classe Person
, já que um funcionário também é uma pessoa com propriedades name
e age
e o método print
.
A herança permite que uma classe obtenha automaticamente funcionalidades de outras classes, reduzindo assim a quantidade de código. Para herdar de uma classe, usamos a palavra-chave extends
:
class Base {}
class Derived extends Base {}
Após o nome da classe derivada, colocamos a palavra-chave extends
, seguida do nome da classe da qual queremos herdar funcionalidades.
Assim, modificamos as classes Person
e Employee
aplicando a herança:
class Person {
name;
age;
print() {
console.log(`Name: ${this.name} Age: ${this.age}`);
}
}
class Employee extends Person {
company;
work() {
console.log(`${this.name} works in ${this.company}`);
}
}
const tom = new Person();
tom.name = "Tom";
tom.age = 34;
const bob = new Employee();
bob.name = "Bob";
bob.age = 36;
bob.company = "Google";
tom.print(); // Name: Tom Age: 34
bob.print(); // Name: Bob Age: 36
bob.work(); // Bob works in Google
Agora, a classe Employee
herda da classe Person
. Neste contexto, a classe Person
é também chamada de classe base ou classe pai, e Employee
é uma classe derivada ou classe filha. Como a classe Employee
herda funcionalidades de Person, não precisamos redefinir as propriedades name
, age
e o método print
. Como resultado, o código da classe Employee
fica mais curto, mas o resultado do programa é o mesmo.
Herança de Classe com Construtor
Junto com toda a funcionalidade, a classe derivada também herda o construtor da classe base. Por exemplo, definimos no classe base Person um construtor:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
print() {
console.log(`Name: ${this.name} Age: ${this.age}`);
}
}
class Employee extends Person {
company;
work() {
console.log(`${this.name} works in ${this.company}`);
}
}
const tom = new Person("Tom", 34);
tom.print(); // Name: Tom Age: 34
const sam = new Employee("Sam", 25); // construtor herdado
sam.print(); // Name: Sam Age: 25
Neste caso, a classe Person
define um construtor com dois parâmetros. Neste caso, a classe Employee
herda esse construtor e o utiliza para criar um objeto Employee
.
Definindo um Construtor na Classe Derivada e a Palavra-Chave super
A classe derivada também pode definir seu próprio construtor. Se a classe derivada define um construtor, então o construtor da classe base deve ser chamado dentro dele. Para acessar a funcionalidade da classe base, incluindo o construtor, usamos a palavra-chave super
:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
print() {
console.log(`Name: ${this.name} Age: ${this.age}`);
}
}
class Employee extends Person {
constructor(name, age, company) {
super(name, age);
this.company = company;
}
work() {
console.log(`${this.name} works in ${this.company}`);
}
}
const tom = new Person("Tom", 34);
tom.print(); // Name: Tom Age: 34
const sam = new Employee("Sam", 25, "Google");
sam.print(); // Name: Sam Age: 25
sam.work(); // Sam works in Google
A classe Employee
define seu próprio construtor com três parâmetros, e a primeira linha desse construtor chama o construtor da classe base Person
com super(name, age)
. Como o construtor da classe Person
tem dois parâmetros, eles são passados adequadamente. Além disso, o construtor da classe base deve ser chamado antes de acessarmos as propriedades do objeto atual usando this
.
Sobrescrevendo Métodos da Classe Base
Assim como com o construtor, a classe derivada pode sobrescrever métodos da classe base. Por exemplo, no exemplo acima, o método print()
da classe Person
exibe o nome e a idade da pessoa. Mas e se quisermos que para um empregado o método print()
também mostre a empresa? Nesse caso, podemos definir nosso próprio método print()
na classe Employee
:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
print() {
console.log(`Name: ${this.name} Age: ${this.age}`);
}
}
class Employee extends Person {
constructor(name, age, company) {
super(name, age);
this.company = company;
}
print() {
console.log(`Name: ${this.name} Age: ${this.age}`);
console.log(`Company: ${this.company}`);
}
}
const sam = new Employee("Sam", 25, "Google");
sam.print(); // Name: Sam Age: 25
// Company: Google
No entanto, no código acima, a primeira linha do método print()
na classe Employee basicamente repete o código do método print()
da classe Person
. Embora neste caso seja apenas uma linha, em outras situações o código repetido poderia ser mais extenso. Para evitar repetições, podemos simplesmente chamar a implementação do método print()
da classe pai através de super
:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
print() {
console.log(`Name: ${this.name} Age: ${this.age}`);
}
}
class Employee extends Person {
constructor(name, age, company) {
super(name, age);
this.company = company;
}
print() {
super.print();
console.log(`Company: ${this.company}`);
}
}
const sam = new Employee("Sam", 25, "Google");
sam.print(); // Name: Sam Age: 25
// Company: Google
Assim, a chamada super.print();
representa a execução da implementação do método da classe base. Dessa forma, com this
e super
, podemos diferenciar o acesso às funcionalidades da classe atual ou de sua classe base.
Herança e Campos e Métodos Privados
Ao trabalhar com herança, é importante considerar que a classe derivada pode acessar qualquer funcionalidade da classe base, exceto campos e métodos privados. Por exemplo:
class Person {
#name;
constructor(name, age) {
this.#name = name;
this.age = age;
}
print() {
console.log(`Name: ${this.#name} Age: ${this.age}`);
}
}
class Employee extends Person {
constructor(name, age, company) {
super(name, age);
this.company = company;
}
print() {
super.print();
console.log(`Company: ${this.company}`);
}
work() {
console.log(`${this.#name} works in ${this.company}`); // Erro - campo #name não está acessível em Employee
}
}
Neste caso, o campo #name
na classe Person
é definido como privado, portanto, está acessível apenas dentro dessa classe. Tentativas de acessar esse campo na classe derivada Employee resultarão em erro, independentemente de tentar acessá-lo através de this.#name
ou super.#name
. Se necessário, na classe base, podemos definir getters e setters que acessam campos privados, e a classe derivada pode então acessar esses campos privados por meio desses getters e setters.
Verificação da Pertinência de um Objeto a uma Classe
O fato de uma classe derivada ser herdada de uma classe base significa que um objeto da classe derivada também é um objeto da classe base. Podemos verificar a qual classe um objeto pertence usando o operador instanceof
:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
print() {
console.log(`Name: ${this.name} Age: ${this.age}`);
}
}
class Employee extends Person {
constructor(name, age, company) {
super(name, age);
this.company = company;
}
print() {
super.print();
console.log(`Works in ${this.company}`);
}
}
class Manager extends Person {
constructor(name, age, company) {
super(name, age);
this.company = company;
}
print() {
super.print();
console.log(`Manager in ${this.company}`);
}
}
const sam = new Employee("Sam", 25, "Google");
console.log(sam instanceof Person); // true
console.log(sam instanceof Employee); // true
console.log(sam instanceof Manager); // false
Aqui, a constante sam
representa um objeto da classe Employee
, que é derivada de Person
. Portanto, as expressões sam instanceof Person
e sam instanceof Employee
retornarão true
. Mas como sam
não é um objeto da classe Manager
, a expressão sam instanceof Manager
retornará false
.