자바스크립트 딥다이브 - Set , Map

ChoiYongHyeun·2023년 12월 22일
0

Set

브라우저 관련 자바스크립트 강의 전 마지막 챕터 !!!!!!!!!!!

Set 객체는 중복되지 않는 유일한 값들의 집합이다.

배열과 유사하지만 다음과 같은 차이가 존재한다.

특성 Set 배열
데이터 구조 중복되지 않는 유일한 값들의 집합 순서가 있는 요소들의 집합
요소 추가 add 메서드 사용 push 또는 다른 메서드 사용
중복 처리 자동으로 중복된 값을 허용하지 않음 중복된 값을 허용
인덱스 인덱스가 없음 인덱스로 각 요소에 접근 가능
삭제 delete 메서드 사용 splice 또는 다른 메서드 사용

Set 은 수학적 집합의 특성과 일치한다. 이를 통해 교집합, 합집합, 차집합, 여집합 등을 구현 할 수 있다.

Set 객체의 생성

const set = new Set([1, 2, 3]);
console.log(set); // Set(3) { 1, 2, 3 }

set 은 빌트인 생성자 함수인 Set 을 이용해 생성한다.

안에 들어갈 수 있는 인수는 이터러블 한 인수를 전달 받아 객체를 생성한다.

아쉽게도 객체 리터럴 형식으로는 못만든다.

Set 은 중복값을 허용하지 않으며, 중복값이 존재하는 객체를 중복값이 없는 객체 형태로 반환한다.

const set = new Set([1, 1, 1, 2, 2, 2, 3, 3, 3, 3]);
console.log(set); // Set(3) { 1, 2, 3 }

Set 의 작동 원리

예전 파이썬에서 set 을 배울 때 해쉬테이블을 이용한다고 하였는데 자바스크립트도 그렇다.
이터러블한 자료구조를 받으면 임의의 해쉬테이블을 생성하고, 원소들을 프로퍼티, 해당 값에 더미 데이터를 넣어 해쉬테이블을 생성한다.
중복된 값들은 동일한 프로퍼티로 계속 값이 덮어씌워지기 때문에 중복된 값이 제거되고
나중에 프로퍼티 키들만 반환하면 된다.

function makeSet(arr) {
  let hash = {};
  arr.forEach((num) => (hash[num] = true));
  return Object.keys(hash);
}
const set = makeSet([1, 1, 1, 1, 2, 2, 2, 3, 3, 3]);
console.log(set);  // [ '1', '2', '3' ]

뭐 이런 느낌일려나싶다

filter 를 이용하여 중복값을 제거하는 방법도 있지만 Set 을 이용해서 쉽게 구현 할 수 있다.

다만 걸리는 시간 복잡도는 동일하다.

요소 개수 확인

Set 객체의 요소 개수를 확인 할 대에는 Set.prototype.size 를 이용한다.

const arr = [1, 2, 3];
const set = new Set(arr);
console.log(set.size); // 3

size 프로퍼티는 접근자 함수이지만 get 만 구현되어있고 set 은 구현되어 있지 않아 외부에서 동적으로 값을 할당하는 것은 불가능하다.

size 의 값은 내부 요소를 추가하거나 제거함에 따라만 변경된다.

요소 추가

set 에서의 요소 추가는 add 메소드를 이용한다.

const arr = [1, 2, 3];
const set = new Set(arr);

set.add(4);
console.log(set); // Set(4) { 1, 2, 3, 4 }
console.log(set.size); // 4

중복된 요소를 추가할 경우 에러는 발생하지 않고 무시된다.

해쉬테이블에서 프로퍼티에 값을 덮어씌우는 재할당과 같으니까 ><

이 때 특이점은 Set 에서는 NaNNaN 을 같은 값으로 인식하여, NaN 마저도 중복 추가를 허용하지 않는다.

Set.add 는 자기 자신을 반환하기 때문에 메소드 체이닝을 이용 할 수 있다.

const arr = [1, 2, 3];
const set = new Set(arr);

set.add(4).add(5).add(6);
console.log(set); // Set(6) { 1, 2, 3, 4, 5, 6 }

메소드 체이닝

메소드 호출을 연속적으로 이어나가는 방식으로 객체의 메소드를 호출한 결과로 다른 메소드를 호출하는 것을 의미한다.

요소 존재 여부 확인

특정 요소가 존재하는지를 확인하는 것은 has 를 통해 확인한다.

const set = new Set([1, 2, 3, 4, 5]);

console.log(set.has(2)); // true
console.log(set.has(999)); // false

요소 삭제

특정 요소를 삭제하는 것은 delete 를 사용한다.

이 때 set 은 프로퍼티 키를 가지는 것이 아니기 때문에 제거 할 때 제거할 인수를 전달한다.

const set = new Set([1, 2, 3, 4, 5]);

set.delete(3);
console.log(set); // { 1, 2, 4, 5 }

delete 는 반환 값으로 해당 값 삭제 성공 여부를 불리언 값을 반환하기 때문에 메소드 체이닝이 불가능하다.

