جدول التجزئة في JavaScript: فهم البنية وتنفيذ المصفوفة الترابطية خطوة بخطوة
ما هو جدول التجزئة في JavaScript؟
يُعد Hash Table من أشهر هياكل البيانات المستخدمة لتخزين القيم على هيئة أزواج من key/value، بحيث يمكن الوصول إلى القيمة بسرعة كبيرة عند معرفة المفتاح المرتبط بها. الفكرة الأساسية تقوم على تحويل المفتاح إلى رقم صحيح عبر دالة تُسمى hash function، ثم يُستخدم هذا الرقم لتحديد الموضع المناسب داخل الذاكرة.
هذا الأسلوب يجعل عمليات البحث والإدراج والحذف سريعة جداً في المتوسط، ولذلك يعتمد عليه المطورون في كثير من التطبيقات التي تتطلب أداءً عالياً وتنظيماً مرناً للبيانات.


من أبرز أسباب شيوع Hash Table أنه يوفّر أداءً ممتازاً في العمليات الأساسية التالية:
| العملية | متوسط التعقيد | أسوأ حالة |
|---|---|---|
| المساحة | O(n) |
O(n) |
| البحث | O(1) |
O(n) |
| الإدراج | O(1) |
O(n) |
| الحذف | O(1) |
O(n) |
وهذا يعني أن الأداء يكون ممتازاً غالباً، لكن قد يتراجع في بعض الحالات إذا حدثت تصادمات كثيرة بين المفاتيح.
كيف توظّف JavaScript مفهوم جدول التجزئة؟
في لغة JavaScript ستجد تطبيقين شائعين لفكرة Hash Table، وهما Object وMap. كلاهما يسمح بتخزين البيانات باستخدام مفاتيح وقيم، لكن بينهما فروق عملية مهمة.
استخدام Object كمصفوفة ترابطية
أبسط مثال على جدول تجزئة في JavaScript هو النوع Object، حيث يمكن ربط اسم خاصية بقيمة محددة:
let obj = {
Nathan: "555-0182",
Jane: "315-0322"
};
هنا تم ربط المفتاح Nathan بالرقم "555-0182"، وربط المفتاح Jane بالقيمة "315-0322".
لكن استخدام Object كجدول تجزئة ليس مثالياً دائماً، والسبب يعود إلى نقطتين رئيسيتين:
- النوع
Objectيرث خصائص وطرائق افتراضية من السلسلة الأصليةprototype. - لا توجد آلية مدمجة وواضحة لتتبع عدد العناصر المضافة من المطوّر فقط.
من الأمثلة المعروفة على ذلك الدالة hasOwnProperty() التي تساعدك على التحقق من أن الخاصية مضافة مباشرة إلى الكائن وليست موروثة:
const obj = {};
obj.name = "Nathan";
console.log(obj.hasOwnProperty("name")); // true
المشكلة أن JavaScript لا تمنعك من الكتابة فوق هذه الدالة نفسها، مما قد يؤدي إلى سلوك غير متوقع:
const obj = {};
obj.name = "Nathan";
obj.hasOwnProperty = true;
console.log(obj.hasOwnProperty("name")); // Error: obj.hasOwnProperty is not a function
لماذا يُعد Map خياراً أفضل في كثير من الحالات؟
لتجاوز بعض قيود Object، توفر JavaScript بنية Map التي صُممت خصيصاً للتعامل مع أزواج key/value بشكل أكثر وضوحاً ومرونة.
const collection = new Map();
collection.set("Nathan", "555-0182");
collection.set("Jane", "555-0182");
console.log(collection.get("Nathan")); // 555-0182
console.log(collection.size); // 2
في Map يجب استخدام set() لإضافة البيانات وget() لاسترجاعها، كما أن الخاصية size تتيح معرفة عدد العناصر بسهولة.
ومن مزايا Map أيضاً أنك لا تستطيع إدراج عنصر جديد بالطريقة التقليدية التي قد تربك البنية الداخلية:
const collection = new Map();
collection.set("Nathan", "555-0182");
collection["size"] = false;
console.log(collection.get("size")); // undefined
console.log(collection.size); // 1
كما أن Map قابلة للتكرار iterable، ويمكن المرور على عناصرها بسهولة:
const myMap = new Map();
myMap.set("Nathan", "555-0182");
myMap.set("Jane", "315-0322");
for (let [key, value] of myMap) {
console.log(`${key} = ${value}`);
}
لماذا قد تحتاج إلى بناء HashTable مخصص بنفسك؟
رغم أن JavaScript تمنحك Object وMap كخيارات جاهزة، فإن تنفيذ جدول تجزئة مخصص بنفسك يظل تمريناً مهماً لفهم هياكل البيانات بعمق. كما أن هذا السؤال يتكرر كثيراً في المقابلات التقنية، لأنه يكشف مدى فهمك لمفاهيم مثل:
- فكرة التجزئة
hashing. - تخزين البيانات في الحاويات
buckets. - مشكلة التصادم
collisionوطرق معالجتها. - تحليل الأداء والتعقيد الزمني.
يمكن تنفيذ جدول تجزئة بسيط في ثلاث مراحل:
- إنشاء الفئة
HashTableوتجهيز خصائص التخزين الأساسية. - إضافة دالة
_hash()لتحويل المفتاح إلى فهرس رقمي. - إضافة الدوال
set()وget()وremove()لإدارة البيانات.
بناء فئة HashTable في JavaScript
الخطوة الأولى: إنشاء الفئة وتجهيز الحاوية
سنبدأ بإنشاء فئة تحتوي على مصفوفة باسم table بعدد ثابت من الخانات، بالإضافة إلى خاصية size لتتبع عدد العناصر المخزنة:
class HashTable {
constructor() {
this.table = new Array(127);
this.size = 0;
}
}
سيتم تخزين جميع أزواج key/value داخل الخاصية table، بينما تحتفظ size بعدد العناصر الفعلية.
الخطوة الثانية: كتابة الدالة _hash()
الآن نحتاج إلى تحويل كل مفتاح نصي إلى رقم. إحدى الطرق التعليمية البسيطة هي جمع قيم ASCII لأحرف المفتاح باستخدام الدالة charCodeAt():
_hash(key) {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash += key.charCodeAt(i);
}
return hash;
}
لكن لأن حجم المصفوفة لدينا هو 127 خانة فقط، فيجب أن يكون الناتج ضمن هذا النطاق. لهذا نستخدم معامل باقي القسمة %:
_hash(key) {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash += key.charCodeAt(i);
}
return hash % this.table.length;
}
بهذا يصبح ناتج _hash() فهرساً صالحاً للاستخدام داخل المصفوفة.
الخطوة الثالثة: كتابة الدالة set()
وظيفة set() هي إدراج زوج جديد من المفتاح والقيمة في الموضع الناتج من دالة التجزئة:
set(key, value) {
const index = this._hash(key);
this.table[index] = [key, value];
this.size++;
}
في هذه النسخة البسيطة، يتم تخزين البيانات مباشرة داخل الفهرس المناسب.
الخطوة الرابعة: كتابة الدالة get()
لاسترجاع البيانات، نحسب الفهرس نفسه مرة أخرى ثم نعيد العنصر المخزن:
get(key) {
const index = this._hash(key);
return this.table[index];
}
إذا لم تكن هناك قيمة في هذا الموضع، فستعيد الدالة undefined.
الخطوة الخامسة: كتابة الدالة remove()
لحذف عنصر من جدول التجزئة نحدد الفهرس أولاً، ثم نحذف المحتوى إذا كان موجوداً:
remove(key) {
const index = this._hash(key);
if (this.table[index] && this.table[index].length) {
this.table[index] = undefined;
this.size--;
return true;
} else {
return false;
}
}
بهذه الخطوات أصبح لدينا تنفيذ أولي يعمل، لكنه لا يزال يعاني من مشكلة مهمة: التصادمات.
اختبار التنفيذ الأولي لجدول التجزئة
قبل معالجة التصادمات، لنراجع النسخة الكاملة الأولى من الفئة:
class HashTable {
constructor() {
this.table = new Array(127);
this.size = 0;
}
_hash(key) {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash += key.charCodeAt(i);
}
return hash % this.table.length;
}
set(key, value) {
const index = this._hash(key);
this.table[index] = [key, value];
this.size++;
}
get(key) {
const target = this._hash(key);
return this.table[target];
}
remove(key) {
const index = this._hash(key);
if (this.table[index] && this.table[index].length) {
this.table[index] = [];
this.size--;
return true;
} else {
return false;
}
}
}
يمكن تجربة الكود بهذه الصورة:
const ht = new HashTable();
ht.set("Canada", 300);
ht.set("France", 100);
ht.set("Spain", 110);
ثم نسترجع القيم:
console.log(ht.get("Canada")); // [ 'Canada', 300 ]
console.log(ht.get("France")); // [ 'France', 100 ]
console.log(ht.get("Spain")); // [ 'Spain', 110 ]
وكذلك يمكن اختبار الحذف:
console.log(ht.remove("Spain")); // true
console.log(ht.get("Spain")); // undefined
حتى الآن يبدو كل شيء جيداً، لكن المشكلة تظهر عندما ينتج مفتاحان مختلفان الفهرس نفسه.
مشكلة التصادم Collision في جدول التجزئة
تحدث مشكلة التصادم عندما تُعيد دالة _hash() القيمة نفسها لمفتاحين مختلفين. عندها يتم تخزين العنصر الجديد في الموضع نفسه، فيكتب فوق العنصر السابق إذا لم تكن هناك آلية مناسبة للمعالجة.
جرّب المثال التالي:
const ht = new HashTable();
ht.set("Spain", 110);
ht.set("ǻ", 192);
console.log(ht.get("Spain")); // [ 'ǻ', 192 ]
console.log(ht.get("ǻ")); // [ 'ǻ', 192 ]
النتيجة هنا تكشف أن القيمة الأولى ضاعت، لأن المفتاحين أنتجا الفهرس نفسه تقريباً وفق الدالة الحالية.
في هذه الحالة، تصبح البنية الداخلية أشبه بما يلي:
[
["Spain", 110],
["France", 100]
]
لكننا نحتاج عملياً إلى تخزين أكثر من عنصر داخل الفهرس نفسه، لتصبح البنية كالتالي:
[
[["Spain", 110], ["ǻ", 192]],
[["France", 100]],
]
هذا الأسلوب يسمى chaining أو الربط بالسلاسل، وهو من أكثر طرق معالجة التصادمات استخداماً.
كيف نعالج التصادمات باستخدام chaining؟
تحديث الدالة set()
بدلاً من تخزين عنصر واحد فقط في كل فهرس، سنجعل كل خانة تحتوي على مصفوفة فرعية. إذا وُجد المفتاح نفسه سابقاً نقوم بتحديث قيمته، وإلا نضيف زوجاً جديداً:
set(key, value) {
const index = this._hash(key);
if (this.table[index]) {
for (let i = 0; i < this.table[index].length; i++) {
// Find the key/value pair in the chain
if (this.table[index][i][0] === key) {
this.table[index][i][1] = value;
return;
}
}
// not found, push a new key/value pair
this.table[index].push([key, value]);
} else {
this.table[index] = [];
this.table[index].push([key, value]);
}
this.size++;
}
تحديث الدالة get()
بعد اعتماد المصفوفة الفرعية، يجب على get() أن تبحث داخل السلسلة الموجودة في الفهرس المستهدف:
get(key) {
const target = this._hash(key);
if (this.table[target]) {
for (let i = 0; i < this.table.length; i++) {
if (this.table[target][i][0] === key) {
return this.table[target][i][1];
}
}
}
return undefined;
}
تحديث الدالة remove()
أما الحذف، فيتطلب المرور على عناصر السلسلة ثم حذف الزوج الصحيح باستخدام splice():
remove(key) {
const index = this._hash(key);
if (this.table[index] && this.table[index].length) {
for (let i = 0; i < this.table.length; i++) {
if (this.table[index][i][0] === key) {
this.table[index].splice(i, 1);
this.size--;
return true;
}
}
} else {
return false;
}
}
إضافة الدالة display()
لإظهار جميع العناصر المخزنة بطريقة مفهومة، يمكن إضافة دالة display() التي تستخدم forEach() وmap():
display() {
this.table.forEach((values, index) => {
const chainedValues = values.map(([key, value]) => `[ ${key} : ${value} ]`);
console.log(`${index} : ${chainedValues}`);
});
}
النسخة الكاملة من HashTable بعد معالجة التصادمات
class HashTable {
constructor() {
this.table = new Array(127);
this.size = 0;
}
_hash(key) {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash += key.charCodeAt(i);
}
return hash % this.table.length;
}
set(key, value) {
const index = this._hash(key);
if (this.table[index]) {
for (let i = 0; i < this.table[index].length; i++) {
if (this.table[index][i][0] === key) {
this.table[index][i][1] = value;
return;
}
}
this.table[index].push([key, value]);
} else {
this.table[index] = [];
this.table[index].push([key, value]);
}
this.size++;
}
get(key) {
const index = this._hash(key);
if (this.table[index]) {
for (let i = 0; i < this.table.length; i++) {
if (this.table[index][i][0] === key) {
return this.table[index][i][1];
}
}
}
return undefined;
}
remove(key) {
const index = this._hash(key);
if (this.table[index] && this.table[index].length) {
for (let i = 0; i < this.table.length; i++) {
if (this.table[index][i][0] === key) {
this.table[index].splice(i, 1);
this.size--;
return true;
}
}
} else {
return false;
}
}
display() {
this.table.forEach((values, index) => {
const chainedValues = values.map(([key, value]) => `[ ${key} : ${value} ]`);
console.log(`${index} : ${chainedValues}`);
});
}
}
تجربة عملية على النسخة المحسنة
يمكنك الآن اختبار التنفيذ الجديد للتأكد من أن التصادمات لم تعد تسبب فقدان البيانات:
const ht = new HashTable();
ht.set("France", 111);
ht.set("Spain", 150);
ht.set("ǻ", 192);
ht.display();
// 83: [ France: 111 ]
// 126: [ Spain: 150 ],[ ǻ: 192 ]
console.log(ht.size); // 3
ht.remove("Spain");
ht.display();
// 83: [ France: 111 ]
// 126: [ ǻ: 192 ]
بهذا الشكل أصبح جدول التجزئة قادراً على تخزين أكثر من عنصر في الفهرس نفسه دون الكتابة فوق العناصر السابقة.
متى تستخدم Object ومتى تستخدم Map أو HashTable مخصصاً؟
- استخدم
Objectعندما تكون البنية بسيطة وتتعامل مع خصائص ثابتة ومباشرة. - استخدم
Mapعندما تحتاج إلى إدارة أوضح للمفاتيح والقيم، ومعرفة الحجم بسهولة، ودعم التكرار بشكل مريح. - أنشئ
HashTableمخصصاً عندما يكون الهدف تعليمياً، أو عندما تريد فهماً أعمق لآلية العمل الداخلية، أو أثناء المقابلات التقنية.
أفضل ممارسات عند شرح هياكل البيانات في المحتوى التقني
إذا كنت تكتب محتوى تقنياً احترافياً، فمن المهم ألا تكتفي بعرض الكود فقط، بل توضح أيضاً سبب تصميمه بهذه الطريقة. في حالة Hash Table مثلاً، يجب شرح العلاقة بين:
- جودة دالة
hash. - عدد الحاويات
buckets. - نسبة التصادمات.
- الأداء النهائي للتطبيق.
هذه النقاط تجعل المقال أكثر فائدة للقارئ، وأكثر جدارة بالقبول في منصات الإعلانات ومحركات البحث، لأنه يقدم شرحاً أصيلاً يخدم المستخدم فعلاً.

الخلاصة التقنية
يُعد Hash Table من أهم هياكل البيانات التي تمنح المطوّر توازناً ممتازاً بين السرعة والمرونة. في JavaScript يمكن الاعتماد على Object أو Map في كثير من السيناريوهات، لكن بناء فئة HashTable مخصصة يظل وسيلة قوية لفهم التجزئة والتصادمات وآليات التخزين الداخلي. ومن الناحية التقنية، فإن نجاح أي تنفيذ لجدول التجزئة يعتمد بدرجة كبيرة على كفاءة دالة hash وطريقة معالجة collision، لأنهما العاملان الأكثر تأثيراً في الأداء الفعلي للبنية.