Desenvolvimento

26 jul, 2016

ECMAScript 6: desmistificando as classes

Publicidade

Desde seu anúncio, as classes no JavaScript têm gerado muita discórdia. Vários desenvolvedores ativos da comunidade tomaram posições divergentes sobre o assunto. Um exemplo é o artigo Two Pillars of javascript, escrito pelo Eric Elliot, no qual ele fala sobre as vantagens da composição sobre a herança de classes e como as classes no JavaScript afetam esse comportamento. Outro artigo que recomendo é o How to fix es6 class keyword, também do Eric Elliot.

Ambos os artigos citados acima apontam o lado ruim das classes – no meu ponto de vista, eles mostram pouco da real responsabilidade das classes no ES6. Classes não são monstros que querem transformar o JavaScript, o que elas querem na verdade é simplificar os casos de uso comuns do dia a dia. Eu, particularmente gosto bastante do artigo Does javascript need classes?, do Nicholas Zakas, bem mais próximo da realidade.

Entendendo as classes no ECMAScript 6

Antes do ES6, o JavaScript não possuía classes, nem maneira alguma para definir uma herança. Para contornar essa situação, foram criadas diversas bibliotecas de suporte como, _underscorejs e jQuery, que permitiam fazer composição de uma forma mais parecida com herança.

Definindo um comportamento similar a classes antes do ES6

O trecho de código a seguir é muito comum:

function Person(name) {
  this.name = name;
}

Person.prototype.sayName = function() {
  console.log(this.name);
};

let person = new Person("Waldemar");
person.sayName();   //"Waldemar"

console.log(person instanceof Person);  // true
console.log(person instanceof Object);  // true

Nesse exemplo, vemos que primeiramente foi definida a função construtura Person e depois foram atribuídos os métodos para o prototype dessa função. Em seguida, uma nova instância de Person é criada e ela possui o método sayName.

Este é o comportamento utilizado por várias das bibliotecas: encapsular a lógica para que fique mais parecido com uma declaração de classe comum entre as linguagens. O papel das classes no ECMAScript 6 é facilitar esse tipo de comportamento.

Declarando uma classe no ECMAScript 6

Assim como temos a keyword “function”, agora também teremos a keyword “class”, que será responsável por definir que o que virá a seguir e será tratada pela engine do JavaScript como uma classe.

O mesmo comportamento do exemplo anterior usando classe ficaria assim:

class Person {
  constructor(name) {
    this.name = name;
  }

  sayName() {
    console.log(this.name);
  }
}

let person = new Person("Waldemar");
person.sayName();   //"Waldemar"
console.log(person instanceof Person);  // true
console.log(person instanceof Object); // true

console.log(typeof Person);                    // "function"
console.log(typeof Person.prototype.sayName);  // "function"

Class é apenas uma sintaxe, por baixo dos panos o comportamento ainda é similar ao primeiro exemplo. A declaração da classe Person cria uma função que será o construtor (constructor). Por isso o “typeof ” disse que tanto o Person quanto o método “sayName” são funçõesEntendendo isso, podemos misturar os comportamentos como no exemplo a seguir:

class Person {
  constructor(name) {
    this.name = name;
  }

  sayName() {
    console.log(this.name);
  }
}

Person.prototype.sayWorks = () => {
  console.log("Works");
}

let person = new Person("Waldemar");
person.sayName();   //"Waldemar"
person.sayWorks(); //"Works"

Atribuir métodos diretamente ao prototype da classe como no primeiro exemplo vai funcionar normalmente

O que a sintaxe class traz de vantagens?

Quais seriam as vantagens de usar classes?

  • A declaração de “class” assim como “let” e “const” não fazem hoisting como “function” e “var”.
  • O escopo interno das classes roda sempre em strict mode.
  • Métodos dentro de classes não possuem construtor, o que impossibilita a chamada com new.
  • Não é possível chamar o construtor de uma classe sem new.
  • Não é possível sobrescrever o nome da classe com um método interno.

Para adicionar essas características ao primeiro exemplo (sem o uso da sintaxe class), seria necessário escrever isto:

let Person = (function() {
  "use strict";
  const Person = function(name) {
    if (typeof new.target === "undefined") {
      throw new Error("Constructor must be called with new.");
    }

    this.name = name;
  }

  Object.defineProperty(Person.prototype, "sayName", {
    value: function() {
      if (typeof new.target !== "undefined") {
        throw new Error("Method cannot be called with new.");
      }
      console.log(this.name);
    },
    enumerable: false,
    writable: true,
    configurable: true
  });

  return Person;
}());

Assim, é possível notar que classes não são coisas de outro mundo, e que talvez o equívoco seja o nome dado a elas, talvez classes em JavaScript não sejam bem classes, e sim apenas um tipo que simplifica a construção de objetos.

Classes como expressões anônimas

Assim como as funções, as classes também podem ser declaradas como expressões anônimas:

let person = new class {

  constructor(name) {
    this.name = name;
  }

  sayName() {
    console.log(this.name);
  }

}("Waldemar");

person.sayName(); //Waldemar

Propriedades de acesso

Getters e setters nem sempre são explícitos na maioria das linguagens, mas o JavaScript resolveu implementar uma forma nativa para criar propriedades de acesso usando “get” e “set” como no exemplo:

class Person {

  constructor(name) {
    this.name = name;
  }

  get fullName() {
    return this.name + "something";
  }

  set fullName(value) {
    this.name = value;
  }
}

var descriptor = Object.getOwnPropertyDescriptor(Person.prototype,"fullName");
console.log("get" in descriptor);   // true
console.log("set" in descriptor);   // true
console.log(descriptor.enumerable); // false

No exemplo acima, “fullName” se torna uma propriedade não enumerada do prototype, ou seja, quando ela for acessada, quem vai ser invocado será o “getter”, e não a propriedade diretamente:

let person = new Person("Waldemar ");
console.log(person.fullName); //Waldemar something

Métodos estáticos em classes

Adicionar funções diretamente ao prototype de um objeto para simular um método estático sempre foi uma prática comum desenvolvimento em JavaScript, por exemplo:

function OldPerson(name) {
  this.name = name;
}

OldPerson.printAge = function(age) {
  console.log(age);
};

OldPerson.printAge(15);

Nesse exemplo, a função “OldPerson” funciona como o construtor e logo depois a função “printAge” é adicionada diretamente ao prototype, ou seja, ela não depende da instância.

Transformando esse mesmo código para usar classes o comportamento seria o seguinte:

class Person {
  constructor(name) {
    this.name = name;
  }

  static printAge(age) {
    console.log(age); //15
  }
}

Person.printAge(15);

O ES6 simplificou esse comportamento adicionando os métodos estáticos com a keyword “static”. Fora no construtor, os métodos estáticos podem ser usados em qualquer lugar.

Não vou abordar herança neste artigo, pois é um conteúdo relativamente grande e vou escrever um texto somente para isso no futuro.

Grande abraço a todos!