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

Image for post
Image for post
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

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

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

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

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store