JavaScript:对象的filter()方法

355

如果我理解正确的话,ECMAScript 5为Array类型提供了filter()原型,但没有为Object类型提供。

在JavaScript中,我该如何为Object实现一个filter()函数呢?

假设我有以下这个对象:

var foo = {
    bar: "Yes"
};

我想编写一个适用于 Objectfilter() 函数:

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

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

    return result;
};
这段代码在下面的演示中可以运行,但是当我将其添加到使用jQuery 1.5和jQuery UI 1.8.9的网站时,在FireBug中会出现JavaScript错误。

Object.prototype.filter = function(predicate) {
  var result = {};
  for (key in this) {
    if (this.hasOwnProperty(key) && !predicate(this[key])) {
      console.log("copying");
      result[key] = this[key];
    }
  }
  return result;
};

var foo = {
  bar: "Yes",
  moo: undefined
};

foo = foo.filter(function(property) {
  return typeof property === "undefined";
});

document.getElementById('disp').innerHTML = JSON.stringify(foo, undefined, '  ');
console.log(foo);
#disp {
  white-space: pre;
  font-family: monospace
}
<div id="disp"></div>


1
你具体遇到了哪些错误? - NT3RP
你遇到了哪些错误?如果可能的话,请将它们发布出来 :) - Zack The Human
关于jQuery和扩展Object.prototype的脚本,存在一些不明确的历史记录:http://bugs.jquery.com/ticket/2721 - Crescent Fresh
正是我需要的,只是你必须从!predicate(this[key])中删除"!"才能得到真正的过滤器方法。 - NoxFly
那些提到扩展Object原型的人,如果不是实习生,我建议立即解雇他们。没有任何使用情况需要这样做。你基本上正在重新编写这个语言,因为你拿走了JS中的一切并说:“让我稍微改变一下”。是的,语言允许你干扰它的最高原型,但你真的应该更好地了解这些知识。为了给你一个类比 - 想象一下,如果你把你的车变得有点不同,当速度在59和60之间时,刹车不再起作用。 - DanteTheSmith
19个回答

458
首先,Object.prototype扩展视为不良实践。相反,应该将您的功能提供为独立函数,或者如果您真的想要扩展全局,请将其作为Object上的实用函数提供,就像已经有的Object.keysObject.assignObject.is等一样。
我在此提供几个解决方案:
  1. 使用reduceObject.keys
  2. 与 (1) 相同,结合使用Object.assign
  3. 使用map和扩展语法代替reduce
  4. 使用Object.entriesObject.fromEntries

1. 使用reduceObject.keys

使用 reduceObject.keys 实现所需的过滤器(使用 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);

请注意,在上面的代码中,predicate 必须是一个包含条件(与 OP 使用的排除条件相反),以便与 Array.prototype.filter 的工作方式保持一致。
2. 与 Object.assign 结合使用(与 1 相同)
在上面的解决方案中,逗号运算符reduce 部分中用于返回突变的 res 对象。当然,这可以写成两个语句而不是一个表达式,但后者更加简洁。如果要避免使用逗号运算符,可以使用 Object.assign,该方法会返回突变的对象。

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. 使用 map 和展开语法代替 reduce

在这里,我们将 Object.assign 调用从循环中移出,这样它只会被调用一次,并将单独的键作为分开的参数传递给它(使用 展开语法):

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. 使用Object.entriesObject.fromEntries

由于此解决方案将对象转换为中间数组,然后将其转换回普通对象,因此使用Object.entries(ES2017)和相反的方法(即从键/值对数组创建对象)与Object.fromEntries(ES2019)会非常有用。

这将导致在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);

这里的谓词函数将会获得一个键/值对作为参数,这有些不同,但是允许谓词函数在逻辑上有更多可能性。

2
@IamStalker,你试过了吗?只要在第二个参数中提供一个有效的函数就可以了。注意:我不知道结尾处的.Filter是什么,但如果它是一个函数,你需要调用它(x => x.Expression.Filters.Filter())。 - trincot
1
新的功能可能会减少代码量,但它们也会导致性能变慢。在大多数浏览器中,符合Ed 3标准的代码运行速度比ES2015和ES2016快两倍以上。 - RobG
3
最后一个变体的TypeScript版本:https://gist.github.com/OliverJAsh/acafba4f099f6e677dbb0a38c60dc33d - Oliver Joseph Ash
1
做得好!感谢您提供这些出色的解释和示例。 - Slavik Meltser
2
我毫不怀疑这是最好的解决方案。恭喜!我的最爱是与 entriesfromEntries 结合使用的解决方案。如此简单,却又如此易读、易懂、强大且高级抽象! - Victor
显示剩余10条评论

294

千万不要扩展Object.prototype

这将导致你的代码出现可怕的问题,可能会导致程序崩溃。你将扩展所有对象类型,包括对象字面量。

以下是一个可以快速尝试的示例:

    // 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!"

相反,创建一个函数并将对象作为参数传递。

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;
};

