• #javascript

Criando seu próprio pick e omit em JavaScript

Publicado em ago, 12 2021


Em nosso dia a dia de trabalho com JavaScript, é bastante comum criarmos novos objetos a partir de outros, mas apenas com algumas chaves do objeto original. E para este trabalho, temos ao menos duas possibilidades:

  1. Selecionando os campos que queremos

  2. Ignorando os campos que não queremos

Provavelmente a lib de função utilitária mais usada hoje em dia, o lodash, possui duas funções para essas abordagens: a pick, que gera um novo objeto selecionando as keys, e a função omit, que faz o inverso.

Hoje o objetivo é trazer uma implementação simples dessas duas funções, e aproveitar e trazer para a mesa conceitos como recursividade e referência. Vamos lá?

Função pick

Primeiramente, vamos criar uma função que aceita dois valores (um array de strings e um objeto) e retorna um novo objeto.

const pick = (keys, original) => {
  return {};
};

A função acima não faz nada além do que retornar um objeto vazio, vamos colocar nossa lógica nela:

const pick = (keys = [], original = {}) => {
  return keys.reduce((acc, currentKey) => {
    acc[currentKey] = original[currentKey];
    return acc;
  }, {});
};

Bastante coisa não? Vamos por partes.

Primeiro, a função Array.reduce aqui tem por objetivo permitir que eu itere sobre o meu array de keys, e construa um novo dado (no caso, um objeto), a partir dessas keys. E tudo isso ocorre através de recursão. Existe um texto excelente da Victoria Drake em que ela explica o assunto de recursão e a função reduce usando uma torta de maçã 🙂.

Segundo a assinatura da função reduce, ela recebe como parâmetro:

  • Uma função de callback, que recebe o dado incrementado (no caso, o meu objeto) como primeiro parâmetro; o dado atual do meu conjunto (no caso) a key em questão, como segundo parâmetro; e por fim, o índice do dado atual - não preciso dele aqui; e

  • Um valor inicial, no caso, um objeto vazio.

O que a minha função de callback faz? Se reparamos, ela possui apenas duas instruções:

acc[currentKey] = original[currentKey];

O JavaScript tem algo bem bacana que é permitir que eu acesse e altere um campo de um objeto de maneira dinâmica. Explico.

Imagine que eu tenho o seguinte objeto:

const user = {
  name: 'Emanuel',
  age: 24,
  hobbies: ['play chess', 'watch series', 'watch movies'],
};

Eu posso acessar as chaves desse objeto, ao menos de três maneiras:

user.name // 'Emanuel'

user['name'] // 'Emanuel'

// magia negra
const key = 'name'
user[key] // 'Emanuel'

Como eu não sei quais chaves eu receberei na minha função pick, eu uso a última abordagem para dinamicamente selecionar as chaves do meu objeto original.

Por fim, o retorno da função de callback é necessária porque este retorno será usado na próxima vez que a função de callback for executada na minha pilha de chamadas da função reduce (que é uma função recursiva)

Função omit

A função omit possui um funcionamento parecido com a pick, porém, o objetivo dela é retornar um objeto que não possui as chaves que eu for passar. Uma implementação básica dessa função poderia ser:

const omit = (keys = [], original = {}) => {
  return Object.keys(original).reduce((acc, currentKey) => {
    if (!keys.includes(currentKey)) {
      acc[currentKey] = original[currentKey]
    }

    return acc
  }, {})
}

O que há de diferente aqui?

  1. Para que saibamos quais keys vamos retornar, podemos usar o Object.keys do JavaScript que retorna um array com as keys daquele objeto e usamos esse array no reduce;

  2. Dentro do callback do reduce, a gente verifica se a key do objeto original consta no array de keys que eu quero ignorar. Senão consta, significa que eu quero essa key no meu objeto final.

Já discutimos as duas funções acima, vamos trazer para a mesa o assunto da referência?

A temida referência

Imaginemos que eu tenha dois objetos, um objeto original e um outro gerado a partir do original:

const original = {
  name: 'Emanuel',
  age: 24,
  hobbies: ['play chess', 'watch series', 'watch movies'],
};

const newObject = pick(['name', 'hobbies'], original);

Agora, eu quero alterar o campo hobbies deste novo objeto, adicionando um novo hobby:

newObject.hobbies.push('play guittar');

Até aqui, tudo ok, correto? Mas vamos conferir o que acontece com essa mesma key no objeto original:

console.log(original.hobbies);

// ['play chess', 'watch series', 'watch movies', 'play guittar']

Bem, agora o novo hobby também está no objeto original. Mas porquê isso ocorre? Vamos voltar à nossa função pick, mais especificamente quando a gente altera o novo objeto:

acc[currentKey] = original[currentKey]

Quando se é executado a linha acima, no caso de objetos e arrays por exemplo, você não está passando o valor para o novo objeto, mas sim sua referência. Este tópico é extenso e recomendo a leitura do Blog do Fernando Daciuk sobre o assunto.

Em resumo: mesmo que ao criarmos nossa função pick e omit, elas retornem um novo objeto, podemos ainda ter problemas com referência se os valores das chaves no novo objeto apontarem para estruturas de dados que são passadas como referência e não como valor (Arrays e Objects são um exemplo).


Links úteis:


Bom pessoal, espero que este texto tenha sido proveitoso e servido como uma forma de exercício nesta linguagem tão querida por nós e que permeia a web. Até a próxima!