JavaScript: filter () para objetos

187

O ECMAScript 5 possui o filter()protótipo para Arraytipos, mas não para Objecttipos, se bem entendi.

Como eu implementaria um filter()for Objects em JavaScript?

Digamos que eu tenho esse objeto:

var foo = {
    bar: "Yes"
};

E eu quero escrever um filter()que funcione em Objects:

Object.prototype.filter = function(predicate) {
    var result = {};

    for (key in this) {
        if (this.hasOwnProperty(key) && !predicate(this[key])) {
            result[key] = this[key];
        }
    }

    return result;
};

Isso funciona quando eu o uso na demonstração a seguir, mas quando o adiciono ao meu site que usa jQuery 1.5 e jQuery UI 1.8.9, recebo erros de JavaScript no FireBug.

AgileMeansDoAsLittleAsPossible
fonte
Quais erros você recebe, especificamente?
NT3RP 21/02
Quais são os erros que você está recebendo? Publicá-las, se possível :)
Zack A Human
Há um pouco de histórico ambíguo em jQuery e scripts que se estendem Object.prototype: bugs.jquery.com/ticket/2721
Crescent Fresh
exatamente o que eu precisava, exceto que você deve remover o "!" no predicado! (essa [chave]) para ter o método de filtro real.
NoxFly 25/03

Respostas:

201

Nunca, nunca estenda Object.prototype.

Coisas horríveis acontecerão com o seu código. As coisas vão quebrar. Você está estendendo todos os tipos de objetos, incluindo literais de objetos.

Aqui está um exemplo rápido que você pode tentar:

    // Extend Object.prototype
Object.prototype.extended = "I'm everywhere!";

    // See the result
alert( {}.extended );          // "I'm everywhere!"
alert( [].extended );          // "I'm everywhere!"
alert( new Date().extended );  // "I'm everywhere!"
alert( 3..extended );          // "I'm everywhere!"
alert( true.extended );        // "I'm everywhere!"
alert( "here?".extended );     // "I'm everywhere!"

Em vez disso, crie uma função para a qual você passa o objeto.

Object.filter = function( obj, predicate) {
    let result = {}, key;

    for (key in obj) {
        if (obj.hasOwnProperty(key) && !predicate(obj[key])) {
            result[key] = obj[key];
        }
    }

    return result;
};
user113716
fonte
61
@patrick: dê um pão a um homem e você irá alimentá-lo por um dia, ensiná-lo a assar e você o alimentará por toda a vida (ou algo assim, sou dinamarquês, não sei o que é o inglês correto) ;)
Martin Jespersen
13
Você está fazendo errado ... predicado (obj [key]) deve ser predicado (obj [key])!
pyrotechnick
6
@pyrotechnick: Não. Primeiro, o ponto principal da resposta é não estender Object.prototype, mas apenas colocar a função Object. Segundo, este é o código do OP . Claramente, a intenção do OP é .filter()ser tal que filtre os resultados positivos. Em outras palavras, é um filtro negativo, onde um valor de retorno positivo significa que ele é excluído do resultado. Se você olhar para o exemplo jsFiddle, ele está filtrando as propriedades existentes que são undefined.
user113716
4
@patrick dw: Não. Primeiro, eu não mencionei a extensão / não extensão de protótipos. Segundo, developer.mozilla.org/en/JavaScript/Reference/Global_Objects/… - "Cria uma nova matriz com todos os elementos que passam no teste implementado pela função fornecida." Implementar exatamente o oposto em um mundo global parece bem bobo, não é?
pirotecnia
8
@pyrotechnick: Isso mesmo, você não mencionou a extensão / não extensão de protótipos, e esse é o meu ponto. Você disse que estou fazendo errado, mas a única coisa que estou fazendo é dizer ao OP para não estender Object.prototype. Da pergunta: "Isso funciona ..., mas quando o adiciono ao meu site ..., recebo erros de JavaScript" Se o OP decidir implementar um .filter()com o comportamento oposto ao de Array.prototpe.filter, isso é com ele. Deixe um comentário sob a pergunta se quiser notificar o OP de que o código está errado, mas não me diga que estou fazendo errado quando não é o meu código.
user113716
284

Primeiro de tudo, é considerado uma prática ruim estender-seObject.prototype . Em vez disso, fornecer o seu recurso como função utilidade em Object, assim como já existem Object.keys, Object.assign, Object.is, ... etc.

Eu forneço aqui várias soluções:

  1. Usando reduceeObject.keys
  2. Como (1), em combinação com Object.assign
  3. Usando mape espalhando sintaxe em vez dereduce
  4. Usando Object.entrieseObject.fromEntries

1. Usando reduceeObject.keys

Com reducee Object.keyspara implementar o filtro desejado (usando a sintaxe de seta ES6 ):

Object.filter = (obj, predicate) => 
    Object.keys(obj)
          .filter( key => predicate(obj[key]) )
          .reduce( (res, key) => (res[key] = obj[key], res), {} );

// Example use:
var scores = {
    John: 2, Sarah: 3, Janet: 1
};
var filtered = Object.filter(scores, score => score > 1); 
console.log(filtered);

Observe que no código acima predicatedeve ser uma condição de inclusão (ao contrário da condição de exclusão usada pelo OP), para que esteja alinhado com o Array.prototype.filterfuncionamento.

2. Como (1), em combinação com Object.assign

Na solução acima, o operador de vírgula é usado na reduceparte para retornar o resobjeto mutado . Obviamente, isso pode ser escrito como duas declarações em vez de uma expressão, mas a última é mais concisa. Para fazê-lo sem que o operador vírgula, você poderia usar Object.assignem vez disso, o que faz retornar o objeto mutante:

Object.filter = (obj, predicate) => 
    Object.keys(obj)
          .filter( key => predicate(obj[key]) )
          .reduce( (res, key) => Object.assign(res, { [key]: obj[key] }), {} );

// Example use:
var scores = {
    John: 2, Sarah: 3, Janet: 1
};
var filtered = Object.filter(scores, score => score > 1); 
console.log(filtered);

3. Usando map e espalhar sintaxe em vez dereduce

Aqui, movemos a Object.assignchamada para fora do loop, para que seja feita apenas uma vez e passamos as chaves individuais como argumentos separados (usando a sintaxe de propagação ):

Object.filter = (obj, predicate) => 
    Object.assign(...Object.keys(obj)
                    .filter( key => predicate(obj[key]) )
                    .map( key => ({ [key]: obj[key] }) ) );

// Example use:
var scores = {
    John: 2, Sarah: 3, Janet: 1
};
var filtered = Object.filter(scores, score => score > 1); 
console.log(filtered);

4. Usando Object.entrieseObject.fromEntries

Como a solução converte o objeto em uma matriz intermediária e depois a converte em um objeto simples, seria útil usar Object.entries(ES2017) e o oposto (por exemplo, criar um objeto a partir de uma matriz de pares de chave / valor ) comObject.fromEntries ( ES2019).

Isso leva a esse método "one-liner" em Object:

Object.filter = (obj, predicate) => 
                  Object.fromEntries(Object.entries(obj).filter(predicate));

// Example use:
var scores = {
    John: 2, Sarah: 3, Janet: 1
};

var filtered = Object.filter(scores, ([name, score]) => score > 1); 
console.log(filtered);

A função predicado obtém um par de chave / valor como argumento aqui, que é um pouco diferente, mas permite mais possibilidades na lógica da função predicada.

trincot
fonte
Poderia ser uma consulta mais complexa? Por exemplo:x => x.Expression.Filters.Filter
IamStalker
2
@IamStalker, você tentou? Não importa, desde que você forneça uma função válida no segundo argumento. NB: Eu não tenho idéia do que .Filterestá no final, mas se for uma função, você precisa chamá-lo ( x => x.Expression.Filters.Filter())
trincot
1
Recursos mais recentes podem gerar menos código, mas também desempenho mais lento . O código compatível com Ed 3 é executado duas vezes mais rápido na maioria dos navegadores.
RobG
1
Versão original datilografado da última variante: gist.github.com/OliverJAsh/acafba4f099f6e677dbb0a38c60dc33d
Oliver Joseph Ash
1
Bom trabalho! Obrigado por estas excelentes explicações e exemplos.
Slavik Meltser 13/07
21

Se você estiver disposto a usar sublinhado ou lodash , poderá usar pick(ou o oposto omit).

Exemplos dos documentos de sublinhado:

_.pick({name: 'moe', age: 50, userid: 'moe1'}, 'name', 'age');
// {name: 'moe', age: 50}

Ou com um retorno de chamada (para lodash , use pickBy ):

_.pick({name: 'moe', age: 50, userid: 'moe1'}, function(value, key, object) {
  return _.isNumber(value);
});
// {age: 50}
Bogdan D
fonte
1
O lodash é uma solução ruim porque a filtragem de objetos vazios também removerá números.
Mibbit
Acabei de usar o lodash para isso e é uma ótima solução. Obrigado @Bogdan D!
Ira Herman
@ mibbit você pode ser mais específico? Eu acredito que é apenas uma questão de implementar o retorno de chamada corretamente
Bogdan D
11

ES6Abordagem ...

Imagine que você tem este objeto abaixo:

const developers = {
  1: {
   id: 1,
   name: "Brendan", 
   family: "Eich"
  },
  2: {
   id: 2,
   name: "John", 
   family: "Resig"
  },  
  3: {
   id: 3,
   name: "Alireza", 
   family: "Dezfoolian"
 }
};

Crie uma função:

const filterObject = (obj, filter, filterValue) => 
   Object.keys(obj).reduce((acc, val) => 
   (obj[val][filter] === filterValue ? acc : {
       ...acc,
       [val]: obj[val]
   }                                        
), {});

E chame:

filterObject(developers, "name", "Alireza");

e retornará :

{
  1: {
  id: 1,
  name: "Brendan", 
  family: "Eich"
  },
  2: {
   id: 2,
   name: "John", 
   family: "Resig"
  }
}
Alireza
fonte
1
Parece bom! Mas por que ele retorna apenas o outro objeto (e não aquele com o nome / filterValue de "Alireza")?
Pille
1
@Pille, o OP solicitou que fosse assim (observe o filtro negativo com !predicateseu próprio código).
trincot
7

Como Patrick já declarou, essa é uma péssima idéia, pois quase certamente quebrará qualquer código de terceiros que você possa desejar usar.

Todas as bibliotecas como jquery ou prototype serão interrompidas se você estender Object.prototype, o motivo é que a iteração lenta sobre objetos (sem hasOwnPropertyverificações) será interrompida, pois as funções adicionadas farão parte da iteração.

Martin Jespersen
fonte
votado por explicar o "porquê" de ser uma má ideia de forma clara e concisa.
Michael Liquori
5

E se:

function filterObj(keys, obj) {
  const newObj = {};
  for (let key in obj) {
    if (keys.includes(key)) {
      newObj[key] = obj[key];
    }
  }
  return newObj;
}

Ou...

function filterObj(keys, obj) {
  const newObj = {};
  Object.keys(obj).forEach(key => {
    if (keys.includes(key)) {
      newObj[key] = obj[key];
    }
  });
  return newObj;
}
shaunw
fonte
4

Dado

object = {firstname: 'abd', lastname:'tm', age:16, school:'insat'};

keys = ['firstname', 'age'];

então :

keys.reduce((result, key) => ({ ...result, [key]: object[key] }), {});
// {firstname:'abd', age: 16}

Abdennour TOUMI
fonte
3
Isso não usa uma determinada função de predicado, conforme exigido pela pergunta.
#
4

Eu criei um Object.filter() que não apenas filtra por uma função, mas também aceita uma matriz de chaves para incluir. O terceiro parâmetro opcional permitirá que você inverta o filtro.

Dado:

var foo = {
    x: 1,
    y: 0,
    z: -1,
    a: 'Hello',
    b: 'World'
}

Matriz:

Object.filter(foo, ['z', 'a', 'b'], true);

Função:

Object.filter(foo, function (key, value) {
    return Ext.isString(value);
});

Código

Isenção de responsabilidade : optei por usar o núcleo Ext JS por questões de brevidade. Não considerou necessário escrever verificadores de tipos para objetos, pois isso não fazia parte da pergunta.

// Helper function
function print(obj) {
    document.getElementById('disp').innerHTML += JSON.stringify(obj, undefined, '  ') + '<br />';
    console.log(obj);
}

Object.filter = function (obj, ignore, invert) {
    let result = {}; // Returns a filtered copy of the original list
    if (ignore === undefined) {
        return obj;   
    }
    invert = invert || false;
    let not = function(condition, yes) { return yes ? !condition : condition; };
    let isArray = Ext.isArray(ignore);
    for (var key in obj) {
        if (obj.hasOwnProperty(key) &&
                !(isArray && not(!Ext.Array.contains(ignore, key), invert)) &&
                !(!isArray && not(!ignore.call(undefined, key, obj[key]), invert))) {
            result[key] = obj[key];
        }
    }
    return result;
};

let foo = {
    x: 1,
    y: 0,
    z: -1,
    a: 'Hello',
    b: 'World'
};

print(Object.filter(foo, ['z', 'a', 'b'], true));
print(Object.filter(foo, (key, value) => Ext.isString(value)));
#disp {
    white-space: pre;
    font-family: monospace
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/extjs/4.2.1/builds/ext-core.min.js"></script>
<div id="disp"></div>

Mr. Polywhirl
fonte
Verifique minha resposta e essência da
Z. Khullah
1
@ Trincot Obrigado, atualizei a resposta para retornar uma cópia do objeto, em vez de retornar no local.
Mr. Polywhirl
2

Minha solução opinativa:

function objFilter(obj, filter, nonstrict){
  r = {}
  if (!filter) return {}
  if (typeof filter == 'string') return {[filter]: obj[filter]}
  for (p in obj) {
    if (typeof filter == 'object' &&  nonstrict && obj[p] ==  filter[p]) r[p] = obj[p]
    else if (typeof filter == 'object' && !nonstrict && obj[p] === filter[p]) r[p] = obj[p]
    else if (typeof filter == 'function'){ if (filter(obj[p],p,obj)) r[p] = obj[p]}
    else if (filter.length && filter.includes(p)) r[p] = obj[p]
  }
  return r
}

Casos de teste:

obj = {a:1, b:2, c:3}

objFilter(obj, 'a') // returns: {a: 1}
objFilter(obj, ['a','b']) // returns: {a: 1, b: 2}
objFilter(obj, {a:1}) // returns: {a: 1}
objFilter(obj, {'a':'1'}, true) // returns: {a: 1}
objFilter(obj, (v,k,o) => v%2===1) // returns: {a: 1, c: 3}

https://gist.github.com/bernardoadc/872d5a174108823159d845cc5baba337

Z. Khullah
fonte
2

Solução em Vanilla JS a partir do ano 2020.


let romNumbers={'I':1,'V':5,'X':10,'L':50,'C':100,'D':500,'M':1000}

Você pode filtrar o romNumbersobjeto por chave:

const filteredByKey = Object.fromEntries(Object.entries(romNumbers).filter(([key, value]) => key === 'I'))
// filteredByKey = {I: 1} 

Ou filtre o romNumbersobjeto por valor:

 const filteredByValue = Object.fromEntries(Object.entries(romNumbers).filter(([key, value]) => value === 5))
 // filteredByValue = {V: 5} 
Qui-Gon Jinn
fonte
1

Se você deseja alterar o mesmo objeto em vez de criar um novo.

O exemplo a seguir excluirá todos os valores 0 ou vazios:

const sev = { a: 1, b: 0, c: 3 };
const deleteKeysBy = (obj, predicate) =>
  Object.keys(obj)
    .forEach( (key) => {
      if (predicate(obj[key])) {
        delete(obj[key]);
      }
    });

deleteKeysBy(sev, val => !val);
yairniz
fonte
0

Como todos disseram, não brinque com o protótipo. Em vez disso, basta escrever uma função para fazer isso. Aqui está a minha versão com lodash:

import each from 'lodash/each';
import get from 'lodash/get';

const myFilteredResults = results => {
  const filteredResults = [];

  each(results, obj => {
    // filter by whatever logic you want.

    // sample example
    const someBoolean = get(obj, 'some_boolean', '');

    if (someBoolean) {
      filteredResults.push(obj);
    }
  });

  return filteredResults;
};
saran3h
fonte
0

Eu uso isso quando preciso:

const filterObject = (obj, condition) => {
    const filteredObj = {};
    Object.keys(obj).map(key => {
      if (condition(key)) {
        dataFiltered[key] = obj[key];
      }
    });
  return filteredObj;
}
Анна
fonte
-1

Nesses casos, eu uso o jquery $ .map, que pode manipular objetos. Como mencionado em outras respostas, não é uma boa prática alterar protótipos nativos ( https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain#Bad_practice_Extension_of_native_prototypes )

Abaixo está um exemplo de filtragem apenas verificando alguma propriedade do seu objeto. Retorna o próprio objeto se sua condição for verdadeira ou retornará undefinedse não. A undefinedpropriedade fará com que esse registro desapareça da sua lista de objetos;

$.map(yourObject, (el, index)=>{
    return el.yourProperty ? el : undefined;
});
Marcel Kohls
fonte
1
$.mappode pegar um objeto, mas ele retorna uma matriz, para que os nomes das propriedades originais sejam perdidos. O OP precisa de um objeto simples filtrado.
trincot