将数组分成块

1005

假设我有一个JavaScript数组,如下所示:

["Element 1","Element 2","Element 3",...]; // with close to a hundred elements.

如何将一个数组分成多个小数组,每个小数组最多包含10个元素?


参见 如何将长数组拆分为较小的数组使用underscore.js拆分javascript数组 (以及链接问题中的许多重复项 linked questions)。 - Bergi
可能是将JS数组分成N个数组的重复问题。 - T J
70
对于lodash用户,您需要查找_.chunk - Ulysse BN
如果你需要最后一个块的最小大小,这里有几个选项:https://stackoverflow.com/questions/57908133/splitting-an-array-up-into-chunks-of-a-given-size-with-a-minimum-chunk-size - Ahmet Cetin
我创建了一个解决方案,合并了最佳答案:https://dev59.com/4Goy5IYBdhLWcg3wg-Ym#71483760 - Fernando Leal
@webatrisans 这回答了你的问题。如果你使用正确的标签,那么我们更容易给出正确的指针指向正确的重复内容。 - RiggsFolly
84个回答

1276

array.slice() 方法可以从数组的开头、中间或结尾提取一个切片,以满足您需要的任何目的,而不更改原始数组。

const chunkSize = 10;
for (let i = 0; i < array.length; i += chunkSize) {
    const chunk = array.slice(i, i + chunkSize);
    // do whatever
}

最后一个可能比chunkSize小。例如,当给定一个包含12个元素的数组时,第一个块将有10个元素,第二个块只有2个。

请注意,chunkSize0会导致无限循环。


36
请记住,如果这是一个实用函数,需要断言 chunk 不为 0,以避免无限循环。 - Steven Lu
29
没错,最后一块应该只比其他块小一点。 - Blazemonger
11
@Blazemonger 确实!下次在得出结论之前,我会先自己尝试一下。我错误地认为将一个超出数组边界的输入传递给array.slice会是一个问题,但它实际上完美地运行了! - rysqui
134
对于喜欢简洁代码的人:const array_chunks = (array, chunk_size) => Array(Math.ceil(array.length / chunk_size)).fill().map((_, index) => index * chunk_size).map(begin => array.slice(begin, begin + chunk_size)); 可以将数组按指定大小切割成块。 - Константин Ван
17
为什么需要 j?一开始我以为这是一个优化方法,但实际上它比 for(i=0;i<array.length;i++){} 还要慢。 - Alex
显示剩余14条评论

302

这里是一个使用reduce的ES6版本

const perChunk = 2 // items per chunk    

const inputArray = ['a','b','c','d','e']

const result = inputArray.reduce((resultArray, item, index) => { 
  const chunkIndex = Math.floor(index/perChunk)

  if(!resultArray[chunkIndex]) {
    resultArray[chunkIndex] = [] // start a new chunk
  }

  resultArray[chunkIndex].push(item)

  return resultArray
}, [])

console.log(result); // result: [['a','b'], ['c','d'], ['e']]

如果您准备进一步链接map/reduce操作,您可以放心,输入数组不会被修改。


如果您希望版本更加简短,但可读性较差,您可以在操作中添加一些concat,以达到相同的结果:

inputArray.reduce((all,one,i) => {
   const ch = Math.floor(i/perChunk); 
   all[ch] = [].concat((all[ch]||[]),one); 
   return all
}, [])

你可以使用取余运算符将连续的项放入不同的块中:

const ch = (i % perChunk); 

1
这似乎是最简洁的解决方案。chunkIndex = Math.floor(index/perChunk) 是什么意思?它是平均值吗? - me-me
1
5/2 = 2.5,而 Math.floor(2.5) = 2,因此索引为5的项将放置在桶2中。 - Andrei R
10
我喜欢你在这里使用“all”和“one”的方式——相比我看过和用过的其他例子,这使得reduce更容易理解。 - Michael Liquori
3
一个热爱函数式编程的人认为,使用for循环比将元素归约到新数组中更易读。 - Hutch Moore
8
看到这样的解决方案,我真的很想知道人们是否还考虑他们的算法的空间/时间复杂度。concat()会克隆数组,这意味着这个算法不仅像@JPdelaTorre注意到的那样迭代每个元素,而且对于每个其他元素也要这么做。如果有一百万个项目(对于任何实际用例来说确实并不奇怪),这个算法在我的电脑上运行需要近22秒,而接受的答案只需要8毫秒。走FP团队! - SFG
显示剩余5条评论

