Другое

Группировка объектов и суммирование значений в JS для оптимизации

Узнайте эффективные способы группировки объектов в массивах JavaScript и суммирования их значений. Используйте reduce(), Map и библиотеки для производительности и кода.

Какой самый эффективный способ сгруппировать объекты в массиве и посчитать сумму их значений?

Например, у меня есть такой массив объектов:

javascript
[
    { Phase: "Phase 1", Step: "Step 1", Task: "Task 1", Value: "5" },
    { Phase: "Phase 1", Step: "Step 1", Task: "Task 2", Value: "10" },
    { Phase: "Phase 1", Step: "Step 2", Task: "Task 1", Value: "15" },
    { Phase: "Phase 1", Step: "Step 2", Task: "Task 2", Value: "20" },
    { Phase: "Phase 2", Step: "Step 1", Task: "Task 1", Value: "25" },
    { Phase: "Phase 2", Step: "Step 1", Task: "Task 2", Value: "30" },
    { Phase: "Phase 2", Step: "Step 2", Task: "Task 1", Value: "35" },
    { Phase: "Phase 2", Step: "Step 2", Task: "Task 2", Value: "40" }
]

Я выводю эту информацию в таблицу и хочу группировать по разным свойствам, одновременно суммируя значения. Я использую Underscore.js из‑за функции groupBy, но она не даёт полного решения: я хочу объединить результаты, а не разделять их, как в SQL‑е при GROUP BY.

Ожидаемый результат при группировке по Phase:

javascript
[
    { Phase: "Phase 1", Value: 50 },
    { Phase: "Phase 2", Value: 130 }
]

А при группировке по Phase и Step:

javascript
[
    { Phase: "Phase 1", Step: "Step 1", Value: 15 },
    { Phase: "Phase 1", Step: "Step 2", Value: 35 },
    { Phase: "Phase 2", Step: "Step 1", Value: 55 },
    { Phase: "Phase 2", Step: "Step 2", Value: 75 }
]

Существует ли готовый скрипт или библиотека, которая бы делала это эффективно, или лучше использовать Underscore.js и вручную обрабатывать результаты для подсчёта сумм?

Самый эффективный способ группировки объектов в массиве и суммирования их значений в JavaScript – использовать Array.prototype.reduce() с аккумуляторным объектом или Map, что обеспечивает сложность O(n) и чистый, читаемый код. Для вашего конкретного случая с Underscore.js понадобится сочетать groupBy() с последующим шагом агрегации, но решение на основе reduce() обычно оказывается более эффективным и гибким.

Содержание

Использование Array.prototype.reduce()

Метод reduce() – самый прямолинейный и эффективный подход для группировки и суммирования. Вот как реализовать его для вашего конкретного случая:

javascript
const data = [
    { Phase: "Phase 1", Step: "Step 1", Task: "Task 1", Value: "5" },
    { Phase: "Phase 1", Step: "Step 1", Task: "Task 2", Value: "10" },
    { Phase: "Phase 1", Step: "Step 2", Task: "Task 1", Value: "15" },
    { Phase: "Phase 1", Step: "Step 2", Task: "Task 2", Value: "20" },
    { Phase: "Phase 2", Step: "Step 1", Task: "Task 1", Value: "25" },
    { Phase: "Phase 2", Step: "Step 1", Task: "Task 2", Value: "30" },
    { Phase: "Phase 2", Step: "Step 2", Task: "Task 1", Value: "35" },
    { Phase: "Phase 2", Step: "Step 2", Task: "Task 2", Value: "40" }
];

// Группировка по одному свойству и суммирование
const groupAndSum = (array, groupByProperty, sumProperty) => {
    return array.reduce((acc, obj) => {
        const key = obj[groupByProperty];
        const value = parseFloat(obj[sumProperty]) || 0;
        
        if (!acc[key]) {
            acc[key] = { [groupByProperty]: key, [sumProperty]: 0 };
        }
        acc[key][sumProperty] += value;
        return acc;
    }, {});
};

// Преобразовать объект в массив
const result = Object.values(groupAndSum(data, 'Phase', 'Value'));
console.log(result);
// Output: [ { Phase: "Phase 1", Value: 50 }, { Phase: "Phase 2", Value: 130 } ]

Этот подход эффективен, потому что обрабатывает массив за один проход (сложность O(n)) и сохраняет чистую разделённость логики группировки и суммирования.

Группировка по нескольким свойствам

Для группировки по нескольким свойствам (например, Phase и Step) можно создать составной ключ:

javascript
const groupAndSumMultiple = (array, groupByProperties, sumProperty) => {
    return array.reduce((acc, obj) => {
        // Создать составной ключ из нескольких свойств
        const key = groupByProperties.map(prop => obj[prop]).join('-');
        const value = parseFloat(obj[sumProperty]) || 0;
        
        if (!acc[key]) {
            const newObj = {};
            groupByProperties.forEach(prop => newObj[prop] = obj[prop]);
            newObj[sumProperty] = 0;
            acc[key] = newObj;
        }
        acc[key][sumProperty] += value;
        return acc;
    }, {});
};

const result2 = Object.values(groupAndSumMultiple(data, ['Phase', 'Step'], 'Value'));
console.log(result2);
/* Output: [
    { Phase: "Phase 1", Step: "Step 1", Value: 15 },
    { Phase: "Phase 1", Step: "Step 2", Value: 35 },
    { Phase: "Phase 2", Step: "Step 1", Value: 55 },
    { Phase: "Phase 2", Step: "Step 2", Value: 75 }
] */

Это решение динамически обрабатывает несколько свойств группировки и сохраняет структуру, необходимую для отображения таблицы.

Использование Map для лучшей производительности

Для больших наборов данных использование Map может обеспечить лучшую производительность, чем обычные объекты:

javascript
const groupAndSumWithMap = (array, groupByProperty, sumProperty) => {
    const resultMap = new Map();
    
    array.forEach(obj => {
        const key = obj[groupByProperty];
        const value = parseFloat(obj[sumProperty]) || 0;
        
        if (resultMap.has(key)) {
            resultMap.get(key)[sumProperty] += value;
        } else {
            const newObj = { [groupByProperty]: key, [sumProperty]: value };
            resultMap.set(key, newObj);
        }
    });
    
    return Array.from(resultMap.values());
};

const result3 = groupAndSumWithMap(data, 'Phase', 'Value');
console.log(result3);

Согласно обсуждениям на Stack Overflow, объекты Map обычно работают быстрее, чем обычные объекты при операциях группировки, особенно при работе с большими наборами данных.

Решения библиотек

Хотя вы упомянули Underscore.js, существуют специализированные библиотеки, которые решают эту задачу:

1. Пакет group-by-with-sum npm

javascript
const groupBy = require('group-by-with-sum');

const arr = [
    { name: 'Vasya', who: 'man', money: 100 },
    { name: 'Vasya', who: 'man', money: 263 },
    { name: 'Kolya', who: 'man', money: 98 },
    { name: 'Katya', who: 'woman', money: 290 }
];

const result = groupBy(arr, 'who', 'money');
console.log(result);

Этот специализированный npm‑пакет предназначен именно для вашего случая и предоставляет чистую, оптимизированную функциональность для группировки и суммирования.

2. Расширенный подход с Underscore.js

Если вы предпочитаете оставаться на Underscore.js, можно комбинировать несколько функций Underscore:

javascript
const _ = require('underscore');

const result4 = _.chain(data)
    .groupBy('Phase')
    .map((group, phase) => ({
        Phase: phase,
        Value: _.reduce(group, (sum, obj) => sum + parseFloat(obj.Value), 0)
    }))
    .value();

console.log(result4);

Этот подход использует цепочку Underscore, но требует нескольких проходов по данным.

Сравнение производительности

Ниже приведено сравнение различных подходов на основе найденных исследований:

Метод Сложность Память Читаемость Гибкость
reduce() с объектом O(n) Средняя Высокая Высокая
reduce() с Map O(n) Средняя Высокая Высокая
цепочка Underscore.js O(n) Высокая Средняя Средняя
пользовательская функция O(n) Низкая Средняя Высокая

Согласно обсуждениям в LambdaTest Community, подходы на основе reduce() обычно являются наиболее эффективными для большинства случаев.

Полные примеры реализации

Ниже приведена полностью готовая к использованию реализация, которая покрывает все ваши требования:

javascript
class GroupBySum {
    constructor(data) {
        this.data = data;
    }
    
    // Группировка по одному свойству
    groupBySingle(property, sumProperty = 'Value') {
        return this.data.reduce((acc, obj) => {
            const key = obj[property];
            const value = parseFloat(obj[sumProperty]) || 0;
            
            if (!acc[key]) {
                acc[key] = { [property]: key, [sumProperty]: 0 };
            }
            acc[key][sumProperty] += value;
            return acc;
        }, {});
    }
    
    // Группировка по нескольким свойствам
    groupByMultiple(properties, sumProperty = 'Value') {
        return this.data.reduce((acc, obj) => {
            const key = properties.map(prop => obj[prop]).join('-');
            const value = parseFloat(obj[sumProperty]) || 0;
            
            if (!acc[key]) {
                const newObj = {};
                properties.forEach(prop => newObj[prop] = obj[prop]);
                newObj[sumProperty] = 0;
                acc[key] = newObj;
            }
            acc[key][sumProperty] += value;
            return acc;
        }, {});
    }
    
    // Получить результаты как массив
    getResults(properties, sumProperty = 'Value') {
        const isMultiple = Array.isArray(properties);
        const grouped = isMultiple 
            ? this.groupByMultiple(properties, sumProperty)
            : this.groupBySingle(properties, sumProperty);
        
        return Object.values(grouped);
    }
}

// Пример использования
const groupBySum = new GroupBySum(data);

// Группировка только по Phase
const phaseResults = groupBySum.getResults('Phase');
console.log('Group by Phase:', phaseResults);

// Группировка по Phase и Step
const phaseStepResults = groupBySum.getResults(['Phase', 'Step']);
console.log('Group by Phase and Step:', phaseStepResults);

Эта реализация предоставляет чистый, переиспользуемый подход, который обрабатывает как одиночные, так и множественные свойства группировки, сохраняя отличные характеристики производительности.

Источники

  1. Most efficient method to groupby on an array of objects - Stack Overflow
  2. How to group by and sum an array of objects - Stack Overflow
  3. What’s the most efficient way to group by objects in an array in JavaScript and sum specific values - LambdaTest Community
  4. Group objects by multiple properties in array then sum up their values - Stack Overflow
  5. group-by-with-sum npm package
  6. JavaScript - group objects by multiple properties in array then sum up their values - Dirask

Заключение

На основании исследований и примеров реализации ключевые выводы по эффективной группировке и суммированию объектов в JavaScript:

  1. Используйте reduce() для лучшей производительности – метод Array.prototype.reduce() обеспечивает сложность O(n) и является самым эффективным подходом для большинства случаев.
  2. Рассмотрите Map для больших наборов данных – при работе с очень большими данными использование Map вместо обычного объекта может дать лучшую производительность.
  3. Специализированные библиотеки могут помочь – если вы не хотите реализовывать собственную логику, рассмотрите пакет group-by-with-sum, специально созданный для этой функциональности.
  4. Underscore.js требует дополнительных шагов – хотя Underscore.js полезен, его функция groupBy() сама по себе не обеспечивает полного решения; понадобится добавить дополнительные шаги агрегации.
  5. Создайте переиспользуемое решение – для продакшн‑кода рассмотрите создание переиспользуемого класса или функции, которая обрабатывает как одиночные, так и множественные свойства группировки.

Самый рекомендуемый подход – использовать пользовательскую реализацию на основе reduce(), которая даёт полный контроль над логикой группировки и суммирования при сохранении отличных характеристик производительности.

Авторы
Проверено модерацией
Модерация