<think>我们有一个对象数组,需要根据某个属性(例如 type)的值将具有相同属性值的对象归类到同一个子数组中。
参考引用[1]和[2]中的方法,我们可以使用reduce方法或者循环遍历来实现。
方法一:使用reduce方法(引用[1]的方法)
使用数组的reduce方法遍历原始数组,并构建一个对象(或Map),其中键是我们要分组的属性值,值是一个数组,用于存放具有该属性值的所有对象。
方法二:使用循环遍历(引用[3]和[4]的方法)
创建一个临时数组来存储已经出现过的属性值,然后遍历原始数组,如果属性值在临时数组中不存在,则在新数组中创建一个新对象(包含该属性值和存放对象的数组),否则将当前对象添加到对应的子数组中。
根据用户需求,我们选择一种方法实现。这里我们将展示两种方法,并说明其步骤。
假设我们有一个数组 `arr`,我们要根据属性 `key` 进行分组。
方法一(reduce)示例:
步骤:
1. 使用reduce,初始值为一个空对象{}。
2. 对于每个当前元素cur,获取其属性key的值(例如cur[key])。
3. 如果结果对象中还没有以该属性值为键的数组,则创建一个空数组。
4. 将当前对象cur(或者根据需要,可以只存储部分属性)添加到该键对应的数组中。
5. 最后,我们可以通过Object.values将结果对象转换为数组(每个元素是一个分组对象,包含属性值key和对应的对象数组)。
但是注意,我们最终需要的可能是一个数组,每个元素是一个子数组(包含相同属性值的对象),或者是一个对象数组,每个对象包含分组键和子数组。
根据引用[1]的示例,我们可能得到这样的结构:
[
{ key: value1, children: [obj1, obj2, ...] },
{ key: value2, children: [obj3, obj4, ...] },
...
]
或者如果我们只需要子数组的数组,那么可以这样:
[
[obj1, obj2, ...],
[obj3, obj4, ...],
...
]
根据问题描述,我们需要将相同属性值的对象归类到子数组中,所以我们可以选择第二种结构(数组的数组)或者第一种结构(带分组键的对象数组)。通常,为了保留分组信息,我们会采用第一种结构。
这里我们按照第一种结构实现(带分组键和子数组)。
方法一代码(使用reduce):
const grouped = arr.reduce((acc, cur) => {
const keyValue = cur[key]; // 获取当前对象的属性值
// 如果acc中还没有这个keyValue,则初始化一个对象,并放入一个空数组
if (!acc[keyValue]) {
acc[keyValue] = { [key]: keyValue, children: [] };
}
// 将当前对象添加到children数组中
acc[keyValue].children.push(cur);
return acc;
}, {});
// 然后将grouped对象转换为数组,注意grouped是一个对象,键是属性值,值是我们定义的分组对象
const result = Object.values(grouped);
方法二(循环遍历):
let temp = []; // 用于存储已经出现过的属性值
let result = []; // 结果数组
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
const keyValue = item[key];
// 检查temp中是否已经有这个keyValue
if (temp.indexOf(keyValue) === -1) {
// 没有出现过,则创建一个新的分组对象
temp.push(keyValue);
result.push({
[key]: keyValue,
children: [item]
});
} else {
// 已经出现过,则找到result中对应的分组对象,将当前对象添加到children中
for (let j = 0; j < result.length; j++) {
if (result[j][key] === keyValue) {
result[j].children.push(item);
break;
}
}
}
}
方法二在引用[3]和[4]中都有体现,但注意,当数据量较大时,内层循环查找可能会影响性能。而方法一使用对象索引,效率更高。
因此,我们推荐使用方法一(reduce)。
但是,如果属性值可能是复杂类型(比如对象),那么使用对象作为键可能会有问题(因为对象作为键会被转换为字符串"[object Object]"),这种情况下我们可以使用Map。
不过,通常分组属性都是基本类型(字符串、数字等),所以我们这里假设属性值是基本类型。
下面我们写一个完整的函数,根据给定的属性名对数组进行分组。
注意:如果属性值不是基本类型,我们需要考虑使用其他方式(比如转为字符串,或者使用Map)。
函数实现:
function groupBy(arr, key) {
return Object.values(arr.reduce((acc, cur) => {
const keyValue = cur[key];
// 如果keyValue是对象,我们可以使用JSON.stringify(keyValue)来作为键,但这里我们假设是基本类型
if (!acc[keyValue]) {
acc[keyValue] = { groupKey: keyValue, children: [] };
}
acc[keyValue].children.push(cur);
return acc;
}, {}));
}
但是,我们也可以让返回的结构更通用,比如返回一个对象,键是分组值,值是对应的数组。但根据问题,我们需要的是数组,所以使用Object.values转换。
另外,我们也可以返回一个数组,每个元素是一个子数组(不包含分组键),但是这样我们就丢失了分组键的信息。所以,我们按照上述结构返回。
如果用户不需要分组键,只需要子数组,那么可以这样:
const result = groupBy(arr, key).map(item => item.children);
但根据问题,我们是要归类到子数组中,并且保留分组信息,所以我们返回带分组键的结构。
示例数据:
const dataList = [
{ name: 'Apple', type: 'Fruit', price: '¥1.5' },
{ name: 'Banana', type: 'Fruit', price: '¥3.5' },
{ name: 'Grape', type: 'Fruit', price: '¥2.5' },
{ name: 'Cabbage', type: 'Vegetable', price: '¥2.5' },
{ name: 'Tomoto', type: 'Vegetable', price: '¥5.5' }
];
调用 groupBy(dataList, 'type') 将返回:
[
{
groupKey: 'Fruit',
children: [
{ name: 'Apple', type: 'Fruit', price: '¥1.5' },
{ name: 'Banana', type: 'Fruit', price: '¥3.5' },
{ name: 'Grape', type: 'Fruit', price: '¥2.5' }
]
},
{
groupKey: 'Vegetable',
children: [
{ name: 'Cabbage', type: 'Vegetable', price: '¥2.5' },
{ name: 'Tomoto', type: 'Vegetable', price: '¥5.5' }
]
}
]
注意:我们使用了`groupKey`作为分组键的属性名,这样比较通用。如果用户希望属性名就是原来的`key`,那么可以动态设置属性名。
修改一下,使用动态属性名:
function groupBy(arr, key) {
return Object.values(arr.reduce((acc, cur) => {
const keyValue = cur[key];
if (!acc[keyValue]) {
acc[keyValue] = { [key]: keyValue, children: [] }; // 这里使用传入的key作为属性名
}
acc[keyValue].children.push(cur);
return acc;
}, {}));
}
这样返回的对象中,分组键的属性名就是传入的`key`,例如上面的例子就会是`type: 'Fruit'`。
但是,如果传入的`key`是动态的,这样设置属性名是合适的。
因此,我们采用动态属性名的方式。
另外,注意引用[1]中使用了解构,但这里我们不需要,因为我们直接使用整个对象。
现在,我们根据这个思路编写代码。
但是,我们也可以考虑使用Map,特别是当属性值可能不是字符串或数字时(例如,可能是布尔值、null、undefined等),但使用对象作为reduce的初始值,这些值会被转换为字符串(如null转为'null',undefined转为'undefined'),所以也可以。但是,如果属性值是数字1和字符串'1',它们会被当作同一个键,这可能是问题。所以,如果我们需要区分类型,可以使用Map。
使用Map的版本:
function groupBy(arr, key) {
const map = new Map();
arr.forEach(item => {
const keyValue = item[key];
if (!map.has(keyValue)) {
map.set(keyValue, { [key]: keyValue, children: [] });
}
map.get(keyValue).children.push(item);
});
return Array.from(map.values());
}
这个版本可以区分不同类型的键值,比如1和'1'是不同的。
但是,我们通常希望分组时,如果属性是数字,在对象中作为键时会被转换为字符串。而Map可以保留类型。所以,如果要求严格区分类型,使用Map。
不过,在大多数情况下,我们分组的属性值都是同类型的,所以两种方法都可以。这里我们提供一个使用Map的版本,因为更严谨。
但是,根据引用[1]到[5]中的例子,都是使用字符串作为键,所以我们也可以使用对象。这里我们提供两种实现,并说明区别。
我们决定使用Map版本,因为它更安全。
最终代码实现(Map版本):</think>### JavaScript 将对象数组中相同属性值的对象归类到子数组
在 JavaScript 中,将对象数组按相同属性值归类到子数组,主要有两种高效方法:
#### 方法 1:使用 `reduce()`(推荐)
```javascript
function groupBy(arr, key) {
return Object.values(
arr.reduce((acc, cur) => {
const keyValue = cur[key];
if (!acc[keyValue]) {
acc[keyValue] = { [key]: keyValue, children: [] };
}
acc[keyValue].children.push(cur);
return acc;
}, {})
);
}
// 使用示例
const data = [
{ id: 1, type: "Fruit", name: "Apple" },
{ id: 2, type: "Fruit", name: "Banana" },
{ id: 3, type: "Vegetable", name: "Carrot" }
];
const groupedData = groupBy(data, "type");
console.log(groupedData);
```
**输出结果:**
```javascript
[
{
type: "Fruit",
children: [
{ id: 1, type: "Fruit", name: "Apple" },
{ id: 2, type: "Fruit", name: "Banana" }
]
},
{
type: "Vegetable",
children: [
{ id: 3, type: "Vegetable", name: "Carrot" }
]
}
]
```
**原理解析:**
1. `reduce()` 遍历数组,初始值为空对象 `{}`
2. 提取当前对象的属性值 `keyValue = cur[key]`
3. 若结果对象中不存在该属性值,创建新分组:`{ [key]: keyValue, children: [] }`
4. 将当前对象推入对应 `children` 数组
5. `Object.values()` 将结果对象转换为数组[^1]
#### 方法 2:使用 `Map`(支持复杂类型)
```javascript
function groupByMap(arr, key) {
const map = new Map();
arr.forEach(item => {
const keyValue = item[key];
if (!map.has(keyValue)) {
map.set(keyValue, { [key]: keyValue, children: [] });
}
map.get(keyValue).children.push(item);
});
return Array.from(map.values());
}
```
**两种方法对比:**
| 特性 | `reduce()` 方法 | `Map` 方法 |
|--------------------|-----------------------|---------------------|
| 时间复杂度 | $O(n)$ | $O(n)$ |
| 是否保留原对象 | 是 | 是 |
| 键值类型支持 | 字符串/数字 | 任意类型(含对象) |
| 浏览器兼容性 | ES5+ | ES6+ |
#### 使用场景示例
```javascript
// 按年龄分组用户
const users = [
{ name: "Alice", age: 28 },
{ name: "Bob", age: 32 },
{ name: "Charlie", age: 28 }
];
console.log(groupBy(users, "age"));
// 按城市分组订单
const orders = [
{ id: 101, city: "Beijing", amount: 150 },
{ id: 102, city: "Shanghai", amount: 200 },
{ id: 103, city: "Beijing", amount: 75 }
];
console.log(groupBy(orders, "city"));
```
**注意事项:**
1. 当属性值为 `null` 或 `undefined` 时会被归为单独分组
2. 数字和字符串形式的相同值会被视为不同分组(如 `10` 和 `"10"`)
3. 如需特殊处理,可在获取 `keyValue` 后添加格式化逻辑:
```javascript
const keyValue = String(cur[key]).trim(); // 统一转为字符串并去空格
```