개요
-
object만 key로 사용 가능
String, Number, Symbol 등 다양한 프리미티브 타입의 key로 사용이 가능한 Map과 다르게 object만 key로 사용 가능합니다. value는 제한이 없습니다.
-
Map에서 key로 참조한 object를 삭제하면 object를 사용할 수 없게 되지만 Map에 object가 남습니다.
let sports = {like: "축구"}; --- (1)
const obj = new Map([
[sports, "like:축구"]
]);
sports = {like: "농구"}; --- (2)
- let sports = {like: "축구"};
오브젝트를 sports 변수에 할당
- const obj = new Map([[sports, "like:축구"]]);
Map 인스턴스를 생성하면서 sports를 key로 사용
- sports = {like: "농구"};
새 오브젝트를 만들어서 sports 변수에 할당
sports 변수가 참조하는 오브젝트의 메모리 주소가 변경
1번에서 2번으로 주소가 변경, 1번을 나중에 찾을 때 주소가 날라갔으므로 메모리 릭 발생
오브젝트를 삭제 했을 때 Map은 그대로 남아있지만, WeakMap은 Garbage Collection이 개발자가 지우지 않아도 알아서 WeakMap의 오브젝트를 같이 지웁니다. 따라서, 메모리 릭이 발생하지 않습니다.
- WeakMap 오브젝트 메소드
- set(), get(), has(), delete()
- CRUD와 관련된 메소드만 있습니다.
- WeakMap entry 열거 불가
- 이터레이션 불가 - next()로 엔트리 읽는 것 불가
new WeakMap()
- WeakMap 인스턴스 생성, 반환
- 파라미터 작성
- 대괄호[] 안에 이터러블 오브젝트 작성
const empty = new WeakMap();
const sports = {};
const obj = new WeakMap([
[sports, "sports 오브젝트"]
]);
console.log(typeof obj);
// object
WeakMap 오브젝트 구조
const map = Map;
const weakmap = WeakMap;
const sports = {};
const obj = new WeakMap([
[sports, "종목"]
]);
- map과 weakmap의 scope를 전개해 구조를 살펴보면 크게 다르지는 않습니다.
- Map 오브젝트에는 Symbol(Symbol.species)가 있지만 WeakMap 오브젝트에는 없습니다.
- constructor 오버라이드가 WeakMap에서는 불가능합니다.
- Symbol.iterator도 Map 오브젝트에는 있지만 WeakMap에는 없습니다.
- 이터레이션이 WeakMap에서는 불가능합니다.
- 그 밖에 entries, forEach, keys()등 메소드가 WeakMap에는 없습니다.
- obj를 scope를 전개해 보면 [[Entries]]가 있습니다
- 0: {Object => "종목"} 형태로 [sports, "종목"] 형태로 작성한 것을 인덱스를 부여하여 배열로 만들고 엘리먼트에 {Object => "종목"} 형태로 변환합니다. Map 인스턴스와 구조는 같고 안에 들어있는 메소드만 다릅니다.
WeakMap 오브젝트 메소드
4개의 메소드만 존재하며, 모든 메소드가 key를 사용합니다.
key는 반드시 object이며 전체를 열거하거나 반복하는 기능은 없습니다.
get()
- Map 인스턴스에서 key 값이 같은 value 반환
- key 값이 같지 않거나 타입이 다르면 undefined 반환
const fn = () => {};
const obj = new WeakMap([
[fn, "함수"]
]);
console.log(obj.get(fn));
// 함수
set()
- WeakMap 인스턴스에 key, value 설정
- 첫 번째 파라미터에 key로 사용할 오브젝트를 작성합니다.
- primitive value(String, Number, ...)는 사용 불가능합니다.
- 두 번째 파라미터는 값
- 첫 번째 파라미터의 오브젝트에 대한 값
- 오브젝트 구분 등의 용도이며 오브젝트에 따라 연동하는 함수 등록
const fn = function(){};
const obj = new WeakMap([
[fn, "함수"]
]);
console.log(obj.get(fn));
obj.set(fn, "함수 변경");
console.log(obj.get(fn));
// 함수
// 함수 변경
⇒ fn 메모리 주소가 key로 등록되어 있으면 같은 메모리 주소로 값을 설정하므로 [fn, "함수"]에서 "함수"가 "함수 변경"으로 변경됩니다.
has()
- WeakMap 인스턴스에서 key의 존재여부를 반환하며 존재할 경우 true 아니면 false를 반환합니다.
const obj = {};
const weakObj = new WeakMap([
[obj, "오브젝트"]
]);
console.log(weakObj.has(obj));
// true
delete()
- WeakMap 인스턴스에서 key와 일치하는 entry를 삭제합니다.
- 삭제를 성공하면 true 실패하면 false 반환
const fn = function(){};
const obj = new WeakMap([
[fn, "함수"]
]);
console.log(obj.delete(fn));
console.log(obj.has(fn));
// true
// false
가비지 컬렉션 처리
참조했던 object가 바뀌면 참조했던 오브젝트가 가비지 컬렉션(GC) 처리됩니다.
처리 과정
let obj = new WeakMap();
let sports = () => {point: 1};
obj.set(sports, "변경전");
sports = () => {point: 2};
obj.set(sports, "변경후");
setTimeout(function(){
console.log(obj.get(sports));
}, 2000);
-
let sports = () => {point: 1};
obj.set(sports, "변경전");
⇒ sports에 Function 오브젝트를 생성하여 할당하고 이것을 WeakMap 인스턴스에 key로 설정합니다.
-
sports = () =>{point: 2};
- 새로운 함수({point:2})를 생성 후 sports에 할당합니다.
- sports가 참조하는 메모리 주소가 변경됩니다.
- 메모리 주소가 바뀌면 앞에 sports에서 참조했던 오브젝트를 호출할 수 없게 됩니다.
- 이렇게 사용할 수 없게 된 {point: 1} 오브젝트는 GC 대상이 됩니다.
- 엔진이 주기적으로 GC 처리를 합니다.
-
obj.set(sports, "변경후");
- sports를 key로 하여 WeakMap에 설정합니다.
- 앞에서 sports를 key로 설정했기에 여기서 sports를 key로 하여 설정하면 값이 대체되야하지만
- 두 sports가 참조하는 메모리주소가 각각 다르기 때문에 sports가 추가됩니다. 2개가 WeakMap 인스턴스에 설정됩니다.
구조분석
- sports가 {point:2}로 새로운 오브젝트를 할당해서 sports가 참조하는 오브젝트가 바뀝니다.
- 변경 후 obj의 [[Entries]]를 펼쳐보면 0과 1 인덱스에 두 개의 형태가 등록되어있습니다. 변수값은 바뀌어도 하나지만 WeakMap 인스턴스에는 두 개가 있습니다.
- {point: 1}과 {point: 2}의 메모리 주소가 다르며
sports 변수는 사람이 보는 것으로 하나이지만, WeakMap은 값인 메모리 주소가 다르므로 각각 저장합니다.
- 그렇기에 sports로 저장하는게 아닌 인덱스를 부여하여 저장하는 것입니다.
- 엔진내부에서는 인덱스가 key이며 sports는 프로퍼티 value에서 프로퍼티 키가 됩니다.
- {point: 1}의 sports를 사용할 수 없으므로 GC가 {point: 1}의 sports를 메모리에서 지웁니다. 또한, obj의 "변경전"도 삭제합니다.
- 인덱스 1번이 0번이 됩니다.
Map과 WeakMap의 차이
- 참조하는 object를 삭제하면 Map은 그대로 갖고 있지만 WeakMap은 GC처리로 삭제됩니다.
- Map은 열거하거나 반복하는 처리가 가능하지만 WeakMap은 불가능합니다.
- GC 처리로 인해 엔트리가 유동적으로 변하기 때문에 WeakMap의 키로 설정된 object를 설정, 읽음, 삭제만 가능합니다.
- 따라서, 삭제나 변경될 가능성이 있는 경우에는 WeakMap을 사용해야합니다.
let mapObj = new Map();
(function(){
const obj = {key: "value"};
mapObj.set(obj, "Map");
}()); // ----- (1)
let weakObj = new WeakMap();
(function(){
const obj = {key: "value"};
weakObj.set(obj, "WeakMap");
}()); // ----- (2)
setTimeout(function() {
console.log(weakObj);
console.log(mapObj);
}, 1000);
즉시 실행함수는 일회용이기에 변수를 저장하지 않을 때 사용합니다. 그렇기에 함수가 끝나면 obj변수는 GC가 메모리에서 지웁니다.
(1): Map은 obj 변수가 함수 종료후 GC에 의해 지워지더라도 Map에 설정된 obj를 지우지 않고 유지합니다. 엔트리가 남아있습니다. 변수가 삭제되는 상황처럼 불일치가 발생할 때는 사용이 제한됩니다.
(2): WeakMap은 앞의 (1)과 동일하지만 Map이 아닌 WeakMap에 저장합니다. WeakMap은 obj 변수가 삭제되면 WeakMap에 설정된 obj를 삭제합니다. 엔트리가 없어집니다.
구조 분석
- setTimeout(function(){...});
GC가 수거대상을 수거하기 위해 즉시 실행되는게 아니기 때문에 setTimeout()으로 3초뒤 출력하도록 setTimeout()함수를 사용합니다.
- 전개구조를 보면 mapObj 인스턴스에는 key로 {key:"value"}가 등록되어 있습니다. 실제로 GC에 의해 obj는 수거되었지만 말이죠. 이는 메모리 릭(memory leak)을 유발하여 크리티컬할 수 있습니다.
- weakObj의 [[Entries]]를 전개해보면 No properties입니다. GC에서 obj({key: "value})를 수거할 때 weakMap의 해당 key도 지우기 때문입니다.
정리
Map은 기본적인 CRUD 메소드 뿐아니라 forEach, keys, values 등 반복 순회하는 이터레이션 메소드들 또한 연결되어있습니다.
Map의 경우 GC에 의해 key가 수거되거나 하지 않습니다. 그렇기 때문에 엔트리 사이즈가 정적이고 명시적으로만 변경할 수 있습니다
하지만, WeakMap은 CRUD 메소드만 연결되어있고 Key를 통해서만 접근할 수 있으며 열거할 수 없습니다.
WeakMap은 GC에 의해 엔트리가 유동적으로 변경될 수 있기 때문에 열거나 나열할 수 없습니다.