173

改编自dbaseman的答案:https://dev59.com/OWPVa4cB1Zd3GeqP6Hsa#10456344

Object.defineProperty(Array.prototype, 'chunk_inefficient', {
  value: function(chunkSize) {
    var array = this;
    return [].concat.apply([],
      array.map(function(elem, i) {
        return i % chunkSize ? [] : [array.slice(i, i + chunkSize)];
      })
    );
  }
});

console.log(
  [1, 2, 3, 4, 5, 6, 7].chunk_inefficient(3)
)
// [[1, 2, 3], [4, 5, 6], [7]]


小补充:

需要指出的是,上述方法在我看来并不够优雅,只是一个利用 Array.map 的简单解决方法。它实际上执行了以下操作,其中 ~ 表示连接:

[[1,2,3]]~[]~[]~[] ~ [[4,5,6]]~[]~[]~[] ~ [[7]]

这种方法与下面的方法具有相同的渐进运行时间,但由于生成空列表而可能导致更差的常量因子。 可以按照以下方式重写此方法(大部分与Blazemonger的方法相同,这就是我最初没有提交此答案的原因):

更有效的方法:

// refresh page if experimenting and you already defined Array.prototype.chunk

Object.defineProperty(Array.prototype, 'chunk', {
  value: function(chunkSize) {
    var R = [];
    for (var i = 0; i < this.length; i += chunkSize)
      R.push(this.slice(i, i + chunkSize));
    return R;
  }
});

console.log(
  [1, 2, 3, 4, 5, 6, 7].chunk(3)
)


我现在更喜欢使用上述方法之一,或者以下的方法之一:

Array.range = function(n) {
  // Array.range(5) --> [0,1,2,3,4]
  return Array.apply(null,Array(n)).map((x,i) => i)
};

Object.defineProperty(Array.prototype, 'chunk', {
  value: function(n) {

    // ACTUAL CODE FOR CHUNKING ARRAY:
    return Array.range(Math.ceil(this.length/n)).map((x,i) => this.slice(i*n,i*n+n));

  }
});

演示:

> JSON.stringify( Array.range(10).chunk(3) );
[[1,2,3],[4,5,6],[7,8,9],[10]]

或者如果你不想使用Array.range函数,实际上只需要一行代码(不包括杂项):

var ceil = Math.ceil;

Object.defineProperty(Array.prototype, 'chunk', {value: function(n) {
    return Array(ceil(this.length/n)).fill().map((_,i) => this.slice(i*n,i*n+n));
}});

或者
Object.defineProperty(Array.prototype, 'chunk', {value: function(n) {
    return Array.from(Array(ceil(this.length/n)), (_,i)=>this.slice(i*n,i*n+n));
}});

66
嗯,我建议不要去改变原型,因为虽然在数组上调用“chunk”函数会让你感觉很酷,但增加的复杂度和可能出现的微妙错误并不值得。修改内置原型可能会导致问题。 - Gordon Gustafson
16
他并不是在与它们玩耍,而是为数组扩展它们。我明白不要触及Object.prototype,因为那会影响到所有对象(一切),但对于这个特定的数组函数,我认为没有任何问题。 - rlemon
5
根据 Mozilla 开发者网站上的兼容性表格,Array.map 可在 IE9 及以上版本中使用。但需要注意。 - Maikel D
12
这里是引起问题的原因。请永远不要修改本地原型,特别是没有供应商前缀的情况下:https://developers.google.com/web/updates/2018/03/smooshgate如果您添加array.myCompanyFlatten,那么没问题,但请不要添加array.flatten,并祈祷它永远不会引起问题。正如您所看到的,mootools多年前的决定现在影响了TC39标准。 - Florian Wendelborn
1
@rlemon,这仍然是危险的,因为它可能会与其他人的工作或未来方法发生冲突。为了安全起见,只需创建一个接受该数组并避免所有问题的方法。https://humanwhocodes.com/blog/2010/03/02/maintainable-javascript-dont-modify-objects-you-down-own/ - Ruan Mendes
显示剩余4条评论

