JavaScript do futuro no presente

Publicado por Pedro Nauck no dia dev

O JavaScript tem tomando uma grande fatia do mercado de desenvolvimento web e estado cada vez mais presente na vida dos programadores. Apesar de muito popular, a linguagem tem alguns problemas bem peculiares quando comparada à outras.

Coisas básicas que você vê em outras linguagens, as vezes se tornam complexas em JavaScript. Mesmo assim, podemos dizer que esses problemas estão com seus dias contados. A linguagem tem evoluído muito em suas últimas versões e discutiremos algumas melhorias neste post.

ECMAScript 6 não é mais futuro

Talvez boa parte dos programadores JavaScript não saibam que a linguagem já tem uma nova versão oficial, denominada ECMAScript 2015. A ECMAScript 6, ou ES6, foi a maior mudança em termos de estrutura da linguagem JavaScript desde sua criação. Ela é uma atualização poderosa, projetada durante meses e que contou com a presença de muitos especialistas do mundo todo.

Todo esse projeto e empenho fez com que esta nova versão ganhasse inúmeras funcionalidades interessantes e realmente úteis. Podemos destacar:

Você pode ver mais detalhes sobre essas funcionalidades na talk abaixo:

Ou se preferir, você pode pular direto para os slides:

Para quem está empolgado com as mudanças que o JavaScript vem sofrendo, a linguagem já tem outra versão sendo projetada em paralelo. Isso quer dizer que em breve teremos uma ES7, e é sobre ela que vamos falar um pouco.

ECMAScript 7 está no forno

Com o JavaScript adentrando novos ambientes, a linguagem e a comunidade tiveram que se moldar às novas exigências. A linguagem, que foi projetada para gerenciar comportamentos no browser, faz tarefas pesadas de servidor e é usada em dispositivos embarcados.

Na grande maioria dos casos os problemas que envolvem a linguagem estão relacionados ao paradigma de programação assíncrona que o JavaScript propõe. Ao mesmo tempo que este paradigma dá superpoderes à linguagem, ele torna bem complexo o desenvolvimento, principalmente para quem não está acostumado a lidar com este tipo de programação.

Outro ponto complexo dentro da linguagem é a maneira com que ela trabalha com programação orientada a objeto. Ela faz a orientação a objetos baseada em protótipos. Trabalhar com prototype pode ser algo bem diferente da maneira clássica, e entender este conceito não é algo simples.

Pessoalmente, estes são os principais problemas que enxergo em relação à linguagem. Trabalhar de forma assíncrona em tarefas que tenham como dependência a leitura de dados remotos – seja usando uma API interna ou externa – pode custar caro em termos de legibilidade de código. Pode até causar problemas sérios no retorno dos dados. Ao mesmo tempo, ter que lidar com questões de herança e projetar uma aplicação orientada a objetos pode não ser tão simples quanto você pensa.

Felizmente, nas novas versões da linguagem temos funcionalidades que suprem estas necessidades de forma elegante. Por exemplo:

Async flag

Temos que concordar que trabalhar de forma assíncrona é realmente intrigante. Passar uma função como parâmetro para outra função que retorna uma função, ao mesmo tempo que é a solução para muitos problemas, pode ser um caos. Particularmente, gosto muito de trabalhar de forma assíncrona e funcional. Hoje em dia já estou bem familiarizado com estes conceitos e confesso que eles já não me incomodam tanto assim. Porém, algumas vezes precisamos de uma solução mais concisa e legível para lidar com alguns tipos de casos.

Vou supor um cenário que pode se tornar um caos:

Você está projetando uma aplicação com NodeJS. Uma das funcionalidades da sua app é o upload de uma série de imagens adicionadas pelo seu usuário em uma instância da Amazon. Nesta funcionalidade, você precisa aguardar o upload de uma imagem para usar o metadado dela e, somente então, processar algum outro algoritmo. Depois disso, você pode seguir para a próxima imagem.

Nesse caso temos um problema que deveria ser resolvido de forma síncrona. Porém, como no exemplo acima estamos citando o uso do NodeJS - uma ferramenta que trabalha de forma assícrona - teríamos que criar uma solução para de alguma forma bloquear sua thread. Dessa forma poderíamos simular um comportamento síncrono. Isso tornaria o código muito pior em termos de legibilidade e manutenibilidade, pois estaríamos misturando paradigmas diferentes.

Imagina se tívessemos uma solução nativa para isso. Não seria muito melhor? A nova flag async, proposta no ECMAScript 7, vem exatamente para isso. Vamos dar uma olhada em como ela funciona:

1
2
3
4
5
6
7
8
9
10
var fetchUsers = function() {
  return http.get('/api/users');
};

