JavaScript | Hashmap, Map, Set, Weak*

ES6-ში დაემატა ახალი მონაცემთა სტრუქტურები Map და Set რომლებმაც მოგვცა ამოცანების გადაჭრის გზები Object და Array-ის გამოყენების გარეშე. Map არის მონაცემთა სტრუქტურა Key, Value მონაცემების შესანახად, Set კი მონაცემთა სტრუქტურაა სადაც ერთი და იგივე ტიპის, უნიკალური, მონაცემები ინახება თანმიმდევრულად. ამ სტატიაში განვიხილავთ Hashmap-ს, Map და Set მონაცემთა სტრუქტურებს, მოვიყვან პრაქტიკულ მაგალითებს და ასევე დავწერ WeakMap და WeakSet-ზე, რომლებსაც პრაქტიკაში ნაკლებად ვხვდებით, მაგრამ მათი მუშაობის პრინციპი უნდა ვიცოდეთ.

Hashtable and Hashmap

JavaScript-ში Map-ის დამატებამდე ვხმარობდით Object-ს როგორც Hashmap-ს, რომელიც ზუსტად არ არის Hashmap-ის იმპლემენტაცია, მას გააჩნია პროტოტიპში მეთოდები რომლებიც Hashmap მონაცემთა სტრუქტურაში ზედმეტია, ამიტომ JS-ს დაემატა Map-ი რომელიც HashMap-ის უფრო ზუსტი იმპლემენტაციაა.

Hashtable და Hashmap არის მასივის მაგვარი მონაცემთა სტრუქტურები სადაც ინდექსად “დაჰეშილი” Key ინახება. მათ შორის მთავარი განსხვავებაა რომ Hashtable-ის შეუძლია სინქრონულად განაახლოს Key-ები და ყველა ნაკადისთვის იყოს გაზიარებული. Hashmap-ი კი ერთი ნაკადისთვის არის ლიმიტირებული, ის JS-ში Map-ის სახით არის წარმოდგენილი.

Hashmap-ი როგორც Array ისე არის ორგანიზებული, შემავალი Key გადის ჰეშ ფუნქციას, რომელიც დააბრუნებს ინდექსს სადაც ეს Key შეინახება, ამიტომ მასში მოძებნა, ჩაწერა და წაშლა O(1) დროში მოხდება.

Map

What is Map

როგორც ზემოთ ავღნიშნე Map არის მონაცემთა სტრუქტურა, რომელიც JavaScript-ის გარდა ბევრ სხვა პროგრამირების ენაში გვხვდება, Map-ში ინახება Key, Value მონაცემები, სადაც Key უნიკალური არის მონაცემთა ტიპის მიხედვით. Map-დან ყველა ელემენტს O(1) დროში ვიღებთ.

Map vs Object

მთავარი განსხვავება Object და Map-ს შორის არის ის რომ ობიექტში მხოლოდ String და Symbol მონაცემთა ტიპის Key-ები შეგვიძია შევინახოთ, ხოლო Map-ში ნებისმიერი მონაცემთა ტიპის შენახვა შეგვიძლია Key–ით.

Object

const obj = {
    1: 'Number 1',
    '1': 'String 1'
}

console.log(obj[1]) // String 1
console.log(obj['1']) // String 1

Map

const map = new Map([[1: 'Number 1'], ['1', 'String 1']])

console.log(map[1]) // Number 1
console.log(map['1']) // String 1

// Set Boolean data type as Key
map.set(true, 'Boolean');

console.log(map.get(true)) // Boolean

გარდა ამისა Map-ი Object-გან განსხვავებით იტერირებადია, შეგვიძლია forEach, for of მეთოდებით Map-ზე იტერირება.

const object = {
    'x': 'String 1',
    'y': 'String 2'
}

for(const key of object) {
    console.log(key);
}

ამ შემთხვევაში რადგან ობიექტზე არ შეგვიძლია იტერირება JavaScript-ის შეცდომა გვექნება

Uncaught TypeError: object is not iterable

ხოლო Map-ის შემთხვევაში შეგვიძლია იტერირება

Map.prototype.forEach - აბრუნებს [key, value] ტიპს callback პარამეტრად.

Map.prototype.values - დააბრუნებს Value-ებს Map-იდან

Map.prototype.keys - დააბრუნებს Key-ებს Map-იდან

ვრცლად Map-ის მეთოდებს შეგიძლიათ გაეცნოთ აქ - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map

დავწეროთ რამოდენიმე მაგალითი:

const map = new Map([
    ['x', 'String 1'],
    ['y', 'String 2']
])

// Iterate with for of
for(const key of map.values()) {
    console.log(key);
}

// Iterate with forEach
map.forEach((value, key) => {
  console.log(key);
})

ასევე Map-ი უფრო კარგი გადაწყვეტილებაა Key,Value-ს შესანახად რადგან Object-ს prototype-ში აქვს მეთოდები რომლებიც საერთოდ არ გვჭირდება ამ კონკრეტულ მონაცემთა სტრუქტურაში. ამის გარდა მარტივად შეგვიძლია ვნახოთ თუ მაპში გვაქვს ელემენტი map.has(elem)-ის გამოყენებით და გავიგოთ Map-ის სიგრძე map.size property-ით.

Map practical case word counting

გასაუბრებებზე ხშირად გვხვდება ასეთი ამოცანა:

მოცემული გვაქვს სიტყვა hello უნდა დავთვალოთ რამდენჯერ მეორდება თვითოუელი ასო ამ სიტყვაში.

