Desenvolvimento

8 mai, 2019

O lado escuro do JavaScript

Publicidade

Usamos JavaScript todos os dias. Quando não estamos usando-o diretamente, estamos usando alguma aplicação que é baseada nele.

Atualmente, quase todas as aplicações que executam na web rodam algum tipo de JavaScript, ou então até mesmo nossas aplicações desktop estão executando alguma coisa parecida com ele.

Toda essa visibilidade e fama deu a nós, desenvolvedores, formas padrões de resolução de erros. Não estou falando de padrões de projeto, estou falando das formas que temos de resolver problemas comuns, como remover callbacks, ordenar um array e iterar através de objetos.

Isso fez com que usássemos sempre as mesmas funcionalidades da linguagem. Mas você sabia que existem muito mais coisas no JavaScript que quase nunca usamos?

Bitwise Operators

Operadores bitwise existem desde os primórdios das linguagens de programação. Basicamente, todas as linguagens já criadas têm algum tipo de sistema de operação bit a bit.

Um bitwise operator realiza operações diretamente nos valores binários que estão sendo operados. As operações comuns são as operações básicas da lógica booleana.

Todas as operações bitwise vão passar pelos bits um a um do primeiro valor comparado e vão comparar com a mesma posição do segundo valor.

AND

Representado por &, seta um bit para 1, se os dois bits das duas posições comparadas forem 1:

const valor1 = 5 // 0101 em binário
const valor2 = 1 // 0001 em binário

5 & 1 // 0001

Utilizaremos este como exemplo para todos os demais. Vamos mapear nossos bits da seguinte maneira:

Ao aplicar 5 & 1, vamos passar primeiramente pelo bit da direita (D). Compararemos o I com II, e se ambos forem 1, o resultante III será 1 também.

OR

Representado por |, seta o bit comparado para 1 se um dos dois valores comparados tiverem a mesma posição com valor 1.

const valor1 = 5 // 0101 em binário
const valor2 = 1 // 0001 em binário

5 | 1 // 0101

XOR

Representado por ^ faz a mesma coisa que o OR, porém, só vai setar um bit para 1 se os bits forem diferentes.

const valor1 = 5 // 0101 em binário
const valor2 = 1 // 0001 em binário

5 ^ 1 // 0100

Caso de uso

O XOR é muito utilizado para criptografia de chaves. Imagine que temos duas pessoas querendo se comunicar – ambas conhecem uma chave comum, então a uma pessoa poderia enviar a mensagem criptografada e a outra poderia receber do outro lado usando o XOR:

const chave = 19485
const mensagem = 42

// Antes de enviarmos, encriptamos a mensagem
const criptografada = mensagem ^ chave // 19511

// Ao recebermos o número 19511 saberemos que podemos aplicar a chave novamente
const descriptografada = mensagem ^ chave // 42

NOT

Representado por ~, inverte todos os bits de um valor.

const valor1 = 5 // 0101 em binário

~5 // 1010

Caso de uso

Usando o operador ~ podemos checar se um elemento existe ou não em um array. Por exemplo, ao invés de usarmos o modelo tradicional:

const x = [1,2,3,4,5,6]

if (x.indexOf(2) > -1) // forma 1

Podemos simplesmente fazer da seguinte maneira:

const x = [1,2,3,4,5,6]
if(~x.indexOf(2))

Isso porque o ~ transforma o -1 em 0. Portanto, quando o item não existe, ele já retornaria false. Hoje temos o operador includes, que faz basicamente a mesma coisa.

Left Shift

Representado por <<, vai mover todos os valores uma posição para a esquerda, acrescentando um zero na ponta direita.

const valor1 = 5 // 0101 em binário
const valor2 = 1 // 0001 em binário

5 << 1 // 1010

Da mesma forma temos o Right Shift que se divide em duas partes:

  • Signed right shift: >> vai rotacionar os bits para a direita, colocando uma cópia do bit mais à esquerda e ignorando o bit à direita
  • Zero fill right shift: >>> o mesmo que o anterior, porém, vai incluir um zero a partir da esquerda

Caso de uso:

Um dos grandes casos de uso para bitwise operators é que podemos operar multiplicações e divisões sobre qualquer número, apenas realizando um shift nos seus bits.

Isso é útil, por exemplo, quando estamos trabalhando com aplicações que exigem alto desempenho, como renderização 3D.

Se quisermos multiplicar um número por qualquer base de 2 (2^1=2, 2^2=4, 2^3=8), o segredo é realizar um left shift:

5 * 2 // 10
5 << 1 // 10, só que mais rápido

Veja que o número depois do operador << é o número de shifts que vamos fazer. Para cada shift temos o aumento de 1 na potência:

5 * 4 // 5 << 2
5 * 8 // 5 << 3
5 * 1024 // 5 << 10

Da mesma, forma se quisermos dividir, basta executarmos um right shift:

5 / 2 // 5 >> 1

Bônus

Podemos exibir o número de forma binária diretamente como string através do comando:

5..toString(2) // 101 (zero à esquerda é removido)

Dinamismo com Eval

O Eval é uma função bem maligna da linguagem. Ele permite que uma string seja interpretada como código. Isso abre caminho para todo o tipo de problema possível de injeção de código, desde que ele não esteja sendo executado em strict mode.

O Strict mode é um modo de execução do JS que impede o sistema de fazer algumas coisas más. Uma dessas coisas é o impedimento do eval de criar variáveis fora do seu escopo, por exemplo:

let valor = 10
let resultado = eval('x + 10') // 20

eval('var z = x * 4')
console.log(z) // 40

Veja que estamos criando uma nova variável z fora do escopo (isso também se dá porque criamos no escopo global com var), porém, se ativamos o modo estrito:

'use strict'
let valor = 10
let resultado = eval('x + 10') // 20

eval('var z = x * 4')
console.log(z) // z is not defined

Funções são funções

Em 2018, durante o JSExperience, dei uma palestra sobre “Herança e protótipos no JS“. Em um determinado momento da palestra falo sobre como funções herdam propriedades de outros objetos.

Deixando detalhes de lado, uma das coisas mais interessantes no Javascript, é que grande parte do que usamos para declarar tipos primitivos são, na verdade, funções que retornam um objeto com um protótipo diferente. Uma dessas funções é – veja só – a própria Function.

Isso significa que podemos criar funções de forma completamente dinâmica usando apenas strings:

let multiplicar = Function('x', 'y', 'return x*y')
multiplicar(2,2) // 4

Os n primeiros argumentos da função são os parâmetros dela. O último argumento é o corpo da mesma – você pode checar mais sobre isso aqui.

Operador in

Quando queremos verificar se uma propriedade existe em um objeto, uma das primeiras soluções que nos vêm à mente é esta:

if (Object.keys(obj).includes('minhaprop')) { /* propriedade existe */ }

Porém, existe uma outra forma de verificarmos uma propriedade dentro de um objeto, muito mais simples e legível: o operador in:

if ('minhaprop' in obj) { /* propriedade existe */ }

A diferença principal entre o in e o keys() é que o método Object.keys() não enumera propriedades que estejam na cadeia de protótipos deste objeto. Ou seja, ele só mostrará as propriedades criadas pelo usuário, e não as propriedades herdadas de outros objetos.

O in lista todas as propriedades, inclusive as de seus protótipos – por isso que o for .. in .. retorna sempre uma lista de chaves ao invés de valores (como é o caso do for .. of).

Propriedades somente leitura

Uma das grandes reclamações de desenvolvedores JavaScript é que não temos modificadores de acesso, como public e private nativos (embora isso possa mudar com os modificadores de classe).

Há muitos anos e até hoje utilizamos um método pouco comum em objetos que nos permite modificar e definir propriedades que são somente leitura. Isso ainda permite que a propriedade seja vista de fora, porém, não permite a alteração – o que já é um começo.

Esse método é o Object.defineProperties e sua variação singular Object.defineProperty:

const objeto = {}
const atributos = {
  writable: false, /* não podemos sobrescrever o valor desta propriedade */ 
  value: 10, /* Valor inicial */ 
  enumerable: false, /* a propriedade não vai ser enumerada */
  configurable: false /* a propriedade não pode ser alterada ou removida */
}
Object.defineProperty(objeto, 'propriedade', atributos)

É possível também passar uma chave get e set, que são funções ou valores que serão chamados sempre que a propriedade for lida ou escrita.

Object.freeze

Este é um método que já foi bastante usado, principalmente para desenvolvimento front-end. O método freeze de um objeto “congela” este objeto de forma que ele não possa mais ser alterado.

Congelar significa que não vamos mais poder criar novas propriedades, remover propriedades existentes, e também não vamos mais poder mudar a ordem de enumeração, permissões de escrita ou leitura dessas propriedades e, por fim, não poderemos alterar os seus valores atuais.