var fetchCars = async function(userId) {
  var users = await fetchUsers();
  var user = mylib.find(users, { id: userId });

  return http.get('/api/users/' + user.id + '/cars/');
};

No exemplo acima temos duas funções. A primeira, fetchUser(), faz uma chamada ajax para um endpoint de uma API e retorna uma promise. A segunda função, fetchCars(), busca uma lista de carros baseando-se no ID de um usuário. Porém, notem que a segunda função tem alguns tokens que não estamos acostumados a ver. É o caso do async e await. Abaixo, explico melhor sobre eles.

Com o token async você pode indicar que a função declarada é assíncrona. O JavaScript, por sua vez, transformará automaticamente o retorno da função em uma promessa. Por exemplo:

1
2
3
4
5
6
7
8
var myfunc = async function() {
  if (something()) {
    return 'Success';
  }
  else {
    return 'Fail';
  }
}

É exatamente a mesma coisa que:

1
2
3
4
5
6
7
8
9
10
var myfunc = function() {
  return Promise(function(resolve, reject) {
    if (something()) {
      resolve('Success');
    }
    else {
      reject('Fail');
    }
  });
}

Como podem ver, a flag async faz com que possamos “promessificar” uma função de uma maneira muito mais simples. Porém, se fosse apenas isso não seria tão gratificante. Fazer uma função retornar uma promise não é um dos nossos problemas, o que realmente importa é a execução desta função, e neste ponto que a flag await vem para resolver nossos problemas.

Usando await dentro de uma função assíncrona, você tem o poder de parar o fluxo de execução do resto desta função até que algo seja resolvido ou rejeitado. Vejamos como funciona dentro da nossa função fetchCars():

1
2
3
4
5
6
var fetchCars = async function(userId) {
  var users = await fetchUsers();
  var user = mylib.find(users, { id: userId });

  return http.get('/api/users/' + user.id + '/cars/');
};

O que acontece nesse caso é que a função fetchCars() aguarda o retorno da função fetchUser() para, somente então, retornar a próxima promessa. Dessa forma conseguimos controlar facilmente o fluxo de execução de uma função.

Mas como podemos tratar os erros deste tipo de função? Simples, usamos um bloco de try/catch para lidar com nossos handlers. Veja como ficaria a função fetchCars() agora:

1
2
3
4
5
6
7
8
9
10
11
var fetchCars = async function(userId) {
  try {
    var user = await fetchUsers();
    var user = mylib.find(users, { id: userId });

    return http.get('/api/users/' + user[0].id + '/cars/');
  }
  catch(err) {
    return throw new Error(err);
  }
};

Atribuindo a flag await dentro do bloco de try/catch, a função irá verificar automaticamente o retorno da promessa. Caso a função fetchUser() seja rejeitada, o bloco de catch será executado; caso a função seja resolvida, então o bloco de try será executado. Fácil, não?

Classes

Outro ponto que torna a linguagem JavaScript um pouco complexa – principalmente para quem vem de outras linguagens de programação – é a maneira com que ela lida com programação orientadas a objeto. Como falado anteriormente, JavaScript trabalha com base em protótipos e não com uma orientação a objetos clássica.

Vamos supor um exemplo de “classe” em JavaScript:

1
2
3
var User = function(email) {
  this.email = email;
};

Com estas 3 linhas de código temos algo muito parecido com uma “classe”. Mas não é exatamente uma classe e, sim, um objeto. O que estamos fazendo nesse caso é criar o que chamamos de função construtora que poderá ser instanciada ao longo da aplicação usando o operador new. Ainda dentro desta função construtora, podemos atribuir métodos e propriedades internas para o nosso objeto. Por exemplo:

1
2
3
4
5
6
7
User.prototype.getEmail = function() {
  return this.email;
};

User.prototype.setEmail = function(newEmail) {
  this.email = newEmail;
};

Agora para usar estes dois métodos basta você instanciar um novo objeto a partir da sua função construtura:

1
2
var user = new User('name@email.com');
user.getEmail(); // name@email.com

Acessando o prototype da nossa função construtora, conseguimos definir seus métodos e propriedades. Por padrão, como você está uma função, ela herdará o prototype do objeto Function e Object, ou seja, você poderá acessar métodos e propriedades destes objetos também. Todo esse conceito de prototype pode as vezes confundir um pouco a cabeça de algumas pessoas e gerar uma certa dificuldade para lidar com alguns pontos básicos da orientação à objetos.

Para a felicidades de alguns, ou tristeza de outros, a nova versão já oficializada do Javascript trás uma solução nativa para a criação de classes.

DISCLAIMER: Há pessoas que concordam e acham que realmente Javascript precisa ter uma definição nativa para isso, ou já tem uma opinião um pouco contrária. A grande questão é que a nova versão apenas trás um “syntatic sugar” para a criação de classes, mas por trás dos panos, a linguagem continua sendo baseada em protótipos. Por isso existe essa polêmica.

