티스토리 뷰

5.7 Map and Set

Map

맵은 Object와 달리 어떤 타입이든 모두 키가 될 수 있다. (Object는 string, symbol)

맵의 함수 및 속성들

map을 Object처럼 map[key] 방식으로 사용하지 않는 걸 추천한다. 물론, 잘 동작하지만 이렇게 사용할 경우 map이라는 자료구조가 아닌 일반 객체로 취급하는 것이기 때문이다.

 

객체를 키로 사용가능하다. 

let john = { name: "John" };
let ben = { name: "Ben" };

let visitsCountObj = {}; // try to use an object

visitsCountObj[ben] = 234; // try to use ben object as the key
visitsCountObj[john] = 123; // try to use john object as the key, ben object will get replaced

// That's what got written!
alert( visitsCountObj["[object Object]"] ); // 123

Iteration over Map

  • map.keys() – 맵의 모든 키를 반환
  • map.values() – 맵의 모든 값을 반환
  • map.entries() – 모든 엔트리 셋을 반환함. for ... of 문에 디폴트로 사용됨
let recipeMap = new Map([
  ['cucumber', 500],
  ['tomatoes', 350],
  ['onion',    50]
]);

// iterate over keys (vegetables)
for (let vegetable of recipeMap.keys()) {
  alert(vegetable); // cucumber, tomatoes, onion
}

// iterate over values (amounts)
for (let amount of recipeMap.values()) {
  alert(amount); // 500, 350, 50
}

// iterate over [key, value] entries
for (let entry of recipeMap) { // the same as of recipeMap.entries()
  alert(entry); // cucumber,500 (and so on)
}

Object.entries: Map from Object

배열이나 일반 객체를 맵으로 만들 수도 있다.

// array of [key, value] pairs
let map = new Map([
  ['1',  'str1'],
  [1,    'num1'],
  [true, 'bool1']
]);

alert( map.get('1') ); // str1

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

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

alert( map.get('name') ); // John

Objec.fromEntries: Object from Map

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

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

alert(prices.orange); // 2

// ---------------------

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

let obj = Object.fromEntries(map.entries()); // make a plain object (*)

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

alert(obj.orange); // 2

// ---------------------

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

Set

자바의 set이랑 같은 특징을 가진다. map과 마찬가지로 size가 함수가 아닌 속성이라는 것에 주의하자.

Iteration over Set

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

for (let value of set) alert(value);

// the same with forEach:
set.forEach((value, valueAgain, set) => {
  alert(value);
});

 

forEach 파라미터로 세개가 전달되는 것은 Map과의 호환성 때문이다. 실제로 그냥 같은 값을 넣으면 된다.

5.8 WeakMap and WeakSet

앞서, 어떤 값에 대한 참조가 변경되어도 어떤 배열이나 다른 객체에 여전히 참조가 포함되어 있으면 해당 값은 여전히 메모리에 남아있다고 했다. 맵도 마찬가지이다.

let john = { name: "John" };

let map = new Map();
map.set(john, "...");

john = null; // overwrite the reference

// john is stored inside the map,
// we can get it by using map.keys()

 

실제로 참조를 제거하고 맵을 출력해보았을 때, 여전히 키가 살아있는 것을 확인할 수 있었다.

WeakMap

WeakMap은 좀 다른데, 일단 key는 무조건 객체가 되어야 한다. 이후 그 객체로의 참조가 사라진다면 (null로 덮어쓰는 등) 해당 객체는 garbage collector에 의해 사라진다.

또한 iteration에 관한 함수나 keys(), values(), entries() 와 같은 메서드가 없다. 오로지 아래의 메서드만 허용된다.

  • weakMap.set(key, value)
  • weakMap.get(key)
  • weakMap.delete(key)
  • weakMap.has(key)

이렇게 제한적인 함수만을 제공하는 이유는 WeakMap 내부 데이터의 모든 참조가 사라졌을 때, 결국엔 garbage collector에 의해 메모리 공간이 제거 될 것 인데, 제거되는 시점이 명확하지가 않기 때문이다. 참조가 사라지는 즉시 제거할 수도, 나중에 제거할 수도, 부분적으로 제거할 수도 있다. 따라서 현재 WeakMap에 존재하는 요소의 수를 세는 방법은 제공하지 않고 있고 WeakMap의 모든 키, 값에 접근하는 방법도 제공되지 않고 있다. 

Use case:additional data

다른 코드에 속해진 특정 객체에 대해 부가적인 데이터를 저장하고 싶을 때, WeakMap을 사용한다. 해당 객체가 사라지면 함께 사라져야 하기 때문에 적절한 사용 방식이 된다.

weakMap.set(john, "secret documents");
// if john dies, secret documents will be destroyed automatically

// ----------------------
// 📁 visitsCount.js
let visitsCountMap = new Map(); // map: user => visits count

// increase the visits count
function countUser(user) {
  let count = visitsCountMap.get(user) || 0;
  visitsCountMap.set(user, count + 1);
}

아래 예시를 보면, 사용자의 방문 횟수를 기록하다가, 사용자가 떠난 이후로는 더이상 해당 사용자에 대한 방문횟수를 저장하고 싶지 않다.

// 📁 main.js
let john = { name: "John" };

countUser(john); // count his visits

// later john leaves us
john = null;

// -------------------
// 📁 visitsCount.js
let visitsCountMap = new WeakMap(); // weakmap: user => visits count

// increase the visits count
function countUser(user) {
  let count = visitsCountMap.get(user) || 0;
  visitsCountMap.set(user, count + 1);
}

Use case: caching

캐싱에도 자주 쓰인다. 기존 맵을 캐시저장소로 사용하게 되면 더이상 해당 데이터를 사용할 필요가 없더라도 데이터가 여전히 메모리 공간을 차지하게 된다. 이럴 때, WeakMap을 사용하기에 적절하다.

// 📁 cache.js
let cache = new Map();

// calculate and remember the result
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* calculations of the result for */ obj;

    cache.set(obj, result);
    return result;
  }

  return cache.get(obj);
}

// Now we use process() in another file:

// 📁 main.js
let obj = {/* let's say we have an object */};

let result1 = process(obj); // calculated

// ...later, from another place of the code...
let result2 = process(obj); // remembered result taken from cache

// ...later, when the object is not needed any more:
obj = null;

alert(cache.size); // 1 (Ouch! The object is still in cache, taking memory!)

WeakSet

WeakMap과 마찬가지로 객체만 추가할 수 있고, reachable 할 때만 해당 객체가 존재한다. 역시나 size, keys() 와 같은 iterations는 제공되지 않는다. 

let visitedSet = new WeakSet();

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

visitedSet.add(john); // John visited us
visitedSet.add(pete); // Then Pete
visitedSet.add(john); // John again

// visitedSet has 2 users now

// check if John visited?
alert(visitedSet.has(john)); // true

// check if Mary visited?
alert(visitedSet.has(mary)); // false

john = null;

// visitedSet will be cleaned automatically

위 두 자료 구조는 메인이 아닌 부가적인 저장소로 사용된다는 것을 기억해두자. 

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday