Map و Set در جاوااسکریپت

اکثر برنامه‌نویسان جاوااسکریپت با ساختارهای داده‌ی زیر آشنایی دارند:

  • آبجکت‌ها (Object) برای ذخیره کردن داده با استفاده از کلید (key).
  • آرایه‌ها (Array) برای ذخیره کردن داده به شکل ترتیبی.

اما در عمل این دو نوع ساختار کافی نبوده‌اند. برای همین ساختارهای Map و Set در جاوااسکریپت معرفی شده‌اند که در ادامه آن‌ها را بررسی می‌کنیم. بنابراین با ما همراه باشید.

ساختار داده‌ی Map

Map مجموعه‌ای از داده‌های دارای کلید (key) است. دقیقا مشابه با آبجکت‌ها. اما تفاوت اساسی در این است که کلید‌ها در Map می‌توانند از هر نوعی باشند. مثلا می‌توانند عدد، رشته، بولن یا حتی آبجکت باشند.

مهم‌ترین متدها و پراپرتی‌هایی که در هنگام کار با Map با آن‌ها سر و کار داریم عبارتند از:

new Map() – برای درست کردن یا تعریف کردن یک Map استفاده می‌شود.
map.set(key, value) – یک مقدار را با کلید مشخصی داخل map ذخیره می‌کند.
map.get(key) – با دادن کلید، داده‌ی متناظر با آن را برمی‌گرداند. اگر داده‌ای با آن کلید وجود نداشته باشد، مقدار undefined برگردانده می‌شود.
map.has(key) – اگر کلید داده شده، در map وجود داشته باشد مقدار true و در غیر این صورت مقدار false را برمی‌گرداند.
map.delete(key) – داده‌ی متناظر با کلید را پاک می‌کند.
map.clear() – همه چیز را داخل map پاک می‌کند.
map.size – تعداد داده‌های داخل map را برمی‌گرداند.

برای مثال:

let map = new Map();

map.set('1', 'str1');   // a string key (kilide reshte)
map.set(1, 'num1');     // a numeric key (kilide addadi)
map.set(true, 'bool1'); // a boolean key (kilid az no-e boolean)

// dar object-ha, key-ha tabdil be reshteh (string) mishavand.
// amma dar Map-ha kilide 1 ba '1' motafavet ast:
alert( map.get(1)   ); // 'num1'
alert( map.get('1') ); // 'str1'

console.log( map.size ); // 3

در مثال بالا می‌توانیم ببینیم که بر خلاف آبجکت‌ها، در Mapها کلید‌ها صرفا رشته نیستند و استفاده از هر نوع داده به عنوان کلید امکان‌پذیر خواهد بود (کلید از نوع عددی، بولن، آبجکت و …).


