同时映射和过滤数组
Posted
技术标签:
【中文标题】同时映射和过滤数组【英文标题】:Map and filter an array at the same time 【发布时间】:2016-03-27 16:20:31 【问题描述】:我有一个对象数组,我想对其进行迭代以生成一个新的过滤数组。而且,我需要根据参数从新数组中过滤掉一些对象。我正在尝试这个:
function renderOptions(options)
return options.map(function (option)
if (!option.assigned)
return (someNewObject);
);
这是一个好方法吗?有没有更好的方法?我愿意使用任何库,例如 lodash。
【问题讨论】:
“object.keys”方法怎么样? developer.mozilla.org/fr/docs/Web/javascript/Reference/… 使用减少:developer.mozilla.org/fr/docs/Web/JavaScript/Reference/….reduce()
绝对比我在其他地方看到的.filter(...).map(...)
更快。我设置了一个 JSPerf 测试来演示 ***.com/a/47877054/2379922
【参考方案1】:
您应该为此使用Array.reduce
。
var options = [
name: 'One', assigned: true ,
name: 'Two', assigned: false ,
name: 'Three', assigned: true ,
];
var reduced = options.reduce(function(filtered, option)
if (option.assigned)
var someNewValue = name: option.name, newProperty: 'Foo'
filtered.push(someNewValue);
return filtered;
, []);
document.getElementById('output').innerhtml = JSON.stringify(reduced);
<h1>Only assigned options</h1>
<pre id="output"> </pre>
或者,reducer 可以是一个纯函数,像这样
var reduced = options.reduce(function(result, option)
if (option.assigned)
return result.concat(
name: option.name,
newProperty: 'Foo'
);
return result;
, []);
【讨论】:
对我来说,第一个参数filtered
是一个对象。所以 filtered.push
对我来说是未定义的。
我也有一个问题,filtered
是一个对象。这是因为我没有传入“初始值”——reduce 函数之后的空数组 ([]
)。例如不正确var reduced = options.reduce(function(filtered, option) ... );
正确var reduced = options.reduce(function(filtered, option) ... , []);
@Marko 我还介绍了一个纯减速器。 PTAL。
Yap 现在很纯净。 +1 为您的努力。并不是说我是纯粹的函数式编程倡导者,远非如此,我只是看不出重点:) 但实际上我在看到它之后就使用了你的技巧,因为它在给定的情况下非常方便。但是对于这个任务,您可能想看看 flatMap 功能,我认为它在您做出回答后成为标准(也因为这个原因,某些浏览器可能不支持)。它应该更高效,因为像这样连接数组会使 O(n^2) 任务脱离 O(n) 任务。
@Marko flatMap
不能在这里使用,因为它不允许我们过滤。除非,在这种情况下我们返回一个空数组。我相信这对读者来说并不容易。【参考方案2】:
自 2019 年以来,Array.prototype.flatMap 是不错的选择。
options.flatMap(o => o.assigned ? [o.name] : []);
来自上面链接的 MDN 页面:
flatMap
可以用作添加和删除项目的一种方式(修改 地图期间的项目数)。换句话说,它允许您映射 许多项目对许多项目(通过分别处理每个输入项目), 而不是总是一对一的。从这个意义上说,它的工作原理类似于 过滤器的对面。只需返回一个 1 元素数组来保留该项目, 用于添加项目的多元素数组,或用于删除的 0 元素数组 项目。
【讨论】:
这在性能方面与使用 2 个循环(分别调用 array.filter 和 array.map)相比如何? 我使用 jsbench.me 和 jsben.ch 进行了比较,得到了非常不同的结果。第一次 flatMap 慢了 5 倍,第二次快了 2 倍。所以我不知道如何对其进行基准测试。【参考方案3】:使用 reduce,卢克!
function renderOptions(options)
return options.reduce(function (res, option)
if (!option.assigned)
res.push(someNewObject);
return res;
, []);
【讨论】:
【参考方案4】:使用 ES6,你可以做的很短:
options.filter(opt => !opt.assigned).map(opt => someNewObject)
【讨论】:
@hogan 但这是原始查询的正确答案(可能不是最佳解决方案) @vikramvi 谢谢你的来信。问题是,我们可以通过多种方式实现相同的目标,我更喜欢最好的方式。 这并不完全正确,因为在这里您丢失了初始定位值的索引,并且该信息在运行地图 fn 时非常有用。 这种方式很好,但这个问题是同时映射和过滤数组最好的方式是options.reduce(res,options....
answer @Zuker【参考方案5】:
一行reduce
与 ES6 花哨spread syntax 在这里!
var options = [
name: 'One', assigned: true ,
name: 'Two', assigned: false ,
name: 'Three', assigned: true ,
];
const filtered = options
.reduce((result, name, assigned) => [...result, ...assigned ? [name] : []], []);
console.log(filtered);
【讨论】:
真的很好@Maxim!我赞成这个!但是...在添加的每个元素上,它必须传播result
...中的所有元素...就像filter(assigned).map(name)
解决方案
不错的一个。我花了一秒钟才意识到...assigned ? [name] : []
发生了什么——它可能更易读为...(assigned ? [name] : [])
【参考方案6】:
我会发表评论,但我没有所需的声誉。对 Maxim Kuzmin 原本非常好的答案的一个小改进,以提高效率:
const options = [
name: 'One', assigned: true ,
name: 'Two', assigned: false ,
name: 'Three', assigned: true ,
];
const filtered = options
.reduce((result, name, assigned ) => assigned ? result.push(name) && result : result, []);
console.log(filtered);
说明
我们不是在每次迭代中一遍又一遍地传播整个结果,而是只追加到数组中,并且只有在实际有一个值要插入时。
【讨论】:
.concat()
返回一个新数组,它实际上与将旧数组和新项分散到一个新数组中相同,MDN here。您想按照您的描述使用.push()
“追加”。
@MarceDev 啊,你是对的,谢谢。将更新我的答案来做到这一点。然后语法变得有点奇怪,因为 push 不返回数组。【参考方案7】:
在某些时候,使用forEach
是不是更容易(或同样容易)
var options = [
name: 'One', assigned: true ,
name: 'Two', assigned: false ,
name: 'Three', assigned: true ,
];
var reduced = []
options.forEach(function(option)
if (option.assigned)
var someNewValue = name: option.name, newProperty: 'Foo'
reduced.push(someNewValue);
);
document.getElementById('output').innerHTML = JSON.stringify(reduced);
<h1>Only assigned options</h1>
<pre id="output"> </pre>
但是,如果有一个 malter()
或 fap()
函数结合了 map
和 filter
函数,那就太好了。它会像过滤器一样工作,除了返回 true 或 false 之外,它会返回任何对象或 null/undefined。
【讨论】:
你可能想检查你的模因... ;-) 但是你不会炫耀你的 1 行 133t JS,除了你之外没有人理解。 这些名字是金,你应该向 TC39 提交提案【参考方案8】:使用Array.prototy.filter 本身
function renderOptions(options)
return options.filter(function(option)
return !option.assigned;
).map(function (option)
return (someNewObject);
);
【讨论】:
如果你想要的值是在做过滤器的过程中计算出来的,这样就不太好用了;你必须复制那部分代码。【参考方案9】:我用以下几点优化了答案:
-
将
if (cond) stmt;
重写为cond && stmt;
使用 ES6 Arrow Functions
我将介绍两种解决方案,一种使用forEach,另一种使用reduce:
解决方案 1:使用 forEach
该解决方案通过使用forEach
遍历每个元素来工作。然后,在forEach
循环的主体中,我们有条件充当过滤器,它决定我们是否要向结果数组附加一些东西。
const options = [
name: 'One', assigned: true ,
name: 'Two', assigned: false ,
name: 'Three', assigned: true ,
];
const reduced = [ ];
options.forEach(o =>
o.assigned && reduced.push( name: o.name, newProperty: 'Foo' );
);
console.log(reduced);
解决方案 2:使用 reduce
此解决方案使用Array.prototype.reduce
而不是forEach
来遍历数组。它认识到reduce
具有内置的初始化程序和循环机制这一事实。除此之外,此解决方案与forEach
解决方案或多或少相同,因此差异归结为修饰语法糖。
const options = [
name: 'One', assigned: true ,
name: 'Two', assigned: false ,
name: 'Three', assigned: true ,
];
const reduced = options.reduce((a, o) =>
o.assigned && a.push( name: o.name, newProperty: 'Foo' );
return a;
, [ ] );
console.log(reduced);
由您决定采用哪种解决方案。
【讨论】:
你到底为什么要使用cond && stmt;
?这更难阅读,而且根本没有任何好处。【参考方案10】:
使用 reduce,您可以在一个 Array.prototype 函数中执行此操作。这将从数组中获取所有偶数。
var arr = [1,2,3,4,5,6,7,8];
var brr = arr.reduce((c, n) =>
if (n % 2 !== 0)
return c;
c.push(n);
return c;
, []);
document.getElementById('mypre').innerHTML = brr.toString();
<h1>Get all even numbers</h1>
<pre id="mypre"> </pre>
您可以使用相同的方法并将其推广到您的对象,就像这样。
var arr = options.reduce(function(c,n)
if(somecondition) return c;
c.push(n);
return c;
, []);
arr
现在将包含过滤后的对象。
【讨论】:
【参考方案11】:直接使用.reduce
可能难以阅读,因此我建议创建一个为您生成reducer 的函数:
function mapfilter(mapper)
return (acc, val) =>
const mapped = mapper(val);
if (mapped !== false)
acc.push(mapped);
return acc;
;
像这样使用它:
const words = "Map and filter an array #javascript #arrays";
const tags = words.split(' ')
.reduce(mapfilter(word => word.startsWith('#') && word.slice(1)), []);
console.log(tags); // ['javascript', 'arrays'];
【讨论】:
【参考方案12】:您可以使用Array.reduce
与箭头函数是单行代码
const options = [
name: 'One', assigned: true ,
name: 'Two', assigned: false ,
name: 'Three', assigned: true ,
];
const reduced = options.reduce((result, option) => option.assigned ? result.concat( name: option.name, newProperty: 'Foo' ) : result, []);
document.getElementById('output').innerHTML = JSON.stringify(reduced);
<h1>Only assigned options</h1>
<pre id="output"> </pre>
【讨论】:
【参考方案13】:我已将这些出色的答案转换为实用函数,我想分享它们:
示例:仅过滤奇数并将其递增
例如[1, 2, 3, 4, 5]
-filter-> [1, 3, 5]
-map-> [2, 4, 6]
通常你会用 filter
和 map
这样做
const inputArray = [1, 2, 3, 4, 5];
const filterOddPlusOne = inputArray.filter((item) => item % 2).map((item) => item + 1); // [ 2, 4, 6 ]
使用reduce
const filterMap = <TSource, TTarget>(
items: TSource[],
filterFn: (item: TSource) => boolean,
mapFn: (item: TSource) => TTarget
) =>
items.reduce((acc, cur): TTarget[] =>
if (filterFn(cur)) return [...acc, mapFn(cur)];
return acc;
, [] as TTarget[]);
使用flatMap
const filterMap = <TSource, TTarget>(
items: TSource[],
filterFn: (item: TSource) => boolean,
mapFn: (item: TSource) => TTarget
) => items.flatMap((item) => (filterFn(item) ? [mapFn(item)] : []));
用法(reduce
和 flatMap
解决方案相同):
const inputArray = [1, 2, 3, 4, 5];
const filterOddPlusOne = filterMap(
inputArray,
(item) => item % 2, // Filter only odd numbers
(item) => item + 1 // Increment each number
); // [ 2, 4, 6 ]
JavaScript 版本
上面的代码在 TypeScript 中,但问题是关于 JavaScript 的。所以,我已经为你删除了所有的泛型和类型:
const filterMap = (items, filterFn, mapFn) =>
items.reduce((acc, cur) =>
if (filterFn(cur)) return [...acc, mapFn(cur)];
return acc;
, []);
const filterMap = (items, filterFn, mapFn) =>
items.flatMap((item) => (filterFn(item) ? [mapFn(item)] : []));
【讨论】:
【参考方案14】:同时执行filter
+ map
的最有效方法是将数据作为通用可迭代对象处理,并同时执行这两项操作。在这种情况下,您最多只能浏览一次数据。
下面的示例使用iter-ops 库,并且正是这样做的:
import pipe, filter, map from 'iter-ops';
const i = pipe(
inputArray,
filter(value => value === 123), // filter on whatever key you want
map(value => /* any mapping here*/) // remap data as you like
);
// i = iterable that can be processed further;
console.log([...i]); //=> list of new objects
上面,我说的是at most
,因为如果你对可迭代的结果应用进一步的逻辑,比如限制映射项的数量,你最终会迭代你的对象列表甚至少于一次:
const i = pipe(
inputArray,
filter(value => value === 123), // filter on whatever key you want
map(value => /* any mapping here*/), // remap as you like
take(10) // take up to 10 items only
);
在上面,我们进一步限制了迭代,一旦生成了 10 个结果项就停止,因此我们对数据的迭代次数少于一次。这已经是最有效的了。
更新
我被要求补充为什么这个解决方案比reduce
更有效的答案,所以这里是......
Array 的reduce
是一个有限运算,它遍历完整的数据集,以产生结果。因此,当您需要对该输出数据进行进一步处理时,您最终会生成一个新的迭代序列,依此类推。
当您有一个复杂的业务逻辑要应用于序列/可迭代对象时,链接该逻辑总是更有效,同时只对序列进行一次迭代。在许多情况下,您最终会对一个序列进行复杂的处理,甚至一次都没有检查完整的数据集。这就是可迭代数据处理的效率。
附:我是上述库的作者。
【讨论】:
【参考方案15】:嘿,我刚刚从事这个项目,想在 MDM 文档上分享我基于 Array.prototype.flatMap() 的解决方案:
const places = [
latitude: 40,longitude: 1, latitude:41, longitude:2, latitude:44, longitude:2, latitude:NaN, longitude:NaN , latitude:45, longitude:4,latitude:48, longitude:3, latitude:44, longitude:5, latitude:39, longitude:13, latitude:40, longitude:8, latitude:38, longitude:4
]
let items = places?.map((place) => [
latitude: (place.latitude),
longitude:(place.longitude),
,
]);
console.log("Items: ", items);
//Remove elements with NaN latitude and longitude
let newItems = places?.flatMap((o) =>
Number(o.longitude, o.latitude)
? lng: Number(o.longitude), lat: Number(o.latitude)
: []
);
console.log("Coordinates after NaN values removed: ", newItems);
一种
【讨论】:
以上是关于同时映射和过滤数组的主要内容,如果未能解决你的问题,请参考以下文章