요소 일괄 삭제

모든 요소를 일괄 삭제하기 위해선 clear 메소드를 사용한다.

const set = new Set([1, 2, 3, 4, 5]);

set.clear();
console.log(set); // Set(0) {}

요소 순회

Set 은 객체와 비슷한 생김새이지만 특별한 점은 이터레이블한 개체라는 것이다.

이를 통해 다양한 반복문을 사용 할 수 있다.

const set = new Set([1, 2, 3, 4, 5]);

for (const num of set) {
  console.log(num); // 1 2 3 4 5
}

와우 ~~~

이 때 배열처럼 forEach 를 사용 할 수 있으나 넘겨받는 인수가 다르다.

배열에서는 [값, 인덱스 , 배열 자체] 를 전달했지만 Set 은 순서를 보장하지 않기에 인덱스가 존재하지 않는다.

그래서 Set값 , 값 , 배열자체 를 전달한다.

값을 두 번 전달하는 것은 큰 의미가 없다. 그냥 배열과 동일한 형태로 하기 위함이다.

const set = new Set([1, 2, 3, 4, 5]);

set.forEach((...rest) => console.log(rest));
[ 1, 1, Set(5) { 1, 2, 3, 4, 5 } ]
[ 2, 2, Set(5) { 1, 2, 3, 4, 5 } ]
[ 3, 3, Set(5) { 1, 2, 3, 4, 5 } ]
[ 4, 4, Set(5) { 1, 2, 3, 4, 5 } ]
[ 5, 5, Set(5) { 1, 2, 3, 4, 5 } ]

Set 으로 교집합, 합집합, 차집합 구현하기

난 이게 특별한 메소드라도 존재하는지 알았드니만 그런게 아니라 구현이 가능하단 거였다.

어이없어

교집합

const set1 = new Set([1, 2, 3, 4, 5]);
const set2 = new Set([3, 5, 7]);

const result = new Set();

set2.forEach((value) => {
  if (set1.has(value)) {
    result.add(value);
  }
});

console.log(result); // Set(2) { 3, 5 }

합집합

합집합은 그냥 우다다다 더해도 알아서 중복값을 제거하니 크게 어렵지 않다.

const set1 = new Set([1, 2, 3, 4, 5]);
const set2 = new Set([3, 5, 7]);

const result = new Set([...set1]); // 얕은 복사 

set2.forEach((value) => result.add(value));

console.log(set1); // Set(5) { 1, 2, 3, 4, 5 }
console.log(result); // Set(6) { 1, 2, 3, 4, 5, 7 }

차집합

const set1 = new Set([1, 2, 3, 4, 5]);
const set2 = new Set([3, 5, 7]);

const result = new Set([...set1]);

set2.forEach((value) => result.delete(value));

console.log(result); // Set(3) { 1, 2, 4 }

부분집합과 전체집합

예를 들어 집합 A가 집합 B에 모두 포함된다면 A 는 B의 부분집합이다.

const set1 = new Set([1, 2, 3, 4, 5]); // 전체집합
const set2 = new Set([3, 5, 7]); // 부분집합이 아님
const set3 = new Set([1, 2, 3]); // 부분집합임

function isSuperset(set1, set2) {
  for (const num of set2) {
    if (!set1.has(num)) return false;
  }

  return true;
}

console.log(isSuperset(set1, set2)); // false
console.log(isSuperset(set1, set3)); // true

그런데 이렇게 하니 eslint 에서 iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations. 라는 경고가 떴다.
살펴보니 런타임을 프로젝트에 추가하거나 배열 반복 메소드를 사용하라고 한다.
함수형 프로그래밍을 지향하는 자바스크립트에 맞게 배열 반복 메소드를 사용해보자

const set1 = new Set([1, 2, 3, 4, 5]); // 전체집합
const set2 = new Set([3, 5, 7]); // 부분집합이 아님
const set3 = new Set([1, 2, 3]); // 부분집합임

function isSuperset(A, B) {
  const arr = [...B];

  return arr.every((num) => A.has(num));
}

console.log(isSuperset(set1, set2));
console.log(isSuperset(set1, set3));

every 는 배열의 모든 요소가 콜백 함수의 조건을 만족할 경우 true 를 반환하고 만족하지 못할 경우 false 를 반환한다.
배열 반복문을 사용하니 훨씬 가독성이 좋아졌다.


Map

Map 은 키와 값으로 쌍으로 이뤄진 컬렉션이다.

객체와 유사하지만 특징적인 차이가 있다.

특성 Map Object
키 타입 모든 데이터 타입 가능 (문자열, 객체, 함수 등) 주로 문자열 또는 심볼
이터레이션 순서가 보장된 이터레이션 가능 키의 순서가 보장되지 않음 (ES6 이후 순서가 보장되기 시작)
크기 size 프로퍼티를 통해 크기를 쉽게 확인 가능 Object.keys() 등을 사용하여 크기 확인
프로퍼티 개수 임의의 데이터 타입으로 키가 될 수 있어 프로퍼티의 개수를 확인하기 어려움 Object.keys() 등을 사용하여 프로퍼티의 개수 확인 가능
활용 키-값 쌍의 동적인 저장 및 접근에 유용 주로 문자열로 된 키를 사용하여 속성에 접근

