تخفیف ویژه برای همه دوره‌ها از شنبه

مرتب‌سازی یک لیست بر اساس یک دسته‌بندی مشترک یکی از وظایف رایج در جاوااسکریپت است که معمولاً با استفاده از متد Array.prototype.reduce انجام می‌شود. اگرچه reduce ابزار قدرتمندی است، اما برای این نوع وظایف کمی پیچیده و سنگین به نظر می‌رسد. سال‌ها، این روش برنامه‌نویسی functional، یک الگوی رایج برای تبدیل داده‌ها به یک ساختار گروه‌بندی‌شده بود. اما حالا با معرفی Object.groupBy در جاوااسکریپت، راهی ساده‌تر و شهودی‌تر در اختیار توسعه‌دهندگان قرار گرفته است؛ ابزاری جدید که مرورگرها از اواخر سال ۲۰۲۴ شروع به پشتیبانی از آن کرده‌اند.

Object.groupBy برای ساده‌سازی فرآیند گروه‌بندی داده‌ها معرفی شده است؛ متدی که گروه‌بندی و مرتب‌سازی لیست‌ها را بر اساس دسته‌بندی مشترک، راحت‌تر و خواناتر می‌کند. در این مقاله، متد فانکشنال reduce را با متد جدید گروه‌بندی مقایسه می‌کنیم، نحوه پیاده‌سازی آن‌ها را بررسی کرده و نکاتی درباره عملکرد هر کدام ارائه خواهیم داد.

reduce چگونه کار می‌کند؟

متد reduce یک ابزار قدرتمند برای پردازش آرایه‌ها است. توسعه‌دهندگان در برنامه‌نویسی functional از اصطلاح reducer استفاده می‌کنند؛ واژه‌ای که اغلب با fold یا accumulate هم معنی در نظر گرفته می‌شود.

در این پارادایم، reduce به عنوان یک تابع higher-order عمل می‌کند و ساختارهای داده‌ای (مثل آرایه) را به یک مقدار تجمیعی تبدیل می‌نماید. به عبارت دیگر، این متد مجموعه‌ای از مقادیر را با استفاده از یک عملیات ترکیبی به یک مقدار واحد کاهش می‌دهد، مانند جمع کردن اعداد یا ادغام اشیا.

signature تابع reduce به شکل زیر است:

reduce<T, U>(
  callbackFn: (accumulator: U, currentValue: T, currentIndex: number, array: T[]) => U,
  initialValue: U
): U;
// signature of callback function
callbackFn: (accumulator: U, currentValue: T, currentIndex: number, array: T[]) => U

توضیح پارامترها:

پس از پردازش تمام آیتم‌های آرایه، این متد یک مقدار واحد را return می‌کند، یعنی نتیجه نهایی تجمیع شده (U).

مثال: گروه‌بندی سفارشات با reduce

فرض کنید یک آرایه از آبجکت‌های مربوط به سفارشات داریم:

const orders = [
    { category: 'electronics', title: 'Smartphone', amount: 100 },
    { category: 'electronics', title: 'Laptop', amount: 200 },
    { category: 'clothing', title: 'T-shirt', amount: 50 },
    { category: 'clothing', title: 'Jacket', amount: 100 },
    { category: 'groceries', title: 'Apples', amount: 10 },
    // ...
];

ما می‌خواهیم این لیست را بر اساس category گروه‌بندی کنیم تا خروجی به صورت زیر باشد:

{
    electronics: [
        {
            category: "electronics",
            title: "Smartphone",
            amount: 100
        },
        {
            category: "electronics",
            title: "Laptop",
            amount: 200
        }
    ],
    clothing: [
        {
            category: "clothing",
            title: "T-shirt",
            amount: 50
        },
        {
            category: "clothing",
            title: "Jacket",
            amount: 100
        }
    ],
    // ...
}

پیاده‌سازی با reduce

const groupedByCategory = orders.reduce((acc, order) => {
    const { category } = order;
    // Check if the category key exists in the accumulator object
    if (!acc[category]) {
        // If not, initialize it with an empty array
        acc[category] = [];
    }
    // Push the order into the appropriate category array
    acc[category].push(order);
    return acc;
}, {});

این کد ابتدا بررسی می‌کند که آیا category در accumulator وجود دارد یا نه. اگر وجود نداشته باشد، یک آرایه خالی ایجاد می‌کند و سپس سفارش مربوطه را درون آن قرار می‌دهد. در نهایت، reduce یک آبجکت حاوی گروه‌های دسته‌بندی‌شده را return می‌کند.

گروه‌بندی ساده شده با Object.groupBy در جاوااسکریپت

در ادامه کد قبلی را با پیاده‌سازی جدیدی که از متد استاتیک Object.groupBy در جاوااسکریپت استفاده می‌کند، مقایسه می‌کنیم:

const ordersByCategory = Object.groupBy(orders, order => order.category);

این راه‌حل بسیار ساده و قابل فهم است. تابع callback در Object.groupBy باید برای هر عنصر (در اینجا order) در آرایه ورودی (orders) یک کلید return نماید.

در این مثال، callback مقدار category را برمی‌گرداند، زیرا هدف ما گروه‌بندی تمام سفارشات بر اساس دسته‌بندی‌های منحصربه‌فرد است. ساختار داده‌ای ایجاد شده دقیقاً مشابه نتیجه استفاده از متد reduce خواهد بود.

دسته‌بندی محصولات بر اساس بازه قیمتی

برای نشان دادن اینکه callback می‌تواند هر رشته‌ای را به عنوان کلید return کند، محصولات را بر اساس بازه‌های قیمتی گروه‌بندی می‌کنیم:

const products = [
    { name: 'Wireless Mouse', price: 25 },
    { name: 'Bluetooth Headphones', price: 75 },
    { name: 'Smartphone', price: 699 },
    { name: '4K Monitor', price: 300 },
    { name: 'Gaming Chair', price: 150 },
    { name: 'Mechanical Keyboard', price: 45 },
    { name: 'USB-C Cable', price: 10 },
    { name: 'External SSD', price: 120 }
  ];

const productsByBudget = Object.groupBy(products, product => {
    if (product.price < 50) return 'budget';
    if (product.price < 200) return 'mid-range';
    return 'premium';
});

در این مثال، callback مقدار قیمت هر محصول را بررسی کرده و بر اساس محدوده‌های مشخص، یکی از دسته‌های budget، mid-range یا premium را برمی‌گرداند. در نتیجه، محصولات در این سه دسته قرار خواهند گرفت.

مقدار productsByBudget به این صورت خواهد بود:

{
    budget: [
        {
            "name": "Wireless Mouse",
            "price": 25
        },
        {
            "name": "Mechanical Keyboard",
            "price": 45
        },
        {
            "name": "USB-C Cable",
            "price": 10
        }
    ],
    "mid-range": [
        {
            "name": "Bluetooth Headphones",
            "price": 75
        },
        {
            "name": "Gaming Chair",
            "price": 150
        },
        {
            "name": "External SSD",
            "price": 120
        }
    ],
    premium: [
        {
            "name": "Smartphone",
            "price": 699
        },
        {
            "name": "4K Monitor",
            "price": 300
        }
    ]
}

نمونه‌ای دیگر از Object.groupBy

مثال دیگری را در نظر می‌گیریم:

const numbers = [1, 2, 3, 4];
const isGreaterTwo = Object.groupBy(numbers, x => x > 2);

مقدار isGreaterTwo به این صورت خواهد بود:

{
    "false": [1, 2],
    "true": [3, 4]
}

این مثال نشان می‌دهد که Object.groupBy در جاوااسکریپت به طور خودکار مقادیر غیر‌رشته‌ای را هنگام ایجاد دسته‌بندی‌ها به کلیدهای رشته‌ای تبدیل می‌کند. در این مورد، تابع callback بررسی می‌کند که آیا هر عدد بزرگ‌تر از ۲ است یا نه و مقدار Boolean را return می‌کند. این مقادیر سپس به کلیدهای رشته‌ای "true" و "false" در آبجکت نهایی تبدیل می‌شوند.

البته باید حواسمان به این نکته باشد که تبدیل خودکار نوع داده همیشه انتخاب مناسبی نیست؛ زیرا، ممکن است در مدیریت داده‌ها مشکل ایجاد نماید.

محدودیت‌های Object.groupBy در جاوااسکریپت

Object.groupBy در جاوااسکریپت در ساده‌سازی عملیات گروه‌بندی عملکرد بسیار خوبی دارد، اما دارای محدودیت‌هایی نیز هست. این متد، آیتم‌های آرایه را دقیقاً در گروه‌های خروجی قرار می‌دهد و ساختار اصلی آن‌ها را حفظ می‌کند.

با این حال، اگر بخواهیم آیتم‌های آرایه را هنگام گروه‌بندی تغییر دهیم، باید یک مرحله اضافی برای تبدیل داده‌ها پس از اجرای Object.groupBy انجام دهیم. در اینجا یک پیاده‌سازی ممکن را مشاهده می‌کنیم که سفارش‌ها را گروه‌بندی کرده اما ویژگی اضافی category را حذف می‌کند:

const cleanedGroupedOrders = Object.fromEntries(
    Object.entries(Object.groupBy(myOrders, order => order.category))
        .map(([key, value]) => [key, value.map(groupedValue => (
            { 
                title: groupedValue.title, 
                amount: groupedValue.amount 
            }
        ))])
);

همچنین می‌توانیم از reduce استفاده کنیم که انعطاف‌پذیری بیشتری برای تبدیل ساختار داده‌ها دارد:

const cleanedGroupedOrders = orders.reduce((acc, order) => {
    const { category } = order;
    if (!acc[category]) {
        acc[category] = [];
    }
    acc[category].push({ title: order.title, amount: order.amount });
    return acc;
}, {});

گروه‌بندی آیتم‌ها در یک ساختار Map با استفاده از Map.groupBy

اگر نیاز داریم که آبجکت‌های گروه‌بندی شده را پس از گروه‌بندی تغییر دهیم، Map.groupBy احتمالاً انتخاب بهتری خواهد بود:

const groupedOrdersMap = Map.groupBy(orders, order => order.category);

با مشاهده API آن در کنسول، می‌بینیم که مشابه Object.groupBy است.

اگر عناصر آرایه از نوع داده‌های Primitive یا آبجکت‌های فقط خواندنی باشند، بهتر است از Object.groupBy استفاده کنیم، زیرا عملکرد بهتری دارد.

مقایسه عملکرد Object.groupBy با Map.groupBy و reduce در جاوااسکریپت

برای مقایسه عملکرد این روش‌های مختلف گروه‌بندی، یک لیست بزرگ از سفارش‌ها ایجاد کرده و بر روی آن، الگوریتم‌های گروه‌بندی مختلف را اجرا نموده‌ایم:

const categories = ['electronics', 'clothing', 'groceries', 'books', 'furniture'];
const orders = [];
// Generate 15 million orders with random categories and amounts
for (let i = 0; i < 15000000; i++) {
    const category = categories[Math.floor(Math.random() * categories.length)];
    // Random amount between 1 and 500
    const amount = Math.floor(Math.random() * 500) + 1; 
    orders.push({ category, amount });
}

با داشتن این داده‌های تستی، از performance.now برای اندازه‌گیری زمان اجرای هر روش استفاده کردیم. این تست ۲۵ بار در مرورگرهای مختلف اجرا شد و میانگین زمان اجرا برای هر روش محاسبه گردید:

const runtimeData = {
  'Array.reduce': [],
  'Object.groupBy': [],
  'Map.groupBy': [],
  'reduce with transformation': [],
  'Object.groupBy + transformation': []
};
for (let i = 0; i < 25; i++) {
  console.log(`Run ${i + 1}`);
  measureRuntime();
}
// Log average runtimes
console.log('Average runtimes:');
for (const [variant, runtimes] of Object.entries(runtimeData)) {
  const average = runtimes.reduce((a, b) => a + b, 0) / runtimes.length;
  console.log(`${variant}: ${average.toFixed(2)} ms`);
}
function measureRuntime() {
  // Object.groupBy
  start = performance.now();
  const groupedOrdersGroupBy = Object.groupBy(orders, order => order.category);
  end = performance.now();
  runtimeData['Object.groupBy'].push(end - start);

  // Array.reduce
  // ...

  // Map.groupBy
  // ...

  // Reduce with transformation
  // ...

  // Object.groupBy + transformation
  // ...
}

نتایج عملکرد متدهای مختلف به این صورت می‌باشد که:

بر اساس این نتایج، متد Object.groupBy به‌طور متوسط سریع‌ترین روش برای گروه‌بندی داده‌ها بوده است، و پس از آن Map.groupBy قرار می‌گیرد. اما اگر نیاز به اعمال تغییرات اضافی روی داده‌های گروه‌بندی‌شده داشته باشیم، reduce می‌تواند از نظر عملکرد گزینه بهتری باشد.

پشتیبانی مرورگرها

Object.groupBy و Map.groupBy تا پایان سال ۲۰۲۴ به سطح پشتیبانی پایه‌ای در مرورگرها رسیده‌اند. برای بررسی سازگاری دقیق در مرورگرهای مختلف، می‌توانیم به صفحات CanIUse مربوط به Object.groupBy و Map.groupBy مراجعه نماییم.

اگر نیاز به پشتیبانی از مرورگرهای قدیمی داشته باشیم، می‌توانیم از یک shim/polyfill استفاده کنیم.

جمع‌بندی

در گروه‌بندی داده‌ها در جاوااسکریپت، انتخاب بین متدهای جدید و Array.prototype.reduce بستگی به سطح انعطاف‌پذیری و تغییراتی دارد که لازم داریم.

با توجه به نیازهای هر پروژه، می‌توانیم متد مناسب را انتخاب کنیم.