Desde seu anúncio, as classes no javascript tem 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, onde ele fala sobre as vantagens da composição sobre a herança de classes e como as classes no javascript afetam esse comportamento. Um outro artigo que recomendo é o How to fix es6 class keyword, também do Eric Elliot.
Ambos 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
Neste 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”, ela será responsável por definir que o que virá a seguir será tratado 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 Person quanto o método “sayName” são funções. Entendendo 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 no primeiro exemplo (sem o uso da sintaxe class) seria necessário escrever isso:
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 equivoco 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 explicitos 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 sera 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);
Neste 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 nesse post pois é um conteúdo relativamente grande e vou escrever um post somente para isso no futuro.
Grande abraço a todos!