Vamos ver como ficaria nossa “classe” usando a nova definição:

1
2
3
4
5
6
7
8
9
10
11
class User {
  constructor(email) {
    this.email = email
  }
  get email() {
    return this.email;
  }
  set email(newEmail) {
    this.email = newEmail;
  }
}

Nada muito diferente da maneira com que é feito em outras linguagens. Basicamente ganhamos alguns novos tokens e regras para facilitar a definição. Um ponto interessante a ser notado, é que agora toda a classe de JavaScript precisa de um método constructor, que é o local onde você conseguirá acessar e definir o this da sua classe.

Outro ponto interessante é em relação a herança entre classes. Agora podemos criar o conceito de herança de uma maneira muito simples:

1
2
3
4
5
6
class BasicUser extends User {
  constructor(name) {
    super(name);
    this.type = 'basic';
  }
}

Usando o operador extends, você consegue atribuir uma herança entre classes. Porém, dentro do construtor da classe que está herdando, você tem que executar o método super() passando os parâmetros necessários da classe pai. Assim você estará executando o construtor do pai dentro do filho e criando as propriedades e métodos já existentes no pai.

Decorator pattern

Agora que vimos um pouco sobre como usar classes e criar funções assíncronas em JavaScript, vamos dar uma olhada em como as novas versões da linguagem podem nos ajudar a reutilizar código e deixar nossa aplicação cada vez mais modular.

Para que o JavaScript se torne uma linguagem mais poderosa em termos de reutilização de código, a versão ECMAScript 7 está trazendo o decorator pattern. A funcionalidade é conhecida em outras linguagens por annotation, e alguns ainda chamam este conceito de mixin.

Decorators são amplamente usados como patterns, ou seja, padrões que uma linguagem adota para deixar o código mais legível e mantenível. Os decorator patterns podem ser facilmente criado com JavaScript, sem a necessidade de um script de terceiros, apenas usando a linguagem pura. Este conceito representa um padrão de design que permite a adição de comportamento a um objeto individual. Por exemplo:

1
2
3
4
5
6
7
8
9
10
const MarioBros = function(target) {
  target.isFriendOfLuigi = true;
};

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

E para usar sua classe, basta você instanciar assim como feito anteriormente usando funções construturoras:

1
2
var john = new Person('John');
console.log(john.isFriendOfLuigi) // true

Dessa forma, você pode atribuir propriedades à uma classe utilizando apenas o uso dos Decorators, que irão encapsular estas propriedades e defini-lás automaticamente na sua classe. Você pode ainda fazer o uso do paradigma funcional para de alguma forma passar parâmetros para seu decorator:

1
2
3
4
5
6
7
8
9
10
11
12
13
const MarioBros = function(uniformColor) {
  return function(target) {
    target.isFrinedOfLuigi = true;
    target.uniformColor = uniformColor;
  };
};

@MarioBros('red');
class Person() {
  constructor(name) {
    this.name = name;
  }
}

Como usar hoje em dia

Como sabemos que nem tudo é maravilhoso no mundo do JavaScript, infelizmente ainda não conseguimos usar puramente todas estas funcionalidades dentro da linguagem. Porém, a notícia boa é que podemos começar a programar usando as novas versões através de um transpiler. Trata-se de uma biblioteca onde você pode escrever o código com as novas versões para ele gerar automaticamente um código cross-browser com a versão mais suportada atualmente.

Existem algumas possibilidades de transpilers. Acredito que o mais popular e completo hoje em dia seja o Babel, amplamente usado em grandes empresas e também em projetos open source.

O Babel é uma ferramenta realmente simples de usar. Você pode usufruir dela via linha de comando, ou integrado com algum task runner, como Grunt ou Gulp. Veja um exemplo usando Gulp:

1
2
3
4
5
6
7
8
var gulp = require("gulp");
var babel = require("gulp-babel");

gulp.task("default", function () {
  return gulp.src("src/app.js")
    .pipe(babel())
    .pipe(gulp.dest("dist"));
});

Se você não quiser usar nenhuma dessas opções, o Babel tem uma série de integrações prontas que podem lhe ajudar.

Conclusão

Neste post vimos apenas um pedaço do que está vindo por aí nas novas versões do JavaScript. Coisas que realmente irão mudar a forma de escrever a linguagem, ampliando a entrada de novos membros comunidade. É importante estarmos sempre engajados com as mudanças e contribuir para o crescimento da linguagem. O ambiente e a comunidade são um dos principais responsáveis por moldar a linguagem e deixá-la cada vez mais otimizada.

Pedro Nauck

Pedro Nauck

JavaScript Developer

Comentários