أساسيات JavaScript

WeakSet و WeakMap

45 دقيقة الدرس 39 من 60

مقدمة في المجموعات الضعيفة

يوفر JavaScript نوعين متخصصين من المجموعات -- WeakSet و WeakMap -- يعملان بشكل مشابه لـ Set و Map ولكن مع فرق جوهري واحد: يحتفظان بمراجع ضعيفة لمفاتيحهما. المرجع الضعيف يعني أن جامع القمامة في محرك JavaScript يمكنه استعادة ذاكرة كائن مخزن في مجموعة ضعيفة إذا لم تكن هناك مراجع أخرى لذلك الكائن في أي مكان في برنامجك. هذا السلوك يجعل المجموعات الضعيفة مثالية للسيناريوهات التي تحتاج فيها إلى ربط بيانات وصفية بكائنات دون منع تلك الكائنات من التنظيف عندما لم تعد هناك حاجة إليها.

يتطلب فهم المجموعات الضعيفة استيعابًا قويًا لكيفية إدارة JavaScript للذاكرة. في JavaScript، يقوم جامع القمامة تلقائيًا بتحرير الذاكرة التي تشغلها الكائنات التي لم يعد يمكن الوصول إليها من جذر البرنامج (النطاق العام، ونطاقات الدوال النشطة، وما إلى ذلك). مع Set و Map العادية، يؤدي تخزين كائن إلى إنشاء مرجع قوي يبقي الكائن حيًا حتى لو لم يشير إليه أي جزء آخر من الكود. المجموعات الضعيفة تحل هذه المشكلة بالسماح لجامع القمامة بأداء عمله بغض النظر عما إذا كان الكائن موجودًا في المجموعة.

ملاحظة: المجموعات الضعيفة ليست بديلاً عن Set و Map. إنها تخدم غرضًا مختلفًا جوهريًا. استخدم Set و Map عندما تحتاج إلى التكرار على العناصر، أو التحقق من حجم المجموعة، أو تخزين قيم أولية. استخدم WeakSet و WeakMap عندما تريد إرفاق بيانات مساعدة بالكائنات دون التدخل في عملية جمع القمامة.

WeakSet -- الإنشاء والعمليات الأساسية

WeakSet هي مجموعة من الكائنات حيث يمكن لكل كائن أن يظهر مرة واحدة فقط، تمامًا مثل Set العادية. ومع ذلك، تقبل WeakSet الكائنات فقط كأعضاء -- لا يُسمح بالقيم الأولية مثل السلاسل النصية أو الأرقام. يمكنك إنشاء WeakSet باستخدام المُنشئ new WeakSet()، مع تمرير اختياري لمُكرر من الكائنات.

مثال: إنشاء WeakSet

// إنشاء WeakSet فارغة
const ws = new WeakSet();

// إنشاء كائنات لإضافتها
const user = { name: 'Alice' };
const admin = { name: 'Bob', role: 'admin' };
const guest = { name: 'Charlie' };

// إنشاء WeakSet بقيم أولية
const wsWithValues = new WeakSet([user, admin, guest]);

console.log(wsWithValues.has(user));   // true
console.log(wsWithValues.has(admin));  // true

إضافة العناصر باستخدام add()

يقوم التابع add() بإدراج كائن في WeakSet. إذا كان الكائن موجودًا بالفعل، فلن يكون للاستدعاء أي تأثير. يُرجع التابع WeakSet نفسه، مما يسمح لك بربط عدة استدعاءات add() معًا.

مثال: إضافة كائنات إلى WeakSet

const ws = new WeakSet();

const objA = { id: 1 };
const objB = { id: 2 };
const objC = { id: 3 };

// إضافة الكائنات بشكل فردي
ws.add(objA);
ws.add(objB);

// ربط استدعاءات add
ws.add(objA).add(objC);

console.log(ws.has(objA)); // true
console.log(ws.has(objB)); // true
console.log(ws.has(objC)); // true

// إضافة نفس الكائن مرة أخرى ليس لها تأثير
ws.add(objA);
console.log(ws.has(objA)); // لا يزال true، لا توجد نسخة مكررة

التحقق من العضوية باستخدام has()

يُرجع التابع has() القيمة true إذا كان الكائن المحدد موجودًا في WeakSet و false في الحالة الأخرى. هذه هي الطريقة الأساسية للاستعلام عن WeakSet لأنه لا يمكنك التكرار على محتوياتها.

مثال: التحقق مما إذا كان كائن موجودًا في WeakSet

const ws = new WeakSet();

const existing = { status: 'active' };
const missing = { status: 'inactive' };

ws.add(existing);

console.log(ws.has(existing)); // true
console.log(ws.has(missing));  // false
console.log(ws.has({}));       // false -- مرجع كائن مختلف
مهم: تستخدم WeakSet هوية الكائن (المساواة المرجعية)، وليس المساواة الهيكلية. كائنان لهما خصائص متطابقة لا يزالان كائنين مختلفين. ws.has({ status: 'active' }) تُرجع false حتى لو أضفت كائنًا بنفس الخصائص، لأنه كائن مختلف في الذاكرة.

إزالة العناصر باستخدام delete()

يقوم التابع delete() بإزالة كائن من WeakSet. يُرجع true إذا تم العثور على الكائن وإزالته، و false إذا لم يكن الكائن في WeakSet.

مثال: الحذف من WeakSet

const ws = new WeakSet();

const item = { value: 42 };
ws.add(item);

console.log(ws.has(item));    // true
console.log(ws.delete(item)); // true -- تمت الإزالة بنجاح
console.log(ws.has(item));    // false
console.log(ws.delete(item)); // false -- لم يكن في المجموعة

قيود WeakSet

لدى WeakSet عدة قيود مقصودة تميزها عن Set العادية. هذه القيود موجودة بسبب دلالات المرجع الضعيف والتوقيت غير المتوقع لجمع القمامة.

الكائنات فقط مسموح بها

لا يمكنك إضافة قيم أولية (سلاسل نصية، أرقام، قيم منطقية، رموز، null، undefined، أو BigInt) إلى WeakSet. محاولة القيام بذلك تطرح خطأ TypeError. هذا القيد موجود لأن القيم الأولية لا تُجمع قمامتها بنفس طريقة الكائنات -- يتم إدارتها بالقيمة وليس بالمرجع.

مثال: WeakSet ترفض القيم الأولية

const ws = new WeakSet();

// جميع هذه تطرح TypeError
try { ws.add(42); } catch (e) { console.log(e.message); }
// "Invalid value used in weak set"

try { ws.add('hello'); } catch (e) { console.log(e.message); }
// "Invalid value used in weak set"

try { ws.add(true); } catch (e) { console.log(e.message); }
// "Invalid value used in weak set"

try { ws.add(null); } catch (e) { console.log(e.message); }
// "Invalid value used in weak set"

// هذه تعمل بشكل جيد -- إنها كائنات
ws.add({});
ws.add([]);
ws.add(new Date());
ws.add(new Map());
ws.add(function() {});

لا تكرار، لا حجم

لا تملك WeakSet خاصية size، ولا تابع forEach()، وليست قابلة للتكرار (لا for...of، لا keys()، values()، أو entries()). لا يمكنك تعداد أو حساب العناصر في WeakSet. هذا بالتصميم: لأن جامع القمامة يمكنه إزالة العناصر في أي وقت، فإن محتويات WeakSet غير حتمية. كشف التكرار سيخلق سلوكًا غير متوقع.

مثال: WeakSet غير قابلة للتكرار

const ws = new WeakSet();
ws.add({ a: 1 });
ws.add({ b: 2 });

// لا شيء من هذه يعمل
console.log(ws.size);      // undefined
// ws.forEach(...)          // TypeError: ws.forEach is not a function
// for (const item of ws)   // TypeError: ws is not iterable
// [...ws]                  // TypeError: ws is not iterable
// Array.from(ws)           // TypeError: ws is not iterable

// العمليات المتاحة الوحيدة هي:
// ws.add(obj)    -- إضافة كائن
// ws.has(obj)    -- التحقق من وجود كائن
// ws.delete(obj) -- إزالة كائن
نصيحة احترافية: فكر في WeakSet كأداة سؤال نعم/لا. يمكنك أن تسأل "هل هذا الكائن في المجموعة؟" ويمكنك إضافة أو إزالة الكائنات، لكنك لا تستطيع أن تسأل "ماذا يوجد في المجموعة؟" أو "كم عنصرًا في المجموعة؟" هذا يجعل WeakSet مثالية لوسم أو تمييز الكائنات.

سلوك جمع القمامة

الميزة المحددة لـ WeakSet هي تفاعلها مع جامع القمامة. عندما تضيف كائنًا إلى WeakSet، تحتفظ WeakSet بمرجع ضعيف لذلك الكائن. إذا أصبح الكائن غير قابل للوصول من أي مكان آخر في برنامجك، يكون جامع القمامة حرًا في استعادة تلك الذاكرة، ويُزال الكائن تلقائيًا من WeakSet. لا تحتاج أبدًا إلى تنظيف WeakSet يدويًا -- فهي تعتني بنفسها.

مثال: جمع القمامة مع WeakSet

const ws = new WeakSet();

// إنشاء نطاق دالة لتوضيح جمع القمامة
function createAndAdd() {
    const tempObj = { data: 'temporary' };
    ws.add(tempObj);
    console.log(ws.has(tempObj)); // true
    return; // tempObj يخرج من النطاق هنا
}

createAndAdd();
// بعد هذا الاستدعاء، لم يعد يمكن الوصول إلى tempObj
// جامع القمامة قد يزيله من WeakSet في أي وقت

// مقارنة مع مرجع قوي
let keepAlive = { data: 'persistent' };
ws.add(keepAlive);
console.log(ws.has(keepAlive)); // true

// حتى بعد مرور بعض الوقت، يبقى keepAlive في ws
// لأن المتغير لا يزال يشير إليه

keepAlive = null; // الآن الكائن غير قابل للوصول
// جامع القمامة قد يزيله من WeakSet
ملاحظة: توقيت جمع القمامة غير حتمي. لا يمكنك التنبؤ بالضبط متى سيُزال كائن غير قابل للوصول من WeakSet. لهذا السبب لا تكشف WeakSet عن حجمها أو تسمح بالتكرار -- الإجابة يمكن أن تتغير بين سطرين متتاليين من الكود.

حالات استخدام WeakSet

على الرغم من أن القيود على WeakSet قد تبدو محدودة، إلا أن هناك عدة أنماط مهمة حيث توفر الحل المثالي.

تتبع عناصر DOM

واحدة من أكثر الاستخدامات العملية لـ WeakSet هي تتبع عناصر DOM التي تمت معالجتها بواسطة الكود الخاص بك. عندما تُزال العناصر من DOM، يمكن جمع قمامتها، وتقوم WeakSet بالتنظيف تلقائيًا دون أي تدخل يدوي.

مثال: تتبع عناصر DOM المعالجة

const processedElements = new WeakSet();

function enhanceElement(element) {
    // تخطي إذا تمت المعالجة بالفعل
    if (processedElements.has(element)) {
        return;
    }

    // إضافة فئة الحركة، وإرفاق مستمعي الأحداث، إلخ.
    element.classList.add('enhanced');
    element.addEventListener('click', handleClick);

    // وضع علامة كمعالج
    processedElements.add(element);
}

// معالجة جميع الأزرار الحالية
document.querySelectorAll('button').forEach(enhanceElement);

// لاحقًا، إذا أُضيفت أزرار جديدة ديناميكيًا
const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
        mutation.addedNodes.forEach((node) => {
            if (node.tagName === 'BUTTON') {
                enhanceElement(node);
            }
        });
    });
});

// إذا أُزيل زر من DOM ولم يعد مُشارًا إليه،
// سيتم جمع قمامته تلقائيًا وإزالته من
// processedElements -- لا تسرب ذاكرة!

وضع علامة على الكائنات كمعالجة

في خطوط معالجة البيانات، قد تحتاج إلى تتبع الكائنات التي تمت معالجتها بالفعل لتجنب المعالجة المزدوجة. تسمح لك WeakSet بتمييز الكائنات دون منعها من جمع القمامة بمجرد مرورها عبر خط المعالجة.

مثال: منع المعالجة المزدوجة

const validated = new WeakSet();

function validateUser(user) {
    if (validated.has(user)) {
        console.log('المستخدم تم التحقق منه بالفعل، تخطي.');
        return true;
    }

    // إجراء تحقق مكلف
    const isValid = user.name && user.email && user.age > 0;

    if (isValid) {
        validated.add(user);
    }

    return isValid;
}

const user1 = { name: 'Alice', email: 'alice@example.com', age: 30 };
validateUser(user1); // يقوم بالتحقق الكامل
validateUser(user1); // يتخطى -- تم التحقق بالفعل

