بازگشت به دوره

مجموعه ها و نقشه های ضعیف (WeakSets & WeakMaps)

در فصل سوم با دو نوع داده‌ی مجموعه (Set) و نقشه (Map) آشنا شدیم. در این بخش قصد داریم با دو نوع داده‌ی دیگر در جاوا اسکریپت آشنا شویم که شباهت زیادی به مجموعه‌ها و نقشه‌ها دارند. این دو نوع داده عبارتند از مجموعه‌های ضعیف (WeakSets) و نقشه‌های ضعیف (WeakMaps).

 

مجموعه‌های ضعیف (WeakSets)

حتماً به یاد دارید که برای ایجاد یک مجموعه‌ی جدید باید از کلمه‌ی کلیدی new به همراه تابع سازنده‌ی Set استفاده می‌کردیم. همچنین برای اضافه کردن اعضای جدید به مجموعه از متد add و برای حذف اعضای موجود، از متد delete استفاده می‌کردیم. به عنوان مثال در قطعه کد زیر ابتدا یک مجموعه ایجاد شده و در متغیر mySet ذخیره می‌شود. سپس دو عضو جدید به آن اضافه شده و در انتها یکی از این اعضا حذف می‌شود.


const mySet = new Set();
mySet.add('a').add('b');
console.log(mySet);
← Set(2) {"a", "b"}
mySet.delete('a');
console.log(mySet);
← Set(1) {"b"}

همچنین برای بررسی وجود یک مقدار خاص در یک مجموعه می‌توان از متد has استفاده کرد. با استفاده از متد clear نیز می‌توان کل اعضای یک مجموعه را حذف کرد. قطعه کد زیر نحوه‌ی استفاده از این متدها را نشان می‌دهد.


const mySet = new Set();
mySet.add('a').add('b');
console.log(mySet.has('a'));
← true
console.log(mySet.has('c'));
← false
mySet.clear();
console.log(mySet);
← Set(0) { }

مجموعه‌های ضعیف نیز نوعی مجموعه هستند و تمام متدهای فوق (به غیر از clear) برای مجموعه‌های ضعیف نیز به همین شکل قابل استفاده هستند. اما چند تفاوت بسیار مهم بین مجموعه‌های عادی و مجموعه‌های ضعیف وجود دارد که در ادامه به بررسی آنها می‌پردازیم.

برای تعریف یک مجموعه‌ی ضعیف باید از تابع سازنده‌ی WeakSet استفاده کرد. این تابع سازنده نیز مانند تابع سازنده‌ی Set می‌تواند اعضای اولیه‌ی مجموعه را به صورت یک آرایه دریافت کند. اما نکته‌ی بسیار مهم در رابطه با مجموعه‌های ضعیف این است که اعضای این مجموعه‌ها نمی‌توانند از انواع داده‌ی اولیه باشند. یعنی فقط اشیاء را می‌توان در مجموعه‌های ضعیف ذخیره کرد. در نتیجه اجرای دستورات زیر امکانپذیر نبوده و خطاهایی از نوع TypeError تولید می‌کنند.


const myWeakSet = new WeakSet();
myWeakSet.add('a');
← "Uncaught TypeError: Invalid value used in weak set"
// یا
const myWeakSet = new WeakSet(['a' , 2]);
← "Uncaught TypeError: Invalid value used in weak set"

مشاهده می‌کنید که امکان ذخیره‌سازی انواع داده‌ی اولیه در مجموعه‌های ضعیف وجود ندارد. اما می‌توان هر نوع شیئی را در مجموعه‌های ضعیف ذخیره کرد. به عنوان مثال در قطعه کد زیر، یک آرایه و یک شئ پایه در یک مجموعه‌ی ضعیف ذخیره می‌شوند.


const myWeakSet = new WeakSet();
let array = [1 , 2 , 3];
let obj1 = {a:1 , b:2};
let obj2 = {c:3 , d:4};
myWeakSet.add(array).add(obj1);
console.log(myWeakSet.has(obj1));
← true
console.log(myWeakSet.has(obj2));
← false

در این مثال پس از ایجاد یک مجموعه‌ی ضعیف، یک آرایه و دو شئ پایه ایجاد می‌شوند. سپس آرایه‌ی ایجاد شده و شئ obj1، با استفاده از متد add به مجموعه‌ی ضعیف اضافه می‌شوند. اما شئ دوم (obj2) به مجموعه‌ی ضعیف اضافه نمی‌شود. بنابراین در دو دستور بعدی مشاهده می‌کنید که خروجی متد has به ازای obj1 برابر با true و به ازای obj2 برابر با false است.

پس در مجموعه‌های ضعیف فقط می‌توان اشیاء را ذخیره کرد. اما این تنها تفاوت میان مجموعه‌های عادی و مجموعه‌های ضعیف نیست. تفاوت مهم دیگری که بین این دو نوع داده وجود دارد این است که اشیاء ذخیره شده در مجموعه‌های ضعیف، فقط تا زمانی در حافظه باقی می‌مانند که یک اشاره‌گر به آنها وجود داشته باشد. برای درک بهتر این موضوع به قطعه کد زیر توجه کنید.


