위크맵 (WeakMap
)
- 키를 메모리에 유지하지 않고 키와 관련된 값을 저장할 수 있다.
- 만일 키를 참조하는 것이 사라지면, 가비지 컬렉트 때 해당 키가 사라진다.
위크맵은 Iterable
이 아니다.
Map
은 이터러블이었지만,WeakMap
은 아니다.- 이러한 특성 때문에 키를 알아야만 값을 가져올 수 있다.
Iterator
를 제공하면 제공된Iterator
에 의해 키가 계속 참조되고, 영원히 가비지 컬렉트 될 수 없기 때문이다.
위크맵이 제공하는 메서드
has()
: 키의 존재 여부 반환get()
: 키에 대한 값 반환, 없다면undefined
delete()
: 키 삭제 성공하면true
실패하면false
WeakMap
은 약한 참조를 이용하는 만큼, 키를 모르면 참조할 수 없기 때문에 키에 접근하는size
,forEach
,keys
,values
등이 없다.
WeakMap
은 이름이 비슷하고 인터페이스가 비슷하지만Map
의 서브 클래스가 아니다.
위크맵의 용례
- 어떤 경우에 위크맵을 쓰면 좋을까?
private 한 정보를 보관할 때
- 아래 예제에서 private 한 정보를 보관하는
Map
으로 사용되었다. - ES2021 에서는 Private class feature 가 나와서 사실 이것보다 훨씬 간단한 문법으로 이를 수행할 수 있다.
const Example = (() => {
const privateMap = new WeakMap();
return class Example {
constructor() {
privateMap.set(this, 0);
}
incrementCounter() {
const result = privateMap.get(this) + 1;
privateMap.set(this, result);
return result;
}
showCounter() {
console.log(`Counter is ${privateMap.get(this)}`);
}
};
})();
const e1 = new Example(); // Example {}
e1.incrementCounter();
console.log(e1); // Example {}
const e2 = new Example();
e2.incrementCounter();
e2.incrementCounter();
e2.incrementCounter();
e1.showCounter(); // Counter is 1
e2.showCounter(); // Counter is 3
사실
Map
을 사용해도 비공개라는 속성 자체는 유지된다. 그러나, 계속 맵에 데이터가 쌓이고 쌓일수록 가비지 컬렉트 되지 않고 맵의 덩치가 커질 것이다.
제어할 수 없는 객체에 대한 정보 저장
- 이를테면, 브라우저의 DOM 객체를 키로 걸고 그에 대한 값을 저장해둘 수 있다.
- 이 DOM 객체는 사용자의 인터렉션을 통해 지워질 수 있는데, 지워지면
WeakMap
키의 레퍼런스도 날아가기 때문에 추후에 가비지컬렉트된다.
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Storing Data for DOM Elements</title>
</head>
<style>
.person {
cursor: pointer;
}
</style>
<body>
<label>
<div id="status"></div>
<div id="people"></div>
<div id="person"></div>
<script src="storing-data-for-dom.js"></script>
</body>
</html>
(async () => {
const statusDisplay = document.getElementById("status");
const personDisplay = document.getElementById("person");
try {
// The WeakMap that will hold the information related to our DOM elements
const personMap = new WeakMap();
await init();
async function init() {
const peopleList = document.getElementById("people");
const people = await getPeople();
// In this loop, we store the person that relates to each div in the
// WeakMap using the div as the key
for (const person of people) {
const personDiv = createPersonElement(person);
personMap.set(personDiv, person);
peopleList.appendChild(personDiv);
}
}
async function getPeople() {
// This is a stand-in for an operation that would fetch the person
// data from the server or similar
return [
{ name: "Joe Bloggs", position: "Front-End Developer" },
{ name: "Abha Patel", position: "Senior Software Architect" },
{ name: "Guo Wong", position: "Database Analyst" },
];
}
function createPersonElement(person) {
const div = document.createElement("div");
div.className = "person";
div.innerHTML =
'<a href="#show" class="remove">X</a> <span class="name"></span>';
div.querySelector("span").textContent = person.name;
div.querySelector("a").addEventListener("click", removePerson);
div.addEventListener("click", showPerson);
return div;
}
function stopEvent(e) {
e.preventDefault();
e.stopPropagation();
}
function showPerson(e) {
stopEvent(e);
// Here, we get the person to show by looking up the clicked element
// in the WeakMap
const person = personMap.get(this);
if (person) {
const { name, position } = person;
personDisplay.textContent = `${name}'s position is: ${position}`;
}
}
function removePerson(e) {
stopEvent(e);
this.closest("div").remove();
}
} catch (error) {
statusDisplay.innerHTML = `Error: ${error.message}`;
}
})();
키를 참조하는 값이 있다면 메모리 해제가 가능할까?
- 구현 스펙에 키로 사용되는 객체가 값에서 시작되는 경로로만 참조가 가능하다면, 가비지컬렉트 된다고 나와있다.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Values Referring Back to the Key</title>
</head>
<body>
<label>
Objects to create:
<input type="text" id="objects" value="100000" />
</label>
<input type="button" id="btn-create" value="Create" />
<input type="button" id="btn-release" value="Release" />
<script src="value-referring-to-key.js"></script>
</body>
</html>
const log = (msg) => {
const p = document.createElement("pre");
p.appendChild(document.createTextNode(msg));
document.body.appendChild(p);
};
const AAAAExample = (() => {
const privateMap = new WeakMap();
return class AAAAExample {
constructor(secret, limit) {
privateMap.set(this, { counter: 0, owner: this });
}
get counter() {
return privateMap.get(this).counter;
}
incrementCounter() {
return ++privateMap.get(this).counter;
}
};
})();
const e = new AAAAExample();
document.getElementById("btn-create").addEventListener("click", function (e) {
const counter = +document.getElementById("objects").value || 100000;
log(`Generating ${count} objects...`);
for (let n = count; n > 0; n--) {
a.push(new AAAAExample());
}
log(`Done, ${a.length} objects in the array`);
});
document.getElementById("btn-release").addEventListener("click", function (e) {
a.length = 0;
log("All objects released");
});
owner
에서this
를 이용해 자기 자신을 참조하고 있다.- 동작은 아주 간단하다.
Create
버튼을 누르면AAAAExample
클래스를 10만개 만든다.- 만들어진 10만개의
AAAAExample
을 배열a
에서 참조하도록 만든다.
- 만들어진 10만개의
Release
버튼을 누르면 배열a
의length
를0
으로 만들어서 참조를 끊는다.
Create
버튼을 눌렀을 때의 스냅샷이다.
Release
버튼을 눌렀을 때의 스냅샷이다.
Collect garbage
버튼을 누르면 더 빠르게 가비지 컬렉트를 시킬 수 있다.
위크셋, 위크세트 (WeakSet
)
WeakMap
의Set
버전이다.WeakMap
과 대부분의 특성이 동일하다.- 다만, 엔트리를 저장하는 것이 아니라 값을 저장한다.
용례
- 이전에 이 객체를 보았는지 확인할 때 유용하다.
일회성 토큰 예제
- 단 한번만 사용되어야 하는 일회성 토큰을 저장하는 경우, 해당 토큰이 이미 사용되었는지 판단해주는데에 유용하다.
- 만일 해당 토큰의 유효기간이 끝났다면 참조가 해제될테고 가비지컬렉트 될 것이다.
- 이 경우 다시 해당 토큰을 사용할 수 있게 되는 원리다.
const SingleUseObject = (() => {
const used = new WeakSet();
return class SingleUseObject {
constructor(name) {
this.name = name;
}
use() {
if (used.has(this)) {
throw new Error(`${this.name} has already been used`);
}
console.log(`Using ${this.name}`);
used.add(this);
}
};
})();
const obj1 = new SingleUseObject("hello");
const obj2 = new SingleUseObject("what");
obj1.use();
obj1.use();
출처 확인 예제
- 이 객체의 출처가 정확히 어느곳인지 확인하는데 사용할 수 있다.
- 물론 이 경우에도 해당 값에 대한 참조가 끊기면 자동으로 가비지 컬렉트 될 것이다.
const Thingy = (() => {
const known = new WeakSet();
let nextId = 1;
return class Thingy {
constructor(name) {
this.name = name;
this.id = nextId++;
Object.freeze(this);
known.add(this);
}
action() {
if (!known.has(this)) {
throw new Error("정상적인 방법으로 만들어진 Thingy 가 아닙니다.");
}
console.log(`Action on Thingy #${this.id} (${this.name})`);
}
};
})();
const t1 = new Thingy("t1");
t1.action();
const t2 = new Thingy("t2");
t2.action();
const faket2 = Object.create(Thingy.prototype);
faket2.name = "faket2";
faket2.id = 2;
Object.freeze(faket2);
faket2.action(); // 오직 생성자로 만든 Thingy 만 사용 가능.
반응형
'자바스크립트 > 모던 자바스크립트' 카테고리의 다른 글
모던 자바스크립트, 모듈 2 - 모듈의 동작 방식 (0) | 2023.03.26 |
---|---|
모던 자바스크립트, 모듈 1 - import 와 export 방식 (0) | 2023.03.26 |
모던 자바스크립트, 셋 혹은 세트 (Set) (0) | 2023.03.23 |
모던 자바스크립트, 맵 (Map) (0) | 2023.03.23 |
모던 자바스크립트, 4가지 동등성 비교 알고리즘 (0) | 2023.03.22 |