JavaScript: Propriedades estáticas com herança em Classes

Eduardo Rabelo
3 min readNov 20, 2018
Pré-ES6 e Pós-ES6, utilizar classes em JavaScript nunca foi fácil!

Desde o ES6, JavaScript tem suporte para classes e funções estáticas, semelhantes a funções estáticas em outras linguagens orientadas a objeto. Infelizmente, o JavaScript não tem suporte para propriedades estáticas e as soluções recomendadas no Google não levam em conta a herança de classes. Eu encontrei esse problema ao implementar um novo recurso no Mongoose, que requer uma noção mais robusta de propriedades estáticas. Especificamente, eu preciso de propriedades estáticas que suportem herança através do prototype ou extends. Nesse artigo iremos ver como implementar essas propriedades estáticas no ES6.

Métodos estáticas e herança

Veja a classe ES6 a seguir:

class Base {
static foo() {
return 42;
}
}

Você pode usar extends para criar uma subclasse e ainda ter acesso a função foo():

class Sub extends Base {}

Sub.foo(); // 42

Você também pode usar getters e setters estáticos para definir uma propriedade estática na classe Base:

let foo = 42;

class Base {
static get foo() { return foo; }
static set foo(v) { foo = v; }
}

Infelizmente, utilizando esse padrão, teremos um comportamento indesejável quando você definir uma subclasse de Base. Ao definir foo() para uma subclasse, essa definição será propagada para a classe Base e todas as outras subclasses de Base, por exemplo:

class Sub extends Base {}

// Mostra "42 42"
console.log(Base.foo, Sub.foo);

Sub.foo = 43;

// Mostra "43 43". E também altera `Base.foo`
console.log(Base.foo, Sub.foo);

O problema piora se sua propriedade for um array ou um objeto. Por causa da herança prototípica do JavaScript, se foo for um array, cada subclasse terá uma referência à mesma cópia do array, conforme mostrado abaixo:

class Base {
static get foo() { return this._foo; }
static set foo(v) { this._foo = v; }
}

Base.foo = [];

class Sub extends Base {}

console.log(Base.foo, Sub.foo);

Sub.foo.push('foo');

// Ambos arrays tem 'foo' porque eles são o mesmo array!
console.log(Base.foo, Sub.foo);
console.log(Base.foo === Sub.foo); // true

Portanto, o JavaScript suporta getters e setters estáticos, mas usá-los com objetos ou arrays é um tiro no pé! Para resolver isso, podemos criar essa funcionalidade com uma pequena ajuda da função hasOwnProperty().

Propriedades estáticas com herança

A idéia principal é que uma classe JavaScript é apenas outro objeto, para que você possa distinguir entre propriedades próprias e propriedades herdadas, podemos fazer:

class Base {
static get foo() {
// Se `_foo` for herdado ou não existir ainda, trate-o como `undefined`
return this.hasOwnProperty('_foo') ? this._foo : void 0;
}
static set foo(v) { this._foo = v; }
}

Base.foo = [];

class Sub extends Base {}

// Mostra "[] undefined"
console.log(Base.foo, Sub.foo);
console.log(Base.foo === Sub.foo); // false

Base.foo.push('foo');

// Mostra "['foo'] undefined"
console.log(Base.foo, Sub.foo);
console.log(Base.foo === Sub.foo); // false

Esse padrão é perfeito com classes, mas também funciona com herança em JavaScript pré-ES6. Isso é importante porque o Mongoose ainda usa herança de estilo pré-ES6. Pensando sobre, nós deveríamos ter mudado mais cedo, mas com esse recurso, é a primeira vez em que vimos uma clara vantagem em usar classes com herança em ES6 ao invés de apenas definir uma função no .prototype:

function Base() {}

Object.defineProperty(Base, 'foo', {
get: function() { return this.hasOwnProperty('_foo') ? this._foo : void 0; },
set: function(v) { this._foo = v; }
});

Base.foo = [];

// Herança em Pre-ES6
function Sub1() {}
Sub1.prototype = Object.create(Base.prototype);

// Propriedades estáticas eram bem chatas em pre-ES6
Object.defineProperty(Sub1, 'foo', Object.getOwnPropertyDescriptor(Base, 'foo'));

// Herança em ES6
class Sub2 extends Base {}

// Mostra "[] undefined"
console.log(Base.foo, Sub1.foo);

// Mostra "[] undefined"
console.log(Base.foo, Sub2.foo);

Base.foo.push('foo');

// Mostra "['foo'] undefined"
console.log(Base.foo, Sub1.foo);

// Mostra "['foo'] undefined"
console.log(Base.foo, Sub2.foo);

Continuando e finalizando

As classes ES6 têm uma grande vantagem em relação à velha escolha de Sub.prototype = Object.create(Base.prototype) pois extends também copiam propriedades e funções estáticas. Com um pouco de trabalho extra com Object.hasOwnProperty(), você pode criar getters e setters estáticos que manipulam a herança corretamente. Tenha muito cuidado com propriedades estáticas em JavaScript: extends ainda usa herança prototípica por baixo dos panos.

Isso significa que objetos e arrays estáticos são compartilhados entre todas as subclasses, a menos que você use a idéia discutida nesse artigo e utilize a função hasOwnProperty().

⭐️ Créditos

--

--