Novas JavaScript* Class Patterns

 

"Você mentiu."
"Eu exagerei."
- Saavik e Spock, A Ira de Khan

Quando eu era um calouro na faculdade, eu tinha um amigo que já era veterano e estudava química. Ele resumiu sua experiência dizendo que como um veterano ele aprendeu como os professores de química mentiram para ele em todos os anos anteriores, mas que agora ele estava aprendendo a verdade. O que ele queria dizer era que conforme a sua educação progredia, as simplificações feitas anteriormente eram substituídas por modelos mais complexos de iterações químicas.

Agora eu vou falar toda a verdade, mas ás vezes eu simplifico, por isso, acredite em mim quando eu digo que "this" e "new" são muitas vezes mais complicados do que parecem, e apesar do fato de que tudo em Javascript é um objeto, a linguagem não é exatamente orientada a objetos, apesar dela ter todo o encanamento necessário para suportar a orientação. E eu admito que existe mais sobre classes do que funções membro e variáveis privadas, mas indo ao que interessa, precisamos encontrar um padrão de classe que funcione para dados públicos e privados.

Como mencionei no artigo anterior, podemos usar o que aprendemos sobre variáveis estáticas para usar variáveis privadas. Para variáveis estáticas, nos beneficiamos de variáveis locais em um escopo oculto, enquanto aproveitados os objetos e o ponteiro "this" do Javascript para criar uma aproximação das classes. Podemos ficar tentados a combinar os dois assim:

var p = function () {
    var x= 0
    var y= 0
    var z= 0

    return {
        moveBy: function (i, j, k) {this.x+=i; this.y+=j; this.z+=k;},
        moveTo: function (i, j, k) {this.x=i; this.y=j; this.z=k;},
        show: function () {console.log("Current position is",this.x,",",this.y,",",this.z)}
    }
}()

Mas isso não funciona. As variáveis privadas x, y e z não são referenciadas nas funções membro, que fazem referência a this.x, this.y e this.z. Podemos tentar isso:

var p = function () {
    this.x= 0
    this.y= 0
    this.z= 0

    return {
        moveBy: function (i, j, k) {this.x+=i; this.y+=j; this.z+=k;},
        moveTo: function (i, j, k) {this.x=i; this.y=j; this.z=k;},
        show: function () {console.log("Current position is",this.x,",",this.y,",",this.z)}
    }
}()

Mas também não vai funcionar. É aqui que o "this" fica confuso. Quando você referencia o "this" de dentro de um objeto, ele faz referência ao objeto, mas quando ele é chamado de uma função, ele depende de como a função foi chamada. Em particular, no contexto global do Javascript, "this" se refere ao contexto global (ou o objeto janela). Em outras palavras, this.x é o mesmo que a variável global x. Se você abrir uma console Javascript e executar o código acima, você irá inicializar com zero 3 variáveis globais, x, y, e z. Por outro lado, o "this.x" dentro do objeto retornado não fui inicializado, então incrementa-lo em moveBy() não faz nada de útil. Além disso, ele adiciona três variáveis públicas ao objeto, o que desmonta toda nossa ideia inicial. Acima disso tudo, ao chama-la localmente, como fizemos com a função estática, não nos deixa nenhum modo de termos outras instâncias da mesma classe. Enquanto tivermos um ponto único "p", não podemos criar outros. Precisamos sentar e pensar novamente sobre isso.

O que queremos é algo parecido com um construtor, que irá inicializar todos os membros apropriados, manter de forma privada as coisas que forem privadas e permitir o acesso ao que é público. Nós devemos chamar isso como um construtor, e cada instância deveria ser separada das outras. Queremos que este construtor retorne um objeto que será uma instância da nossa classe.

Basicamente queremos algo como isso aqui:

function Point() {
    var x = 0;
    var y = 0;
    var z = 0;

    return {
        moveBy: function (i, j, k) {x+=i; y+=j; z+=k;},
        moveTo: function (i, j, k) {x=i; y=j; z=k;},
        show: function () {console.log("Current position is",x,",",y,",",z)}
    }
}

Note que agora Point é o construtor, e não uma instância. Para ter uma instância, chamamos Point() para obter um objeto representando a instância da classe Point, assim:

p1 = Point();
p2 = Point();

Uma última coisa. Anteriormente eu mencionei o "new". É agora que ele se torna útil. Ele faz algo muito parecido com o que a nossa função Point() faz. Quando ele é usado antes de uma chamada de função, ele cria um novo objeto e chama a função, setando a variável "this" para referenciar o objeto que acabou de ser criado. Qualquer coisa que retorne desta função é ignorado e o retorno é um novo objeto. Qualquer modificação feita a "this" irá refletir no objeto retornado, assim:

function f() {
    this.x = 1;
}

> x = new f()
{ x: 1 }

Isto é parecido com o que queremos para o nosso construtor, ex criar um novo objeto e inicializa-lo, exceto que agora podemos depender da definição de this e portanto usa-lo de forma efetiva:

function Point() {
    var x = 0;
    var y = 0;
    var z = 0;

    this.moveBy = function (i, j, k) {x+=i; y+=j; z+=k;},
    this.moveTo = function (i, j, k) {x=i; y=j; z=k;},
    this.show = function () {console.log("Current position is ", x,",", y,",", z)}
}
> p1 = new Point()
{ moveBy: [Function],
  moveTo: [Function],
  show: [Function] }
> p1.show()
Current position is  0 , 0 , 0
undefined
> p1.moveBy(1,2,3)
undefined
> p1.show()
Current position is  1 , 2 , 3

Isso nos dá um padrão limpo e adequado. Qualquer coisa que desejamos ter como "privado" deve ser uma variável, enquanto qualquer coisa que queiramos deixar "público" deve ser um campo do objeto "this". Claro, não terminamos ainda. Nós não temos herança, mas vou deixar isso para outro dia.

 
Para obter informações mais completas sobre otimizações do compilador, consulte nosso aviso de otimização.