نکته: استفاده از ترکیب [map[key راه درستی برای استفاده از Map ها نیست.

درست است که map[key] کار می‌کند اما استفاده از این ترکیب همانند استفاده از آبجکت‌های معمولی در جاوااسکریپت است و محدودیت‌های آن را نیز دارد. مثلا ما نمی‌توانیم از آبجکت‌ها به عنوان کلید استفاده کنیم. به جای استفاده از این ترکیب، ما باید از متدهای get و set استفاده کنیم.


استفاده از آبجکت‌ها به عنوان کلید

به مثال زیر توجه کنید:

let john = { name: "John" };

// baraye har karbar, tedade bazdid raa zakhireh mikonim:

let visitsCountMap = new Map();

// john be onvane kilid baraye map estefadeh mishavad:

visitsCountMap.set(john, 123);

console.log( visitsCountMap.get(john) ); // 123

استفاده از آبجکت‌ها به عنوان کلید برای map، از مهم‌ترین ویژگی‌های Mapها محسوب می‌شود. برای کلیدهای رشته‌ای، آبجکت‌ها قابل استفاده هستند اما برای کلیدهای غیر رشته‌ای نه! بیایید امتحان کنیم:

let john = { name: "John" };

// az object estefadeh mikonim:
let visitsCountObj = {}; 

// az object be onvane kilid estefadeh mikonim:
visitsCountObj[john] = 123; 

// dar vaqe chizi ke be onvane kilid neveshtim be surate zir ast:
console.log( visitsCountObj["[object Object]"] ); // 123

چون visitsCountObj یک آبجکت است، همه کلید‌ها را به رشته تبدیل می‌کند. وقتی john به رشته تبدیل شود، رشته‌ی "[object Object]" را خواهیم داشت که مسلما چیزی نیست که ما می‌خواهیم.


چگونه Map کلیدها را مقایسه می‌کند؟

برای بررسی برابری کلید‌ها، Map از الگوریتم SameValueZero استفاده می‌کند. این الگوریتم تقریبا مشابه همان اوپراتور برابری === است. با این تفاوت که NaN با NaN برابر در نظر گرفته می‌شود. بنابراین NaN هم می‌تواند به عنوان کلید مورد استفاده قرار گیرد. این الگوریتم قابل تغییر یا ویرایش نیست.


زنجیربافی

هر map.set خود map را برمی‌گرداند. بنابراین می‌توانیم setها را به شکل زنجیره‌ای به کار بگیریم:

map.set('1',  'str1' )
   .set( 1 ,  'num1' )
   .set(true, 'bool1'); 

ایتراسیون Map

برای ایتراسیون (Iteration) یا استفاده از فرآیندهای For … loop در Mapها سه متد وجود دارد:

map.keys() – یک ایتریبل (Iterable) از کلیدها می‌سازد. ایتریبل به آیتم‌هایی اطلاق می‌شود که می‌توان آن را با استفاده از حلقه‌های for یا map کردن یا با فراخوانی متد next و امثال این موارد پیمایش کرد.
map.values() – یک ایتریبل از مقادیر می‌سازد.
map.entries() – یک ایتریبل به شکل [key, value] می‌سازد که فرمت پیش‌فرض برای استفاده در for...of است.

برای مثال:

let recipeMap = new Map([
  ['cucumber', 500],
  ['tomatoes', 350],
  ['onion',    50]
]);

// iterate kardane kilid-ha: 
for (let vegetable of recipeMap.keys()) {
  console.log(vegetable); // cucumber, tomatoes, onion
}

// iterate kardane maghadir: 
for (let amount of recipeMap.values()) {
  console.log(amount); // 500, 350, 50
}

// iterate kardan ruye [key, value]:
// iteration ruye recipeMap = iteration ruye recipeMap.entries()
for (let entry of recipeMap) { 
  console.log(entry); // [cucumber,500] (va elaa aakhar)
}

توجه به این نکته ضروری است که در ایتراسیون Mapها، بر خلاف آبجکت‌های معمول جاوااسکریپت، ترتیب وارد کردن داده به Map حفظ می‌شود. یعنی داده‌ها با همان ترتیبی که به Map داده شده‌اند، ایتریت می‌شوند.

علاوه بر این روش‌ها، Map دارای متد forEach هم هست. مشابه با آرایه‌ها می‌توان نوشت:

// function raa be ezaye har (key, value) ejraa mikonad:
recipeMap.forEach( (value, key, map) => {
  console.log(`${key}: ${value}`); // cucumber: 500 va ...
});

ساختن Map از روی آبجکت‌ها با Object.entries

زمانی که Map درست می‌کنیم، می‌توانیم با افزودن یک آرایه (یا هر ایتریبل دیگر) که حاوی جفت‌های (کلید و مقدار) باشد، Map را مقداردهی اولیه کنیم. برای مثال:

// arayeye joft-haye [key, value]
let map = new Map([
  ['۱',  'str1'],
  [۱,    'num1'],
  [true, 'bool1']
]);

console.log( map.get('1') ); // str1

اگر یک آبجکت معمولی داشته باشیم و بخواهیم از آن یک Map درست کنیم، می‌توانیم از متد Object.entries(obj) استفاده کنیم که جفت‌های (کلید و مقدار) از روی آبجکت می‌سازد. دقیقا با همان فرمتی که در مثال قبل ذکر شد. بنابراین به شکل زیر می‌توانیم از روی یک آبجکت، یک Map درست کنیم:

let obj = {
  name: "John",
  age: 30
};

let map = new Map(Object.entries(obj));

console.log( map.get('name') ); // John

در این‌جا، Object.entries آرایه‌ای به شکل [ ["name","John"], ["age",30] ] تولید می‌کند. دقیقا چیزی که Map به عنوان وروی به آن احتیاج دارد.

ساختن آبجکت از Map با Object.fromEntries

دیدیم که چطور از آبجکت‌های معمولی جاوااسکریپت Map بسازیم. متد Object.fromEntries برعکس این کار را انجام می‌دهد. روش کار این متد به این صورت است که با دادن آرایه‌ای از جفت‌های [key, value] به آن، آبجکتی از روی آن ساخته می‌شود:

let prices = Object.fromEntries([
  ['banana', 1],
  ['orange', 2],
  ['meat', 4]
]);

// prices = { banana: 1, orange: 2, meat: 4 }

console.log(prices.orange); // 2

با استفاده از همین ویژگی هست که می‌توانیم از Map آبجکت معمولی درست کنیم. مثلا فرض کنید ما داده‌هایی را در Map ذخیره کرده‌ایم اما می‌خواهیم آن‌ها را به یک کد آماده یا کتابخانه و غیره به عنوان ورودی بدهیم که این کد یا کتابخانه ورودی را فقط به شکل آبجکت می‌پذیرد. در این صورت می‌نویسیم:

let map = new Map();
map.set('banana', 1);
map.set('orange', 2);
map.set('meat', 4);

// dorost kardane object mamooli:
let obj = Object.fromEntries(map.entries()); 

// obj = { banana: 1, orange: 2, meat: 4 }

console.log(obj.orange); // 2

صدا زدن map.entries جفت‌های کلید / مقدار را دقیقا با همان فرمتی درست می‌کند که ورودی متد Object.fromEntries انتظار دارد. البته می‌توان به جای استفاده از map.entries مستقیما خود map را نیز به کار برد:

let obj = Object.fromEntries(map); // be jaye map.entries()

هر دو این کدها درست است. چون Object.fromEntries به عنوان آرگومان یک ایتریبل نیاز دارد که لزوما آرایه نیست و ایتراسیون خود map دقیقا مانند ایتراسیون map.entries جفت‌های کلید / مقدار برمی‌گرداند. بنابراین با دادن خود map هم به عنوان ورودی متد Object.fromEntries، آبجکت معمولی ما تولید می‌شود.

ساختار داده‌ی Set

Set کلکسیونی از داده‌هاست که بدون «کلید» کنار هم می‌آیند (مانند آرایه‌ها)؛ با این تفاوت که هر مقداری تنها یک بار می‌تواند وجود داشته باشد (داده‌های تکراری نخواهیم داشت). مهم‌ترین متدهای مورد استفاده با Setها به شرح زیر است:

new Set(iterable) – یک Set درست می‌کند و اگر آبجکت با قابلیت ایتراسیون به آن داده شود (معمولا آرایه) این مقادیر را داخل Set ساخته شده وارد می‌کند.
set.add(value) – یک مقدار را وارد Set کرده و کل Set را برمی‌گرداند.
set.delete(value) – مقدار را از Set پاک می‌کند. اگر مقدار مورد نظر در هنگام پاک کردن داخل Set حضور داشت مقدار true و در غیر این صورت مقدار false برمی‌گرداند.
set.has(value) – اگر مقدار value داخل Set وجود داشته باشد مقدار true و در غیر این صورت مقدار false را برمی‌گرداند.
set.clear() – همگی مقادیر داخل Set را پاک می‌کند.
set.size – تعداد مقادیر داخل Set (اندازه‌ی Set) را برمی‌گرداند.

مهم‌ترین ویژگی Set این است که فراخوانی set.add() با داده‌های تکراری هیچ کاری انجام نمی‌دهد. بنابراین هر مقداری در داخل Set فقط یکبار قابل وارد کردن است. برای مثال می‌خواهیم لیستی از بازدیدکنندگان را داشته باشیم تا بفهمیم چه کسانی از سایت بازدید کرده‌اند. با این حال نمی‌خواهیم بازدیدهای تکراری ثبت شوند و بازدیدکنندگان تنها باید یکبار ثبت شوند. استفاده از Set در این گونه موارد راه درستی خواهد بود:

let set = new Set();

let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };

// barzdid-hayi anjam va sabt mishavad:
set.add(john);
set.add(pete);
set.add(mary);
set.add(john);
set.add(mary);

// set tanha maqadire monhaser be fard ra zakhire mikonad:
console.log( set.size ); // 3

for (let user of set) {
  console.log(user.name); // John (dar edame Pete and Mary)
}

روش دیگر حل این مساله استفاده از آرایه‌ها است. اما قبل از وارد کردن داده باید با استفاده متد find() تکراری نبودن داده را در آرایه چک کنیم. اما این موضوع پرفورمنس خوبی ندارد. چون باید تک تک اعضای آرایه مورد بررسی قرار بگیرند. استفاده از Set روش بسیار بهینه‌تری برای ثبت داده‌های منحصر به فرد (یونیک) است.

ایتراسیون روی Setها

هم با استفاده از for...of و هم با استفاده از forEach() می‌توانیم برای Setها حلقه درست کنیم:

let set = new Set(["oranges", "apples", "bananas"]);

for (let value of set) console.log(value);

// be tor moshabeh baa forEach:
set.forEach((value, valueAgain, set) => {
  console.log(value);
});

به نکته جالب کد توجه کردید؟ تابع کال‌بک برای forEach دارای سه آرگومان است. یک مقدار (value)، دوباره همان مقدار (valueAgain) و خود set. بنابراین یک مقدار دو بار در آرگومان‌ها تکرار می‌شود. این موضوع به دلیل سازگاری با Mapها است که در آن متد forEach دارای سه آرگومان است. اندکی عجیب به نظر می‌آید اما در شرایط خاصی این امکان را می‌دهد که Map و Set به راحتی با همدیگر تعویض شوند (یا برعکس).

در این‌جا متدهایی که Map پشتیبانی می‌کند نیز وضعیت مشابهی دارند. یعنی توسط Setها هم پشتیبانی می‌شوند اما ممکن است مقادیر جالبی برگردانند:

set.keys() – یک آبجکت ایتریبل از مقادیر را برمی‌گرداند!
set.values() – مانند set.keys() هست؛ به خاطر سازگاری با Mapها
set.entries() – آبجکت ایتریبل از جفت‌های [value, value] می‌سازد و برای سازگاری با Mapها این امکان فراهم شده است.

حال با Map و Set در جاوااسکریپت آشنا شدیم. نظرات خودتان را در رابطه با این دو مفهوم جاوااسکریپتی برای من بفرستید. به امید دیدار

منبع: Map and Set

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *