【问题标题】:Using functional programming properly: how do I use .map() efficiently here?正确使用函数式编程:我如何在这里有效地使用 .map()?
【发布时间】:2017-01-05 15:59:18
【问题描述】:

我有两个数组:hobbieList 和 hobbieTypes。我想根据 hobbieTypes 数组中的索引值来分隔 hobbieList 中的元素。像这样的:

hobbieList = ["Rock", "Pop", "Surf", "Blues", "Soccer"];
hobbieTypes= ["music", "music", "sport", "music", "sport"];

...所以使用命令式编程和使用 fors 的典型方法是:

let musicHobbies = [];
for(let i=0;i<hobbieList.length;i++){
   if(hobbieTypes[i] === "music"){
      musicHobbies.push(hobbieList[i]);
   }
}
// Now "musicHobbies" has only the hobbies which type is "music".

但我想通过函数式编程来做到这一点,使用 ma​​p() 函数,并尝试使代码尽可能简短和高效。像这样的:

let musicHobbies = 
     hobbieList.map( (elem, ind) => (hobbieTypes[ind] === "music") ? elem : null;

我觉得很好看,但是有一个问题:空位没有被擦除,所以数组在不满足条件的地方显示为“空”


我可以进入的第二个方法,效果很好,但我不知道它是否是以函数方式编码的正确方法,是这样的:

 let musicHobbies = [];
 rawHobbies.map( (elem, ind) => (hobbiesTypes[ind] === "music") ? musicHobbies.push(elem) : elem);

第二个解决方案是否设计良好?或者我应该改变什么?另外,我应该使用 map() 函数返回的数组(如案例 1),还是只在外部数组中获取结果(如案例 2)?

感谢您的帮助!

编辑:

其他方法呢?

      rawHobbies.map( (elem, ind) => {
        switch(hobbiesTypes[ind]){
           case "music":    this.hobbies.music.push(elem); break;
           case "plan":    this.hobbies.plans.push(elem); break;
           case "hobbie":  this.hobbies.hobbies.push(elem); break;
        }
      });

我想我会用 forEach 做得更好。你认为我应该使用它,将它与 forEach 一起使用,还是做类似的事情......?

this.hobbies.music = rawHobbies.filter( (x,i) => hobbieTypes[i] == "music");
this.hobbies.plans = rawHobbies.filter( (x,i) => hobbieTypes[i] == "plan");
this.hobbies.hobbies = rawHobbies.filter( (x,i) => hobbieTypes[i] == "hobbie");

我不太确定这些选项中的哪一个效果最好。意见?

编辑 2:

我最终出于性能目的使用了命令式编程,但感谢大家为我在函数式编程中使用的不同方法。我的最终解决方案只是一个简单的 foo 循环:

     for(let i=0;i<rawHobbies.length;i++){
          switch(hobbiesTypes[i]){
           case "music":   this.hobbies.music.push(rawHobbies[i]); break;
           case "plan":    this.hobbies.plans.push(rawHobbies[i]); break;
           case "hobbie":  this.hobbies.hobbies.push(rawHobbies[i]); break;
        }
     }

【问题讨论】:

  • 为什么不使用过滤器?我认为它比地图更合适
  • 除非您真正的意思是“有效地”,否则不要使用“有效地”这个词。
  • 您的最后一句话:“我的最终解决方案只是一个简单的 foo 循环” – 这个解决方案很糟糕,因为数据值被硬编码到循环中。如果添加了其他类型的爱好,则必须调整代码。

标签: javascript functional-programming ecmascript-6


【解决方案1】:

编辑后,您似乎想要更多类似于 groupBy 的内容。

您可以通过.reduce() 实现这一目标。这应该对你有用:

const hobbieList = ["Rock", "Pop", "Surf", "Blues", "Soccer"];
const hobbieTypes= ["music", "music", "sport", "music", "sport"];
var groupedHobbies = hobbieTypes.reduce( (acc, item, index) => { 
        acc[item] = [...(acc[item] || []), hobbieList[index]];
        return acc;
 }, {});

console.log(groupedHobbies);

通常,您希望尽可能少的副作用,并且不要在 map 函数上同时做很多事情(应该将每个元素映射/转换/转换为新元素,而不更改原始元素)。通过拥有小函数,您可以稍后将它们组合起来(如果您继续遵循函数式方法,您可能会阅读更多相关内容)

我建议您使用.filter() 方法而不是.map()

类似的东西:

hobbieList.filter((elem, ind) => hobbieTypes[ind] === "music")

这将返回一个数组,如:

[
  "Rock",
  "Pop",
  "Blues"
]

【讨论】:

  • 阅读我的编辑!我猜这个过滤器非常好,但是对于这样的情况,我必须用 1 个来提供 3 个不同的数组……写我在我提供的两个新解决方案中的第一个不是更好吗?编辑,但使用 .forEach() 而不是 .map() ?因为它只会迭代数组一次,但在最后一个解决方案(使用 3 个过滤器)中,它将迭代它三次。你怎么看?
  • 我刚刚更新了我的答案。 Reduce 是一种很好的处理方式
  • 我不明白你的 reduce() 示例是如何工作的:/你能扩展一下吗?
  • 您可以使用扩展语法代替concat() 方法:[...(acc[item] || []), hobbieList[index]]
  • 但基本上:我们从一个空的对象字面量开始,遍历所有的 hobbieTypes 元素,将我们当前的项目值(例如:“sport”)分配给一个属性,即 hobbieList 的值与我们当前的索引位置相同。
【解决方案2】:

我只会使用filter() 方法:

const hobbieList = ["Rock", "Pop", "Surf", "Blues", "Soccer"];
const hobbieTypes = ["music", "music", "sport", "music", "sport"];
console.log(hobbieList.filter((x, i) => hobbieTypes[i] === "music"));

至于您的编辑:我认为使用filter()map()(您实际上用作forEach())要干净得多。这可能不是最快的方法,但它肯定更实用。

【讨论】:

  • @Zerok 已编辑。​
  • 所以使用 forEach() 似乎更快,不是吗?由于数组只迭代一次。
  • @Zerok 是的,但是函数式方法总是会比较慢。如果速度是优先级,请使用常规循环。
  • @Zerok 是的,forEach() 比常规 for 循环慢得多。见jsperf.com/for-vs-foreach/37
  • @Zerok 但除非您处理大量数据,否则这并不重要。
【解决方案3】:

您似乎想做的是按类型分组爱好。这个分组过程的输出应该是什么?为每种类型的爱好列表创建单独的变量不是非常可扩展的。最好创建一个对象,其键是类型,值是该类型的爱好列表:

hobbiesByType = {
  music: ["Rock", "Pop", "Blues"],
  sport: ["Surf", "Soccer"]
};

现在我可以将所有的音乐爱好都称为hobbiesByType.music

这个分组过程的输入应该是什么?目前你有两个并行数组:

hobbieList = ["Rock", "Pop", "Surf", "Blues", "Soccer"];
hobbieTypes= ["music", "music", "sport", "music", "sport"];

但这不是表示此信息的最佳方式。任何时候我想检查一些东西,我都必须引用这两个数组。我可能必须并行遍历它们。有一个单一的表示会好得多,通常的方法是对象数组

hobbies = [
  {name: "Rock", type: "music"},
  ...
];

现在让我们考虑如何进行分组。您将在 SO 上找到许多示例和方法。这是一个非常基本的使用 for 循环:

var hobbiesByType = {};

for (var i = 0; i < hobbies.length; i++) {
  var hobby = hobbies[i];
  var hobbyName = hobby.name;
  var hobbyType = hobby.type;
  if (!(hobbyType in hobbiesByType)) hobbiesByType[hobbyType] = [];
  hobbiesByType[hobbyType].push(hobby.name);
}

但您想使用“函数式”数组方法。上面最紧凑的形式是使用reduce,如下所示:

hobbiesByType = hobbies.reduce((result, hobby) => {
  if (!(hobby.type in result)) result[hobby.type] = [];
  result[hobby.type].push(hobby.name);
  return result;
}, {});

这都是 ES5,而不是 ES6。你可以使用 ES6 参数解构来简化它:

hobbiesByType = hobbies.reduce((result, type, name}) => {
  (result[type] = result[type] || []).push(name);
  return result;
}, {});