Map 객체의 생성

Map 도 표준 빌트인 생성자를 통해 생성한다.

이 때 인수는 [프로퍼티, 값] 이 쌍으로 이뤄진 배열을 넣어준다.

const map = new Map([
  ['a', 1],
  ['b', 2],
]);

console.log(map); // Map(2) { 'a' => 1, 'b' => 2 }

mapSet 과 같이 해쉬테이블로 관리되기 때문에 중복된 프로퍼티를 가지지 않는다.

const map = new Map([
  ['a', 1],
  ['b', 2],
  ['a', 999],
]);

console.log(map); // Map(2) { 'a' => 999, 'b' => 2 }

중복된 프로퍼티가 있을 경우 가장 마지막에 설정 된 것이 우선시 된다.

요소 개수 확인

const map = new Map([
  ['a', 1],
  ['b', 2],
  ['c', 3],
]);

console.log(map.size); // 3

size 를 통해 객체에 담긴 요소 개수를 확인 할 수 있으며

접근자 프로퍼티로 get 만 존재한다.

요소 추가

요소를 추가 할 때는 set 을 이용한다.

const map = new Map([
  ['a', 1],
  ['b', 2],
  ['c', 3],
]);

map.set('d', 4).set('e', 5);

console.log(map); // Map(5) { 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5 }

자기 자신을 반환하기 때문에 메소드 체이닝 가능하다.

요소 취득

객체에서는 프로퍼티 명을 이용해서 프로퍼티 값에 접근했다

map에선 get 이라는 접근자 메소드를 이용해 프로퍼티 값에 접근한다.

const map = new Map([
  ['a', 1],
  ['b', 2],
  ['c', 3],
]);

map.set('d', 4).set('e', 5);

console.log(map.get('d')); // 4
console.log(map.get('zzz')); // undefined

요소 존재 확인, 삭제, 일괄 삭제

모두 Set 과 동일하다.

프로퍼티 키

map 은 객체와 비슷해보이지만 큰 차이점은 프로퍼티 키가 문자열이 아닌 다른 다양한 요소도 가능하다.

객체에선 프로퍼티는 문자열만 가능했고 문자열이 아닌 값이 들어오면 자동으로 문자열로 변환되었다.

하지만 map 에선 프로퍼티키가 어떤 것이든 가능하다.

function Person(name, age) {
  this.name = name;
  this.age = age;
}

const tom = new Person('tom', 16);
const jerry = new Person('jerry', 20);

const map = new Map([
  [tom, 'developer'],
  [jerry, 'designer'],
]);
console.log(map);
Map(2) {
  Person { name: 'tom', age: 16 } => 'developer',
  Person { name: 'jerry', age: 20 } => 'designer'
}

이처럼 프로퍼티에 객체같은 자료구조도 넣을 수 있으며, 조회도 가능하다.

console.log(map.get(tom)); // developer

요소 순회

이터러블 객체이기 때문에 for offorEach 같은 것도 가능하다.

이 때 for of를 하게 되면 프로퍼티의 키와 값이 담긴 배열을 반환한다.

function Person(name, age) {
  this.name = name;
  this.age = age;
}

const tom = new Person('tom', 16);
const jerry = new Person('jerry', 20);

const map = new Map([
  [tom, 'developer'],
  [jerry, 'designer'],
]);

for (const job of map) {
  console.log(job);
}
[ Person { name: 'tom', age: 16 }, 'developer' ]
[ Person { name: 'jerry', age: 20 }, 'designer' ]

forEach 를 사용하면 [프로퍼티 값, 키 , Map 객체] 를 반환한다.

map.forEach((...rest) => console.log(rest));
[
  'developer',
  Person { name: 'tom', age: 16 },
  Map(2) {
    Person { name: 'tom', age: 16 } => 'developer',
    Person { name: 'jerry', age: 20 } => 'designer'
  }
]
[
  'designer',
  Person { name: 'jerry', age: 20 },
  Map(2) {
    Person { name: 'tom', age: 16 } => 'developer',
    Person { name: 'jerry', age: 20 } => 'designer'
  }
]

정리

  • Set ,Map 모두 이터러블한 자료구조 객체이다.

  • Set 은 이터러블한 자료구조를 인수로 받으며, 중복값을 허용하지 않은 인수들을 저장하는 자료구조이다.

  • Map 은 이터러블한 프로퍼티 키와 값을 인수로 받으며, 중복된 프로퍼티를 허용하지 않는다.

  • Map의 프로퍼티에는 문자열이 아닌 다양한 값이 존재 할 수 있다.

  • Map은 들어온 순서를 보장한다

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글