كيفية استخدام الفرز المتقدم في JavaScript لترتيب البيانات حسب أكثر من خاصية
مقدمة: لماذا نحتاج إلى فرز متعدد الخصائص؟
عند العمل على مصفوفات الكائنات في JavaScript، يكون من السهل عادةً تنفيذ عمليات التصفية باستخدام filter() على أكثر من خاصية. لكن عندما ننتقل إلى الفرز، تبدأ المسألة في أن تصبح أكثر حساسية، خصوصاً إذا كنا نريد ترتيب العناصر بناءً على أكثر من معيار في الوقت نفسه.
على سبيل المثال، قد ترغب في ترتيب المستخدمين حسب اسم العائلة أولاً، ثم حسب الاسم الأول إذا تشابهت أسماء العائلات. وهنا تظهر أهمية فهم آلية عمل Array.sort() بعمق، لأن هذا الفهم يتيح لك إنشاء عمليات فرز قوية، مرنة، وسهلة التوسعة.

الفرق بين التصفية والفرز في JavaScript
تعمل الدالة Array.filter() بأسلوب تسلسلي ممتاز، إذ يمكنك ربط أكثر من استدعاء بسهولة. فعند انتهاء أول .filter()، يمكنك تمرير الناتج مباشرة إلى .filter() أخرى، وهكذا حسب الحاجة.
أما الفرز باستخدام Array.sort() فيختلف قليلاً. فإذا قمت بالفرز حسب خاصية واحدة ثم أعدت الفرز حسب خاصية ثانية، فقد لا تحصل على النتيجة المنطقية المتوقعة ما لم تكن دالة المقارنة نفسها مبنية لدعم أكثر من معيار.
كيف تعمل دالة Array.sort() فعلياً؟
تعتمد Array.sort() على دالة مقارنة تستقبل عادةً وسيطين هما (a, b). والمطلوب منك أن تعيد رقماً يحدد ترتيب العنصرين:
- إذا كانت القيمة المعادة أقل من
0، يبقىaقبلb. - إذا كانت القيمة المعادة أكبر من
0، ينتقلbقبلa. - إذا كانت القيمة المعادة تساوي
0، فهذا يعني أن العنصرين متساويان من حيث معيار الفرز الحالي.
وهنا تكمن الفكرة الأساسية: عندما تكون نتيجة المقارنة الأولى 0، يمكننا الانتقال إلى مقارنة ثانية، ثم ثالثة، إلى أن نجد فرقاً حقيقياً بين العنصرين.
فهم العلاقة بين القيم العددية وBoolean
في JavaScript، تتحول القيم -1 و1 إلى true عند تحويلها منطقياً، بينما تتحول القيمة 0 إلى false. وهذا ما يجعل استخدام العامل || فعالاً جداً في بناء فرز متعدد المستويات.
Boolean(-1) === true;
Boolean(1) === true;
Boolean(0) === false;
بمعنى آخر، إذا أعادت المقارنة الأولى قيمة غير صفرية، فسيتم اعتمادها فوراً. أما إذا أعادت 0، فسينتقل التنفيذ إلى المقارنة التالية.
الفكرة الجوهرية: دمج أكثر من مقارنة باستخدام ||
يمكن كتابة دالة المقارنة بحيث تعيد نتيجة أول مقارنة، وإذا لم تكن حاسمة، تنتقل تلقائياً إلى المقارنة التالية:
return firstComparatorResult || secondComparatorResult;
هذه الصيغة بسيطة جداً، لكنها قوية للغاية. فهي تسمح لك ببناء ترتيب متدرج يبدأ بمعيار أساسي، ثم ينتقل إلى معايير فرعية عند الحاجة.
مثال عملي على فرز مصفوفة من الكائنات
لنفترض أن لدينا المصفوفة التالية:
const myArray = [
{
firstName: 'Bob',
lastName: 'Francis',
age: 34,
city: 'Holland',
state: 'Massachusetts',
country: 'USA',
online: true
},
{
firstName: 'Janet',
lastName: 'Francis',
age: 41,
city: 'Holland',
state: 'Massachusetts',
country: 'USA',
online: false
},
{
firstName: 'Alexia',
lastName: 'Francis',
age: 39,
city: 'Paris',
state: 'Ile de France',
country: 'France',
online: true
},
{
firstName: 'Lucille',
lastName: 'Boure',
age: 29,
city: 'Paris',
state: 'Ile de France',
country: 'France',
online: true
}
];
الفرز حسب اسم العائلة فقط
يمكننا أولاً تنفيذ فرز بسيط حسب الخاصية lastName:
const sortByLastName = function (a, b) {
return a.lastName.localeCompare(b.lastName);
};
console.log(myArray.sort(sortByLastName));
تُعد الدالة localeCompare() خياراً عملياً عند مقارنة النصوص، لأنها مصممة للتعامل مع السلاسل النصية بشكل مناسب داخل sort()، وتعيد -1 أو 0 أو 1 بحسب نتيجة المقارنة.
وسيكون الناتج بالشكل التالي تقريباً:
[
{ firstName: 'Lucille', lastName: 'Boure' },
{ firstName: 'Bob', lastName: 'Francis' },
{ firstName: 'Janet', lastName: 'Francis' },
{ firstName: 'Alexia', lastName: 'Francis' }
]
كيف نفرز حسب اسم العائلة ثم الاسم الأول؟
إذا تشابهت قيمة lastName بين عنصرين، ننتقل إلى firstName:
const sortByLastAndFirst = function (a, b) {
return a.lastName.localeCompare(b.lastName) ||
a.firstName.localeCompare(b.firstName);
};
هذا الأسلوب يعني ببساطة:
- قارِن أولاً بين
lastName. - إذا كانت النتيجة غير صفرية، استخدمها مباشرة.
- إذا كانت النتيجة
0، انتقل إلى مقارنةfirstName.
بهذه الطريقة تحصل على فرز هرمي واضح ومنطقي، دون الحاجة إلى حلول معقدة مثل استخدام reduce() أو إعادة تشكيل البيانات قبل ترتيبها.
منطق التنفيذ بشكل مبسط
return:
- comparison of a.lastName and b.lastName
- if equal, comparison of a.firstName and b.firstName
تحسين قابلية القراءة عبر دوال صغيرة قابلة للتركيب
بدلاً من كتابة منطق المقارنة كاملاً في كل مرة، يمكننا تقسيمه إلى دوال صغيرة مستقلة:
const byLast = (a, b) => a.last.localeCompare(b.last);
const byFirst = (a, b) => a.first.localeCompare(b.first);
const byLastAndFirst = (a, b) => byLast(a, b) || byFirst(a, b);
هذا النمط يمنحك كوداً أكثر وضوحاً، لأن اسم كل دالة يعبّر مباشرة عن وظيفتها. كما أنه يسهّل إعادة الاستخدام في أكثر من موضع داخل المشروع.
مشكلة التكرار: هل نحتاج إلى دالة جديدة لكل خاصية؟
رغم أن تقسيم المقارنات إلى دوال صغيرة خطوة ممتازة، إلا أن كتابة دالة منفصلة لكل خاصية قد يصبح مرهقاً مع ازدياد عدد الخصائص. لذلك من الأفضل تصميم دالة عامة تستطيع مقارنة أي خاصية نمررها إليها.
إنشاء دالة عامة باسم sortByProp
const sortByProp = function (prop, a, b) {
if (typeof a[prop] === 'number') return a[prop] - b[prop];
return a[prop].localeCompare(b[prop]);
};
ويمكن استخدامها كما يلي:
myArray.sort((a, b) =>
sortByProp('lastName', a, b) || sortByProp('firstName', a, b)
);
هذا الحل جيد من ناحية المرونة، لأنه يدعم مقارنة الأرقام والنصوص ضمن دالة واحدة. لكنه لا يزال أقل أناقة عند إعادة الاستخدام، لأن sort() تتوقع دالة بالصيغة (a, b) مباشرة، لا دالة تستقبل prop أيضاً.
الحل الأفضل: الدوال عالية الرتبة وميزة closure
هنا نصل إلى أسلوب أكثر احترافية: إنشاء دالة تُرجع دالة أخرى. يُعرف هذا النمط باسم higher-order function، وهو من الأساليب القوية في JavaScript.
الفكرة هي أن نمرر اسم الخاصية مرة واحدة فقط، ثم نحصل على دالة مقارنة جاهزة تتوافق مباشرة مع sort().
إعادة تعريف sortByProp بشكل احترافي
const sortByProp = function (prop) {
return function (a, b) {
if (typeof a[prop] === 'number') return a[prop] - b[prop];
return a[prop].localeCompare(b[prop]);
};
};
عند استدعاء sortByProp('lastName')، ستحصل على دالة جديدة تتذكر قيمة prop داخلياً. هذا هو مفهوم closure باختصار: دالة داخلية تحتفظ بإمكانية الوصول إلى نطاق الدالة الخارجية حتى بعد انتهاء تنفيذها.
استخدام الدوال الناتجة في الفرز
const byLast = sortByProp('lastName');
const byFirst = sortByProp('firstName');
const byLastAndFirst = function (a, b) {
return byLast(a, b) || byFirst(a, b);
};
console.log(myArray.sort(byLastAndFirst));
بهذا الشكل أصبحت المقارنات قابلة لإعادة الاستخدام والتركيب بسهولة، مع الحفاظ على وضوح الشيفرة.
التوسع إلى أكثر من مستوى فرز
إذا احتجت إلى ترتيب العناصر حسب الدولة، ثم الولاية، ثم المدينة، ثم اسم العائلة، ثم الاسم الأول، يمكنك فعل ذلك بالطريقة نفسها:
const byLast = sortByProp('lastName');
const byFirst = sortByProp('firstName');
const byCountry = sortByProp('country');
const byState = sortByProp('state');
const byCity = sortByProp('city');
const byAll = (a, b) =>
byCountry(a, b) ||
byState(a, b) ||
byCity(a, b) ||
byLast(a, b) ||
byFirst(a, b);
console.log(myArray.sort(byAll));
صحيح أن المثال السابق يبدو عميقاً بعض الشيء، لكنه يوضح بجلاء كيف يمكن توسيع نفس الأسلوب إلى أي عدد من المستويات دون تغيير جوهر الفكرة.
نسخة مختصرة بأسلوب ES6
إذا كنت تفضّل الصياغة المختصرة باستخدام الدوال السهمية، فيمكن كتابة نفس الفكرة بهذا الشكل:
const byProp = (prop) => (a, b) =>
typeof a[prop] === 'number'
? a[prop] - b[prop]
: a[prop].localeCompare(b[prop]);
هذه الصياغة أنيقة ومختصرة، لكنها ليست بالضرورة الأفضل في جميع الحالات. فإذا كان الفريق يضم مطورين مبتدئين أو كانت قابلية الصيانة أولوية، فقد تكون النسخة التقليدية أكثر وضوحاً وأسهل في الفهم.
أفضل الممارسات عند بناء فرز متعدد الخصائص
- استخدم
localeCompare()عند مقارنة النصوص للحصول على نتائج أدق. - افصل بين مقارنة النصوص والأرقام داخل دالة عامة.
- اعتمد على العامل
||لتسلسل المقارنات من الأكثر أهمية إلى الأقل. - أنشئ دوال قابلة لإعادة الاستخدام بدلاً من تكرار نفس المنطق.
- لا تُضحِّ بوضوح الشيفرة من أجل الاختصار المبالغ فيه.
متى يكون هذا الأسلوب مفيداً جداً؟
يصبح هذا النمط مهماً في تطبيقات الويب التي تعرض بيانات مركبة، مثل:
- لوحات التحكم الإدارية.
- قوائم العملاء والمستخدمين.
- أنظمة إدارة الطلبات.
- الجداول التفاعلية في الواجهات الأمامية.
- نتائج البحث التي تحتاج إلى ترتيب متعدد المعايير.
في هذه السيناريوهات، يمنحك الفرز متعدد الخصائص تحكماً أكبر في تجربة المستخدم، ويجعل عرض البيانات أكثر منطقية واحترافية.
الخلاصة التقنية
السر الحقيقي في تنفيذ فرز متقدم داخل JavaScript ليس في تعقيد الدالة، بل في فهم سلوك Array.sort() وكيفية استغلال القيم المعادة من دوال المقارنة. باستخدام العامل || مع دوال صغيرة أو مولدة ديناميكياً عبر closure، يمكنك بناء نظام فرز مرن، واضح، وقابل للتوسع. ومن الناحية التقنية، فإن أفضل حل ليس الأقصر دائماً، بل الحل الذي يوازن بين الأداء، الوضوح، وسهولة الصيانة على المدى الطويل.