JavaScript

10 set, 2020

Técnicas Avançadas com JavaScript

Publicidade

Depois de passarmos por muitos artigos sobre JavaScript e como podemos implementar técnicas como Padrões de Projetos, agora chegou a hora de sair do básico e aprender algumas técnicas mais avançadas que podem transformar a sua vida como pessoa desenvolvedora na hora de codar JavaScript!

Esta é uma pequena lista de algumas técnicas interessantes que aprendi ao longo da minha carreira e que me foram muito úteis. Você provavelmente não vai precisar utilizar elas todas sempre, porém quando estamos lidando com alguns tipos de problemas mais elaborados, muitas vezes podemos precisar delas!

Closures

Closures são consideradas uma das técnicas de programação mais avançadas em JavaScript, não porque elas são complicadas de se fazer, mas sim porque são difíceis de entender. Closures envolvem o uso de escopos, como já explicamos neste artigo. Apesar de serem complicadas de entender, o conceito é bem simples.

Esta técnica permite estender o escopo de uma variável além do seu escopo local. Mas não para o escopo global, pense nelas como um escopo regional que é mais extenso do que o escopo local mas ainda sim não tem a abrangência de um escopo global.

Essencialmente, para usar closures, precisamos criar uma função aninhada em outra função. Portanto a função interior vai ter acesso a todas as variáveis locais do seu escopo e também vai ter acesso a variáveis da função pai. Veja um exemplo:

function objetoExterno () {
  this.propriedade1 = 'Valor 1'
  this.propriedade2 = 100
  
  const valorRegional = this.propriedade1
  
  this.metodoEmClosure = function () {
    valorInterno = valorRegional

    return valorInterno
  }
}
  
const instancia = new objetoExterno()
console.log(instancia.metodoEmClosure())

Preste atenção em duas partes desse script, veja o this.metodoEmClosure e o nosso console.log no final do mesmo. Como o console.log está chamando um método interno que está em uma função dentro de outra função, esse método pode acessar o valor de valorRegional mesmo que essa variável não esteja no mesmo escopo.

Closures são muito utilizadas para burlar a limitação do JavaScript não possuir modificadores de acesso (embora agora tenhamos propriedades privadas no em classes), criando variáveis que são privadas e públicas.

Literais para argumentos opcionais

Eu não considero esta técnica como sendo avançada, mas é uma técnica que pode te salvar algumas linhas de código, principalmente em sistemas maiores.

O problema acontece quando temos funções que aceitam vários argumentos, alguns são obrigatórios – digo isto porque nenhum argumento do JavaScript é, de fato, obrigatório – e outros opcionais. Quantos mais argumentos opcionais temos, mais temos que checar sua existência. Por exemplo:

function mostrarDados (nome, idade, twitter, facebook, site) {
    console.log(`Nome: ${nome}`)
    console.log(`Idade: ${idade}`)
    if (typeof arguments[2] === 'string') console.log(`Twitter: ${twitter}`)
    if (typeof arguments[3] === 'string') console.log(`Facebook: ${facebook}`)
    if (typeof arguments[4] === 'string') console.log(`Site: ${site}`)
}
  
mostrarDados('Lucas', '25')
mostrarDados('Lucas', '25', '@_staticvoid')
mostrarDados('Lucas', '25', '@_staticvoid', '/lsantos.dev', 'https://blog.lsantos.dev')

Não checamos os dois primeiros argumentos porque são obrigatórios, mas estamos checando os demais por serem opcionais e estamos observando um array de argumentos posicionais, o que é muito ruim para a leitura e também para a manutenção do código, ao invés disso podemos fazer a passagem de argumentos como um objeto literal:

function mostrarDados (nome, idade, twitter, facebook, site) {
    console.log(`Nome: ${dados.nome}`)
    console.log(`Idade: ${dados.idade}`)
    if (typeof dados.twitter === 'string') console.log(`Twitter: ${dados.twitter}`)
    if (typeof dados.facebook === 'string') console.log(`Facebook: ${dados.facebook}`)
    if (typeof dados.site === 'string') console.log(`Site: ${dados.site}`)
}
  
mostrarDados({ nome: 'Lucas', idade: '25'})
mostrarDados({ nome: 'Lucas', idade: '25', twitter: '@_staticvoid'})
mostrarDados({ nome: 'Lucas', idade: '25', twitter: '@_staticvoid', facebook: '/lsantos.dev', site: 'https://blog.lsantos.dev'})

Além de ficar muito mais simples de entender, também temos um rastro menor de variáveis sendo criadas, já que substituímos cinco variáveis por um único objeto (veja mais sobre garbage collection e alocação de variáveis). O código pode ser um pouco maior, mas acredite, seu eu do futuro vai te agradecer quando tiver que dar manutenção.

Tornando argumentos obrigatórios em obrigatórios

Uma dica bastante interessante que vai ajudar muito na hora de criar aquele sistema complexo utilizando JavaScript puro ou até mesmo TypeScript. Você deve ter percebido que utilizei a palavra obrigatório dentro de aspas antes, certo?

Isso é porque, no JavaScript, todos os argumento são opcionais. A linguagem em si não checa pela existência ou a completude de um argumento em uma função devido a sua natureza dinâmica, por exemplo, se passarmos um valor string para uma variável que deveria ser um number, a linguagem não vai nos avisar e não vai reclamar. Veja um exemplo:

function foo (nome) {
    console.log(`Meu nome é ${nome}`) // Nome deveria ser uma string
}
  
foo('Lucas') // Meu nome é Lucas
foo() // Meu nome é undefined

Isso fica ainda mais evidente quando usamos objetos, pois vamos ter um erro de propriedade não existente:

function foo (args) {
    console.log(`Meu nome é ${args.nome}`) // Nome deveria ser uma string
}
  
foo({ nome: 'Lucas' }) // Meu nome é Lucas
foo() // Cannot read property 'nome' of undefined

Perceba que o erro não é descritivo, não está dizendo o que está acontecendo e pode estar vindo de qualquer objeto. Uma forma de contornar este problema é utilizando os argumentos opcionais com valores default! Porém, ao invés de dar um valor padrão, criamos uma função:

function required (argumentName) { throw new Error(`Argumento "${argumentName}" é obrigatório`)}
  
function foo (args = required('args.name')) {
    console.log(`Meu nome é ${args.nome}`) // Nome deveria ser uma string
}
  
foo({ nome: 'Lucas' }) // Meu nome é Lucas
foo() // Error: Argumento "args.name" é obrigatório

Podemos incrementar ainda mais com um erro nomeado e o nome da função:

class RequiredParameterError extends Error { constructor (argumentName, functionName) { super(`Argumento "${argumentName}" é obrigatório na função "${functionName}"`) } }
  
function required (argumentName, functionName) { throw new RequiredParameterError(argumentName, functionName)}
  
function foo (args = required('args.name', foo.name)) {
    console.log(`Meu nome é ${args.nome}`) // Nome deveria ser uma string
}
  
foo({ nome: 'Lucas' }) // Meu nome é Lucas
foo() // Error: Argumento "args.name" é obrigatório na função "foo"

Promisify

Uma das funções mais legais que apareceram no JavaScript foram as Promises. Desde então, muitas pessoas vem tentando transformar as APIs antigas (que ainda usavam callbacks) em Promises – leia mais sobre promises aqui – e isso é relativamente simples de fazer, porém exige um conhecimento sobre o fluxo assíncrono do JavaScript de uma forma que não é muito intuitivo para quem está começando agora.

Para transformarmos uma API que utiliza callbacks em uma API que utiliza promises, podemos fazer a seguinte mudança:

Uma nota: vamos exemplificar com as funções do módulo fs do Node.js, porém isso vale para qualquer função que aceite um callback no JavaScript
const fs = require('fs')
  
function readFileAsync (filePath) {
  return new Promise((resolve, reject) => {
    fs.readFile(filePath, (err, data) => {
        if (err) reject(err)
        resolve(data)
    })
  })
}
  
readFileAsync('./index.md')
    .then(console.log)
    .catch(console.error)

Além disso, para quem estiver trabalhando exclusivamente com Node.js, temos o pacote util, que contém uma função chamada promisify, que faz essencialmente o que acabamos de escrever:

const fs = require('fs')
const {promisify} = require('util')
const readFileAsync = promisify(fs.readFile)
  
readFileAsync('./index.md')
    .then(console.log)
    .catch(console.error)

Async Iterators

Para entender os async iterators, primeiramente é necessário ter um domínio dos iterators. Sendo muito simplista, o iterator é um Symbol interno que faz com que arrays e outras coleções sejam iteráveis, ou seja, faz com que possamos fazer um for sobre elas.