const objeto = { name: 'Lucas' }
Object.freeze(objeto) // vai retornar a mesma instancia do objeto original

objeto.idade = 24 // Erro se estivermos no strict mode

Fora do strict mode, não vamos ter nenhum erro sendo disparado. Porém, o objeto não será alterado, ou seja, se tentarmos executar um console.log no mesmo objeto depois de tentar incluir a propriedade idade nele, vamos obter somente { nome: ‘Lucas’ }.

Gerando ranges

Algumas linguagens de programação (como o Python), possuem estruturas internas para gerar um intervalo de números.

Por exemplo, como poderíamos gerar um intervalo de números de 1 a 10 em um array em JavaScript? A solução mais óbvia e mais comum é iterar de 1 a 10 e incluir cada item em um array.

Depois que os operadores rest/spread foram lançados no ES6, agora temos uma forma muito mais eficaz e limpa de gerar um range de valores:

const range = [...Array(10).keys()] // [0,1,2,3,4,5,6,7,8,9]

Ou, se quisermos gerar um array começando de um determinado valor, podemos lembrar que a função from recebe uma função de mapeamento como segundo argumento:

const range2 = Array.from({length: 10}, (valor, chave) => chave + 1)

Promise.finally

Promises são o calcanhar de Aquiles para muitos desenvolvedores experientes e novatos – tanto é que escrevi um artigo somente sobre isso em meu Medium. Em determinado momento conversamos sobre um dos métodos mais interessantes das Promises: o finally.

Assim como o finally que estamos acostumados no try/catch, o finally de uma Promise será sempre executado, independente de haver erro ou sucesso:

umaPromise
  .then(console.log)
  .catch(console.error)
  .finally(() => console.log('Fim'))

Neste exemplo, temos que qualquer sucesso será direcionado para a saída padrão, e qualquer erro será redirecionado para a saída de erros. Independente do resultado, sempre teremos um texto “Fim” sendo printado ao final da Promise.

Atomics.exchange

Este é um método muito específico, porém, muito útil. Todos os métodos da classe Atomics trabalham com o que chamamos de TypedArrays, que nada mais são do que uma visualização em forma de array de um buffer de dados.

Por consequência disso, todos os TypedArrays são numéricos e levam como argumento um tamanho em bytes.

O que o Atomics.exchange faz, é trocar o valor de uma posição deste array por outro valor, devolvendo o valor antigo no final da execução.

A parte interessante é que o Atomics, como o próprio nome já diz, vai garantir que não haja nenhuma operação de escrita ou leitura enquanto o exchange está em processo.

const buffer = new SharedArrayBuffer(16) // Buffer de 16 bytes
const array = new Uint8Array(buffer)
array[0] = 10
array[1] = 20

console.log(Atomics.load(array, 0)) // Lê a posição 0 -> 10

const old = Atomics.exchange(array, 0, 5) // Troca a posição 0 por 5
console.log(old) // 10
console.log(Atomics.load(array, 0)) // 5

Isso é especialmente útil quando estamos trabalhando com escritas concorrentes em fluxos assíncronos.

Construção dinâmica por reflexão

Quando criamos uma classe, estamos acostumados a escrever o famoso new. O que ele faz é basicamente chamar um método constructor da classe que retorna a instância em questão.

O JS tem uma API de reflexão muito boa, ou seja, podemos executar nosso próprio código dentro dele mesmo. Os construtores dinâmicos são um exemplo:

class Person {
  constructor (name, age) {
    this.name = name
    this.age = age
  }
}

const Lucas = new Person('Lucas', 24) // Person {name: 'Lucas', age: 24}
const Ana = Reflect.construct(Person, ['Ana', 22]) // Person {name: 'Ana', age: 22}

Lembre-se de que funções também são construtores, então também podemos fazer algo assim:

function sum (a,b) { return a+b }
const c = Reflect.construct(sum, [1,2]) // 3

Conclusão

Estes são alguns dos métodos menos conhecidos do JavaScript para determinados casos específicos. Existem muitos mais! Dê uma olhada na lista completa de métodos do JS pelo MDN e veja muitos outros métodos que ainda não conhecíamos. Um deles pode ser muito útil para você!

Além destes métodos, temos alguns conceitos no JavaScript que também são pouco conhecidos:

Além disso, este site contém alguns dos exemplos que dei neste artigo e algumas outras coisas diferentes também.

Até mais!