مرتبسازی یک لیست بر اساس یک دستهبندی مشترک یکی از وظایف رایج در جاوااسکریپت است که معمولاً با استفاده از متد Array.prototype.reduce انجام میشود. اگرچه reduce
ابزار قدرتمندی است، اما برای این نوع وظایف کمی پیچیده و سنگین به نظر میرسد. سالها، این روش برنامهنویسی functional، یک الگوی رایج برای تبدیل دادهها به یک ساختار گروهبندیشده بود. اما حالا با معرفی Object.groupBy در جاوااسکریپت، راهی سادهتر و شهودیتر در اختیار توسعهدهندگان قرار گرفته است؛ ابزاری جدید که مرورگرها از اواخر سال ۲۰۲۴ شروع به پشتیبانی از آن کردهاند.
Object.groupBy برای سادهسازی فرآیند گروهبندی دادهها معرفی شده است؛ متدی که گروهبندی و مرتبسازی لیستها را بر اساس دستهبندی مشترک، راحتتر و خواناتر میکند. در این مقاله، متد فانکشنال 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
accumulator
: مقدار تجمیع شده از اجرای قبلی callback یا مقدار initialValue
در اولین اجرا. نوع آن برابر با مقدار اولیه (U
) است.currentValue
: عنصر فعلی آرایه که در حال پردازش است (T
).currentIndex
: ایندکس currentValue
در آرایه (number).array
: آرایهای که متد reduce روی آن فراخوانی شده (T[]
).initialValue
: مقدار اولیه برای accumulator (U
) که اگر ارائه شود، از آن به عنوان مقدار شروع استفاده میشود، در غیر این صورت مقدار اولیه برابر با اولین آیتم آرایه خواهد بود.پس از پردازش تمام آیتمهای آرایه، این متد یک مقدار واحد را return میکند، یعنی نتیجه نهایی تجمیع شده (U
).
فرض کنید یک آرایه از آبجکتهای مربوط به سفارشات داریم:
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 در جاوااسکریپت استفاده میکند، مقایسه میکنیم:
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 } ] }
مثال دیگری را در نظر میگیریم:
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 انجام دهیم. در اینجا یک پیادهسازی ممکن را مشاهده میکنیم که سفارشها را گروهبندی کرده اما ویژگی اضافی 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.groupBy
احتمالاً انتخاب بهتری خواهد بود:
const groupedOrdersMap = Map.groupBy(orders, order => order.category);
با مشاهده API آن در کنسول، میبینیم که مشابه Object.groupBy است.
اگر عناصر آرایه از نوع دادههای Primitive یا آبجکتهای فقط خواندنی باشند، بهتر است از Object.groupBy استفاده کنیم، زیرا عملکرد بهتری دارد.
برای مقایسه عملکرد این روشهای مختلف گروهبندی، یک لیست بزرگ از سفارشها ایجاد کرده و بر روی آن، الگوریتمهای گروهبندی مختلف را اجرا نمودهایم:
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 // ... }
نتایج عملکرد متدهای مختلف به این صورت میباشد که:
Array.reduce
:
Object.groupBy
:
Map.groupBy
:
reduce
همراه با تغییرات روی دادهها:
Object.groupBy
همراه با تغییرات روی دادهها:
بر اساس این نتایج، متد Object.groupBy بهطور متوسط سریعترین روش برای گروهبندی دادهها بوده است، و پس از آن Map.groupBy
قرار میگیرد. اما اگر نیاز به اعمال تغییرات اضافی روی دادههای گروهبندیشده داشته باشیم، reduce
میتواند از نظر عملکرد گزینه بهتری باشد.
Object.groupBy و Map.groupBy تا پایان سال ۲۰۲۴ به سطح پشتیبانی پایهای در مرورگرها رسیدهاند. برای بررسی سازگاری دقیق در مرورگرهای مختلف، میتوانیم به صفحات CanIUse مربوط به Object.groupBy و Map.groupBy مراجعه نماییم.
اگر نیاز به پشتیبانی از مرورگرهای قدیمی داشته باشیم، میتوانیم از یک shim/polyfill استفاده کنیم.
در گروهبندی دادهها در جاوااسکریپت، انتخاب بین متدهای جدید و Array.prototype.reduce
بستگی به سطح انعطافپذیری و تغییراتی دارد که لازم داریم.
با توجه به نیازهای هر پروژه، میتوانیم متد مناسب را انتخاب کنیم.