使用函数式JavaScript将数组拆分为两个不同的数组

14

我在想,使用JavaScript将数组拆分为两个不同的数组的最佳方法是什么,但要保持函数式编程的范围。

假设这两个数组应该根据某些逻辑创建。例如,将一个数组拆分只包含少于四个字符的字符串,而另一个数组则包含其余字符串。

const arr = ['horse', 'elephant', 'dog', 'crocodile', 'cat'];

我考虑了不同的方法:

筛选器

const lessThanFour = arr.filter((animal) => {
    return animal.length < 4;
});
const fourAndMore = arr.filter((animal) => {
    return animal.length >= 4;
});

对我来说,这个方法的问题在于你需要两次处理数据,但它非常易读。如果你有一个相当大的数组,这样做两次会有很大影响吗?

Reduce(归约):

const threeFourArr = arr.reduce((animArr, animal) => {
  if (animal.length < 4) {
    return [[...animArr[0], animal], animArr[1]];
  } else {
    return  [animArr[0], [...animArr[1], animal]];
  }
}, [[], []]);

数组的第0个索引包含小于四的数组,而第1个索引包含大于三的数组。

我不太喜欢这个数据结构,因为它似乎会带来一些问题,因为它是一个嵌套的数组。我考虑使用reduce构建一个对象,但我无法想象它会比数组内嵌数组的解决方案更好。

我已经在网上查看了类似的问题以及Stack Overflow,但许多这样做的方法都使用push()打破了不可变性的思想,或者它们有着非常难懂的实现方式,这在我看来破坏了函数式编程的表达力。

还有其他方法吗?(当然是函数式的)


2
你可以简化你的映射代码:const lessThanFour = arr.filter(animal => animal.length < 4); const fourAndMore = arr.filter(animal => animal.length >= 4); - kevin ternet
如果你想保持不可变性,那么你是对的,但是你是否认为逐步构建对象的行为会导致它发生变异?我认为这有点微妙... - Amit
在循环中使用animArr[0].concat(animal)会引入O^2复杂度,因为它需要在每次迭代中复制一个越来越长的数组... 在大型数组上,直接修改对象(animalArr[0].push(animal); return animalArr)将更具性能优势。 - Sergey
1
@worker11811 是否要求不使用.push(),也不创建数组的数组?不确定期望结果是什么?或者,必须使用特定方法来返回期望的结果吗? - guest271314
我进行了一些重构,使用展开运算符代替声明一个新数组,如@naomik所示。 - Jan Swart
显示剩余4条评论
7个回答

16

collateBy

我刚在这里分享了一个类似的答案

我更喜欢这个解决方案,因为它抽象了排序,但允许您使用高阶函数控制排序方式。

请注意,在collateBy中,我们没有提到animal.length<4animals[0].push。这个过程对你可能要整理的数据的种类没有任何了解。

// generic collation procedure
const collateBy = f => g => xs => {
  return xs.reduce((m,x) => {
    let v = f(x)
    return m.set(v, g(m.get(v), x))
  }, new Map())
}

// custom collator
const collateByStrLen4 =
  // collate by length > 4 using array concatenation for like elements
  // note i'm using `[]` as the "seed" value for the empty collation
  collateBy (x=> x.length > 4) ((a=[],b)=> [...a,b])

// sample data
const arr = ['horse','elephant','dog','crocodile','cat']

// get collation
let collation = collateByStrLen4 (arr)

// output specific collation keys
console.log('greater than 4', collation.get(true))
console.log('not greater than 4', collation.get(false))

// output entire collation
console.log('all entries', Array.from(collation.entries()))

请查看我发布的另一个答案,以了解其他用法变体。这是一个非常方便的过程。


bifilter

这是另一种解决方案,可以捕获过滤函数的两个输出,而不像Array.prototype.filter那样丢弃过滤值。

这基本上就是您的reduce实现,但它被抽象成一个通用的、参数化的过程。它不使用Array.prototype.push,但在闭包的主体中,局部变异通常是可以接受的。

const bifilter = (f,xs) => {
  return xs.reduce(([T,F], x, i, arr)=> {
    if (f(x, i, arr) === false)
      return [T, [...F,x]]
    else
      return [[...T,x] ,F]
  }, [[],[]])
}

const arr = ['horse','elephant','dog','crocodile','cat']

let [truthy,falsy] = bifilter(x=> x.length > 4, arr)
console.log('greater than 4', truthy)
console.log('not greater than 4', falsy)

虽然这种方法可能会更加简单,但是它远不如collateBy强大。无论哪种方法,选择你喜欢的,必要时进行适应以满足你的需求,并享受其中的乐趣!


如果这是你自己的应用程序,请随意将其添加到Array.prototype中。

// attach to Array.prototype if this is your own app
// do NOT do this if this is part of a lib that others will inherit
Array.prototype.bifilter = function(f) {
  return bifilter(f,this)
}

我喜欢collateBy方法。稍微编辑了一下并将其用于我的未来使用 :) 谢谢。对于collateBy的示例:在聚合器函数中使用(a = [], b) => { a.push(b); return a; }而不是(a=[],b)=> [...a,b],尽管它更冗长,但会极大地提高性能。这是一个示例问题,而不是方法本身的性能问题。 - Ozan
Ozan 是正确的,a.push(b) 在这里是完全可接受的,因为变异仅局限于收集过程中,没有泄漏状态。我在这里使用不可变操作,因为 OP 特别寻找如何在没有变异的情况下完成它。 - Mulan