118
@patrick: 给一个人一片面包,你可以让他一天不饿,教他如何烤面包,你可以让他终身不饿。(或类似的表达,我是丹麦人,不知道正确的英语谚语 ;) ) - Martin Jespersen
22
你做错了...!predicate(obj[key])应该改为predicate(obj[key]) - user254766
13
@pyrotechnick:不行。首先,答案的重点不在于扩展 Object.prototype,而是将函数放置在 Object 上。其次,这是楼主的代码。显然,楼主的意图是使 .filter() 这样过滤掉正数结果。换句话说,这是一种负面的过滤器,其中正数返回值意味着它被排除在结果之外。如果你看一下 jsFiddle 的示例,他正在过滤掉已经存在但值为 undefined 的属性。 - user113716
6
@patrick dw:不。首先,我没有提到扩展/不扩展原型。其次,https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/filter --“使用提供的函数实现测试的所有通过元素创建一个新数组。”在全局上实现完全相反的功能似乎很愚蠢,不是吗? - user254766
11
@pyrotechnick说得对,你没有提到扩展/不扩展原型,这也就是我的意思。你说我做错了,但我唯一做的就是告诉OP不要扩展Object.prototype。从问题中可以看出:“这个函数运行没问题...但当我把它加入我的网站时...我会收到JavaScript错误提示”。如果OP决定实现一个与Array.prototpe.filter相反的行为.filter(),那就由他/她决定。如果你想通知OP代码有问题,请在问题下留言,但不要告诉我我错了,因为这不是我的代码。 - user113716
显示剩余13条评论

160

2020年使用原生JavaScript的解决方案。


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

您可以通过键来筛选 romNumbers 对象:

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

或者按照元素值过滤 romNumbers 对象:

 const filteredByValue = Object.fromEntries(
    Object.entries(romNumbers).filter(([key, value]) => value === 5) )
 // filteredByValue = {V: 5} 

3
太棒了!将一个对象拆分为 entries,按照所需条件进行过滤,然后使用 fromEntries 创建一个新对象。 - Victor
3
我用这个解决方案通过 includes 方法按多个键过滤对象。Object.fromEntries(Object.entries(romNumbers).filter(([key, value]) => ['a', 'b', 'c', 'd', 'e'].includes(key))) - Victor
2
不确定为什么您重复了一个超过一年前给出的答案。 - trincot
对于使用多维对象的任何人,请将“=> value === 5”更改为“=> value.secondaryValue === 5”。 - Rasmus

30
如果您愿意使用underscorelodash,您可以使用pick(或其相反的omit)。
来自underscore文档的示例:
_.pick({name: 'moe', age: 50, userid: 'moe1'}, 'name', 'age');
// {name: 'moe', age: 50}

或者使用回调函数(对于lodash,请使用pickBy):

_.pick({name: 'moe', age: 50, userid: 'moe1'}, function(value, key, object) {
  return _.isNumber(value);
});
// {age: 50}

1
Lodash是一个不好的解决方案,因为过滤空对象也会删除数字。 - mibbit
1
我刚刚使用了lodash,这是一个很好的解决方案。谢谢@Bogdan D! - Ira Herman
@mibbit你能具体说明一下吗?我相信这只是正确实现回调的问题。 - Bogdan D

25

ES6 方法...

想象你有以下这个对象:

const developers = {
  1: {
   id: 1,
   name: "Brendan", 
   family: "Eich"
  },
  2: {
   id: 2,
   name: "John", 
   family: "Resig"
  },  
  3: {
   id: 3,
   name: "Alireza", 
   family: "Dezfoolian"
 }
};
创建一个函数:
const filterObject = (obj, filter, filterValue) => 
   Object.keys(obj).reduce((acc, val) => 
   (obj[val][filter] === filterValue ? acc : {
       ...acc,
       [val]: obj[val]
   }                                        
), {});

并将其称为:

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

并将返回:

{
  1: {
  id: 1,
  name: "Brendan", 
  family: "Eich"
  },
  2: {
   id: 2,
   name: "John", 
   family: "Resig"
  }
}

1
看起来不错!但为什么它只返回另一个对象(而不是名称/筛选值为“Alireza”的对象)? - Pille
1
@Pille,OP要求代码中使用!predicate的负过滤器。 - trincot

10

鉴于

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

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

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

// Helper
function filter(object, ...keys) {
  return keys.reduce((result, key) => ({ ...result, [key]: object[key] }), {});
  
};

//Example
const person = {firstname: 'abd', lastname:'tm', age:16, school:'insat'};

// Expected to pick only firstname and age keys
console.log(
  filter(person, 'firstname', 'age')
)


3
这不使用问题所要求的给定谓词函数。 - trincot

9

正如Patrick所说,这是一个糟糕的想法,因为它几乎肯定会破坏您希望使用的任何第三方代码。

所有类似jquery或prototype的库都会在您扩展Object.prototype时中断,原因是没有hasOwnProperty检查的惰性迭代对象将中断,因为您添加的函数将成为迭代的一部分。


6

ES6 原始语法:

var foo = {
    bar: "Yes"
};

const res = Object.keys(foo).filter(i => foo[i] === 'Yes')

console.log(res)
// ["bar"]

5
我已经创建了一个Object.filter()函数,它不仅可以通过一个函数进行过滤,还可以接受一个包含键的数组以进行筛选。可选的第三个参数将允许您反转筛选。

给定:

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

数组:

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

功能:

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

代码

免责声明:出于简洁考虑,我选择使用了Ext JS核心。由于它不是问题的一部分,因此没有觉得有必要为对象类型编写类型检查器。

// 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>


请查看我的原始答案和代码片段。答案链接代码片段链接 - Bernardo Dal Corno

5

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接