Array.includes vs Array.some
Array em JavaScript é uma estrutura de dados poderosa. Funções como Array.map,Array.filter e Array.reduce resolvem uma grande quantidade de problemas do dia a dia. No entanto, há duas funções parecidas, mas essencialmente diferentes que fazem parte do seu arsenal. Neste artigo veremos as funções Array.includes e Array.some, inclusive pegadinhas e quando aplicar uma ou outra.
O problema
Temos um simples array de números. Precisamos verificar se o número 15 já existe na lista:
const numbers = [10, 11, 15, 20];
const searchNumber = 15;
let found = false;
for(number of numbers) {
if(number === 15) {
found = true;
break;
}
}
if(found) console.log(`${searchNumber} already exists!`);
Nada excepcional aqui, a não ser o uso de for…of introduzido no ES2015 (ES6) que pode ser substituído por um loop for ou while. No entanto, podemos simplificar bastante o código utilizando um recurso introduzido no ES2017 (ES7).
A função Array.includes
No ES2017 (ES7) foi adicionada a função Array.includes. Vejamos o código anterior refatorado para utilizar a nova função:
const numbers = [10, 11, 15, 20];
const searchNumber = 15;
if(numbers.includes(searchNumber))
console.log(`${searchNumber} already exists!`);
Refatorar é alterar a estrutura do código sem mudar seu comportamento final.
A função Array.includes retornará true apenas se o elemento existir. Muito mais enxuto, não? Todavia, essa simplicidade pode levar o desenvolvedor ao erro.
Resultado não esperado
Vamos repetir o exemplo da busca com Array.includes, mas desta vez pesquisando em uma lista de objetos que representam livros:
const books = [
{
name: 'Cangaceiro JavaScript',
author: 'Flávio Almeida',
isbn: '9788594188014'
},
{
name: 'MEAN',
author: 'Flávio Almeida',
isbn: '9788555190476'
}
];
const searchBook = {
name: 'Cangaceiro JavaScript',
author: 'Flávio Almeida',
isbn: '9788594188014'
};
if(books.includes(searchBook))
console.log(`${JSON.stringify(searchBook)} already exists!`);
Por mais que a função Array.includes tenha funcionado no exemplo anterior, ela parece ser incapaz de encontrar nosso livro. Qual a razão desse comportamento errático?
Comparações entre tipos
Internamente, Array.includes realiza uma comparação através de ===. No entanto, comparações com === ou == só funcionam como esperado (comparar o valor) com os tipos:
- Boolean
- Null
- Undefined
- Number
- String
- Symbol (ES2015)
Quando comparamos variáveis que referenciam o tipo object, o interpretador testará se elas apontam para um mesmo objeto em memória. Vejamos essa máxima em um exemplo de escopo menor:
const object1 = { name: 'Flávio'};
const object2 = { name: 'Flávio'};
// false
console.log(object1 == object2);
// false
console.log(object1 === object2);
Porém, o resultado será verdadeiro se fizermos:
const object1 = { name: 'Flávio'};
// a variável de referência object2
// passou a apontar para o mesmo objeto
// referenciado por object1
const object2 = object1;
// true
console.log(object1 == object2);
// true
console.log(object1 === object2);
Nesse contexto, podemos fazer Array.includes retornar true da seguinte maneira:
const books = [
{
name: 'Cangaceiro JavaScript',
author: 'Flávio Almeida',
isbn: '9788594188014'
},
{
name: 'MEAN',
author: 'Flávio Almeida',
isbn: '9788555190476'
}
];
// A variável searchBook agora
// aponta para o mesmo objeto em books[0]
const searchBook = books[0];
if(books.includes(searchBook))
console.log(`${JSON.stringify(searchBook)} already exists!`);
Por mais que essa solução funcione, não faz sentido utilizá-la quando buscamos por um objeto que não sabemos sua posição ou muito menos se existe. Nessa situação, podemos recorrer ao bom e velho Array.some, veterano do ES5.
Utilizando o humilde Array.some
Vejamos o código anterior utilizando a função Array.some:
const books = [
{
name: 'Cangaceiro JavaScript',
author: 'Flávio Almeida',
isbn: '9788594188014'
},
{
name: 'MEAN',
author: 'Flávio Almeida',
isbn: '9788555190476'
}
];
const searchBook = {
name: 'Cangaceiro JavaScript',
author: 'Flávio Almeida',
isbn: '9788594188014'
};
if(books.some(book => book.isbn == searchBook.isbn))
console.log(`${JSON.stringify(searchBook)} already exists!`);
A função Array.some itera em cada elemento do array aplicando uma lógica de comparação. Ela abortará a iteração imediatamente assim que encontrar o primeiro item que retorne true na comparação. Seu retorno será true caso exista algum (some) elemento que se coadune com o critério utilizado.
O critério utilizado foi comparar a propriedade isbn dos objetos. Como a propriedade armazena uma string, comparações com == ou === utilizam o valor primitivo e não a referência.
Ainda podemos comparar todas as propriedades do objeto com o seguinte truque:
// código anterior omitido
const searchBook = {
name: 'Cangaceiro JavaScript',
author: 'Flávio Almeida',
isbn: '9788594188014'
};
const bookAsJSON = JSON.stringify(searchBook);
if(books.some(book => JSON.stringify(book) == bookAsJSON))
console.log(`${bookAsJSON} already exists!`);
Através de JSON.stringify convertemos um objeto JavaScript em um JSON que nada mais é do que a representação textual de um objeto. Sendo uma representação através de uma string, podemos utilizar === ou == que a comparação utilizará seu valor.
Uma das grandes vantagens de Array.some é podermos definir uma lógica de comparação mais rebuscada.
Conclusão
Nem sempre recursos mais modernos resolvem problemas do dia a dia. É importante conhecer o problema e também o recurso utilizado para resolvê-lo para não termos surpresas.
E você? Já se surpreendeu com Array.includes? Já precisou utilizar Array.some para resolver algum problema? Deixe sua opinião.