8
你试图构建的函数通常被称为partition,可以在许多库中找到该名称,例如underscore.js。(据我所知,它不是内置方法)
var threeFourArr = _.partition(animals, function(x){ return x.length < 4 });

我不太喜欢这个,因为它似乎会给数据结构带来一些问题,因为它是一个数组的数组。
好吧,这是在Javascript中返回两个不同值的函数的唯一方法。如果您可以使用解构赋值(一个ES6功能),它看起来会更好:
var [smalls, bigs] = _.partition(animals, function(x){ return x.length < 4 });

把它看作是返回一对数组而不是返回一个数组的数组。"数组的数组"意味着你可能有可变数量的数组。
我已经在网上以及Stack Overflow上查看了类似的问题,但许多这样做都会使用push()来打破不可变性,或者它们具有非常难以阅读的实现,这在我看来破坏了函数式编程的表达能力。
如果你将可变性局限在单个函数内部,那么可变性就不是一个问题。从外部来看,它和以前一样不可变,并且有时使用一些可变性比尝试以纯函数方式完成所有操作更符合惯用法。如果我必须从头开始编写一个分区函数,我会写出以下代码:
function partition(xs, pred){
   var trues = [];
   var falses = [];
   xs.forEach(function(x){
       if(pred(x)){
           trues.push(x);
       }else{
           falses.push(x);
       }
   });
   return [trues, falses];
}

我也在写类似的东西。只需将您的推送逻辑隐藏在一个函数内,然后从此以后使用它即可。 - Ozan
有时候,使用一些可变性会更符合惯用语... - Amit

6
一个更简短的 .reduce() 版本如下:
const split = arr.reduce((animArr, animal) => {
  animArr[animal.length < 4 ? 0 : 1].push(animal);
  return animArr
}, [ [], [] ]);

可能会与解构结合使用:
const [ lessThanFour,  fourAndMore ] = arr.reduce(...)

2
如果您不反对使用下划线,那么有一个非常好的函数叫做“groupBy”,可以完全满足您的需求。请查看官方文档
const arr = ['horse', 'elephant', 'dog', 'crocodile', 'cat'];

var results = _.groupBy(arr, function(cur) {
    return cur.length > 4;
});

const greaterThanFour = results.true;
const lessThanFour = results.false;

console.log(greaterThanFour); // ["horse", "elephant", "crocodile"]
console.log(lessThanFour); // ["dog", "cat"]

1
这个构造起来没有下划线也相当简单,但是OP似乎反对这个想法(实现类似于OP的“数组嵌套数组”解决方案)。 - Amit

1

尽管JavaScript引擎非常高效,可以在几毫秒内执行数千个循环,但排序也可以使用单个循环完成。

const arr = ['horse', 'elephant', 'dog', 'crocodile', 'cat'];

const lessThanFour = [];
const fourAndMore = [];

arr.forEach(i => (i.length < 4 ? lessThanFour : fourAndMore).push(i));

console.log(lessThanFour);
// Array [ "dog", "cat" ]
console.log(fourAndMore);
// Array [ "horse", "elephant", "crocodile" ]

0

非常感谢用户的美丽回应,谢谢!这里提供一种使用递归的替代方案:

const arr = ['horse', 'elephant', 'dog', 'crocodile', 'cat'];


const splitBy = predicate => {
  return x = (input, a, b) => {
    if (input.length > 0) {
      const value = input[0];
      const [z, y] = predicate(value) ? [[...a, value], b] : [a, [...b, value]];
      return x(input.slice(1), z, y);
    } else {
      return [a, b];
    }
  }
}

const  splitAt4 = splitBy(x => x.length < 4);
const [lessThan4, fourAndMore ] = splitAt4(arr, [], []);
console.log(lessThan4, fourAndMore);


-1

我认为除了返回一个数组的数组或包含数组的对象之外,没有其他解决方案。否则,在将它们拆分后,JavaScript函数如何返回多个数组?

编写一个包含您的推送逻辑以提高可读性的函数。

var myArr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var x = split(myArr, v => (v <= 5));
console.log(x);

function split(array, tester) {
  const result = [
    [],
    []
  ];
  array.forEach((v, i, a) => {
    if (tester(v, i, a)) result[0].push(v);
    else result[1].push(v);
  });
  return result;
}


你可以使用许多不同版本的“在循环内将元素推送到两个不同的数组”的方法,但 OP 明确表示这不是他想要的。好奇现有方法与 http://stackoverflow.com/a/38860736/ 中的方法有何不同? - guest271314
并没有什么不同。只是为了可读性和可重用性,额外建议将其放在一个函数内部。经过讨论,我认为没有比返回一个数组的数组或使用push来实现所需结果更好的方法了。我已经在答案中说过了。 - Ozan
“这并没有什么不同。”如果方法并没有不同,那么在https://dev59.com/sFkT5IYBdhLWcg3wRNQR#38861091?noredirect=1#comment65085447_38860736的评论后使用类似模式发布答案的目的是什么? - guest271314
它的目的应该很明显。我认为“用OP声明他们不想要的东西回答问题,告诉他们*你可以使用...*”和“用OP不想要的东西回答问题,但声明即使它不是最初想要的,这是更好的方法”之间存在差异。此外,我认为评论区不是这个讨论的合适场所,所以我将离开它。祝您有愉快的一天。 - Ozan
@Ozan "我认为没有比返回一个数组的数组或使用push来实现所需结果更好的方法。" - 通过使用高阶函数,您可以消除更严格的决策制定。这意味着split不会为您选择数据容器([[],[]]),因此它不知道如何合并结果。我在我的答案中演示了这一点。 - Mulan
显示剩余3条评论

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