Map տվյալների կառուցվածքը. Փորձենք համեմատել օբյեկտների հետ. Ի՞նչ առավելություններ և թերություններ ունի
JavaScript-ում կան տարբեր տվյալների կառուցվածքներ, և կախված ծրագրին ներկայացվող պահանջներից, ճիշտ կառուցվածք ընտրելը կարող է զգալիորեն բարելավել ծրագրի աշխատանքն ու բարձրացնել կոդի ընթեռնելիությունը։ Այսօր կխոսենք մի շատ օգտակար տվյալների կառուցվածքի՝ Map-ի մասին։ Map-ի տարբեր ռեալիզացիաներ այսպես թե այնպես կան բոլոր հանրաճանաչ ծրագրավորման լեզուներում, թեև կարող են ուրիշ անուններով կոչվել։ JavaScript-ում Map-ը ներդրվել է 2015 թվականին ընդունված ստանդարտով (ES6 կամ ECMAScript 2015), նաև հենց այդ ստանդարտով կատարվեցին ահռելի քանակով նորամուծություններ, որոնք JavaScript-ը դարձրեցին լուրջ, հասուն ու շատ հարմարավետ լեզու։
Map-ը դա տվյալների կառուցվածք է, որը բաղկացած է բանալի-արժեք (key - value) զույգից, ինչպես հասարակ օբյեկտները, սակայն որպես բանալի կարող է հանդիսանալ տվյալների ցանկացած տիպ, ի տարբերություն օբեկտների, որտեղ բանալին կարող է լինել միայն String և Symbol տիպի։ Map-ը ունի նմանություններ նաև Զանգվածի հետ, մասնավորապես այն նույնպես տվյալների կարգավորված հավաքածու է, և size հատկության օգնությամբ մենք կարող ենք իմանալ թե քանի էլեմենտ է այն պարունակում։ Map կարող ենք ստեղծել new Map() սինթաքսի օգնությամբ՝
const map = new Map();
Արդյունքում ստեղծվում է դատարկ Map: Արժեքները Map-ի մեջ ավելացնում ենք map.set(key, value) մեթոդի օգնությամբ։ Առաջին արգումենտը միշտ դառնում է բանալի, երկրորդը` արժեք։ Օրինակ՝
map.set("firstName", "Anakin");
map.set("lastName", "Skywalker");
map.set("occupation", "Jedi Knight");
Map-ը օգտագործում է key => value սինթաքսը, իր մեջ գտնվող էլեմենտի բանալի-արժեք կապը ցույց տալու համար։ Օրինակ եթե վերևի օրինակը տպենք կոնսոլում, այն կունենա հետևյալ տեսքը։
{"firstName" => "Anakin",
"lastName" => "Skywalker",
"occupation" => "Jedi Knight"};
Օրինակում մենք որպես բանալի օգտագործեցինք սովորական տող, ինչպես որ օբյեկտներում է։ Օբյեկտները բացի String տիպից, սկսած ES6 ստանդարտից թույլ են տալիս որպես բանալի օգտագործել նաև Symbol և վերջ։ Այսինքն եթե մենք օբյեկտի մեջ փորձենք օգտագործել ցանկացած ուրիշ տիպի պատկանող բանալի, ապա այն անուղղակիորեն կվերափոխվի String-ի։ Օրինակ բերեք փորձենք օբյեկտում որպես բանալի օգտագործել հենց օբյեկտ։
// ստեղծենք որևէ օբյեկտ
const objAsKey = { foo: "bar" };
// օգտագործենք այդ օբյեկտը որպես բանալի
// ուրիշ օբյեկտի համար
const obj = {
[objAsKey]: "What will happen?",
};
եթե հիմա վերևի օրինակը տպենք կոնսոլում,կստանանք․
{'[object Object]': "What will happen?"};
Քանի որ օբյեկտի բանալին կարող է լինել միայն String և Symbol տիպի, ինտերպրետատորը կատարել է օբյեկտի անուղղակի վերափոխում String-ի և ստացել է [object Object]։ Ինչ է սա նշանակում։ Եթե հիմա մենք ստեղծենք ևս մի օբյեկտ, և նույնպես փորձենք ավելացնել obj-ի մեջ որպես բանալի, այն նույնպես կվերափոխվի [object Object]-ի, և մենք նախորդ արժեքը կկորցնենք։
const anotherObjAsKey = { baz: "bar" };
obj[anotherObjAsKey] = "oops";
console.log(obj); // {'[object Object]': "oops"};
Map-ը լուծում է այս պրոբլեմը, նրա մեջ որպես բանալի կարող ենք օգտագործել ինչ ուզեք` օբյեկտ, զանգված, բուլյան արժեքներ, անգամ NaN:
// Ստեղծենք օբյեկտ
const objAsKey = { foo: "bar" };
// Ստեղծենք Map
const map = new Map();
// Map-ում որպես բանալի օգտագործենք օբյեկտը
map.set(objAsKey, "What will happen?");
Եթե այն հիմա տպենք կոնսոլում, ապա կտեսնենք որ մեր բանալին ոչ մի [object Object]- ի էլ չի վերածվել, ոչ մի վերափոխում չի կատարվել։
Բացի map.set(key, value) սինթաքսից մենք կարող ենք նաև հենց Map-ի ստեղծման պահին միանգամից նրան տալ բանալի-արժեք զույգը։ Դրա համար կարող ենք օգտագործել ցանկացած օբյեկտ, որը ենթակա է իտերացիայի։ Հիմնականում դրա համար կիրառելի են երկչափ զանգվածները։ Վերևի օրինակը կարող ենք գրել նաև այսպես՝
const map = new Map([
["firstName", "Anakin"],
["lastName", "Skywalker"],
["occupation", "Jedi Knight"],
]);
Զանգվածի մեջ եղած զանգվածի առաջին էլեմենտը դառնում է բանալի, երկրորդը՝ տվյալ բանալու արժեքը։
Օբյեկտները իտերացիայի հիմնականում չեն ենթարկվում, թեև կան օբյեկտներ, որոնց վրա հնարավոր է ռեալիզացնել [Symbol.iterator] մեթոդը, և նրանց վրա արդեն կարելի է կանչել for of ցիկլը։ Սակայն Object.entries(obj) մեթոդը վերադարձնում է զանգվածների զանգված, ճիշտ նույն կառուցվածքով, որն անհրաժեշտ է Map ստեղծելուց կոնստրուկտորին որպես արգումենտ հաղորդելու համար։ Հետևաբար, եթե մենք ունենք օբյեկտ, որն ուզում ենք վերափոխել Map-ի, կարող ենք նախ Object.entries(obj) սինթաքսը օգտագործելով ստանալ երկչափ զանգված, ապա այն որպես արգումենտ տալ new Map() կոնստրուկտորին։ Օրինակ՝
const anakin = {
firstName: "Anakin",
lastName: "Skywalker",
occupation: "Jedi Knight",
};
const map = new Map(Object.entries(anakin));
Հակառակ գործողությունը՝ ստանալ օբյեկտ Map-ից, նույնպես շատ հեշտ է․
const obj = Object.fromEntries(map);
Map-ից զանգված կարող ենք ստանալ օգտագործելով Array.from(map) սինթաքսը։ Մեր օրինակում՝
const arr = Array.from(map);
Ելքում կունենանք երկչափ զանգված՝
[
["firstName", "Anakin"],
["lastName", "Skywalker"],
["occupation", "Jedi Knight"],
];
Ինչպես արդեն ասվեց, Map-ում կարող ենք ունենալ ցանկացած տիպի բանալի։ Օրինակ փորձենք թվերով՝
map.set(1, "Number one");
Ի տարբերություն օբյեկտի, որտեղ 1-ը կվերափոխվեր String "1"-ի, այստեղ նման բան չի կատարվում: Բանալի կարող է հանդիսանալ նաև բուլյան տիպի արժեքները, օրինակ՝
map.set(true, "A Boolean");
Անգամ Number տիպին պատկանող հատուկ արժեք NaN-ը՝
const myMap = new Map();
myMap.set(NaN, "not a number");
Տպենք կոնսոլում, և կստանանք {NaN => "not a number"}
Երբ ուզում ենք իմանալ Map-ի մեջ կա արդյոք տվյալ բանալին, կարող ենք օգտագործել map.has(key) մեթոդը, որը վերադարձնում է true, եթե Map-ի մեջ կա բանալին, հակառակ դեպքում՝ false: Օրինակ՝
const map = new Map([
["animal", "cat"],
["shape", "triangle"],
["city", "Prague"],
["country", "Armenia"],
]);
map.has("firstName"); // false
map.has("country"); // true
Որպեսզի ստանանք արժեքներն ըստ բանալու, պետք է օգտագործենք map.get(key) մեթոդը։
map.get("animal"); // "cat"
Map-ի առավելություններից է հանդիսանում նաև այն, որ մենք ցանկացած պահի կարող ենք ստանալ նրա չափը՝ թե քանի էլեմենտ է պարունակում։ Դրա համար օգտագործում ենք map.size հատկությունը։
map.size; // 4
Map-ից որևէ էլեմենտ ջնջելու համար կարող ենք օգտագործել delete մեթոդը։ Մեթոդը կվերադարձնի բուլյան արժեք, true եթե գտել և ջնջել է տրված էլեմենտը, և false եթե այդպիսի էլեմենտ չի կարողացել գտնել։
// Ջնջենք էլեմենտը բանալու օգնությամբ
map.delete("shape"); // true
Եվ վերջապես մենք կարող ենք ջնջել Map-ի բոլոր էլեմենտները clear մեթոդի օգնությամբ․
// դատարկում է Map-ը
map.clear();
Map-ի էլեմենտները հերթով արտածելու համար գոյություն ունի 3 ներդրված մեթոդ։ Այդ մեթոդները վերադարձնում են MapIterator կոչվող իտերացվող օբյեկտ։
const map = new Map([
["animal", "cat"],
["shape", "triangle"],
["city", "Prague"],
["country", "Armenia"],
]);
Առաջինը դիտարկենք keys մեթոդը, այն վերադարձնում է Map-ի բոլոր բանալիները:
map.keys(); // {"animal", "shape", "city", "country"}
Հաջորդը՝ map.values մեթոդն է, վերադարձնում է բոլոր արժեքները.
map.values(); // {"cat", "triangle", "Prague", "Armenia"}
Եվ վերջում՝ map.entries() մեթոդը, որը վերադարձնում է բանալի-արժեք զույգերի իտերացվող օբյեկտ։ Այս տարբերակն է օգտագործում նաև for of ցիկլը։
map.entries(); // {"animal" => "cat", "shape" => "triangle", "city" => "Prague", "country" => "Armenia"}
Map-ն ունի նաև ներդրված forEach մեթոդ, որը շատ նման է զանգվածի համանուն մեթոդին, ուղղակի հետադարձ կանչի (callback) ֆունկցիային որպես պարամետր հանդիսանում են value-ն, key-ն և map-ը։ (Զանգվածի դեպքում՝ ընդունված է պարամետրերն անվանել item, index և array)
Ամփոփելով թեման կարող ենք նշել, որ թեև Map-ն իր կառուցվածքով նման է օբյեկտին, սակայն համեմատած ունի մի շարք առավելություններ։
- Map-ն ի տարբերություն օբյեկտի ունի ներդրված size մեթոդը, որը հնարավորություն է տալիս միանգամից ստանալ նրա չափը։
- Map-ն իտերացվող է, օբյեկտները՝ հիմնականում ոչ։
- Map-ը շատ ավելի ճկուն է, այստեղ մենք կարող ենք որպես բանալի օգտագործել ցանկացած տվյալի տիպ։
- Map-ի մեջ էլեմենտները երաշխավորված պահպանվում են ըստ ավելացման հերթականության։
Օբյեկտի մեջ թեև նույնպես հիմնականում այդպես է, բայց երաշխավորել դա չի կարելի։ Օրինակ այսպես կոչված integer property-ների առկայության դեպքում այդ հերթականությունը խախտվում է։
Իհարկե չի կարելի պնդել, որ Map-ը բոլոր առումներով գերազանցում է օբյեկտներին։ Իրականում օբյեկտները նույնպես շատ առավելություններ ունեն համեմատած Map-ի հետ։ Ամեն ինչ կախված է դիտարկվող խնդրի բնույթից։ Օրինակ օբյեկտները հրաշալի են աշխատում տվյալների ստանդարտ ձևաչափ հանդիսացող JSON-ի JSON.parse() և JSON.stringify() ֆունկցիաների հետ։ Բացի դրանից օբյեկտի էլեմենտների հետ աշխատանքը շատ ավելի պարզ է՝ կետ օպերատորի գրելաձևը օգտագործելու շնորհիվ։
Կապված արագագործության հետ՝ օբյեկտների հետ աշխատանքը հիմնականում ավելի արագ ու արդյունավետ է, որովհետև ցանցային դիտարկիչների engine-ները հիանալի օպտիմալացված են օբյեկտների և զանգվածների հետ աշխատելու համար։ Կան սիրողական թեսթերի արդյունքներ, որոնք ցույց են տալիս որ Map-ի հետ աշխատանքը անգամ ավելի արագ է, բայց դրանք այնքան էլ արժանահավատ չեմ համարում, և ավելի շուտ սպեցիֆիկ խնդիրների դեպքում է այդպես։ Ավելի լայն սպեկտրում օբյեկտները թեկուզ զուտ տեսականորեն պետք է, որ ավելի արագ աշխատեն, որովհետև բացի ցանցային դիտարկիչների engine-ների ամենաբարձր մակարդակով կազմակերպված օպտիմալացման, նրանք նաև շատ անգամ ավելի «թեթև» են։
Թեսթավորումն իրականում բավականին բարդ աշխատանք է, և հիմնարար գիտելիքներ ու փորձառություն է պահանջում։ Հիմնվելով սիրողական թեսթերի արդյունքների վրա չի կարելի պնդումներ անել։ Բայց ամեն դեպքում արագագործության այդ տարբերությունը փոքր է, և վճռորոշ դեր չի կարող խաղալ, և եթե Map օգտագործելով կարելի է ավելի ընթեռնելի, պարզ ու կարճ կոդ գրել, միանշանակ պետք է այն օգտագործել։