كما نعلم من فصل جمع البيانات المهملة، يحتفظ محرك JavaScript بقيمة في الذاكرة بينما يكون "قابلاً للوصول" ويمكن استخدامه.
على سبيل المثال:
دع جون = { الاسم: "جون" }; // يمكن الوصول إلى الكائن، وجون هو المرجع إليه // الكتابة فوق المرجع جون = فارغ؛ // ستتم إزالة الكائن من الذاكرة
عادة، تعتبر خصائص كائن أو عناصر مصفوفة أو بنية بيانات أخرى قابلة للوصول ويتم الاحتفاظ بها في الذاكرة أثناء وجود بنية البيانات هذه في الذاكرة.
على سبيل المثال، إذا وضعنا كائنًا في مصفوفة، فطالما أن المصفوفة حية، فسيكون الكائن حيًا أيضًا، حتى لو لم تكن هناك إشارات أخرى إليه.
مثله:
دع جون = { الاسم: "جون" }; دع المصفوفة = [جون]؛ جون = فارغ؛ // الكتابة فوق المرجع // يتم تخزين الكائن الذي أشار إليه جون مسبقًا داخل المصفوفة // لذلك لن يتم جمع البيانات المهملة // يمكننا الحصول عليها كمصفوفة[0]
وبالمثل، إذا استخدمنا كائنًا كمفتاح في Map
عادية، فعند وجود Map
، يكون هذا الكائن موجودًا أيضًا. إنها تشغل الذاكرة وقد لا يتم تجميع البيانات المهملة فيها.
على سبيل المثال:
دع جون = { الاسم: "جون" }; دع الخريطة = خريطة جديدة ()؛ Map.set(john, "..."); جون = فارغ؛ // الكتابة فوق المرجع // يتم تخزين جون داخل الخريطة، // يمكننا الحصول عليه باستخدام Map.keys()
يختلف WeakMap
بشكل أساسي في هذا الجانب. لا يمنع جمع البيانات المهملة من الكائنات الرئيسية.
دعونا نرى ما يعنيه على الأمثلة.
الفرق الأول بين Map
و WeakMap
هو أن المفاتيح يجب أن تكون كائنات، وليست قيمًا أولية:
دع WeakMap = new WeakMap(); دع obj = {}; WeakMap.set(obj, "ok"); // يعمل بشكل جيد (مفتاح الكائن) // لا يمكن استخدام سلسلة كمفتاح WeakMap.set("test", "Whoops"); // خطأ، لأن "الاختبار" ليس كائنًا
الآن، إذا استخدمنا كائنًا كمفتاح فيه، ولا توجد إشارات أخرى لذلك الكائن - فسيتم إزالته من الذاكرة (ومن الخريطة) تلقائيًا.
دع جون = { الاسم: "جون" }; دع WeakMap = new WeakMap(); WeakMap.set(john, "..."); جون = فارغ؛ // الكتابة فوق المرجع // تتم إزالة جون من الذاكرة!
قارنه بمثال Map
العادي أعلاه. الآن، إذا كان john
موجودًا فقط كمفتاح WeakMap
- فسيتم حذفه تلقائيًا من الخريطة (والذاكرة).
لا يدعم WeakMap
التكرار والأساليب keys()
, values()
, entries()
, لذلك لا توجد طريقة للحصول على جميع المفاتيح أو القيم منه.
لدى WeakMap
الطرق التالية فقط:
weakMap.set(key, value)
weakMap.get(key)
weakMap.delete(key)
weakMap.has(key)
لماذا هذا القيد؟ وذلك لأسباب فنية. إذا فقد الكائن جميع المراجع الأخرى (مثل john
في الكود أعلاه)، فسيتم تجميعه تلقائيًا. لكن من الناحية الفنية، لم يتم تحديد موعد حدوث عملية التنظيف بالضبط.
محرك جافا سكريبت يقرر ذلك. قد يختار إجراء تنظيف الذاكرة على الفور أو الانتظار وإجراء التنظيف لاحقًا عند حدوث المزيد من عمليات الحذف. لذا، من الناحية الفنية، عدد العناصر الحالي لـ WeakMap
غير معروف. ربما قام المحرك بتنظيفه أم لا، أو قام بذلك جزئيًا. ولهذا السبب، لا يتم دعم الأساليب التي يمكنها الوصول إلى جميع المفاتيح/القيم.
الآن، أين نحتاج إلى مثل هذه البنية البيانات؟
مجال التطبيق الرئيسي لـ WeakMap
هو تخزين بيانات إضافي .
إذا كنا نعمل مع كائن "ينتمي" إلى كود آخر، وربما حتى مكتبة تابعة لجهة خارجية، ونرغب في تخزين بعض البيانات المرتبطة به، فيجب أن يكون موجودًا فقط عندما يكون الكائن حيًا - إذن WeakMap
هو بالضبط ما هو مطلوب. ضروري.
نضع البيانات في WeakMap
، باستخدام الكائن كمفتاح، وعندما يتم جمع البيانات المهملة للكائن، ستختفي هذه البيانات تلقائيًا أيضًا.
WeakMap.set(john, "المستندات السرية"); // إذا مات جون، فسيتم تدمير المستندات السرية تلقائيًا
دعونا نلقي نظرة على مثال.
على سبيل المثال، لدينا كود يحتفظ بعدد الزيارات للمستخدمين. يتم تخزين المعلومات في الخريطة: كائن المستخدم هو المفتاح وعدد الزيارات هو القيمة. عندما يغادر المستخدم (يتم تجميع البيانات المهملة لكائنه)، لا نريد تخزين عدد زياراته بعد الآن.
فيما يلي مثال لوظيفة العد باستخدام Map
:
// ؟ عدد الزيارات.js السماح للزياراتCountMap = new Map(); // الخريطة: المستخدم => عدد الزيارات // زيادة عدد الزيارات وظيفة العد المستخدم (المستخدم) { دع العد = الزياراتCountMap.get(user) || 0; VisitCountMap.set(user, count + 1); }
وإليك جزءًا آخر من الكود، وربما ملفًا آخر يستخدمه:
// ؟ main.js دع جون = { الاسم: "جون" }; countUser(john); // عد زياراته // في وقت لاحق يتركنا جون جون = فارغ؛
الآن، يجب أن يتم جمع كائن john
من البيانات المهملة، ولكنه يبقى في الذاكرة، حيث إنه مفتاح في visitsCountMap
.
نحتاج إلى تنظيف visitsCountMap
عندما نقوم بإزالة المستخدمين، وإلا فسوف ينمو في الذاكرة إلى أجل غير مسمى. يمكن أن يصبح مثل هذا التنظيف مهمة شاقة في البنى المعقدة.
يمكننا تجنب ذلك عن طريق التبديل إلى WeakMap
بدلاً من ذلك:
// ؟ عدد الزيارات.js السماح للزياراتCountMap = new WeakMap(); // Weakmap: المستخدم => عدد الزيارات // زيادة عدد الزيارات وظيفة العد المستخدم (المستخدم) { دع العد = الزياراتCountMap.get(user) || 0; VisitCountMap.set(user, count + 1); }
الآن ليس علينا تنظيف visitsCountMap
. بعد أن يصبح كائن john
غير قابل للوصول، بكل الوسائل باستثناء مفتاح WeakMap
، تتم إزالته من الذاكرة، بالإضافة إلى المعلومات الموجودة بهذا المفتاح من WeakMap
.
مثال شائع آخر هو التخزين المؤقت. يمكننا تخزين نتائج ("ذاكرة التخزين المؤقت") من إحدى الوظائف، بحيث يمكن للاستدعاءات المستقبلية على نفس الكائن إعادة استخدامها.
ولتحقيق ذلك يمكننا استخدام Map
(ليس السيناريو الأمثل):
// ؟ ذاكرة التخزين المؤقت.js دع ذاكرة التخزين المؤقت = خريطة جديدة ()؛ // احسب النتيجة وتذكرها عملية الوظيفة (الكائن) { إذا (!cache.has(obj)) { Let result = /* حسابات النتيجة لـ */ obj; Cache.set(obj, result); نتيجة الإرجاع؛ } إرجاع ذاكرة التخزين المؤقت.get(obj); } // الآن نستخدم العملية () في ملف آخر: // ؟ main.js Let obj = {/* لنفترض أن لدينا كائنًا */; دع النتيجة 1 = عملية (obj)؛ // محسوبة // ...لاحقًا، من مكان آخر من الكود... دع النتيجة 2 = عملية (obj)؛ // النتيجة المحفوظة مأخوذة من ذاكرة التخزين المؤقت // ...في وقت لاحق، عندما لا تكون هناك حاجة للكائن بعد الآن: obj = null; تنبيه (cache.size)؛ // 1 (أوه! الكائن لا يزال في ذاكرة التخزين المؤقت، ويستهلك الذاكرة!)
بالنسبة لاستدعاءات process(obj)
المتعددة مع نفس الكائن، فإنها تحسب النتيجة في المرة الأولى فقط، ثم تأخذها من cache
المؤقت فقط. الجانب السلبي هو أننا نحتاج إلى تنظيف cache
المؤقت عندما لا تكون هناك حاجة للكائن بعد الآن.
إذا استبدلنا Map
بـ WeakMap
، فستختفي هذه المشكلة. ستتم إزالة النتيجة المخزنة مؤقتًا من الذاكرة تلقائيًا بعد تجميع البيانات المهملة للكائن.
// ؟ ذاكرة التخزين المؤقت.js Let Cache = new WeakMap(); // احسب النتيجة وتذكرها عملية الوظيفة (الكائن) { إذا (!cache.has(obj)) { Let result = /* احسب نتيجة */ obj; Cache.set(obj, result); نتيجة الإرجاع؛ } إرجاع ذاكرة التخزين المؤقت.get(obj); } // ؟ main.js Let obj = {/* بعض الكائنات * /}؛ دع النتيجة 1 = عملية (obj)؛ دع النتيجة 2 = عملية (obj)؛ // ...في وقت لاحق، عندما لا تكون هناك حاجة للكائن بعد الآن: obj = null; // لا يمكن الحصول على حجم ذاكرة التخزين المؤقت، لأنه WeakMap، // لكنه 0 أو سيصبح 0 قريبًا // عندما يتم جمع البيانات المهملة من obj، ستتم إزالة البيانات المخزنة مؤقتًا أيضًا
يتصرف WeakSet
بالمثل:
إنه مشابه لـ Set
، ولكن يمكننا فقط إضافة كائنات إلى WeakSet
(وليس العناصر الأولية).
يوجد كائن في المجموعة بينما يمكن الوصول إليه من مكان آخر.
مثل Set
، فهو يدعم add
has
delete
ولكن ليس size
keys()
ولا يوجد تكرارات.
ولكونه "ضعيفًا"، فهو أيضًا بمثابة مساحة تخزين إضافية. ولكن ليس من أجل البيانات الاعتباطية، بل من أجل حقائق "نعم/لا". قد تعني العضوية في WeakSet
شيئًا ما عن الكائن.
على سبيل المثال، يمكننا إضافة مستخدمين إلى WeakSet
لتتبع أولئك الذين زاروا موقعنا:
دع VisitSet = new WeakSet(); دع جون = { الاسم: "جون" }; دع بيت = { الاسم: "بيت" }; دع ماري = { الاسم: "مريم" }; VisitSet.add(john); // زارنا جون VisitSet.add(pete); // ثم بيت VisitSet.add(john); // جون مرة أخرى // VisitSet لديه مستخدمان الآن // تحقق مما إذا كان جون قد زار؟ تنبيه (visitedSet.has (جون))؛ // حقيقي // تحقق مما إذا كانت ماري قد زارت؟ تنبيه(visitedSet.has(ماري)); // خطأ شنيع جون = فارغ؛ // سيتم تنظيف المجموعة التي تمت زيارتها تلقائيًا
أبرز القيود على WeakMap
و WeakSet
هو غياب التكرارات، وعدم القدرة على الحصول على كل المحتوى الحالي. قد يبدو ذلك غير مريح، لكنه لا يمنع WeakMap/WeakSet
من القيام بعملهم الرئيسي - وهو تخزين "إضافي" للبيانات للكائنات التي يتم تخزينها/إدارتها في مكان آخر.
WeakMap
عبارة عن مجموعة شبيهة Map
تسمح فقط للكائنات كمفاتيح وتزيلها مع القيمة المرتبطة بها بمجرد أن يتعذر الوصول إليها بوسائل أخرى.
WeakSet
عبارة عن مجموعة شبيهة Set
تقوم بتخزين الكائنات فقط وإزالتها عندما يتعذر الوصول إليها بوسائل أخرى.
مزاياها الرئيسية هي أن مرجعها ضعيف للكائنات، لذلك يمكن إزالتها بسهولة بواسطة جامع البيانات المهملة.
ويأتي ذلك على حساب عدم وجود دعم clear
size
keys
values
...
يتم استخدام WeakMap
و WeakSet
كهياكل بيانات "ثانوية" بالإضافة إلى تخزين الكائنات "الأساسي". بمجرد إزالة الكائن من وحدة التخزين الأساسية، إذا تم العثور عليه فقط كمفتاح WeakMap
أو في WeakSet
، فسيتم تنظيفه تلقائيًا.
الأهمية: 5
هناك مجموعة من الرسائل:
السماح للرسائل = [ {نص: "مرحبًا"، من: "جون"}، {نص: "كيف الحال؟"، من: "جون"}، {نص: "أراك قريبًا"، من: "أليس"} ];
يمكن لرمزك الوصول إليه، ولكن تتم إدارة الرسائل بواسطة رمز شخص آخر. تتم إضافة رسائل جديدة، وتتم إزالة الرسائل القديمة بانتظام بواسطة هذا الرمز، ولا تعرف بالضبط اللحظات التي يحدث فيها ذلك.
الآن، ما هي بنية البيانات التي يمكنك استخدامها لتخزين المعلومات حول ما إذا كانت الرسالة "قد تمت قراءتها"؟ يجب أن يكون الهيكل مناسبًا تمامًا لإعطاء الإجابة "هل تمت قراءته؟" لكائن الرسالة المحدد.
ملاحظة: عندما تتم إزالة رسالة من messages
، يجب أن تختفي من البنية الخاصة بك أيضًا.
PPS لا ينبغي لنا تعديل كائنات الرسالة، بل إضافة خصائصنا إليها. نظرًا لأنها تتم إدارتها بواسطة رمز شخص آخر، فقد يؤدي ذلك إلى عواقب وخيمة.
لنقم بتخزين الرسائل المقروءة في WeakSet
:
السماح للرسائل = [ {نص: "مرحبًا"، من: "جون"}، {نص: "كيف الحال؟"، من: "جون"}، {نص: "أراك قريبًا"، من: "أليس"} ]; دع readMessages = new WeakSet(); // تمت قراءة رسالتين readMessages.add(messages[0]); readMessages.add(messages[1]); // تحتوي الرسائل readMessages على عنصرين // ... فلنقرأ الرسالة الأولى مرة أخرى! readMessages.add(messages[0]); // readMessages لا يزال يحتوي على عنصرين فريدين // الجواب: هل تمت قراءة الرسالة [0]؟ تنبيه ("اقرأ الرسالة 0:" + readMessages.has(messages[0])); // حقيقي messages.shift(); // الآن تحتوي readMessages على عنصر واحد (من الناحية الفنية قد يتم تنظيف الذاكرة لاحقًا)
يسمح WeakSet
بتخزين مجموعة من الرسائل والتحقق بسهولة من وجود رسالة فيها.
ينظف نفسه تلقائيًا. والمقايضة هي أننا لا نستطيع التكرار عليه، ولا يمكننا الحصول على "جميع الرسائل المقروءة" منه مباشرة. ولكن يمكننا القيام بذلك عن طريق التكرار على جميع الرسائل وتصفية تلك الموجودة في المجموعة.
قد يكون الحل الآخر المختلف هو إضافة خاصية مثل message.isRead=true
إلى الرسالة بعد قراءتها. نظرًا لأن كائنات الرسائل تتم إدارتها بواسطة رمز آخر، فهذا أمر غير محبذ عمومًا، ولكن يمكننا استخدام خاصية رمزية لتجنب التعارضات.
مثله:
// الخاصية الرمزية معروفة فقط لرمزنا دعونا يقرأ = رمز("isRead"); messages[0][isRead] = true;
الآن ربما لن يتمكن رمز الطرف الثالث من رؤية ممتلكاتنا الإضافية.
على الرغم من أن الرموز تسمح بتقليل احتمالية حدوث مشكلات، إلا أن استخدام WeakSet
أفضل من الناحية المعمارية.
الأهمية: 5
هناك مجموعة من الرسائل كما في المهمة السابقة. الوضع مشابه.
السماح للرسائل = [ {نص: "مرحبًا"، من: "جون"}، {نص: "كيف الحال؟"، من: "جون"}، {نص: "أراك قريبًا"، من: "أليس"} ];
والسؤال الآن هو: ما هي بنية البيانات التي تقترحها لتخزين المعلومات: "متى تمت قراءة الرسالة؟".
في المهمة السابقة كنا نحتاج فقط إلى تخزين حقيقة "نعم/لا". نحتاج الآن إلى تخزين التاريخ، ويجب أن يبقى في الذاكرة فقط حتى يتم تجميع الرسالة المهملة.
يمكن تخزين تواريخ PS ككائنات من فئة Date
المضمنة، والتي سنغطيها لاحقًا.
لتخزين التاريخ، يمكننا استخدام WeakMap
:
السماح للرسائل = [ {نص: "مرحبًا"، من: "جون"}، {نص: "كيف الحال؟"، من: "جون"}، {نص: "أراك قريبًا"، من: "أليس"} ]; دع readMap = new WeakMap(); readMap.set(messages[0], new Date(2017, 1, 1)); // كائن التاريخ الذي سندرسه لاحقًا