172

使用生成器

function* chunks(arr, n) {
  for (let i = 0; i < arr.length; i += n) {
    yield arr.slice(i, i + n);
  }
}

let someArray = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log([...chunks(someArray, 2)]) // [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]]

可以使用TypeScript进行输入,如下所示:
function* chunks<T>(arr: T[], n: number): Generator<T[], void> {
  for (let i = 0; i < arr.length; i += n) {
    yield arr.slice(i, i + n);
  }
}

7
我对这个问题的所有答案都感到惊讶,因为它们几乎都在使用生成器或以更复杂的方式使用它们。这个解决方案简洁而高效。 - Keith
6
这绝对是最佳答案。 - eyettea
你怎么打出这个链接?https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-6.html - Ray Foss
2
function* chunks<T>(arr: T[], n: number): Generator<T[], void> { 然后使用 const foo = [ ...chunks( bar, 4 ) ]; - @RayFoss - Stev
最优雅的答案 - Riza Khan
这是最短的方式。 - Rohit Parte

151

如果你不知道谁会使用你的代码(第三方、同事、将来的自己等),请尽量避免对原型进行修改,包括 Array.prototype

虽然有安全地扩展原型的方法(但并非所有浏览器都支持),也有安全地使用从扩展原型创建的对象的方法,但更好的做法是遵循最小惊讶原则,完全避免这些做法。

如果你有时间,可以观看 Andrew Dupont 在 JSConf 2011 上的演讲"Everything is Permitted: Extending Built-ins",这个话题讨论得很好。

但回到问题上,尽管上述解决方案可行,但过于复杂,需要不必要的计算开销。以下是我的解决方案:

function chunk (arr, len) {

  var chunks = [],
      i = 0,
      n = arr.length;

  while (i < n) {
    chunks.push(arr.slice(i, i += len));
  }

  return chunks;
}

// Optionally, you can do the following to avoid cluttering the global namespace:
Array.chunk = chunk;

43
“避免篡改原生对象的原型”这句话,新的 JavaScript 开发者应该在身上暂时或永久纹上一遍,以此提醒自己。 - Jacob Dalton
6
我已经使用JavaScript多年,几乎不花时间去关注原型,顶多只是调用函数,从不像你看到一些人那样进行修改。 - 1984
2
在我的眼中,最好的建议是:最简单易懂且易于实现的。非常感谢! - Olga Farber
2
@JacobDalton我认为这都是大学的错。人们认为OOP必须无处不在。所以他们害怕“只创建一个函数”。他们想确保把它放在某个东西里。即使完全不适合。如果没有点表示法,就没有“架构”。 - Gherman
@Gherman 这与面向对象编程有什么关系?你是否曾尝试过在内联Excel公式中使用十亿个括号,却无法理解其中的头绪?JS原型是C#扩展方法的类比。流畅的代码调用约定使像我这样的老脑袋容易阅读,不想手动分割每个代码片段来阅读它。我认为JS原型唯一的问题是它们可能会发生冲突。如果它们像C#那样被导入,那就完全没问题了。它还有助于代码的可发现性,并创建一个漂亮的语义API。LISP类型的括号沙拉并不好玩。 - Chris C
显示剩余2条评论

97

使用 ES6 的 Splice 版本

let  [list,chunkSize] = [[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15], 6];
list = [...Array(Math.ceil(list.length / chunkSize))].map(_ => list.splice(0,chunkSize))
console.log(list);


17
它修改了原始的list数组。 - Jacka
23
使用 .slice() 很容易解决问题。.map((_,i) => list.slice(i*chuckSize,i*chuckSize+chuckSize)) 的意思是将一个列表按照指定的块大小分割成多个子列表,可以很方便地实现。 - James Robey
3
在 JSPerf 上,相比其他答案,这个方法的性能显著更好。 - Micah Henning
如果你要进行分割,最好直接映射同一个数组本身: var split=(fields,record=2)=>\n fields.map((field,index,fields)=>\n fields.splice(index,record,fields.slice(index,record))); - bpstrngr
递归的绝佳例子! - João Otero
显示剩余2条评论

93

我在jsperf.com上测试了不同的答案。结果可在此处查看:https://web.archive.org/web/20150909134228/https://jsperf.com/chunk-mtds

而最快的函数(适用于IE8及以上版本)是这个:

function chunk(arr, chunkSize) {
  if (chunkSize <= 0) throw "Invalid chunk size";
  var R = [];
  for (var i=0,len=arr.length; i<len; i+=chunkSize)
    R.push(arr.slice(i,i+chunkSize));
  return R;
}

1
感谢@AymKdn制作这个基准测试:这非常有帮助!我之前一直使用 splice方法,但在块大小为884432时,它会导致我的Chrome v70浏览器崩溃。而现在你建议的“slice”方法被采用后,我的代码不再会导致浏览器的“渲染”过程崩溃。 :) - Benny Code
6
以下是这个函数的 TypeScript 版本:function chunk(array: T[], chunkSize: number): T[][] { const R: T[][] = []; for (let i = 0, len = array.length; i < len; i += chunkSize) R.push(array.slice(i, i + chunkSize)); return R; }该函数将一个传入的数组按照指定的大小分割成多个子数组,并返回一个包含这些子数组的二维数组。函数使用了 TypeScript 泛型,以便可接受任意类型的输入数组。 - MacKinley Smith
chunkSize = 0需要多长时间?一些有效的函数输入不应该停止进程。 - ceving
@ceving 我刚刚添加了一个条件,当chunkSize小于等于0时。 - AymKdn
@AymKdn 我不确定,如果返回未修改的数组是否是最佳的错误处理方式。该函数的预期返回类型为Array<Array>。而且非正的块大小没有任何意义。因此,对我来说抛出错误似乎是合理的。 - ceving

46

我更倾向于使用splice方法:

var chunks = function(array, size) {
  var results = [];
  while (array.length) {
    results.push(array.splice(0, size));
  }
  return results;
};

23
使用此拼接解决方案时必须小心,因为它会修改原始数组,所以一定要清楚地记录副作用。 - bdrx
5
那么请使用“slice”代替。 - mplungjan
1
@mplungjan使用slice时,结果将是相同的数组,因此它并不是一个完全可替换的解决方案,需要进行一些修改。 - nietonfir
2
或者如果您无法使用ES6功能,您可以简单地使用array = array.slice()来创建一个浅拷贝。 - 3limin4t0r
这个更容易处理,我喜欢它。只要你知道方法如何工作! - cherucole
显示剩余2条评论

44

4
我觉得在SO JavaScript问题中应该有免责声明:你尝试过使用lodash吗?这是我在node或浏览器中包含的第一件事情。 - Jacob Dalton
并非每个人都想/能够包含第三方库。 - DarkNeuron

39

老问题:新答案!我实际上正在使用来自这个问题的一个答案,我的朋友对其进行了改进!所以这是它:

Array.prototype.chunk = function ( n ) {
    if ( !this.length ) {
        return [];
    }
    return [ this.slice( 0, n ) ].concat( this.slice(n).chunk(n) );
};

[1,2,3,4,5,6,7,8,9,0].chunk(3);
> [[1,2,3],[4,5,6],[7,8,9],[0]]

18
此方法的性能为O(N^2),因此不应在代码的关键性能部分或使用长数组(特别是当数组的“.length”远大于块大小“n”时)中使用。如果这是一种惰性语言(不像JavaScript),则该算法将不会受到O(N^2)时间的影响。话虽如此,递归实现非常优雅。您可能可以通过首先定义一个在“array, position”上递归的辅助函数,然后进行调度来修改它以提高性能:“Array.prototype.chunk”返回您的辅助函数 - ninjagecko
感谢您的祝福,让我今晚必须引导您的精神。 - bitten
1
返回翻译后的文本:var chunk = (arr, n) => { if ( !arr.length ) return []; return [ arr.slice( 0, n ) ].concat( chunk(arr.slice(n), n) ) } - Ed Williams

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