// إذا تم تعيين user1 لاحقًا إلى null، يمكن لجامع القمامة
// استعادة ذاكرته دون أن ننظف 'validated'

منع المراجع الدائرية في التسلسل

عندما تحتاج إلى اجتياز أو تسلسل رسم بياني للكائنات قد يحتوي على مراجع دائرية، يمكن لـ WeakSet تتبع الكائنات المزارة لمنع الحلقات اللانهائية. بمجرد اكتمال الاجتياز، لا تمنع WeakSet الكائنات المزارة من جمع القمامة.

مثال: اكتشاف المراجع الدائرية

function deepClone(obj, visited = new WeakSet()) {
    // التعامل مع القيم الأولية و null
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }

    // اكتشاف المرجع الدائري
    if (visited.has(obj)) {
        throw new Error('تم اكتشاف مرجع دائري');
    }

    // وضع علامة على هذا الكائن كمُزار
    visited.add(obj);

    // نسخ المصفوفات
    if (Array.isArray(obj)) {
        return obj.map(item => deepClone(item, visited));
    }

    // نسخ الكائنات العادية
    const clone = {};
    for (const key of Object.keys(obj)) {
        clone[key] = deepClone(obj[key], visited);
    }

    return clone;
}

// اختبار مع مرجع دائري
const parent = { name: 'parent' };
const child = { name: 'child', parent: parent };
parent.child = child; // دائري!

try {
    deepClone(parent);
} catch (e) {
    console.log(e.message); // "تم اكتشاف مرجع دائري"
}

WeakMap -- الإنشاء والعمليات الأساسية

WeakMap هي مجموعة من أزواج المفتاح-القيمة حيث يجب أن تكون المفاتيح كائنات ويمكن أن تكون القيم من أي نوع. مثل WeakSet، تحتفظ WeakMap بمراجع ضعيفة لمفاتيحها، مما يعني أن المدخلات تُزال تلقائيًا عندما يتم جمع قمامة كائن المفتاح. هذا يجعل WeakMap الأداة المثالية لربط البيانات الخاصة أو المساعدة بالكائنات.

مثال: إنشاء WeakMap

// إنشاء WeakMap فارغة
const wm = new WeakMap();

// إنشاء كائنات مفاتيح
const key1 = { id: 1 };
const key2 = { id: 2 };
const key3 = { id: 3 };

// إنشاء WeakMap بمدخلات أولية
const wmWithEntries = new WeakMap([
    [key1, 'القيمة الأولى'],
    [key2, 'القيمة الثانية'],
    [key3, 100]
]);

console.log(wmWithEntries.get(key1)); // "القيمة الأولى"
console.log(wmWithEntries.get(key2)); // "القيمة الثانية"
console.log(wmWithEntries.get(key3)); // 100

تعيين القيم باستخدام set()

يقوم التابع set() بإضافة أو تحديث مدخل في WeakMap. يجب أن يكون المفتاح كائنًا؛ يمكن أن تكون القيمة أي قيمة JavaScript. يُرجع التابع WeakMap نفسه، مما يمكّن ربط التوابع.

مثال: تعيين مدخلات في WeakMap

const wm = new WeakMap();

const user = { name: 'Alice' };
const product = { sku: 'ABC-123' };

// تعيين مدخلات فردية
wm.set(user, { role: 'admin', loginCount: 42 });
wm.set(product, { price: 29.99, inStock: true });

// ربط استدعاءات set
const session = {};
const config = {};
wm.set(session, 'abc123').set(config, { theme: 'dark' });

// تحديث مدخل موجود
wm.set(user, { role: 'superadmin', loginCount: 43 });
console.log(wm.get(user)); // { role: "superadmin", loginCount: 43 }

الحصول على القيم باستخدام get()

يسترجع التابع get() القيمة المرتبطة بكائن مفتاح معين. إذا لم يكن المفتاح موجودًا في WeakMap، يُرجع undefined.

مثال: استرجاع القيم من WeakMap

const wm = new WeakMap();

const element = document.createElement('div');
wm.set(element, { clicks: 0, lastClicked: null });

// الحصول على القيمة
const data = wm.get(element);
console.log(data); // { clicks: 0, lastClicked: null }

// تعديل القيمة المسترجعة
data.clicks++;
data.lastClicked = new Date();

// التغيير يستمر لأن الكائنات هي مراجع
console.log(wm.get(element).clicks); // 1

// مفتاح غير موجود يُرجع undefined
console.log(wm.get({})); // undefined

التحقق من المفاتيح باستخدام has()

يُرجع التابع has() القيمة true إذا كانت WeakMap تحتوي على مدخل للمفتاح المحدد و false في الحالة الأخرى.

مثال: التحقق من وجود مفاتيح في WeakMap

const wm = new WeakMap();

const keyObj = { type: 'config' };
wm.set(keyObj, { debug: true });

console.log(wm.has(keyObj)); // true
console.log(wm.has({}));     // false -- مرجع مختلف

حذف المدخلات باستخدام delete()

يقوم التابع delete() بإزالة المدخل لمفتاح معين. يُرجع true إذا تم العثور على مدخل وحذفه، و false في الحالة الأخرى.

مثال: حذف مدخلات WeakMap

const wm = new WeakMap();

const obj = { x: 10 };
wm.set(obj, 'بعض البيانات');

console.log(wm.has(obj));    // true
console.log(wm.delete(obj)); // true
console.log(wm.has(obj));    // false
console.log(wm.delete(obj)); // false -- تمت الإزالة بالفعل

قيود WeakMap

تشترك WeakMap في قيود مشابهة مع WeakSet، وهي موجودة لنفس الأسباب -- الطبيعة غير الحتمية لجمع القمامة تجعل التعداد غير موثوق.

  • المفاتيح يجب أن تكون كائنات. لا يمكن استخدام القيم الأولية مثل السلاسل النصية والأرقام والقيم المنطقية كمفاتيح WeakMap. محاولة استخدام مفتاح أولي تطرح TypeError.
  • لا خاصية size. لا يمكنك تحديد عدد المدخلات في WeakMap.
  • لا تكرار. لا تملك WeakMap تابع forEach()، أو keys()، أو values()، أو entries()، أو [Symbol.iterator]. لا يمكنك التكرار على محتوياتها.
  • لا تابع clear(). على عكس Map، لا يمكنك مسح جميع المدخلات من WeakMap دفعة واحدة. لمسح WeakMap فعليًا، يجب إنشاء واحدة جديدة.

مثال: قيود مفاتيح WeakMap

const wm = new WeakMap();

// هذه تطرح TypeError -- القيم الأولية ليست مفاتيح صالحة
try { wm.set('key', 'value'); } catch (e) { console.log(e.message); }
try { wm.set(42, 'value'); } catch (e) { console.log(e.message); }
try { wm.set(true, 'value'); } catch (e) { console.log(e.message); }
try { wm.set(Symbol(), 'value'); } catch (e) { console.log(e.message); }

// هذه تعمل -- كائنات كمفاتيح، أي نوع كقيم
wm.set({}, 'قيمة نصية');
wm.set([], 42);
wm.set(function() {}, true);
wm.set(new Date(), null);
wm.set(document.body, [1, 2, 3]);

البيانات الخاصة مع WeakMap

واحدة من أقوى حالات استخدام WeakMap هي تنفيذ بيانات خاصة حقيقية للكائنات. قبل أن تصبح حقول الفئة الخاصة (صيغة #) متاحة، كان نمط WeakMap هو النمط القياسي لتحقيق التغليف في JavaScript. حتى اليوم، تظل الخصوصية القائمة على WeakMap مفيدة في العديد من السيناريوهات، خاصة عند العمل خارج صيغة الفئات.

مثال: البيانات الخاصة باستخدام WeakMap

// نطاق الوحدة -- لا يمكن الوصول إلى WeakMap من الخارج
const _privateData = new WeakMap();

class User {
    constructor(name, email, password) {
        // خصائص عامة
        this.name = name;
        this.email = email;

        // بيانات خاصة مخزنة في WeakMap
        _privateData.set(this, {
            password: password,
            loginAttempts: 0,
            lastLogin: null
        });
    }

    authenticate(password) {
        const priv = _privateData.get(this);
        priv.loginAttempts++;

        if (priv.password === password) {
            priv.lastLogin = new Date();
            return true;
        }

        return false;
    }

    getLoginInfo() {
        const priv = _privateData.get(this);
        return {
            attempts: priv.loginAttempts,
            lastLogin: priv.lastLogin
            // كلمة المرور غير مكشوفة
        };
    }
}

const user = new User('Alice', 'alice@example.com', 'secret123');
console.log(user.name);      // "Alice" -- عام
console.log(user.password);   // undefined -- ليس على الكائن!
user.authenticate('wrong');
user.authenticate('secret123');
console.log(user.getLoginInfo());
// { attempts: 2, lastLogin: [Date] }

// عندما يتم جمع قمامة user، يتم تنظيف البيانات الخاصة
// تلقائيًا من _privateData
نصيحة احترافية: نمط WeakMap للبيانات الخاصة له ميزة كبيرة على الإغلاقات: إنه يعمل مع سلسلة النماذج الأولية. تتشارك جميع النُسخ في نفس التوابع على النموذج الأولي ولكن لكل منها بياناتها الخاصة المخزنة في WeakMap. مع الإغلاقات، تحصل كل نسخة على نسختها الخاصة من كل تابع، مما يستخدم ذاكرة أكثر.

التخزين المؤقت مع WeakMap

WeakMap هي أداة ممتازة لتنفيذ ذاكرات التخزين المؤقت التي لا تسبب تسرب الذاكرة. يمكنك ربط النتائج المحسوبة بالكائنات، وعندما لا تعود هناك حاجة للكائنات، يتم تنظيف القيم المخزنة مؤقتًا تلقائيًا. يُعرف هذا باسم الحفظ مع الإخلاء التلقائي للذاكرة المؤقتة.

مثال: ذاكرة تخزين مؤقت قائمة على WeakMap

const cache = new WeakMap();

function expensiveComputation(obj) {
    // التحقق من الذاكرة المؤقتة أولاً
    if (cache.has(obj)) {
        console.log('إرجاع النتيجة المخزنة مؤقتًا');
        return cache.get(obj);
    }

    // محاكاة عمل مكلف
    console.log('حساب النتيجة...');
    const result = {
        hash: JSON.stringify(obj).split('').reduce(
            (acc, char) => acc + char.charCodeAt(0), 0
        ),
        processed: true,
        timestamp: Date.now()
    };

    // التخزين في الذاكرة المؤقتة
    cache.set(obj, result);
    return result;
}

const data1 = { values: [1, 2, 3, 4, 5] };
const data2 = { values: [10, 20, 30] };

expensiveComputation(data1); // "حساب النتيجة..."
expensiveComputation(data1); // "إرجاع النتيجة المخزنة مؤقتًا"
expensiveComputation(data2); // "حساب النتيجة..."

// عندما يتم جمع قمامة data1 و data2،
// تُزال نتائجهما المخزنة مؤقتًا تلقائيًا
// لا حاجة لإبطال الذاكرة المؤقتة يدويًا!

مثال: تخزين مؤقت لقياسات عناصر DOM

const measurementCache = new WeakMap();

function getElementDimensions(element) {
    if (measurementCache.has(element)) {
        return measurementCache.get(element);
    }

    // getBoundingClientRect مكلف نسبيًا
    const rect = element.getBoundingClientRect();
    const dimensions = {
        width: rect.width,
        height: rect.height,
        top: rect.top,
        left: rect.left
    };

    measurementCache.set(element, dimensions);
    return dimensions;
}

// عندما تُزال العناصر من DOM ويُلغى الإشارة إليها،
// يتم تنظيف القياسات المخزنة مؤقتًا تلقائيًا

WeakRef و FinalizationRegistry (مقدمة موجزة)

قدم ES2021 ميزتين مكملتين: WeakRef و FinalizationRegistry. توفران تحكمًا أدنى مستوى في المراجع الضعيفة وتسمحان بتشغيل استدعاءات التنظيف عندما يتم جمع قمامة الكائنات. بينما هما مرتبطتان بـ WeakSet و WeakMap، فإنهما تخدمان حالات استخدام أكثر تقدمًا ويجب استخدامهما بحذر.

مثال: أساسيات WeakRef

// WeakRef تنشئ مرجعًا ضعيفًا لكائن
let targetObj = { data: 'important' };
const weakRef = new WeakRef(targetObj);

// deref() تُرجع الكائن إذا كان لا يزال حيًا
console.log(weakRef.deref()); // { data: "important" }

// بعد إزالة المرجع القوي...
targetObj = null;

// في نقطة ما بعد جمع القمامة:
// weakRef.deref() ستُرجع undefined
// لكن لا يمكنك التنبؤ بالضبط متى!

مثال: أساسيات FinalizationRegistry

// FinalizationRegistry تشغل استدعاءً عندما يتم جمع قمامة
// الكائنات المسجلة
const registry = new FinalizationRegistry((heldValue) => {
    console.log(`الكائن ذو المعرف "${heldValue}" تم جمعه`);
    // تنفيذ التنظيف: إغلاق الاتصالات، تحرير الموارد، إلخ.
});

let connection = { url: 'wss://example.com', socket: {} };
registry.register(connection, 'websocket-connection-1');

// عندما يتم جمع قمامة connection، يتم تشغيل الاستدعاء
connection = null;
// في النهاية يسجل: الكائن ذو المعرف "websocket-connection-1" تم جمعه
مهم: يجب تجنب WeakRef و FinalizationRegistry في معظم كود التطبيقات. سلوك جامع القمامة غير حتمي، لذا لا يمكنك الاعتماد على توقيت إرجاع deref() لـ undefined أو تشغيل استدعاءات الإنهاء. استخدمهما فقط للسيناريوهات المتقدمة مثل ذاكرات التخزين المؤقت المخصصة، أو إدارة الموارد، أو التصحيح. تحذر وثائق اقتراح TC39 صراحة: "يتطلب الاستخدام الصحيح لـ FinalizationRegistry تفكيرًا دقيقًا، ومن الأفضل تجنبه إن أمكن."

المقارنة: Set مقابل WeakSet، Map مقابل WeakMap

فهم الاختلافات بين المتغيرات القوية والضعيفة لهذه المجموعات أمر ضروري لاختيار الأداة الصحيحة. المقارنة التالية تغطي جميع الفروق الرئيسية.

مرجع: Set مقابل WeakSet

// ┌──────────────────────┬────────────────────┬────────────────────┐
// │ الميزة               │ Set                │ WeakSet            │
// ├──────────────────────┼────────────────────┼────────────────────┤
// │ أنواع المفاتيح/القيم │ أي قيمة            │ الكائنات فقط       │
// │ نوع المرجع           │ قوي               │ ضعيف               │
// │ جمع القمامة          │ يمنع GC           │ يسمح بـ GC         │
// │ خاصية size           │ نعم               │ لا                 │
// │ قابل للتكرار         │ نعم               │ لا                 │
// │ forEach()            │ نعم               │ لا                 │
// │ keys()/values()      │ نعم               │ لا                 │
// │ entries()            │ نعم               │ لا                 │
// │ clear()              │ نعم               │ لا                 │
// │ add()                │ نعم               │ نعم                │
// │ has()                │ نعم               │ نعم                │
// │ delete()             │ نعم               │ نعم                │
// │ حالة الاستخدام       │ مجموعة فريدة      │ وسم الكائنات       │
// └──────────────────────┴────────────────────┴────────────────────┘

مرجع: Map مقابل WeakMap

// ┌──────────────────────┬────────────────────┬────────────────────┐
// │ الميزة               │ Map                │ WeakMap            │
// ├──────────────────────┼────────────────────┼────────────────────┤
// │ أنواع المفاتيح       │ أي قيمة            │ الكائنات فقط       │
// │ أنواع القيم           │ أي قيمة            │ أي قيمة            │
// │ نوع المرجع           │ قوي               │ ضعيف (المفاتيح فقط)│
// │ جمع القمامة          │ يمنع GC           │ يسمح بـ GC للمفاتيح│
// │ خاصية size           │ نعم               │ لا                 │
// │ قابل للتكرار         │ نعم               │ لا                 │
// │ forEach()            │ نعم               │ لا                 │
// │ keys()/values()      │ نعم               │ لا                 │
// │ entries()            │ نعم               │ لا                 │
// │ clear()              │ نعم               │ لا                 │
// │ set()/get()          │ نعم               │ نعم                │
// │ has()                │ نعم               │ نعم                │
// │ delete()             │ نعم               │ نعم                │
// │ حالة الاستخدام       │ تخزين مفتاح-قيمة  │ بيانات خاصة/وصفية │
// └──────────────────────┴────────────────────┴────────────────────┘

مثال: اختيار المجموعة الصحيحة

// استخدم Set عندما تحتاج مجموعة فريدة من أي قيم
const uniqueTags = new Set(['javascript', 'css', 'html']);
console.log(uniqueTags.size); // 3

// استخدم WeakSet عندما تحتاج وسم كائنات مؤقتًا
const visited = new WeakSet();
function visitNode(node) {
    if (visited.has(node)) return;
    visited.add(node);
    // معالجة العقدة...
}

// استخدم Map عندما تحتاج أزواج مفتاح-قيمة بأي نوع مفتاح
const settings = new Map();
settings.set('theme', 'dark');
settings.set(42, 'الإجابة');

// استخدم WeakMap عندما تربط بيانات بكائنات
// وتريد تنظيفًا تلقائيًا
const metadata = new WeakMap();
function attachMeta(obj, meta) {
    metadata.set(obj, meta);
}
// عندما يتم جمع قمامة obj، يتم تنظيف meta تلقائيًا

أنماط واقعية

دعونا نلقي نظرة على بعض الأنماط الواقعية المتقدمة التي تجمع بين WeakSet و WeakMap لحل مشكلات عملية ستواجهها في كود الإنتاج.

مثال: متتبع مستمعي الأحداث (WeakMap + WeakSet)

// تتبع المستمعين المرفقين بأي عناصر
const listenerMap = new WeakMap();

function addTrackedListener(element, event, handler) {
    // الحصول على أو إنشاء مجموعة الأحداث لهذا العنصر
    if (!listenerMap.has(element)) {
        listenerMap.set(element, new Map());
    }

    const events = listenerMap.get(element);

    // الحصول على أو إنشاء مجموعة المعالجات لهذا الحدث
    if (!events.has(event)) {
        events.set(event, new WeakSet());
    }

    const handlers = events.get(event);

    // إضافة فقط إذا لم يكن مرفقًا بالفعل
    if (!handlers.has(handler)) {
        element.addEventListener(event, handler);
        handlers.add(handler);
    }
}

function removeTrackedListener(element, event, handler) {
    const events = listenerMap.get(element);
    if (!events) return;

    const handlers = events.get(event);
    if (!handlers) return;

    if (handlers.has(handler)) {
        element.removeEventListener(event, handler);
        handlers.delete(handler);
    }
}

// الاستخدام
const button = document.createElement('button');
const clickHandler = () => console.log('تم النقر');

addTrackedListener(button, 'click', clickHandler);
addTrackedListener(button, 'click', clickHandler); // يتم تجاهله، مضاف بالفعل
// عندما يُزال الزر من DOM ويُلغى الإشارة إليه، يتم تنظيف كل شيء

مثال: عداد وصول الكائنات مع WeakMap

const accessCounter = new WeakMap();

function trackAccess(obj) {
    const count = accessCounter.get(obj) || 0;
    accessCounter.set(obj, count + 1);
    return obj;
}

function getAccessCount(obj) {
    return accessCounter.get(obj) || 0;
}

const resource = { type: 'image', src: '/photo.jpg' };

trackAccess(resource);
trackAccess(resource);
trackAccess(resource);

console.log(getAccessCount(resource)); // 3

// عندما لا يُشار إلى resource بعد الآن،
// يتم جمع قمامة العداد تلقائيًا

تمرين عملي

ابنِ تطبيقًا صغيرًا يوضح استخدام كل من WeakSet و WeakMap. أنشئ فئة TaskManager تدير كائنات المهام. استخدم WeakSet تسمى completedTasks لتتبع المهام التي اكتملت، و WeakMap تسمى taskMetadata لتخزين بيانات وصفية خاصة (طابع زمني للإنشاء، مستوى الأولوية، المستخدم المعين) لكل مهمة. نفّذ التوابع التالية: (1) addTask(task) التي تسجل مهمة جديدة مع بيانات وصفية. (2) completeTask(task) التي تضع علامة على المهمة كمكتملة باستخدام WeakSet. (3) isCompleted(task) التي تتحقق مما إذا كانت المهمة مكتملة. (4) getMetadata(task) التي تُرجع البيانات الوصفية الخاصة لمهمة دون كشف الحالة الداخلية. (5) removeTask(task) التي تزيل مهمة من كل من WeakSet و WeakMap. ثم اكتب دالة تسمى deepEqual(obj1, obj2, visited) تستخدم WeakSet لتتبع الكائنات المزارة وتتعامل بشكل صحيح مع المراجع الدائرية أثناء المقارنة العميقة. اختبر الكود بإنشاء مهام، وإكمال بعضها، والتحقق من حالتها، واسترجاع البيانات الوصفية، والتحقق من أن اكتشاف المراجع الدائرية يعمل بشكل صحيح.