ყველაზე მარტივი გზა რაც პირველი გვაფიქრდება არის O(n^2) ციკლი, სადაც ერთი ციკლი უშუალოდ სიტყვის ბგერებზე მოახდენს იტერაციას ხოლო მეორე ციკლი დაითვლის რამდენჯერ გამეორდება ეს ასო ამ სიტყვაში, მაგრამ ამ ამოცანის გადაჭრა O(n)-ში შეგვიძლია Map-ის გამოყენებით. თუ კი Map-ში შევინახოთ თვითოუელი ასო ბგერის გამეორების სიხშირეს.

const str = 'hello';

function countWords(str) {
    const chars = str.split('');
    const charsCount = new Map();

    for(const char of chars) {
        if(charsCount.has(char)) {
            const charValue = charsCount.get(char);
            charsCount.set(char, charValue + 1);

            continue;
        }

        charsCount.set(char, 1);
    }

    return charsCount;
}

countWords('helllooh');
// Map(4) {'h' => 2, 'e' => 1, 'l' => 3, 'o' => 2}

Set

What is Set

Set მონაცემთა სტრუქტურა ინახავს, უნიკალურ ელემენტებს რომლებსაც ერთი და იგივე მონაცემთა ტიპი გააჩნიათ, გარკვეული თანმიმდევრობით.

Set vs Array

Set-ი ჩვეულებრივი JS-ის მასივისგან განსხვავებით ბევრად ნელია იტერაციის, ჩაწერის და ძებნის დროს, მაგრამ მისი დანიშნულება არ არის ეს ოპერაციები, Set-ი განსაკუთრებით კარგია შემდეგი ოპერაციების დროს

Difference

function getDifference(a, b) {
  return new Set(
    [...a].filter(element => !b.has(element))
  );
}

const set1 = new Set(['a', 'b', 'c']);
const set2 = new Set(['a', 'b']);

console.log(getDifference(set1, set2)); // {'c'}

Intersection

function getIntersection(a, b) {
  return new Set(
    [...a].filter(element => b.has(element))
  );
}

const set1 = new Set(['a', 'b', 'c']);
const set2 = new Set(['a', 'b']);

console.log(getDifference(set1, set2)); // {'a', 'b'}

Uniq

const arr = ['a','a','b','b'];

console.log(new Set([...arr])) // {'a', 'b'}

Union

const set1 = new Set(['a', 'b', 'c']);
const set2 = new Set(['a', 'b', 'd']);

const union = new Set([...set1, ...set2]);
console.log(union); // {'a', 'b', 'c', 'd'}

ამიტომ Set არ არის მასივის კონკურენტი მონაცემთა სტრუქტურა, მას თავის დანიშნულება აქვს, Set შეგვიძლია გამოვიყენოთ Lookup-ის დროს Set.prototype.has() მეთოდის გამოყენებით, იმ შემთხვევაში თუ მხოლოდ უნიკალური ელემენტები გვჭირდება, ასევე ყველა ზემოთ მოყვანილი შემთხვევის დროს Set-ი უფრო სწრაფია ვიდრე მასივი.

WeakMap and WeakSet

პრაქტიკაში იშვიათად გვხვდება WeakMap და WeakSet, მაგრამ სხვადასხვა ბიბლიოთეკებში ისინი აქტიურად გამოიყენება მეხსიერების მენეჯმენტისას. რომ მივხვდეთ Weak*-ის დანიშნულებას პირველ რიგში გავიხსენოთ object reference-ბი.

Strong Reference

let obj = { name: "nikolozz" };

const people = [obj];

obj = null;

console.log(people); // [{ name: "nikolozz" }];

ამ შემთხვევაშ ვხედავთ რომ people ამყარებს ძლიერ კავშირს ობიექტთან obj და იმ შემთხვევაშიც თუ obj მივანიჭებთ მნიშვნელობას null რაც იმას ნიშნავს რომ Garbage Collector-მა უნდა წაშალოს ის მეხსიერებიდან, ვხედავთ რომ people მასივში ის ინახება ანუ მეხსიერებიდან არ იშლება.

ხოლო WeakMap, WeakSet გამოყენების დროს reference ობიექტზე არის სუსტი, ამიტომ Garbage Collector-ი მას მეხსიერებიდან ამოშლის, იმის მიუხედავად რომ მას WeakMap-ში ან WeakSet-ში ვინახავთ.

Weak Reference

let peopleMap = new WeakMap();
let person = { name: "nikolozz" };

peopleMap.set(person, true);
console.log(pets); // WeakMap{ {...} -> true }

person = null;

console.log(peopleMap); // WeakMap(0) 

ამ მაგალითში ვხედავთ რომ როცა people-ის მნიშვნელობს მივანიჭეთ null GC-იმ ის მეხსიერებიდან წაშალა, იმ reference-ის მიუხედავათ რაც WeakMap-ს მასზე ქონდა.

ასევე Weak* არ არის იტერირებადი, რადგან წინასწარ არ ვიცით იქნება თუ არა მასში ობიექტი (ის ნებისმიერ დროს შეიძლება წაიშალოს GC-ის დროს).

const people = new WeakSet();
const nikolozz = {name: "nikolozz"};
const niko = {name: "niko"};

people.add(nikolozz);
people.add(niko);

people.has(nikolozz);    // true
people.has(niko);    // true

people.delete(nikolozz); 
people.has(nikolozz);    // false
people.has(niko);    // true

WeakMap-ში და WeakSet-ში key-ით მხოლოდ ობიექტები შეგვიძლია რომ გამოვიყენოთ

რომ შევაჯამოთ მთავარი განსხვავება Map და Set-გან არის Weak reference, ასევე არ შეგვიძლია ელემენტებზე იტერირება და key-ით მხოლოდ ობიექტები შეგვიძლია რომ შევინახოთ.

Written on November 6, 2022