function addMember(s){
	let array2 = [4 , 5 , 6];
	s.add(array2);
}
const mySet = new Set();
let array1 = [1 , 2 , 3];
mySet.add(array1);
console.log(mySet.has(array1));
← true
addMember(mySet);
console.log(mySet.size);
← 2

این مثال را اینجا اجرا کنید و خروجی را در کنسول CodePen مشاهده کنید. سپس کدهای این مثال را با دقت بررسی کنید. در این مثال از مجموعه‌های عادی استفاده شده است.

مشاهده می‌کنید که در خط 8، با استفاده از متد has بررسی شده است که آیا شئ array1 در مجموعه موجود است یا خیر؟ و با توجه به اینکه در خط 7 این شئ به مجموعه اضافه شده است. مقدار بازگشتی از متد has برابر با true است. سپس در خط 10 تابع addMember فراخوانی می‌شود. با فراخوانی این تابع، یک شئ جدید (از نوع آرایه) به نام array2 ایجاد شده و در خط 3 به مجموعه اضافه می‌شود. در نتیجه در این لحظه، مجموعه‌ی mySet دارای 2 عضو است. پس از اجرای تابع addMember و خروج از تابع، در خط 11 با استفاده از خاصیت size، تعداد اعضای مجموعه نمایش داده می‌شود که برابر با 2 است.

اما آیا می‌توان با استفاده از متد has، وجود شئ array2 را در مجموعه بررسی کرد؟ در خط 8 این کار برای array1 انجام شده است. اما در خط 11 (یا بعد از آن) امکان انجام عمل مشابه برای array2 وجود ندارد. زیرا شناسه‌ی array2 در تابع addMember تعریف شده است و در حوزه‌ی سراسری در دسترس نیست.

پس نمی‌توان وجود یا عدم وجود array2 را در مجموعه‌ی mySet بررسی کرد. اما دستور موجود در خط 11 نشان می‌دهد که 2 عضو در مجموعه ذخیره شده‌اند که یکی از آنها همان array2 است. یعنی در عین حال که array2 در مجموعه ذخیره شده است، راهی برای دستیابی به array2 در مجموعه‌ی mySet وجود ندارد. یعنی array2 فضایی را در حافظه اشغال کرده است، اما نمی‌توان از این فضا استفاده‌ی مفیدی کرد.

حال برنامه‌ای را در نظر بگیرید که تعداد زیادی مجموعه در آن تعریف شده است. و هر مجموعه اعضای زیادی را در خود جای داده است. یعنی فضای نسبتاً زیادی از حافظه‌ی برنامه (و حافظه‌ی سیستم) توسط این مجموعه‌ها اشغال شده است. حال اگر تعداد زیادی از اعضای این مجموعه‌ها مانند مثال فوق قابل دسترسی نباشند، عملاً بخش زیادی از حافظه بدون دلیل اشغال شده است.

شاید در نگاه اول بروز چنین حالتی بعید به نظر برسد. ولی در عمل احتمال وقوع چنین حالتی در برنامه‌های بزرگ و پیچیده نسبتاً زیاد است. این پدیده را در علوم کامپیوتر و برنامه‌نویسی اصطلاحاً Memory Leak یا نشت حافظه می‌نامند. یکی از روش‌های پیشگیری از نشت حافظه، آزادسازی حافظه‌ی اختصاص داده شده به داده‌هایی است که هیچ اشاره‌گری به آنها وجود ندارد. مثلاً در مثال فوق، وقتی اجرای تابع addMember به پایان می‌رسد. با توجه به اینکه هیچ اشاره‌گری به array2 وجود ندارد و دسترسی به آن در حوزه‌ی سراسری ممکن نیست. بهتر است فضای اختصاص داده شده به array2 آزاد شود. این کار در جاوا اسکریپت (و برخی زبانهای دیگر) توسط بخشی به نام Garbage Collector (زباله روب) انجام می‌شود.

البته در مثال فوق ممکن است حتی بعد از پایان اجرای تابع addMember، لازم باشد تا array2 در مجموعه‌ی mySet باقی بماند. زیرا هنوز می‌توان به محتوای این آرایه به صورت غیر مستقیم دسترسی داشت. مثلاً برای دسترسی به array2 در مثال فوق می‌توان از دستور زیر استفاده کرد.


let myArray = [...mySet][1];

در این دستور با استفاده از عملگر Spread، ابتدا مجموعه به یک آرایه تبدیل می‌شود. سپس عضو دوم آن (اندیس 1) انتخاب شده و در myArray ذخیره می‌شود. پس می‌توان به داده‌های ذخیره شده در مجموعه‌ها به صورت غیر مستقیم دسترسی داشت. حتی زمانی که هیچ اشاره‌گری به داده‌ی مورد نظر وجود ندارد.

اما در برخی مواقع نیازی به این دسترسی غیر مستقیم نیست و اعضای داخل مجموعه فقط به صورت مستقیم مورد استفاده قرار می‌گیرند. در نتیجه اگر هیچ اشاره‌گری به یکی از اعضای مجموعه وجود نداشته باشد، بهتر است آن عضو از مجموعه حذف شود تا حافظه‌ی سیستم بیهوده اشغال نشود.

در واقع مجموعه‌های ضعیف برای حل مشکل نشت حافظه به وجود آمده‌اند و همانطور که پیشتر اشاره شد. اشیاء ذخیره شده در مجموعه‌های ضعیف، فقط تا زمانی در حافظه باقی می‌مانند که اشاره‌گری برای دسترسی به آنها وجود داشته باشد. در غیر این صورت حافظه‌ی اختصاص داده شده به آنها به صورت خودکار توسط بخش Garbage Collector آزاد می‌شود. پس اگر مثال فوق به شکل زیر اصلاح شود، بعد از پایان اجرای تابع addMember، حافظه‌ی اختصاص داده شده به array2 آزاد خواهد شد و دیگر امکان دسترسی به این آرایه وجود نخواهد داشت.


function addMember(ws){
	let array2 = [4 , 5 , 6];
	ws.add(array2);
}
const myWeakSet = new WeakSet();
let array1 = [1 , 2 , 3];
myWeakSet.add(array1);
console.log(myWeakSet.has(array1));
← true
addMember(myWeakSet);
// No access to array2

نکته : هرچند در این مثال از خط 11 به بعد امکان دسترسی به array2 وجود ندارد. اما زمان دقیق آزادسازی حافظه‌ی اختصاص یافته به این آرایه را نمی‌توان مشخص کرد. در واقع بخش Garbage Collector با یک الگوریتم و زمانبندی مشخص و در فواصل زمانی خاصی عمل آزادسازی فضاهای بی‌استفاده را انجام می‌دهد. این زمانبندی در محیط‌های مختلف متفاوت است و زمان اجرای Garbage Collector به عوامل زیادی وابسته است. اما معمولاً عمل Garbage Collection هر چند ثانیه یک بار انجام می‌شود.

نکته : برخلاف مجموعه‌های عادی، مجموعه‌های ضعیف خاصیتی به نام size ندارند.

نکته : برخلاف مجموعه‌های عادی، مجموعه‌های ضعیف قابل شمارش (Iterable) نیستند. در نتیجه نمی‌توان آنها را در حلقه‌های for-of به کار برد. همچنین توسط عملگر Spread یا متد Array.from قابل تبدیل به آرایه نیستند.

 

نقشه‌های ضعیف (WeakMaps)

همانطور که مجموعه‌های ضعیف برای حل مشکل نشت حافظه در مجموعه‌های عادی به وجود آمده‌اند. نقشه‌های ضعیف نیز برای حل مشکل نشت حافظه در نقشه‌های عادی به وجود آمده‌اند. نقشه‌های ضعیف را می‌توان با استفاده از عملگر new و تابع سازنده‌‌ی WeakMap ایجاد کرد. در نقشه‌های ضعیف نیز می‌توان مانند نقشه‌های عادی از متدهای get، set، has و delete با همان کاربرد استفاده کرد. اما تفاوت‌هایی نیز بین این دو نوع داده وجود دارد که عبارتند از :

قطعه کد زیر نحوه‌ی استفاده از نقشه‌های ضعیف را نشان می‌دهد.


const myWeakMap = new WeakMap();
let array1 = [1 , 2 , 3];
let array2 = [4 , 5 , 6];
let key1 = {a: 1};
let key2 = {b:2 , c:3};
myWeakMap.set(key1 , array1).set(key2 , array2);
console.log(myWeakMap.get(key1));
← [1, 2, 3]
key2 = 5;
console.log(myWeakMap.get(key2));
← undefined

مشاهده می‌کنید که در خط 7 امکان دسترسی به اولین عضو از نقشه وجود دارد. و مقدار بازگردانده شده از متد get در خط 8 نمایش داده شده است. اما در خط 10 امکان دسترسی به عضو دوم نقشه وجود ندارد. زیرا در خط 9، تنها اشاره‌گری که به کلید عضو دوم نقشه اشاره می‌کرد از بین رفته و مقدار جدیدی در متغیر key2 ذخیره شده است. در نتیجه این عضو باید توسط Garbage Collector از حافظه حذف شود.

مطالب این بخش را می‌توان به این صورت جمع‌بندی کرد. مجموعه‌ها و نقشه‌های ضعیف شباهت زیادی به مجموعه‌ها و نقشه‌های عادی دارند. اما دارای محدودیت‌هایی نیز هستند که به آنها اشاره شد. در شرایط عادی معمولاً از مجموعه‌ها و نقشه‌های عادی استفاده می‌کنیم. اما در شرایطی که تعداد اعضای مجموعه‌ها یا نقشه‌ها زیاد باشد و احتمال نشت حافظه وجود داشته باشد، استفاده از مجموعه‌ها و نقشه‌های ضعیف می‌تواند برای رفع مشکل نشت حافظه بسیار مفید باشد.