但是,与其在每种类型下创建爱好的名称数组,不如自己创建爱好对象的数组。然后,如果爱好包含其他信息——也许是skillLevel——那么你可以很容易地从分组表示中访问它。

顺便说一下,单数形式是“hobby”,而不是“hobbie”。

【讨论】:

  • 由于我的后端结构是这样设计的(我从 Parse 表中接收这两个数组),因此信息包含在两个数组中。我认为最好的方法是使用常规 for 作为用户在其他答案中建议的。不过,感谢您的扩展解释:)
【解决方案4】:

使用变体数据作为对象键会严重滥用对象。相反,Map 会为您所需的数据做出更好的选择。

zip 函数与 Array.prototype.reduce 结合将产生非常有用的结果

let hobbyList = ["Rock", "Pop", "Surf", "Blues", "Soccer"]
let hobbyTypes= ["music", "music", "sport", "music", "sport"]

const zip = ([x,...xs], [y,...ys]) => {
  if (x === undefined || y === undefined)
    return []
  else
    return [[x,y], ...zip(xs, ys)]
}

let result = zip(hobbyTypes, hobbyList).reduce((acc, [type, hobby]) => {
  if (acc.has(type))
    return acc.set(type, [...acc.get(type), hobby])
  else
    return acc.set(type, [hobby])
}, new Map())

console.log(result.get('music'))
// => [ 'Rock', 'Pop', 'Blues' ]

console.log(result.get('sport'))
// => [ 'Surf', 'Soccer' ]

如果你真的必须使用一个对象,虽然你真的不应该这样做,你可以使用map.entriesArray.from来这样做。

Array.from(result.entries())
  .reduce((acc, [type, hobbies]) =>
    Object.assign(acc, { [type]: hobbies }), {})

// => { music: [ 'Rock', 'Pop', 'Blues' ], sport: [ 'Surf', 'Soccer' ] }

【讨论】:

  • 我真的很喜欢这样的代码,但实际上我觉得它们不可读。我什至不明白,但很想。你能评论一下你的代码吗?我想知道它是如何工作的。
  • @Zerok 我很乐意提供一些额外的帮助。你能告诉我一些你不明白的事情吗?
  • const zip = ([x,...xs], [y,...ys]) --> 三点运算符我没搞懂!
  • 让结果 = zip(hobbyTypes, hobbyList).reduce((acc, [type, hobby]) => { if (acc.has(type)) return acc.set(type, [. ..acc.get(type), hobby]) else return acc.set(type, [hobby]) }, new Map()) --> 我根本不懂这个方法.. 我还是不懂不知道 reduce() 是如何工作的,但除此之外,这里的整个语法、最终的“new Map()”和重复使用的“[]”让我有点害怕。
【解决方案5】:

Map 用于将某些内容转换为其他内容,而不是获取匹配条件的元素。在这种情况下,您必须使用过滤功能。

【讨论】:

    猜你喜欢
    • 2012-02-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多