Estamos acostumados com o for … of … para iterar sobre objetos e arrays, porém quando temos um array de promises, isso não funciona muito bem… Temos que resolver as promises através de um Promise.all para depois conseguir realizar um for sobre os resultados. E se a gente conseguisse fazer as duas coisas juntas? Ai que entram os async iterators.

Não vou explicar a fundo como nenhum deles funciona aqui, mas você pode encontrar um artigo completo para iterators aqui e outro só para async iterators aqui.

Essencialmente, o async iterator é um iterator, só que ao invés de retornar um objeto ele retorna uma Promise, fazendo com que seja possível iterar por essa coleção de promises utilizando a instrução for await … of ….

O uso de async iterators é muito legal quando estamos trabalhando com recordSets em bancos de dados. Um recordSet é um objeto que é muito utilizado com o padrão DAO, ele permite que uma coleção muito grande de resultados de uma query sejam carregados de forma mais leve, pois este modelo não busca todos os resultados e os armazena em memória, ao invés disso, ele busca os resultados um por vez, somente retornando o próximo quando é pedido.

Vamos a um exemplo de uma classe Cursor para um banco de dados Oracle em Node.js, perceba que executamos uma query no banco de dados e, como resposta, obtemos um resultSet (que é a mesma coisa que o recordSet). Esse objeto tem um método chamado getRow que busca a próxima linha da resposta, vamos utilizar um async iterator aqui:

Primeiro fazemos a query:

const oracle = require('oracledb')
const options = {
  user: 'usuario',
  password: 'senha',
  connectString: 'string'
}
  
async function start () {
  const connection = await oracle.getConnection(options)
  const { resultSet } = await connection.execute('query', [], { outFormat: oracle.OBJECT, resultSet: true })
  return resultSet
}
  
start().then(console.log)

Então, criamos a classe do nosso cursor:

class Cursor {
  constructor (resultSet) {
    this.resultSet = resultSet
  }
  
  getIterable () {
    return {
      [Symbol.asyncIterator]: () => this._buildIterator()
    }
  }
  
  _buildIterator () {
    return {
      next: () => this.resultSet.getRow().then((row) => ({ value: row, done: row === undefined }))
    }
  }
}
  
module.exports = Cursor

Veja o [Symbol.asyncIterator] que é onde estamos definindo um novo iterador. Ele retorna uma Promise com o valor da linha atual. Então podemos atualizar a nossa chamada para poder iterar por ele:

const oracle = require('oracledb')
const options = {
  user: 'usuario',
  password: 'senha',
  connectString: 'string'
}
  
async function getResultSet () {
  const connection = await oracle.getConnection(options)
  const { resultSet } = await connection.execute('query', [], { outFormat: oracle.OBJECT, resultSet: true })
  return resultSet
}
  
async function start() {
  const resultSet = await getResultSet()
  const cursor = new Cursor(resultSet)
  
  for await (const row of cursor.getIterable()) {
    console.log(row)
  }
}
  
start()

Lazy Loading com Generators

Por último, se você gostou da implementação de iteradores assíncronos, saiba que podemos fazer isso também de forma síncrona! Para isso vamos utilizar o primo mais velho dos iterators, os generators.

Generators são essencialmente funções que retornam um ponteiro e pausam sua execução até que a próxima chamada seja feita, em geral, o papel de um generator é controlar o fluxo de um programa, já que ele pode pausar e voltar a sua execução usando a keyword yield.

Para criar um generator, usamos o caractere * na frente da palavra function: function* generator () {}.

Isso pode ser muito útil para implementar o que é chamado de lazy iteration, ou seja, buscamos os valores de forma preguiçosa, só carregando os mesmos quando são muito necessários, imagine que você precisa fazer uma série de cálculos pesados em cima de uma lista de valores, mas somente quando é requisitado o resultado. Não faz sentido calcular isso tudo de antemão, então podemos recorrer a iteração no formato lazy.

Para exemplificar vamos usar um gerador infinito de números aleatórios:

function* randomGen () {
  while (true){
    yield Math.floor(Math.random()*1000)
  }
}
  
const gen = randomGen()
console.log(gen.next().value) // 45

Conclusão

O JavaScript é uma linguagem de muitas facetas e permite que você faça várias coisas legais de forma simples! Mas ela também pode ser bastante complexa e ter ferramentas que você nunca imaginou!

Conhece alguma outra técnica? Comenta aqui ou então vá lá no meu blog e se inscreva na newsletter para receber semanalmente uma série de conteúdos exclusivos e notícias sobre programação e tecnologia!