Diversity w polskim IT
Bret Cameron
Bret CameronDigital Consultant @ Concurrent Design Group

Jak używanie map w JavaScript może przyspieszyć Twój kod

Zobacz, jak dzięki mapom możesz pisać szybszy, czystszy i wydajniejszy kod w JavaScript.
27.09.20195 min
Jak używanie map w JavaScript może przyspieszyć Twój kod

Wśród dobrodziejstw wprowadzonych do JavaScriptu w ES6, znalazły się zbiory (Set) i mapy (Map). W przeciwieństwie do zwykłych obiektów i tablic są kolekcjami z kluczem (ang. keyed collection). To oznacza, że ich zachowanie jest nieznacznie odmienne i - użyte we właściwym kontekście - oferują znaczne korzyści w zakresie wydajności.

W tym artykule chcę przyjrzeć się mapom i temu, w jaki sposób mogą pomóc nam pisać szybszy i czystszy Javascript. Pokażę, czym różnią się od zbiorów, kiedy są przydatne oraz kiedy mogą zaoferować lepszą wydajność niż zwykłe obiekty JavaScriptu.


Czym różnią się mapy od obiektów?

Istnieją dwie główne różnice pomiędzy mapami a zwykłymi obiektami JavaScript.

1. Brak ograniczeń dla kluczy

Każdy klucz w zwykłym obiekcie JavaScript musi być albo stringiem albo symbolem. Pokazuje to poniższy obiekt.

const symbol = Symbol();
const string2 = 'string2';
const regularObject = {
  string1: 'value1',
  [string2]: 'value2',
  [symbol]: 'value3'
}​

W przeciwieństwie do tego, mapy pozwalają na używanie funkcji, obiektów i innych prymitywnych typów (w tym NaN) jako kluczy - jak pokazano poniżej:

const func = () => null;
const object = {};
const array = [];
const bool = false;

const map = new Map();
map.set(func, 'value1');
map.set(object, 'value2');
map.set(array, 'value3');
map.set(bool, 'value4');
map.set(NaN, 'value5');

Funkcja ta zapewnia większą elastyczność w łączeniu różnych typów danych.

2. Bezpośrednia iteracja

Aby iterować klucze, wartości lub ich pary w obiekcie, musisz albo przekonwertować je do tablicy, używając metody takiej jak Object.keys() , Object.values() lub Object.entries() albo użyć pętli for … in. Ponieważ nie da się bezpośrednio iterować wewnątrz obiektów, to użycie pętli for … in ma kilka ograniczeń. Pozwala na iterowanie nad właściwościami, które są enumerable i nie są symbolami. Dodatkowo robi to w arbitralnej kolejności.

Z drugiej strony mapy są bezpośrednio iterowalne, a ponieważ tworzą kolekcje z kluczem, kolejność iteracji jest taka sama jak kolejność wstawiania. Aby iterować nad wpisami w mapie, można użyć metody forEach lub pętli for … of. Poniższy kod pokazuje obie te metody:

for (let [key, value] of map) {
  console.log(key);
  console.log(value);
};
map.forEach((key, value) => {
  console.log(key);
  console.log(value);
});

Powiązaną korzyścią jest to, że możesz wywołać map.size, aby uzyskać liczbę wpisów. Aby dowiedzieć się tego samego o obiekcie, musisz najpierw przekonwertować go na tablicę w ten sposób: Object.keys({}}.length.


Czym różnią się mapy od zbiorów?

Mapa zachowuje się w bardzo podobny sposób do zbioru i dzieli kilka takich samych metod, w tym has, get, delete i size . Oba są kolekcjami z kluczem, co oznacza, że możesz używać metod takich jak forEach, aby iterować nad elementami w kolejności zgodnej z kolejnością wstawiania.

Główna różnica polega na tym, że mapa jest dwuwymiarowa, z elementami, które występują w parze klucz/wartość. Tak jak możesz przekonwertować tablicę na zbiór, tak samo możesz przekonwertować tablicę 2D na mapę:

const set = new Set([1, 2, 3, 4]);
const map = new Map([['one', 1], ['two', 2], ['three', 3], ['four', 4]]);​

Konwersja typu

Aby skonwertować mapę z powrotem na tablicę, można użyć składni przypisania destrukturyzującego ES6:

const map = new Map([['one', 1], ['two', 2]]);
const arr = [...map];

Do niedawna nie można było konwertować tak wygodnie konwertować mapy do obiektu (i vice versa), więc trzeba było polegać na funkcji takiej jak ta poniżej:

const mapToObj = map => {
  const obj = {};
  map.forEach((key, value) => { obj[key] = value });
  return obj;
};
const objToMap = obj => {
  const map = new Map;
  Object.keys(obj).forEach(key => { map.set(key, obj[key]) });
  return map;
};​

Ale teraz, wraz z wprowadzeniem ES2019 w sierpniu, zobaczyliśmy wprowadzenie dwóch nowych metod obiektu - Object.entries() i Object.fromEntries() - które to znacznie upraszczają:

Object.fromEntries(map); // convert Map to object
new Map(Object.entries(obj)); // convert object to Map

Zanim użyjesz Object.fromEntries, aby przekonwertować mapę do obiektu, upewnij się, że klucze mapy dają unikalne wyniki po konwersji na ciąg znaków. W przeciwnym razie istnieje ryzyko utraty danych podczas konwersji.


Testy wydajnościowe

Aby przygotować się do testu, stworzę obiekt i mapę - każdy z milionem takich samych kluczy i wartości:

let obj = {}, map = new Map(), n = 1000000;
for (let i = 0; i < n; i++) {
  obj[i] = i;
  map.set(i, i);
}

Użyłem console.time() do porównania testów, dzięki czemu są one łatwe do powtórzenia. Chociaż dokładny czas może ulegać wahaniom - a te zarejestrowane poniżej będą specyficzne dla mojego systemu i wersji Node.js - moje wyniki konsekwentnie pokazują wzrost wydajności podczas korzystania z map, szczególnie podczas dodawania i usuwania wpisów.


Znajdowanie wpisów

let result;
console.time('Object');
result = obj.hasOwnProperty('999999');
console.timeEnd('Object');
console.time('Map');
result = map.has(999999);
console.timeEnd('Map');

Obiekt: 0.250m
Mapa: 0,095ms (2,6 razy szybciej)

Dodawanie wpisów

console.time('Object');
obj[n] = n;
console.timeEnd('Object');
console.time('Map');
map.set(n, n);
console.timeEnd('Map');

Obiekt: 0.229ms
Mapa: 0.005ms (45.8 razy szybciej)

Usuwanie wpisów

console.time('Object');
delete obj[n];
console.timeEnd('Object');
console.time('Map');
map.delete(n);
console.timeEnd('Map');

Obiekt: 0.376ms
Mapa: 0.012ms (31 razy szybciej)

Gdzie mapy są wolniejsze

W moich testach znalazłem jeden przypadek, w którym obiekty były szybsze niż mapy: gdy używałem pętli for do stworzenia naszego oryginalnego obiektu i mapy. Wynik ten jest zaskakujący, ponieważ bez pętli, dodawanie wpisów do mapy było znacznie szybsze niż dodawanie wpisów do standardowego obiektu.

let obj = {}, map = new Map(), n = 1000000;
console.time('Map');
for (let i = 0; i < n; i++) {
  map.set(i, i);
}
console.timeEnd('Map');
console.time('Object');
for (let i = 0; i < n; i++) {
  obj[i] = i;
}
console.timeEnd('Object');

Obiekt: 32.143ms
Mapa: 163.828ms (5 razy wolniej)


Przykładowy przypadek użycia

Na koniec przyjrzyjmy się przypadkowi, w którym mapa byłaby lepsza od obiektu. Powiedzmy, że musieliśmy napisać funkcję, która ma określić, czy dwa ciągi znaków są anagramami:

console.log(isAnagram('anagram', 'gramana')); // Should return true
console.log(isAnagram('anagram', 'margnna')); // Should return false

Istnieje wiele sposobów, aby to zrobić, ale tutaj mapy mogą pomóc nam stworzyć jedno z najszybszych rozwiązań:

const isAnagram = (str1, str2) => {
  
  if (str1.length !== str2.length) return false;
  
  const map = new Map();
  
  for (let char of str1) {
    const count = map.has(char) ? map.get(char) + 1 : 1;
    map.set(char, count);
  };
  
  for (let char of str2) {
    if (!map.has(char)) return false;
    const count = map.get(char) - 1;
    if (count === 0) {
      map.delete(char);
      continue;
    };
    map.set(char, count);
  };
  
  return map.size === 0;

};

Tutaj mapy są lepsze od obiektów, bo musimy dynamicznie dodawać i usuwać wartości, a także dlatego, że nie znamy z góry kształtu danych (lub liczby wpisów).

Mam nadzieję, że ten artykuł okazał się przydatny i że - jeśli wcześniej nie spotkałeś się z mapami - otworzyłeś oczy na tę wartościową część nowoczesnego JavaScript.

Podziękowania dla Konstantina Rouda’y za przydatne uwagi dotyczące ES2019 i pętli for … in.

<p>Loading...</p>