ECMAScript 6 Set과 Map

Set과 Map

JavaScript의 역사에서 컬렉션 타입은 Array 타입 하나만 가지고 있었습니다. (Array가 아닌 모든 객체는 키 - 값 쌍의 집합이므로 Array와 원래 의도된 용도는 다릅니다). Array는 다른 언어와 마찬가지로 JavaScript에서도 사용되지만 다른 컬렉션이 없기 때문에 대개 Queue 및 Stack으로 Array를 사용합니다. Array는 숫자 인덱스만 사용하기 때문에 개발자는 숫자가 아닌 인덱스가 필요할 때마다 Array가 아닌 객체를 사용했습니다. 아래에서 설명한 기술을 사용하면 Array가 아닌 객체를 사용하여 Set 및 Map을 사용자 정의로 구현할 수 있습니다.

Set은 중복을 포함할 수 없는 값의 목록입니다. 일반적인 Array의 항목처럼 Set은 개별 항목에 액세스하지 않습니다. 대신 값이 있는지 확인하기 위해 Set를 확인하는 것이 훨씬 더 일반적입니다. Map은 특정 값에 해당하는 키의 모음입니다. 따라서 Map의 각 항목에는 두 개의 데이터가 저장되고 값은 읽을 키를 지정하여 검색됩니다. Map은 캐시로 자주 사용되어 나중에 빠르게 검색할 데이터를 저장하기도 합니다. ECMAScript 5에는 공식적으로 SetMap이 없었지만 개발자는 Array가 아닌 오브젝트를 사용하여 이 제한을 풀었습니다.

ECMAScript 6은 JavaScript에 SetMap을 추가했으며 이 장에서는 이 두 가지 컬렉션 유형에 대해 알아야할 모든 내용에 대해 설명합니다.

먼저 ECMAScript 6 이전에 SetMap을 구현하는데 사용된 방법과 그 구현이 왜 문제가되는지 설명합니다. 중요한 배경 지식을 얻은 후에 ECMAScript 6에서 SetMap의 작동 방법을 설명하겠습니다.

ECMAScript 5에서의 SetMap

ECMAScript 5에서 개발자는 다음과 같이 객체 프로퍼티를 사용하여 SetMap을 모방했습니다.

1
2
3
4
5
6
7
8
9
let set = Object.create(null);
set.foo = true;
// 존재 확인하기
if (set.foo) {
// do something
}

이 예제에서 set 변수는 객체 상에 상속된 프로퍼티가 없음을 보장하는 null 프로토 타입을 가진 객체입니다. 객체 프로퍼티를 검사할 고유 값으로 사용하는 것은 ECMAScript 5에서 일반적인 접근법입니다. 프로퍼티가 set 객체에 추가되면, 이 객체는 true로 설정되어 조건문 (이 예에서 if 문)에서 값이 존재하는지 쉽게 확인할 수 있습니다.

Set으로 사용된 객체와 Map으로 사용되는 객체 사이의 차이점은 저장되는 값뿐입니다. 예를 들어, 아래 예제는 객체를 Map으로 사용합니다.

1
2
3
4
5
6
7
8
let map = Object.create(null);
map.foo = "bar";
// 값을 추출
let value = map.foo;
console.log(value); // "bar"

이 코드는 foo 키 아래에 문자열 값 "bar"를 저장합니다. Set과 달리, Map은 키의 존재를 확인하기 보다는 정보를 검색하는 데 주로 사용됩니다.

해결 방법의 문제점

객체를 SetMap으로 사용하는 것은 간단한 상황에서 문제 없이 작동하지만 객체 프로퍼티의 한계를 뛰어 넘으면 접근법이 더 복잡해질 수 있습니다. 예를 들어 모든 객체 프로퍼티는 문자열이어야 하므로 동일한 문자열로 평가되는 두 개의 키가 없어야합니다. 다음 사항을 생각해 보겠습니다.

1
2
3
4
5
let map = Object.create(null);
map[5] = "foo";
console.log(map["5"]); // "foo"

이 예제에서는 문자열 값 "foo"를 숫자 키 5에 할당합니다. 내부적으로 숫자 값은 문자열로 변환되므로 map["5"]map[5]는 실제로 동일한 프로퍼티를 참조합니다. 내부 변환은 숫자와 문자열을 키로 사용하려는 경우 문제를 일으킬 수 있습니다. 다음과 같이 객체를 키로 사용할 때 또 다른 문제가 발생합니다.

1
2
3
4
5
6
7
let map = Object.create(null),
key1 = {},
key2 = {};
map[key1] = "foo";
console.log(map[key2]); // "foo"

여기서 map[key2]map[key1]은 같은 값을 참조합니다. 객체 프로퍼티는 문자열이어야 하므로 객체 key1key2는 문자열로 변환됩니다. "[object Object]"는 객체의 기본 문자열 표현이기 때문에 key1key2가 모두 해당 문자열로 변환됩니다. 이는 사실상 다른 객체의 키가 같은 값으로 가정되는 것이 논리적의로 명백하지 않을 수있는 오류를 유발할 수 있습니다.

기본 문자열 표현으로 변환하면 객체를 키로 사용하기가 어렵습니다. (객체를 Set으로 사용하려고할 때도 같은 문제가 발생합니다.)

거짓으로 표현되는 값을 가지는 키가 있는 Map은 특정 문제를 발생시킵니다. 거짓 값은 if 문과 같은 부울 값이 필요한 상황에서 사용될 때 자동으로 false로 변환됩니다. 이 변환만으로는 문제가되지 않습니다. 값을 사용하는 방법에 대해 신중해야합니다. 예를 들어 다음 코드를 살펴보십시오.

1
2
3
4
5
6
7
8
let map = Object.create(null);
map.count = 1;
// "count"값이 존재하는지 또는 0이 아닌지 어떤걸 체크 하나요?
if (map.count) {
// ...
}

이 예제는 map.count가 어떻게 사용되어야하는지 모호합니다. if 문은 map.count의 존재를 검사할지 아니면 값이 0이 아닌 것을 검사할지 모호합니다. 그래서 숫자 1은 true로 표현되어 if문 안의 코드가 실행됩니다. 그러나 map.count가 0이거나 map.count가 없으면 if 문 안의 코드는 실행되지 않습니다.

ECMAScript 6가 SetMap 모두를 언어에 추가하는 가장 큰 이유는 대규모 응용 프로그램에서 발생하는 문제를 식별하고 디버그하는 것이 어렵기 때문입니다.

JavaScript는 객체의 값을 읽지 않고 객체에 프로퍼티가 있으면 true를 반환하는 in 연산자를 가지고 있습니다. 그러나 in 연산자는 객체의 프로토 타입을 검색하기 때문에 객체가 null 프로토 타입을 가지고있을 때만 안전하게 사용할 수 있습니다. 그럼에도 불구하고, 많은 개발자들은 여전히 in을 사용하는 대신 마지막 예제 에서처럼 코드를 잘못 사용합니다.

ECMAScript 6에서 Set

ECMAScript 6이 중복되지 않은 값의 정렬된 목록인 Set 타입을 추가 했습니다. Set을 사용하면 포함된 데이터에 빠르게 액세스할 수 있으므로 불연속 값을 보다 효율적으로 추적할 수 있습니다.

Set 만들기와 아이템 추가하기

Setnew Set()를 사용하여 생성할수 있고, 항목은 add() 메서드를 호출하여 추가할 수 있습니다. size 프로퍼티를 확인하여 Set에 포함된 항목 수를 확인할 수 있습니다.

1
2
3
4
5
let set = new Set();
set.add(5);
set.add("5");
console.log(set.size); // 2

Set은 값이 동일한지 확인하기 위해 값을 강제 변환하지 않습니다. 이는 Set이 숫자 5와 문자열 "5"를 두 개의 개별 항목을 포함할 수 있음을 의미합니다. 유일한 예외는 -0+0은 같은 것으로 간주된다는 것입니다. 또한 Set에 여러 객체를 추가할 수 있으며 이러한 객체는 구별됩니다.

1
2
3
4
5
6
7
8
let set = new Set(),
key1 = {},
key2 = {};
set.add(key1);
set.add(key2);
console.log(set.size); // 2

key1key2는 문자열로 변환되지 않으므로 Set에서 두 개의 고유 항목으로 간주됩니다. (문자열로 변환하면 "[Object object]"와 같이 둘은 서로 같습니다.)

add() 메서드가 같은 값으로 두 번 이상 호출되면 첫 번째 호출 이후의 모든 호출은 무시됩니다.

1
2
3
4
5
6
let set = new Set();
set.add(5);
set.add("5");
set.add(5); // duplicate - this is ignored
console.log(set.size); // 2

Array를 사용하여 Set을 초기화할 수 있으며 Set 생성자는 고유 한 값만 사용합니다. 예를 들어 아래와 같습니다.

1
2
let set = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
console.log(set.size); // 5

이 예제에서는 중복 값이 있는 Array를 사용하여 Set을 초기화합니다. 숫자 5Array에 네 번 나타나지만 Set에 한 번만 입력됩니다. 이 기능을 사용하여 기존 코드 또는 JSON을 쉽게 Set으로 사용할 수 있습니다.

Set 생성자는 실제로 모든 반복 가능한 객체를 인수로 받습니다. ArraySetMap처럼 기본적으로 반복 가능하기 때문입니다. Set 생성자는 Iterator를 사용하여 인수에서 값을 추출합니다. (IterablesIterator는 8 장에서 자세히 논의됩니다.)

다음과 같이 has() 메서드를 사용하여 어떤 값이 Set에 있는지 테스트할 수 있습니다.

1
2
3
4
5
6
let set = new Set();
set.add(5);
set.add("5");
console.log(set.has(5)); // true
console.log(set.has(6)); // false

Set에는 값이 없기 때문에 set.has(6)false를 반환합니다.

값 지우기

Set에서 값을 제거할 수도 있습니다. delete() 메서드를 사용하여 단일 값을 제거하거나 clear() 메서드를 호출하여 Set에서 모든 값을 제거할 수 있습니다. 아래 코드는 두 가지를 모두 보여줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let set = new Set();
set.add(5);
set.add("5");
console.log(set.has(5)); // true
set.delete(5);
console.log(set.has(5)); // false
console.log(set.size); // 1
set.clear();
console.log(set.has("5")); // false
console.log(set.size); // 0

delete() 호출 후 5가 삭제되었습니다. clear() 메서드가 실행된 후 set은 모두 비어 있습니다.

이 모든 것이 정렬된 값을 추적하기 위한 매우 쉬운 메커니즘입니다. 그리고 Set에 항목을 추가한 다음 각 항목에 대해 일부 작업을 수행하려면 forEach() 메소드를 이용합니다.

Set의 forEach 메서드

Array 작업에 익숙하다면 이미 forEach() 메서드에 익숙할 것입니다. ECMAScript 5는 forEach()Array에 추가하여 Array의 각 항목에서 for 루프를 설정하지 않고 작업을 쉽게할 수 있게했습니다. 이 방법은 개발자들 사이에서 인기가 높으며 동일한 방법이 Set에서 사용 가능하고 동일한 방식으로 실행됩니다.

forEach() 메서드는 세 개의 파라미터를 받는 콜백 함수가 전달됩니다.

  1. Set 내의 다음의 위치 값입니다.
  2. 첫 번째 파라미터와 같은 값
  3. 값을 읽는 Set

SetforEach()ArrayforEach()와 특이한 차이점은 콜백 함수에 대한 첫 번째 및 두 번째 파라미터가 동일하다는 것입니다. 이것은 실수처럼 보일 수 있지만 타당한 이유가 있습니다.

forEach() 메서드 (ArrayMap)가 있는 다른 객체는 콜백 함수에 세 개의 파라미터를 전달합니다. ArrayMap의 최초의 2 개의 파라미터는, 값과 키 (Array의 인덱스 숫자)입니다.

그러나 Set에는 키가 없습니다. ECMAScript 6 표준을 작성한 사람들은 forEach()Set 버전에서 콜백 함수에 두 개의 파라미터를 허용하지만, ArrayMap의 파라미터와 다르게 만들었습니다. 대신 콜백 함수를 동일하게 유지하고 세 개의 파라미터를 허용하는 방법을 찾았습니다. Set의 첫 번째, 두 번째 파라미터는 키와 값으로 간주됩니다. 따라서 첫 번째와 두 번째 파라미터는 ArrayMapforEach() 메서드와 일관된 기능을 유지합니다.

파라미터의 차이 이외에, forEach()를 사용하는 것은 기본적으로 Array와 같습니다. 다음은 메서드가 실행되는 방식을 보여주는 코드입니다.

1
2
3
4
5
6
let set = new Set([1, 2]);
set.forEach(function(value, key, ownerSet) {
console.log(key + " " + value);
console.log(ownerSet === set);
});

이 코드는 Set의 각 항목을 반복하여 forEach() 콜백 함수에 전달된 값을 출력합니다. 콜백 함수가 실행될 때마다 keyvalue는 같고 ownerSet는 항상 set과 같습니다. 이 코드의 출력 결과는 아래와 같습니다.

1
2
3
4
1 1
true
2 2
true

콜백 함수에서 this를 사용해야한다면 forEach()의 두 번째 파라미터로 this 값을 전달할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let set = new Set([1, 2]);
let processor = {
output(value) {
console.log(value);
},
process(dataSet) {
dataSet.forEach(function(value) {
this.output(value);
}, this);
}
};
processor.process(set);

이 예제에서 processor.process() 메서드는 Set에서 forEach()를 호출하고 this를 콜백을 위한 this 값으로 전달합니다. 그래서 this.output()processor.output() 메서드로 올바르게 해석됩니다. forEach() 콜백 함수는 첫 번째 파라미터인 value만을 사용하므로 나머지는 생략합니다. Arrow 함수를 사용하여 두 번째 파라미터를 전달하지 않고 동일한 효과를 얻을 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
let set = new Set([1, 2]);
let processor = {
output(value) {
console.log(value);
},
process(dataSet) {
dataSet.forEach((value) => this.output(value));
}
};
processor.process(set);

위 예제의 Arrow 함수는 process() 함수에서 this를 읽어서 this.output()processor.output()호출로 올바르게 해석합니다.

Set은 값을 추적하는데 적합하고 forEach()를 사용하여 순차적으로 각 값을 처리할 수 있지만, Array 처럼 인덱스를 사용하여 값에 직접 액세스할 수는 없습니다. 그렇게해야 할 경우, 가장 좋은 방법은 SetArray로 변환하는 것입니다.

Set을 Array로 변환

ArraySet 생성자에 전달할 수 있기 때문에 ArraySet으로 쉽게 변환할 수 있습니다. Spread 연산자를 사용하여 Set을 다시 Array로 변환하는 것도 쉽습니다. 3 장에서는 Array의 항목을 별도의 함수 매개 변수로 분할하는 방법으로 Spread 연산자 (...)를 설명했습니다. 또한 Spread 연산자를 사용하여 Set과 같은 반복 가능한 객체를 Array로 변환할 수 있습니다.

1
2
3
4
let set = new Set([1, 2, 3, 3, 3, 4, 5]),
array = [...set];
console.log(array); // [1,2,3,4,5]

여기서, Set은 초기에 중복을 포함하는 Array로 로드됩니다. Set은 중복을 제거한 다음 Spread 연산자를 사용하여 항목을 새로운 Array에 배치합니다. Set 자체에는 생성될 때받은 것과 동일한 항목 (1, 2, 3, 45)이 여전히 포함되어 새로운 Array로 복사됩니다.

이 방법은 이미 Array가 있지만 새로운 중복값 없이 Array를 만들려는 경우에 유용합니다.

1
2
3
4
5
6
7
8
function eliminateDuplicates(items) {
return [...new Set(items)];
}
let numbers = [1, 2, 3, 3, 3, 4, 5],
noDuplicates = eliminateDuplicates(numbers);
console.log(noDuplicates); // [1,2,3,4,5]

eliminateDuplicates() 함수에서 Set은 중복이 없는 새로운 Array를 만들기 전에 중복 값을 필터링하는데 사용되는 임시 매개체 일뿐입니다.

Weak Set

Set 타입은 오브젝트 참조를 저장하는 방식 때문에 Strong Set이 라고 부를 수 있습니다. Set의 인스턴스에 저장된 객체는 그 객체를 변수 안에 저장하는 것과 실질적으로 동일합니다. 해당 Set 인스턴스에 대한 참조가 존재하는 한, 객체는 가비지 컬랙트되어 메모리를 비울 수 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
let set = new Set(),
key = {};
set.add(key);
console.log(set.size); // 1
// 원래의 참조 삭제
key = null;
console.log(set.size); // 1
// 원래의 참조를 되찾습니다.
key = [...set][0];

이 예제에서 keynull로 설정하면 key 객체의 참조를 지우지만 다른 참조는 set 안에 남아 있습니다. 집합을 Spread 연산자로 Array로 변환하고 첫 번째 항목에 액세스하면 여전히 key를 검색할 수 있습니다. 대부분의 프로그램에서는 괜찮지만 때때로 다른 모든 참조가 사라지면 Set의 참조가 사라지는 것이 좋을 때도 있습니다. 예를 들어 웹 페이지에서 JavaScript 코드가 실행 중이고 다른 스크립트에 의해 제거 될 수있는 DOM 요소를 추적하려는 경우 코드가 DOM 요소에 대한 마지막 참조를 유지하지 못하게해야 합니다. (이 상황을 메모리 누수(memory leak)라고합니다.)

이러한 문제를 줄이기 위해 ECMAScript 6에는 약한 객체 참조만 저장하고 원시 값을 저장할 수없는 Weak Set도 포함하고 있습니다. 객체에 대한 약한 참조가 유일하게 남아있는 참조인 경우 가비지 컬렉션 대상이 됩니다.

Weak Set 생성하기

Weak SetWeakSet 생성자를 사용하여 생성되며 add(), has()delete() 메서드를 포함합니다. 다음은이 세 가지를 모두 사용하는 예제입니다.

1
2
3
4
5
6
7
8
9
10
11
let set = new WeakSet(),
key = {};
// add the object to the set
set.add(key);
console.log(set.has(key)); // true
set.delete(key);
console.log(set.has(key)); // false

Weak Set을 사용하는 것은 보통 Set을 사용하는 것과 같습니다. Weak Set에서 참조를 추가, 제거 및 확인할 수 있습니다. Iterable을 생성자에 전달하여 값이 있는 Weak Set를 생성할 수도 있습니다.

1
2
3
4
5
6
let key1 = {},
key2 = {},
set = new WeakSet([key1, key2]);
console.log(set.has(key1)); // true
console.log(set.has(key2)); // true

이 예제에서 ArrayWeakSet 생성자에 전달됩니다. 이 Array에는 두 개의 객체가 포함되어 있으므로 해당 객체가 Weak Set에 추가됩니다. WeakSetPrimitive 값을 받아 들일 수 없으므로 Array에 객체가 아닌 값이 포함되어 있으면 오류가 발생합니다.

Set 타입 간의 주요 차이점

Weak Set와 정규 Set의 가장 큰 차이점은 객체 값에 약한 참조를 보유한다는 것입니다. 다음은 그 차이를 보여주는 예입니다.

1
2
3
4
5
6
7
8
9
10
let set = new WeakSet(),
key = {};
// add the object to the set
set.add(key);
console.log(set.has(key)); // true
// 키에 대한 마지막 strong reference를 제거하고 Weak Set에서 제거합니다.
key = null;

이 코드가 실행된 후에, Weak Setkey에 대한 참조는 더 이상 접근 가능하지 않습니다. 하지만 has() 메서드에 전달할 객체에 대한 참조가 필요하기 때문에 객체의 제거를 확인할 수 없습니다. 이렇게하면 Weak Set 테스트가 다소 혼란스럽지만, JavaScript 엔진이 참조를 제대로 제거했다는 것을 신뢰해야 합니다.

이 예제는 Weak Set이 일반 Set과 몇 가지 특성은 공유하지만 또한 몇 가지 중요한 차이점이 있음을 알려줍니다.

  1. WeakSet 인스턴스에서, 비 객체 add() 메서드는 오류를 throw합니다.(has()delete()는 객체가 아닌 파라미터에 대해 항상 false를 반환)
  2. Weak SetIterable이 아니므로 for-of 루프에서 사용할 수 없습니다.
  3. Weak SetIterator(keys()values() 메소드와 같은)를 노출시키지 않으므로 프로그래밍 방식으로 Weak Set의 내용을 판별할 수는 없습니다.
  4. Weak Set에는 forEach() 메서드가 없습니다.
  5. Weak Set에는 size 프로퍼티가 없습니다.

메모리를 적절히 처리하려면 Weak Set의 기능을 제한적으로 사용하는 것이 필요합니다. 객체 참조 추적을 한다면 일반 Set 대신 Weak Set를 사용합니다.

Set은 값 목록을 처리하는 새로운 방법을 제공하지만 추가 정보를 해당 값과 연결해야하는 경우 유용하지 않습니다. 그래서 ECMAScript 6은 Map을 추가했습니다.

ECMAScript 6의 Map

ECMAScript 6 Map 타입은 키-값 쌍의 정렬된 목록입니다. 여기서 키와 값은 어떤 타입도 가질 수 있습니다. 키는 Set 객체와 같은 접근법을 사용하여 결정됩니다. 그래서 5키와 '5'키를 가질 수 있습니다. 객체에서 프로퍼티를 키로 사용하는 것과 매우 다릅니다. 왜냐하면 객체의 프로퍼티는 항상 문자열로 강제 변환합니다.

set() 메서드를 호출하고 키와 값을 전달하여 Map에 항목을 추가할 수 있습니다. 나중에 get() 메서드에 키를 전달하여 값을 검색할 수 있습니다.

1
2
3
4
5
6
let map = new Map();
map.set("title", "Understanding ES6");
map.set("year", 2016);
console.log(map.get("title")); // "Understanding ES6"
console.log(map.get("year")); // 2016

이 예에서는 두 개의 키-값 쌍이 저장됩니다. "title"키에는 문자열이 저장되고 "year"키에는 숫자가 저장됩니다. get() 메서드는 나중에 호출되어 두 키의 값을 검색합니다. Map에 키가 하나도 없으면 get()은 값 대신에 undefined라는 특수 값을 반환했을 것입니다.

객체를 키로 사용할 수도 있습니다. 이 방법은 이전에 객체 프로퍼티를 사용하여 Map을 만들던 방법으로는 불가능 했던걸 가능하게 합니다.

1
2
3
4
5
6
7
8
9
let map = new Map(),
key1 = {},
key2 = {};
map.set(key1, 5);
map.set(key2, 42);
console.log(map.get(key1)); // 5
console.log(map.get(key2)); // 42

이 코드는 key1key2 객체를 Map의 키로 사용하여 두 개의 다른 값을 저장합니다. 이러한 키는 다른 형태로 강제 변환되지 않으므로 각 객체는 고유 한 것으로 간주됩니다. 이렇게하면 객체 자체를 수정하지 않고도 객체에 추가 데이터를 연결할 수 있습니다.

Map 메서드

Map은 여러 메서드를 Set과 공유합니다. 이는 의도적인 것이며, 비슷한 방식으로 MapSet과 상호 작용할 수 있습니다. 아래의 세 가지 메서드는 MapSet에서 사용할 수 있습니다.

  • has(key) - 지정된 키가 Map에 있는지 확인합니다.
  • delete(key) - Map에서 키와 관련 값을 제거합니다.
  • clear() - Map에서 모든 키와 값을 제거합니다.

또한 Map에는 키-값 쌍이 얼마나 많은지 나타내는 size 프로퍼티가 있습니다. 아래 코드는 세 가지 메서드와 사이즈를 모두 다른 방식으로 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let map = new Map();
map.set("name", "Nicholas");
map.set("age", 25);
console.log(map.size); // 2
console.log(map.has("name")); // true
console.log(map.get("name")); // "Nicholas"
console.log(map.has("age")); // true
console.log(map.get("age")); // 25
map.delete("name");
console.log(map.has("name")); // false
console.log(map.get("name")); // undefined
console.log(map.size); // 1
map.clear();
console.log(map.has("name")); // false
console.log(map.get("name")); // undefined
console.log(map.has("age")); // false
console.log(map.get("age")); // undefined
console.log(map.size); // 0

Set과 마찬가지로 size 프로퍼티는 항상 키-값 쌍의 수를 Map에 포함합니다. 이 예제에서 Map 인스턴스는 "name""age"키들로 시작합니다. 그래서 has() 메서드는 true를 리턴합니다. "name" 키가 delete() 메서드에 의해 제거된 후에 has() 메서드는
false를 리턴하고 size 프로퍼티는 1을 나타냅니다. 그런 다음 clear() 메서드는 나머지 키를 제거하고, size프로퍼티는 0, has()false를 반환합니다.

clear() 메서드는 Map에서 많은 데이터를 빠르게 제거하는 방법이지만 한 번에 많은 데이터를 Map에 추가하는 방법도 있습니다.

Map 초기화

또한 Set과 유사하게 ArrayMap 생성자에 전달하여 데이터로 Map을 초기화할 수 있습니다. Array의 각 항목은 첫 번째 항목이 키이고 두 번째 항목이 키의 값인 Array이어야합니다. 따라서 전체 Map은 다음과 같은 ArrayArray입니다.

1
2
3
4
5
6
7
let map = new Map([["name", "Nicholas"], ["age", 25]]);
console.log(map.has("name")); // true
console.log(map.get("name")); // "Nicholas"
console.log(map.has("age")); // true
console.log(map.get("age")); // 25
console.log(map.size); // 2

"name""age"는 생성자의 초기화를 통해 map에 추가됩니다. ArrayArray가 조금 이상하게 보일 수도 있지만 키는 모든 데이터 타입이 될 수 있으므로 키를 정확하게 나타내야합니다. 키를 Array에 저장하는 것이 Map에 저장되기 전에 다른 데이터 타입으로 강제 변환되지 않도록하는 유일한 방법입니다.

Map에서의 forEach 메서드

Map에 대한 forEach() 메서드는 세 개의 파라미터를 받는 콜백 함수를 허용한다는 점에서 SetArrayforEach()와 유사합니다.

  1. Map의 다음 위치 값
  2. 그 값의 키
  3. 값을 읽는 Map

이러한 콜백 파라미터는 첫 번째 파라미터가 값이고 두 번째 파라미터가 키 (Array의 숫자 인덱스에 해당) 인 Array의 forEach () 동작과 비슷합니다. 다음은 그 예입니다.

1
2
3
4
5
6
let map = new Map([ ["name", "Nicholas"], ["age", 25]]);
map.forEach(function(value, key, ownerMap) {
console.log(key + " " + value);
console.log(ownerMap === map);
});

forEach() 콜백 함수는 전달된 정보를 출력합니다. valuekey는 직접 출력되고, ownerMapmap과 비교되어 값이 동일 함을 보여줍니다. 결과는 다음과 같습니다.

1
2
3
4
name Nicholas
true
age 25
true

forEach()에 전달된 콜백은 Map에 삽입된 순서대로 각 키-값 쌍을 받습니다. 이 동작은 콜백이 숫자 인덱스 순서대로 각 항목을 받는 ArrayforEach()가 호출되는 것과 약간 다릅니다.

forEach()에 두 번째 파라미터를 제공하여 콜백 함수 내에서 this 값을 지정할 수도 있습니다. 이와 같은 호출은 Set 버전의
forEach() 메서드와 동일하게 작동합니다.

Weak Map

Weak Map은 약한 객체 참조를 저장하는 방법입니다. Weak Map에서는 모든 키가 객체여야 하며 (객체가 아닌 키를 사용하려고하면 오류가 발생 함) 이러한 객체 참조는 가비지 컬랙트를 방해하지 않도록 약하게 유지됩니다. Weak Map 외부의 키에 대한 참조가 없으면 Weak Map에서 키-값 쌍이 제거됩니다.

Weak Map을 사용하는 가장 유용한 장소는 웹 페이지의 특정 DOM 엘리먼트와 관련된 객체를 만드는 경우입니다. 예를 들어 웹 페이지용 JavaScript 라이브러리 중 일부는 라이브러리에서 참조하는 모든 DOM 엘리먼트에 대해 특정 사용자 정의 객체를 유지하고 해당 매핑은 내부적으로 객체 캐시에 저장됩니다.

이 접근법의 어려운 부분은 DOM 엘리먼트가 웹 페이지에 더 이상 존재하지 않아 라이브러리가 관련 객체를 제거할 수 없을 때를 결정하는 것입니다. 그렇지 않으면 라이브러리가 참조 엘리먼트의 유용성을 넘어 DOM 엘리먼트 참조를 유지하고 메모리 누수가 발생합니다. Weak Map으로 DOM 엘리먼트를 추적하면 라이브러리가 모든 DOM 엘리먼트와 객체를 연결할 수 있으며 해당 객체의 DOM 엘러먼트가 더 이상 존재하지 않으면 Map의 객체를 자동으로 삭제할 수 있습니다.

Weak Map 값이 아닌 Weak Map 키만 약한 참조임을 유의해야합니다. Weak Map 값으로 저장된 오브젝트는 다른 모든 참조가 제거되어도 가비지 콜렉션 대상이 아닙니다.

Weak Map 사용하기

ECMAScript 6 WeakMap 타입은 키와 값 쌍의 정렬되지 않은 목록입니다. 여기서 키는 null이 아닌 객체여야 하며 값은 모든 타입이 될 수 있습니다. WeakMapset()get()이 각각 데이터를 추가하고 검색하는 데 사용된다는 점에서 Map의 인터페이스와 매우 유사합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
let map = new WeakMap(),
element = document.querySelector(".element");
map.set(element, "Original");
let value = map.get(element);
console.log(value); // "Original"
// remove the element
element.parentNode.removeChild(element);
element = null;
// the weak map is empty at this point

이 예제는 키-값 쌍 하나가 저장됩니다. element 키는 해당 문자열 값을 저장하는 데 사용되는 DOM 엘리먼트입니다. 그 값은 DOM 엘레멘트를 get() 메서드에 전달해서 얻습니다. 나중에 DOM 엘리먼트가 Document에서 제거되고 이를 참조하는 변수가 null로 설정되면 데이터도 Weak Map에서 제거됩니다.

Weak Set과 마찬가지로 size 프로퍼티가 없기 때문에 Weak Map이 비어 있는지 확인할 방법이 없습니다. 키에 대한 참조가 남아 있지 않으므로 get() 메서드를 호출하여 값을 검색할 수 없습니다. Weak Map은 그 키에 대한 값의 액세스를 차단하고, 가비지 컬렉터가 실행될 때 값으로 점유된 메모리는 해제됩니다.

Weak Map 초기화

Weak Map을 초기화하려면 WeakMap 생성자에 ArrayArray를 전달합니다. 일반 Map을 초기화하는 것처럼 포함하는 Array의 각 Array에는 두 개의 항목이 있어야합니다. 첫 번째 항목은 null이 아닌 객체 키이고 두 번째 항목은 값 (모든 데이터 유형)입니다.

1
2
3
4
5
6
7
8
let key1 = {},
key2 = {},
map = new WeakMap([[key1, "Hello"], [key2, 42]]);
console.log(map.has(key1)); // true
console.log(map.get(key1)); // "Hello"
console.log(map.has(key2)); // true
console.log(map.get(key2)); // 42

key1key2 객체는 Weak Map에서 키로 사용되며 get()has() 메서드를 통해 해당 객체에 액세스할 수 있습니다. WeakMap 생성자가 어떤 키-값 쌍에서 객체가 아닌 키를 받으면 오류가 발생합니다.

Weak Map 메서드

Weak Map에는 키-값 쌍과 상호 작용할 수있는 두 가지 추가 방법이 있습니다. 특정 키가 Map에 있는지 확인하는 has() 메소드와 특정 키-값 쌍을 제거하는 delete() 메소드가 있습니다. Weak Map에서는 Weak Set 처럼 키를 열거하는 것이 불가능 하기 때문에 clear() 메서드가 없습니다. 아래 예제에서는 has()delete() 메서드를 모두 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
let map = new WeakMap(),
element = document.querySelector(".element");
map.set(element, "Original");
console.log(map.has(element)); // true
console.log(map.get(element)); // "Original"
map.delete(element);
console.log(map.has(element)); // false
console.log(map.get(element)); // undefined

위 예제에서 DOM 엘리먼트를 Weak Map에서 키로 사용됩니다. has() 메서드는 참조가 현재 Weak Map에서 키로 사용되고 있는지 확인하는 데 유용합니다. 이것은 키에 대한 null이 아닌 참조가 있는 경우에만 작동한다는 점에 유의하십시오. 키는 delete() 메서드에 의해 Weak Map에서 강제로 제거됩니다. 그리고 has()는 false를 반환하고 get()undefined를 반환합니다.

Private Object Data

대부분의 개발자는 Weak Map의 주요 유스 케이스가 DOM 엘리먼트와 관련된 데이터라고 생각하지만 다른 많은 용도가 있습니다 (의심의 여지가 있지만 아직 발견되지 않은 것). Weak Map을 실제로 사용하는 한 가지 방법은 객체 인스턴스에 Private 데이터를 저장하는 것입니다. ECMAScript 6에서는 모든 객체 프로퍼티가 public이므로 객체에 액세스할 수있는 데이터를 만들려면 창의력을 사용해야하지만 모든 객체에 액세스할 수는 없습니다 다음 예제를 살펴 보겠습니다.

1
2
3
4
5
6
7
function Person(name) {
this._name = name;
}
Person.prototype.getName = function() {
return this._name;
};

이 코드는 일반적인 규칙인 선행 밑줄을 사용하였기 때문에 프로퍼티가 Private으로 간주되므로 객체 인스턴스 외부에서 수정하면 안됩니다. 의도는 getName()을 사용하여 this._name을 읽고 _name 값을 변경하지 못하게하는 것입니다. 그러나 _name 프로퍼티를 작성하는 사람은 의도적으로 또는 실수로 덮어 쓸 수 있습니다.

ECMAScript 5에서는 다음과 같은 패턴을 사용하여 객체를 생성하여 진정한 Private 데이터를 가질 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var Person = (function() {
var privateData = {},
privateId = 0;
function Person(name) {
Object.defineProperty(this, "_id", { value: privateId++ });
privateData[this._id] = {
name: name
};
}
Person.prototype.getName = function() {
return privateData[this._id].name;
};
return Person;
}());

이 예제는 두 개의 Private 변수 인 privateDataprivateId를 포함하는 IIFE에 Person의 정의를 래핑합니다. privateData 객체는 각 인스턴스에 대한 개인 정보를 저장하는 반면 privateId는 각 인스턴스에 대해 고유 한 ID를 생성하는데 사용됩니다. Person 생성자가 호출 될 때, nonumerable, nonconfigurable, 그리고 nonwritable _id 프로퍼티가 추가됩니다.

그런 다음 객체 인스턴스의 ID에 해당하는 privateData 객체에 항목이 만들어지고 그 이름이 저장됩니다. 나중에 getName() 함수에서 this._idprivateData의 키로 사용하여 이름을 검색할 수 있습니다. privateData는 IIFE 외부에서 접근할 수 없으므로
this._id가 공개 되어도 실제 데이터는 안전합니다.

이 접근법의 가장 큰 문제점은 객체 인스턴스가 언제 파괴되는지를 알 수 없기 때문에 privateData의 데이터가 사라지지 않는다는 것입니다. privateData 객체는 항상 여분의 데이터를 포함합니다. 이 문제는 다음과 같이 Weak Map을 대신 사용하여 해결할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let Person = (function() {
let privateData = new WeakMap();
function Person(name) {
privateData.set(this, { name: name });
}
Person.prototype.getName = function() {
return privateData.get(this).name;
};
return Person;
}());

이 버전의 Person 예제는 객체가 아닌 Private 데이터용 Weak Map을 사용합니다. Person 객체 인스턴스 자체를 키로 사용할 수 있기 때문에 별도의 ID를 추적할 필요가 없습니다. Person 생성자가 호출되면 weak 필드에 this키와 개인 정보가 들어있는 객체의 값을 가진 새로운 항목이 만들어집니다. 이 경우 값은 name만 포함하는 객체입니다. getName() 함수는 privateData.get() 메소드에 this를 전달하여 해당 개인 정보를 검색합니다. privateData.get() 메소드는 값 객체를 입력받아 name 프로퍼티에 액세스합니다. 이 기법은 개인 정보를 비공개로 유지하고, 관련 정보가 파괴될 때마다 정보도 파괴됩니다.

Weak Map 사용 및 제한 사항

Weak Map 또는 일반 Map을 사용할지 여부를 결정할 때 고려해야 할 기본 결정은 객체 키만 사용할지 여부입니다. 언제든지 객체 키만 사용하려고 할 때 가장 좋은 선택은 Weak map입니다. 이렇게하면 여분의 데이터는 더이상 유지되지 않고 액세스할 수 없어 메모리 사용을 최적화하고 메모리 누수를 피할 수 있습니다.

Weak Map은 내용을 거의 볼 수 없으므로 forEach() 메서드, size 프로퍼티 또는 clear() 메서드를 사용하여 항목을 관리할 수 없습니다. 몇 가지 검사 기능이 필요한 경우 일반 Map을 사용하는 것이 좋습니다. 메모리 사용을 주시하십시오.

물론 객체가 아닌 키만 사용하려는 경우 일반 Map만 선택할 수 있습니다.

요약

ECMAScript 6은 정식으로 SetMap을 JavaScript에 도입했습니다. 이전에는 개발자가 객체를 사용하여 SetMap을 모방하는 경우가 많았습니다. 하지만 객체 프로퍼티와 관련된 제한으로 인해 종종 문제가 발생했습니다.

Set은 고유한 값의 정렬된 목록입니다. 같은지 여부를 판별하는 값을 가지고 있지 않습니다. Set는 중복값을 자동으로 제거하므로 Set을 사용하여 Array의 중복된 값을 필터링하고 결과를 반환할 수 있습니다. SetArray의 하위 클래스가 아니므로 Set의 값에 임의로 액세스할 수는 없습니다. 대신 has() 메서드를 사용하여 값이 Set에 포함되어 있는지 확인하고 Set의 개수를 알려주는 size 프로퍼티를 사용할 수 있습니다. Set 타입은 각 값을 처리하기 위한 forEach() 메서드를 가지고 있습니다.

Weak Set은 객체만 포함할 수있는 특수 Set입니다. 해당 객체는 약한 참조로 저장됩니다. 즉, Weak Set의 항목은 해당 항목이 객체에 대한 유일한 참조인 경우 가비지 컬렉트 대상이 됩니다. Weak Set 내용은 메모리 관리의 복잡성 때문에 검사할 수 없으므로 함께 그룹화 해야하는 객체 추적에만 Weak Set를 사용하는 것이 가장 좋습니다.

Map은 정렬된 키-값 쌍입니다. 여기서 키는 모든 데이터 타입이 될 수 있습니다. Set과 마찬가지로 키는 같음여부를 판별하기 위해 강제변환이 실행되지 않으므로 숫자 키 5와 문자열 "5"를 별개의 두 개의 키로 가질 수 있습니다. 모든 데이터 타입의 값은 set() 메서드를 사용하여 키의 값이 될수 있으며, 그 값은 나중에 get() 메소드를 사용하여 검색할 수 있습니다. Map에는 size 프로퍼티와 forEach() 메서드가 있어 보다 쉽게 항목에 액세스할 수 있습니다.

Weak Map은 객체 키만 가질 수있는 특별한 타입의 Map입니다. Weak Set과 마찬가지로 객체 키 참조는 약하며 객체에 대한 유일하게 남아있는 참조인 경우 가비지 컬렉트 대상이 됩니다. 키가 가비지 컬렉트되면 키와 연관된 값도 Weak Map에서 제거됩니다. 이 메모리 관리 측면은 추가 정보를 액세스하는 코드 외부에서 라이프사이클이 관리되는 구조에 Weak Map이 적합합니다.


이 내용은 나중에 참고하기 위해 제가 공부하며 정리한 내용입니다.
의역, 오역, 직역이 있을 수 있음을 알려드립니다.
This post is a translation of this original article [https://leanpub.com/understandinges6/read#leanpub-auto-sets-and-maps]

참고

공유하기