【问题标题】:Functional way to extract object values given an array给定数组提取对象值的功能方法
【发布时间】:2017-08-20 23:26:51
【问题描述】:

我有一个键值对对象数组,如下所示:

var d = [{'name':'a', 'value':1}, {'name':'b', 'value':2}, {'name':'c', 'value':3}, {'name':'d', 'value':4}, {'name':'e', 'value':5}, {'name':'f', 'value':6}]

还有一个数组

var a = ['a','e']

如何写一个提取值的函数,本质上我想要的结果是

[{'name':'a', 'value':1},{'name':'e', 'value':5} ]

我想以纯函数方式编写它,而不是使用forwhile 循环或filter 函数。我想知道有没有合适有效的方法?递归是正确的方法吗?我愿意使用lodashramda 或任何其他 FP 包。

【问题讨论】:

  • 你的意思是var a = ['a', 'e']
  • [a,e] 应该是什么? a 届时将是 undefined 并且 e 将引发引用错误。你试过什么?看起来像是 mapfind 的工作。
  • 您是在对象引用上匹配,还是要在名称和值相等的地方匹配?
  • 我们可以用递归来做吗?
  • @cherhan 看到我的回答。这是一个递归解决方案。

标签: javascript functional-programming lodash ramda.js


【解决方案1】:

Array.prototype.map 和 Array.prototype.find

您似乎对实现细节很固执,但我敢肯定您不知道为什么。 forwhilefilter 没有任何问题。在这篇文章的结尾,我不希望你相信我,而是希望你知道这是真的。

您可以使用 mapfind 简单地解决这个问题 - 对于所有意图和目的,如何 mapfind 实现对您来说是个谜。他们是否使用for 循环甚至GOTO 来大声喊叫都没关系。对你来说重要的是你能以一种实用的方式表达你的问题。

mapfind 是谦逊的 JavaScript 函数式程序员的完美伴侣...

var d = [{'name':'a', 'value':1}, {'name':'b', 'value':2}, {'name':'c', 'value':3}, {'name':'d', 'value':4}, {'name':'e', 'value':5}, {'name':'f', 'value':6}]

var a = ['a','e']

var result = a.map(x => d.find(y => y.name === x))

console.log(result)
// [ { name: 'a', value: 1 },
//   { name: 'e', value: 5 } ]

泛型函数促进代码重用

使用一些泛型函数,我们可以用更可重用的方式表达解决方案

var data = [{'name':'a', 'value':1}, {'name':'b', 'value':2}, {'name':'c', 'value':3}, {'name':'d', 'value':4}, {'name':'e', 'value':5}, {'name':'f', 'value':6}]

const findBy = k => xs => v =>
  xs.find(x => x[k] === v)

const findAllBy = k => xs => vs =>
  vs.map(findBy(k)(xs))

console.log(findAllBy ('name') (data) (['a', 'e']))
// [ { name: 'a', value: 1 },
//   { name: 'e', value: 5 } ]

为什么固执无济于事

如果您想看看这是如何在不依赖任何的情况下完成的,forwhilefiltermapfind...

var data = [{'name':'a', 'value':1}, {'name':'b', 'value':2}, {'name':'c', 'value':3}, {'name':'d', 'value':4}, {'name':'e', 'value':5}, {'name':'f', 'value':6}]

const main = ([x,...xs]) => {
  const aux = ([y,...ys]) => {
    if (y === undefined)
      return null
    else if (y.name === x)
      return y
    else
      return aux (ys)
  }
  if (x === undefined)
    return []
  else
    return [aux(data), ...main(xs)]
}

console.log(main(['a', 'e']))
// [ { name: 'a', value: 1 },
//   { name: 'e', value: 5 } ]

很痛苦吧?与这里的其他答案相比,该代码的执行情况更少,这些答案仍然具有 lengthindexOfslice 或整个 lodash 库等依赖项。

所以这样做真的没有意义。在这个示例中编码了大量有用的通用函数,我们希望在其他地方使用它们。这就是为什么像mapreducefilterfind这样的函数首先存在的原因——forwhile也是如此。

每次我想遍历一个数组,或者过滤一个数组,或者在一个数组中查找一个元素时,我都不想手动完成。我使用函数的原因是我不必重复自己……永远。


好奇心

所以也许你对上面的map+find 解决方案没问题,但你也很好奇如果你还没有它们,你将如何自己实现它们——下面是无数种实现这些功能的方法中只有 2 种。

var data = [{'name':'a', 'value':1}, {'name':'b', 'value':2}, {'name':'c', 'value':3}, {'name':'d', 'value':4}, {'name':'e', 'value':5}, {'name':'f', 'value':6}]

const find = f => ([x,...xs]) =>
  x === undefined ? null :
    f(x) === true ? x : find (f) (xs)

const map = f => ([x,...xs]) =>
  x === undefined ? [] : [f(x), ...map(f)(xs)]
  
const main =
  map (x => find (y => y.name === x) (data))
  
console.log(main(['a', 'e']))
// [ { name: 'a', value: 1 },
//   { name: 'e', value: 5 } ]

递归在 JavaScript 中很糟糕

很多人不了解 JavaScript 中的递归,它实际上并不是很好。我们承诺在 ES6 中消除尾调用,但当前的 JavaScript 实现还没有真正支持它。这意味着您编写的任何递归函数(除非您将其蹦床)都存在堆栈溢出的潜在风险。

这意味着用forwhile 实现功能接口所以要好得多。

下面,我们重新实现了mapfind,但这次我们以一种巧妙的方式避免了堆栈溢出的风险。请注意,它对生成的代码并没有真正的影响。我们仍然可以与mapfind 以一种很好的、​​实用的方式进行交互——而且它们都是引用透明的

请特别注意main 函数——它与上面的示例完全没有变化。我们“丑陋的”whilefor 循环作为实现细节被巧妙地隐藏起来了,最终用户一点也不聪明。

var data = [{'name':'a', 'value':1}, {'name':'b', 'value':2}, {'name':'c', 'value':3}, {'name':'d', 'value':4}, {'name':'e', 'value':5}, {'name':'f', 'value':6}]

const find = f => xs => {
  for (let x of xs)
    if (f(x) === true)
      return x
  return null
}

const map = f => xs => {
  const acc = Array(xs.length)
  for (let [i,x] of xs.entries())
    acc[i] = f(x)
  return acc
}

const main =
  map (x => find (y => y.name === x) (data))
  
console.log(main(['a', 'e']))
// [ { name: 'a', value: 1 },
//   { name: 'e', value: 5 } ]

【讨论】:

  • 不错的答案。希望 OP 认真对待您。
  • 总的来说,实用主义是理智的事情。我们在工作中务实。我们的代码是务实的。没有什么不妥。除了它很沉闷。 SO上有许多标签,其中要求务实的答案并给出实际的回应。你不觉得functional-programming标签在这方面有点不同。难道这个标签没有让我们有机会提出相当不实用的问题吗?对代码的美学和优雅以及潜在的推理有疑问吗?但当然你可以使用for/of 等。在你的函数中。
  • 递归在 JavaScript 中很糟糕”这是一个短暂的声明。我认为使用 Node.js,您已经可以从 TCO 中受益,对吧?
  • @ftor 好吧,这就是我提供多个答案的原因——读者可以使用任何知道其优点和缺点的解决方案。有一段时间,我一直在提倡在 JavaScript 中使用递归,借口是尾调用优化指日可待——现在看来,它永远不会出现。如果它在节点中可用,这对我来说是个新闻(请链接),但我仍然不得不提到堆栈溢出警告,直到浏览器开始得到它
【解决方案2】:
var d = [{'name':'a', 'value':1}, {'name':'b', 'value':2}, {'name':'c', 'value':3}, {'name':'d', 'value':4}, {'name':'e', 'value':5}, {'name':'f', 'value':6}];
var a = ['a','e'];

d.filter(function(item){
  return a.indexOf(item.name) > -1; 
})

// will return [{'name':'a', 'value':1}, {'name':'b', 'value':5}]

希望对你有帮助

【讨论】:

  • 那还不循环吗?
  • @Vic 我怀疑有没有办法在不循环的情况下做到这一点。这是我们可以得到的更近的距离。
  • 这就是为什么我觉得它如此荒谬。为什么 OP 不能循环?
  • @Vic OP 希望避免 forwhile 循环,因为它们不是功能性的,而是必要的。
  • 似乎递归是他想要的?
【解决方案3】:

不幸的是,我不得不使用R.map,但使用Ramda

R.flatten(R.map(R.useWith(R.filter, [R.propEq('name'), R.identity])(R.__, d), a))

这是我能想到的,但我知道有更好的解决方案,我很想听听


编辑:我觉得这个更好

R.filter(R.compose(R.flip(R.contains)(a), R.prop('name')), d)

【讨论】:

    【解决方案4】:

    如果你希望你的解决方案是递归的,你会想要这样的。

    function getValues(objects, names, result) {
      if (objects.length === 0 || names.length === 0) {
        return result;
      }
      var index = names.indexOf(objects[0].name)
      if (index > -1) {
        result.push(objects[0]);
        return getValues(objects.slice(1), [...names.slice(0, index), ...names.slice(index, names.length)], result);
    
      } else {
        return getValues(objects.slice(1), names, result);
      }
    }
    
    var d = [{'name':'a', 'value':1}, {'name':'b', 'value':2}, {'name':'c', 'value':3}, {'name':'d', 'value':4}, {'name':'e', 'value':5}, {'name':'f', 'value':6}]
    
    var a = ['a','e']
    
    console.log(getValues(d, a, []));

    【讨论】:

    • 您正在使用 result 参数泄漏私有 api - getValues(d, a, ['foo']) 会产生愚蠢的结果 - 或者更糟糕的是,getValues(d, a, anyNonArrayValue)。没有理由将结果作为参数公开。
    • 如果这是一个问题,你可以创建一个中间函数,用空数组作为第三个参数调用这个函数。没什么大不了的。
    • 我知道如何解决它,答案只是感觉不完整 - 不善于观察的学习者可能看不到它的危害并逐字使用此代码
    【解决方案5】:

    我认为 OP 正在寻找这样的东西:

    const _ = require('lodash');
    
    function solve(array, keys) {
        if (_.isEmpty(array) || _.isEmpty(keys)) {
            return [];
        } else {
            const [ele, ...tailArray] = array;
            const [key, ...tailKeys] = keys;
    
            if (ele.name === key) {
                return [ele, ...solve(tailArray, tailKeys)];
            } else {
                return solve(tailArray, keys);
            }
        }
    }
    
    const d = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}, {'name': 'c', 'value': 3}, {'name': 'd', 'value': 4}, {'name': 'e', 'value': 5}];
    const a = ['a', 'e'];
    
    console.log(solve(d, a));
    

    这假定两个数组都已排序,所有名称都是唯一的,并且array a 中的所有元素也存在于array d 中。其他解决方案留给@cherhan 练习。

    【讨论】:

    • 也许有人愿意解释为什么这被否决了?
    猜你喜欢
    • 1970-01-01
    • 2019-10-07
    • 1970-01-01
    • 2020-05-17
    • 2023-03-04
    • 1970-01-01
    • 1970-01-01
    • 2020-02-07
    • 2019-03-22
    相关资源
    最近更新 更多