Classes são cidadãos de "primeira classe" em JavaScript

2021-12-24

Beleza, o que significa alguma coisa ser cidadão de primeira classe?

##Cidadão de Primeira Classe

As pessoas desenvolvedoras gostam de dar nomes exóticos para coisas "normais" e isso às vezes pode causar muita confusão para pessoas que estão iniciando nesse admirável mundo novo.

Dizer que alguma coisa é cidadão de primeira classe dentro do contexto de programação, significa dizer que essa coisa pode ser tratada como variável. Ou seja, significa que você pode passar essa coisa como argumento de uma função, significa que você pode retornar essa coisa de uma função e várias outras coisas que você poderia fazer com uma variável.

Em JavaScript é bem conhecido e você provavelmente pode já ter visto isso em algum lugar, mas funções são cidadãos de primeira classe. Você pode enviar funções para outras funções, bem como retornar funções de outras funções.

Se você programa com JavaScript, provavelmente já precisou lidar com algum callback... Bingo, callbacks são funções e você passa callbacks para funções, para que sejam "chamados" (daí vem a palavra callback) pelos mecanismos internos da função a qual você enviou o callback.

Outro exemplo que pode ser meio comum é enviar callbacks para o método then ou catch de uma Promise.

const p = Promise.resolve(10)
const log = console.log

p.then(log) // 10

##Classes em JavaScript

Em JavaScript, o conceito de classe se resume a um mecanismo bem sofisticado em cima do mecanismo de funções. Em versões pré ES2015, não havia o mecanismo de classe na linguagem. Naquela época usávamos factory functions, que são funções que têm uma propriedade bem interessante.

Se invocarmos uma factory funciton usando a palavra-chave new, uma nova instância de um objeto é criada e associada ao símbolo que receber o resultado da invocação.

Pareceu confuso? Veja esse exemplo:

function Lightbulb() {
  this.isOn = false;

  this.toggle = function() {
    this.isOn = true;
  }
}

Considere a função Lightbulb. Se você somente executar essa função, nada de especial vai acontecer, ela vai retornar undefined e vida que segue.

Porém a mágica acontece quando você executar essa função usando a palavra-chave new. Veja abaixo:

function Lightbulb() {
  this.isOn = false;

  this.toggle = function() {
    this.isOn = true;
  }
}

const lightbulb = new Lightbulb()
console.log(lightbulb.isOn) // false

lightbulb.toggle()

console.log(lightbulb.isOn) // true

console.log(lightbulb instanceof Lightbulb) // true

Ou seja, lightbulb é uma instância de Lightbulb. O mecanismo interno de herança de protótipos é um pouco complicado e não faz sentido entrar em detalhes nesse texto, mas saiba que quando a um objeto é instânciado a partir de um construtor, o protótipo do construtor é atrelado ao objeto. Por isso que lightbulb têm acesso às propriedades isOn e toggle (que por sinal é uma função).

Voltando às classes...

##E classes são o quê?

Depende de cada linguagem de programação. Em JavaScript é uma camada de sintaxe em cima de um mecanismo bem mais complicado envolvendo funções, protótipos e objetos.

Porém, já que classes são abstrações em cima de funções, isso significa que podemos fazer uso das abstrações que JavaScript nos oferece para lidar com funções. Como passar funções como argumento, retornar funções e chamar funções em lugares bem inusitados.

Vou começar com um exemplo.

class User {
  constructor({ firstName, lastName, roles }) {
    this.firstName = firstName
    this.lastName = lastName
    this.roles = roles
  }

  get name() {
    return this.firstName + this.lastName
  }
}

class Admin extends User { }

class Agent extends User { }


const admin = new Admin({ firstName: 'Name',
                          lastName: 'Last',
                          roles: ['destroy-all-things'] })

const agent = new Agent({ firstName: 'Name',
                          lastName: 'Last',
                          roles: [] })

No trecho acima é definida uma classe User que recebe em seu construtor um objeto que possui os parâmetros firstName, lastName e roles. Logo abaixo são definidas outras duas classes que herdam o comportamento de User.

Porém aqui tem um problema, tanto Admin quanto Agent precisarão receber a lista com as permissões daquele usuário dentro do sistema, sempre que uma nova instância dessas classes for criada.

Esse problema pode ser resolvido de diversas maneiras, mas para mostrar uma estética diferente, veja essa abordagem a seguir.

const User = (roles = []) =>
  class {
    constructor({ firstName, lastName }) {
      this.firstName = firstName
      this.lastName = lastName
    }

    get name() {
      return this.firstName + this.lastName
    }

    roles() {
      return roles
    }
  }


class Admin extends User(['destroy-all-things']) { }

class Agent extends User() { }


const admin = new Admin({ firstName: 'Name', 
                          lastName: 'Last' })

const agent = new Agent({ firstName: 'Name',
                          lastName: 'Last' })

Notou a diferença? Para quem vai usar as classes Admin ou Agent, não precisaria inserir o array de permissões.

A maior diferença está em o que é User nesse contexto. User é uma função que retorna uma classe anônima, essa classe é usada no mecanismo de herança do JavaScript.

Todas funções em JavaScript possuem um escopo local, esse mecanismo é chamado de closure. Dentro do escopo da função User existe o array roles, esse array é acessível em todas as instâncias de qualquer coisa que herde de User.

##Resumo

Eu preciso que fique claro que você pode resolver esse tipo de problema de diversas formas, inclusive isso é uma propriedade que eu gosto muito de JavaScript. Não existe apenas uma forma de resolver os problemas. Por causa disso ter um senso de "estética" é muito importante.

No problema anterior você poderia usar atributos estáticos e funcionaria super bem, porém a estética seria diferente. Isso não é ruim nem bom, só é diferente.

Pessoalmente, acredito que quanto mais coisas diferentes você experimentar no código (e na vida), mais xp você terá na hora de decidir em como organizar uma base de código, um módulo, um namespace, uma função, um escopo local, etc.

Usar o contexto local da função User nem é a parte mais interessante desse assunto, mas saber que é possível retornar uma classe a partir de uma função, talvez abra alguns caminhos que antes não existiam no teu cérebro.

Se isso aconteceu, manda uma DM que vou ficar bem contente em saber :)