근데 사실.. 컬렉션 필요없지 않을까

자바스크립트에서 일반적인 Object는 key-value쌍을 끊임 없이 추가할 수 있는 형태로 구성되어 있다. 그래서 사실 컬렉션이 필요하지 않은 것 처럼 보일 수도 있다. 그러나 이따금씩 object로 부족할 때가 있다.

  • key 충돌 위험이 존재하는 경우
  • 문자열/심볼 이외의 키 값이 필요한 경우
  • 객체에 얼마나 많은 속성이 있는지 알아낼 수 있는 효과적인 방법이 필요한 경우
  • 객체가 iterable하지 않음. 따라서 for..of...를 사용할 수 없음

es6에 추가된 컬렉션들은 따라서 멤버 데이터를 드러내기 위해 property를 사용하지 않는다. (obj.key, obj[key] 불가능.) 그리고 이들에는 자유롭게 메소드를 추가할 수 있다.

Set

Set은 value로 이루어진 컬렉션이다. 그리고 수정가능하다. 배열과 같을 것 같지만, 다르다.

1
2
3
let faces = new Set("😀 😁 😂 🤣 😃 😄 😅");
faces.size; // 8
faces.add("😂");

  • 일단 (당연하게도) Set에는 같은 value가 중복으로 포함될 수 없다. 기존에 있는걸 추가해도 아무런 변화가 없다.
  • Set은 어떤데이터가 자신의 멤버인지 빠르게 확인하기 위한 목적으로 사용한다
  • 그러나 Set은 index로 값을 조회할 수는 없다.

set 으로 할 수 있는 것

  • new Set(): 비어있는 set 생성

  • set.size: set 데이터 개수 조회

  • set.has(value): valueset에 존재하는지 조회

  • set.add(value)

  • set.delete(value)

  • sets[Symbol.iterator](): set 안의 값을 순회할 수 있는 새로운 이터레이러를 리턴한다. set을 iterable하게 만들어 준다.

    1
    2
    3
    4
    5
    let faces = new Set("😀 😁 😂 🤣 😃 😄 😅");
    iteratorFaces = faces[Symbol.iterator]();
    for (let i of iteratorFaces) {
    console.log(i);
    }

    • set.forEach(f)
    • set.clear
    • set.keys()
    • set.values()
    • set.entries()

Map

Map은 잘 알려진 것처럼, key-value pair로 이루어진 컬렉션이다.

Map으로 할 수 있는 것

  • new Map
  • new Map(pairs)
  • map.has(key)
  • map.size
  • mag.get(key)
  • map.set(key, value)
  • map.delete(key)
  • map.clear()
  • map[Symbol.iterator]() === map.entries()
  • map.forEach(f)
  • map.keys()
  • map.values()

javascript 가 다른점

아래의 코드를 보자.

1
2
3
4
5
6
7
8
let messi = new Set();

const 리오넬메시 = { name: "리오넬메시" };
const 라이오넬멧시 = { name: "리오넬메시" };
messi.add(리오넬메시);
messi.add(라이오넬멧시);

console.log(messi.size); //2 ????

리오넬메시라이오넬멧시는 내부의 값이 같아 보이기 때문에, set에 한개의 값만 추가 될 것 같지만 사실은 그렇지 않다. 자바스크립트에서는 두개의 값을 다르게 본다. 이유는 자바스크립트가 값을 비교할 때 두가지 다른 방법을 사용하기 때문이다.

  • string, number같은 primitive는 값을 비교한다
  • array, date, object 등은 reference를 비교한다. (메모리의 같은 위치를 참조하고 있는가?)

1
2
3
4
5
6
7
const 리오넬메시 = { name: "리오넬메시" };
const 라이오넬멧시 = { name: "리오넬메시" };
const 메석대 = 리오넬메시;

메석대 === 리오넬메시; // true
메석대 == 라이오넬멧시; // false
리오넬메시 === 라이오넬멧시; //false

본질적으로 같은 메모리를 참조하는 값 끼리만 true를 반환하는 것을 볼 수 있다. (두 object를 비교하는 방법은 여기를 참조)

다시 Set으로 돌아와서, javascript는 저 두 값을 제대로 비교하지 못하기 때문에 set에 두개의 값이 들어가게 된다. 물론 해시코드를 사용하면 가능하지만, javascipt에는 그런거 없다

또 하나 다른 점이라고 한다면, mapset에 추가한 순서가 곧 순회하는 순서와 같다는 것이다. 이 역시 다른 언어들과는 다른 점이다.

WeakMap, WeakSet

  • WeakMapnew .has() .get() .set() .delete() 만 지원한다
  • WeakSetnew .has() .add() .delete() 만 지원한다.
  • WeakSetWeakMap의 key는 반드시 object여야 한다.

그렇다. 열거형이 존재하지 않는다. 그 이유는 참조하고 있는 오브젝트가 사라지면 해당 key, value가 사라지는 WeakMap, WeakSet의 특징 때문이다.

1
2
3
4
5
let john = { name: "John" };
// 객체에 접근 가능, 해당 객체는 메모리에서 참조되고 있음.
// 참조를 null로 overwrite
john = null;
// 객체는 메모리에서 이제 삭제됨

1
2
3
4
5
6
7
8
let john = { name: "John" };
let array = [john];
john = null; // 참조를 null로 overwrite

// john은 객체안에 살아 있기 때문에 가비지 컬렉팅이 되지 않음.
// 그래서 array[0]으로 접근 가능
array[0];
// { name: "John" }

이는 기존 Map, Set에서도 동일하다.

1
2
3
4
5
6
7
8
9
let john = { name: "John" };

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

john = null; // 참조를 null로 overwrite

// john은 맵안에서 살아있기 때문에
// map.keys() 로 접근 가능

그러나 WeakMap, WeakSet은 다르다

1
2
3
4
5
6
7
8
let john = { name: "John" };

let weakMap = new WeakMap();
weakMap.set(john, "윅");

john = null; // 참조를 null로 overwrite

// john은 메모리에서 사라짐 (가비지 콜렉팅 당함)

그럼 도대체 이것은 언제 쓸까? 객체가 사라지면 자동으로 가비지 콜렉팅 해준다는 특성을 활용해, 아래와 같은 것이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
let john = { name: "John" };
// map: 유저 => 방문횟수
let visitsCountMap = new Map();
visitsCountMap.set(john, 123);

// john이 사라짐
john = null;

// 그러나 Map에서는 계속 남아 있으므로, 따로 처리를 해주어야 함.
// 또한 john은 map에서 key로 사용하고 있으므로 메모리에서도 존재함.
console.log(visitsCountMap.size); // 1

그러나 여기서 WeakMap을 사용하면, 자동으로 가비지 콜렉팅이 되므로 Map에 남아있는 key에 대해서 까지 신경쓰지 않아도 된다.