가비지 수집 장에서 알 수 있듯이 JavaScript 엔진은 값이 "접근 가능"하고 잠재적으로 사용될 수 있는 동안 메모리에 값을 유지합니다.
예를 들어:
let john = { 이름: "John" }; // 객체에 접근할 수 있으며, john은 객체에 대한 참조입니다. // 참조 덮어쓰기 존 = 널; // 객체가 메모리에서 제거됩니다.
일반적으로 객체의 속성이나 배열 요소 또는 다른 데이터 구조는 도달 가능한 것으로 간주되며 해당 데이터 구조가 메모리에 있는 동안 메모리에 유지됩니다.
예를 들어, 객체를 배열에 넣으면 배열이 살아있는 동안 다른 참조가 없더라도 객체도 살아 있을 것입니다.
이와 같이:
let john = { 이름: "John" }; let array = [ 존 ]; 존 = 널; // 참조 덮어쓰기 // 이전에 john이 참조한 객체는 배열 내부에 저장됩니다. // 따라서 가비지 수집되지 않습니다. // 배열[0]으로 얻을 수 있습니다.
이와 유사하게, 일반 Map
에서 객체를 키로 사용하면 Map
이 존재하는 동안 해당 객체도 존재합니다. 메모리를 차지하며 가비지 수집되지 않을 수 있습니다.
예를 들어:
let john = { 이름: "John" }; let map = new Map(); map.set(존, "..."); 존 = 널; // 참조 덮어쓰기 // John은 맵 내부에 저장됩니다. // map.keys()를 사용하여 얻을 수 있습니다.
WeakMap
이 측면에서 근본적으로 다릅니다. 주요 객체의 가비지 수집을 방지하지 않습니다.
예제를 통해 이것이 무엇을 의미하는지 살펴보겠습니다.
Map
과 WeakMap
의 첫 번째 차이점은 키가 기본 값이 아닌 객체여야 한다는 것입니다.
let WeakMap = new WeakMap(); obj = {}로 놔두세요; WeakMap.set(obj, "확인"); // 잘 작동합니다(객체 키) // 문자열을 키로 사용할 수 없습니다. WeakMap.set("test", "으악"); // 오류입니다. "test"는 객체가 아니기 때문입니다.
이제 객체를 키로 사용하고 해당 객체에 대한 다른 참조가 없으면 해당 객체는 메모리(및 맵)에서 자동으로 제거됩니다.
let john = { 이름: "John" }; let WeakMap = new WeakMap(); weakMap.set(존, "..."); 존 = 널; // 참조 덮어쓰기 // John이 메모리에서 제거되었습니다!
위의 일반 Map
예시와 비교해 보세요. 이제 john
WeakMap
의 키로만 존재한다면 맵(및 메모리)에서 자동으로 삭제됩니다.
WeakMap
반복 및 keys()
, values()
, entries()
메서드를 지원하지 않으므로 여기에서 모든 키나 값을 가져올 수 있는 방법이 없습니다.
WeakMap
에는 다음 메서드만 있습니다.
weakMap.set(key, value)
weakMap.get(key)
weakMap.delete(key)
weakMap.has(key)
왜 그런 제한이 있습니까? 기술적인 이유 때문입니다. 객체가 다른 모든 참조(위 코드의 john
과 같은)를 잃은 경우 해당 객체는 자동으로 가비지 수집됩니다. 그러나 기술적으로 정리가 언제 발생하는지 정확하게 지정되지는 않습니다.
JavaScript 엔진이 이를 결정합니다. 즉시 메모리 정리를 수행하거나 더 많은 삭제가 발생하면 나중에 정리를 수행하도록 선택할 수 있습니다. 따라서 기술적으로 WeakMap
의 현재 요소 수는 알려져 있지 않습니다. 엔진이 청소했거나 청소하지 않았거나 부분적으로 청소했을 수 있습니다. 이러한 이유로 모든 키/값에 액세스하는 메서드는 지원되지 않습니다.
이제 그러한 데이터 구조가 어디에 필요합니까?
WeakMap
의 주요 적용 영역은 추가 데이터 저장 입니다.
다른 코드, 어쩌면 제3자 라이브러리에 "속해 있는" 객체로 작업하고 그 객체와 관련된 일부 데이터를 저장하려는 경우 해당 객체는 객체가 살아있는 동안에만 존재해야 합니다. 그러면 WeakMap
이 바로 그 것입니다. 필요합니다.
객체를 키로 사용하여 데이터를 WeakMap
에 저장하고, 객체가 가비지 수집되면 해당 데이터도 자동으로 사라집니다.
weakMap.set(john, "비밀 문서"); // 존이 죽으면 비밀문서는 자동으로 파기됩니다
예를 살펴보겠습니다.
예를 들어 사용자의 방문 횟수를 유지하는 코드가 있습니다. 정보는 지도에 저장됩니다. 사용자 개체가 키이고 방문 횟수가 값입니다. 사용자가 떠날 때(해당 개체가 가비지 수집됨) 우리는 더 이상 방문 횟수를 저장하고 싶지 않습니다.
다음은 Map
사용한 계산 함수의 예입니다.
// ? VisitsCount.js VisitsCountMap = new Map(); // 맵: 사용자 => 방문 횟수 // 방문수 증가 함수 countUser(사용자) { let count = VisitsCountMap.get(user) || 0; VisitsCountMap.set(사용자, 개수 + 1); }
그리고 여기에 코드의 또 다른 부분이 있습니다. 어쩌면 이를 사용하는 또 다른 파일이 있을 수도 있습니다.
// ? main.js let john = { 이름: "John" }; countUser(존); // 방문 횟수를 계산합니다. // 나중에 John이 우리를 떠납니다. 존 = 널;
이제 john
개체는 가비지 수집되어야 하지만 visitsCountMap
의 키이므로 메모리에 남아 있습니다.
사용자를 제거할 때 visitsCountMap
정리해야 합니다. 그렇지 않으면 메모리가 무한정 증가합니다. 복잡한 아키텍처에서는 이러한 청소가 지루한 작업이 될 수 있습니다.
대신 WeakMap
으로 전환하면 이를 방지할 수 있습니다.
// ? VisitsCount.js VisitsCountMap = new WeakMap(); // Weakmap: 사용자 => 방문 횟수 // 방문수 증가 함수 countUser(사용자) { let count = VisitsCountMap.get(user) || 0; VisitsCountMap.set(사용자, 개수 + 1); }
이제 visitsCountMap
정리할 필요가 없습니다. john
객체가 WeakMap
의 키를 제외한 모든 방법으로 접근할 수 없게 되면 WeakMap
의 해당 키에 대한 정보와 함께 메모리에서 제거됩니다.
또 다른 일반적인 예는 캐싱입니다. 함수의 결과를 저장(“캐시”)하여 향후 동일한 개체에 대한 호출이 이를 재사용할 수 있습니다.
이를 달성하기 위해 Map
(최적 시나리오 아님)을 사용할 수 있습니다.
// ? 캐시.js 캐시 = new Map(); // 결과를 계산하고 기억합니다. 함수 프로세스(obj) { if (!cache.has(obj)) { let result = /* 결과 계산 */ obj; 캐시.set(obj, 결과); 결과 반환; } 캐시를 반환합니다.get(obj); } // 이제 다른 파일에서 process()를 사용합니다: // ? main.js let obj = {/* 객체가 있다고 가정해 보겠습니다 */}; result1 = process(obj)라고 하자; // 계산됨 // ...나중에 코드의 다른 위치에서... result2 = process(obj)라고 하자; // 캐시에서 가져온 기억된 결과 // ...나중에 객체가 더 이상 필요하지 않을 때: 객체 = null; 경고(cache.size); // 1 (아야! 개체가 아직 캐시에 있어서 메모리를 차지하고 있습니다!)
동일한 객체를 사용하여 process(obj)
여러 번 호출하는 경우 처음에만 결과를 계산한 다음 cache
에서 가져옵니다. 단점은 객체가 더 이상 필요하지 않을 때 cache
정리해야 한다는 것입니다.
Map
WeakMap
으로 바꾸면 이 문제는 사라집니다. 캐시된 결과는 개체가 가비지 수집된 후 자동으로 메모리에서 제거됩니다.
// ? 캐시.js 캐시 = new WeakMap(); // 결과를 계산하고 기억합니다. 함수 프로세스(obj) { if (!cache.has(obj)) { let result = /* 결과를 계산합니다 */ obj; 캐시.set(obj, 결과); 결과 반환; } 캐시를 반환합니다.get(obj); } // ? main.js let obj = {/* 일부 객체 */}; result1 = process(obj)라고 하자; 결과2 = 프로세스(obj); // ...나중에 객체가 더 이상 필요하지 않을 때: 객체 = null; // WeakMap이므로 캐시 크기를 가져올 수 없습니다. // 하지만 0이거나 곧 0이 됩니다. // obj가 가비지 수집되면 캐시된 데이터도 제거됩니다.
WeakSet
비슷하게 동작합니다:
이는 Set
과 유사하지만 WeakSet
에만 개체를 추가할 수 있습니다(기본 요소는 아님).
개체는 다른 곳에서 접근할 수 있는 동안 집합에 존재합니다.
Set
과 마찬가지로 add
, has
및 delete
지원하지만 size
, keys()
지원하지 않으며 반복은 지원하지 않습니다.
"약함"이므로 추가 저장 장치 역할도 합니다. 그러나 임의의 데이터가 아니라 "예/아니요" 사실에 대한 것입니다. WeakSet
의 멤버십은 객체에 대한 의미를 가질 수 있습니다.
예를 들어 WeakSet
에 사용자를 추가하여 사이트를 방문한 사람들을 추적할 수 있습니다.
VisitedSet = new WeakSet()을 허용합니다. let john = { 이름: "John" }; let pete = { 이름: "피트" }; let mary = { 이름: "메리" }; VisitedSet.add(존); // John이 우리를 방문했습니다. VisitedSet.add(피트); // 그러면 피트 VisitedSet.add(존); // 다시 존 // VisitedSet에는 이제 2명의 사용자가 있습니다. // John이 방문했는지 확인합니까? 경고(visitedSet.has(john)); // 진실 // Mary가 방문했는지 확인합니까? 경고(visitedSet.has(mary)); // 거짓 존 = 널; // VisitedSet이 자동으로 정리됩니다.
WeakMap
및 WeakSet
의 가장 눈에 띄는 제한 사항은 반복이 없고 현재 콘텐츠를 모두 가져올 수 없다는 것입니다. 불편해 보일 수 있지만 WeakMap/WeakSet
주요 작업을 수행하는 것을 방해하지는 않습니다. 즉, 다른 장소에 저장/관리되는 객체에 대한 데이터의 "추가" 저장소가 됩니다.
WeakMap
은 객체만 키로 허용하고 다른 방법으로 액세스할 수 없게 되면 연관된 값과 함께 객체를 제거하는 Map
과 유사한 컬렉션입니다.
WeakSet
객체만 저장하고 다른 방법으로 액세스할 수 없게 되면 객체를 제거하는 Set
과 유사한 컬렉션입니다.
주요 장점은 객체에 대한 참조가 약하기 때문에 가비지 수집기에 의해 쉽게 제거될 수 있다는 것입니다.
이는 clear
, size
, keys
, values
를 지원하지 않는 대가로 발생합니다.
WeakMap
과 WeakSet
은 "1차" 객체 저장소 외에 "2차" 데이터 구조로 사용됩니다. 객체가 기본 저장소에서 제거되면 WeakMap
또는 WeakSet
의 키로만 발견되면 자동으로 정리됩니다.
중요도: 5
일련의 메시지가 있습니다.
메시지를 보자 = [ {텍스트: "안녕하세요", 보낸 사람: "John"}, {텍스트: "어떻게 지내세요?", 보낸 사람: "John"}, {텍스트: "곧 만나요", 보낸 사람: "앨리스"} ];
귀하의 코드는 여기에 액세스할 수 있지만 메시지는 다른 사람의 코드에 의해 관리됩니다. 새 메시지가 추가되고, 오래된 메시지는 해당 코드에 의해 정기적으로 제거되며, 해당 메시지가 발생하는 정확한 순간을 알 수 없습니다.
이제 메시지를 읽었는지 여부에 대한 정보를 저장하는 데 사용할 수 있는 데이터 구조는 무엇입니까? 구조는 "읽었나요?"라는 대답을 제공하는 데 적합해야 합니다. 주어진 메시지 객체에 대해.
PS 메시지가 messages
에서 제거되면 구조에서도 사라져야 합니다.
PPS 메시지 객체를 수정해서는 안 되며, 속성을 추가해야 합니다. 다른 사람의 코드로 관리되기 때문에 나쁜 결과를 초래할 수 있습니다.
읽은 메시지를 WeakSet
에 저장해 보겠습니다.
메시지를 보자 = [ {텍스트: "안녕하세요", 보낸 사람: "John"}, {텍스트: "어떻게 지내세요?", 보낸 사람: "John"}, {텍스트: "곧 만나요", 보낸 사람: "앨리스"} ]; readMessages = new WeakSet(); // 두 개의 메시지를 읽었습니다. readMessages.add(메시지[0]); readMessages.add(메시지[1]); // readMessages에는 2개의 요소가 있습니다. // ...첫 번째 메시지를 다시 읽어보겠습니다! readMessages.add(메시지[0]); // readMessages에는 여전히 2개의 고유 요소가 있습니다. // 대답: 메시지[0]를 읽었나요? Alert("메시지 0 읽기: " + readMessages.has(messages[0])); // 진실 message.shift(); // 이제 readMessages에는 1개의 요소가 있습니다(기술적으로 메모리는 나중에 정리될 수 있습니다).
WeakSet
사용하면 메시지 세트를 저장하고 그 안에 메시지가 있는지 쉽게 확인할 수 있습니다.
자동으로 정리됩니다. 단점은 반복할 수 없고 "읽은 모든 메시지"를 직접 얻을 수 없다는 것입니다. 하지만 모든 메시지를 반복하고 세트에 있는 메시지를 필터링하면 이를 수행할 수 있습니다.
또 다른 해결책은 메시지를 읽은 후 message.isRead=true
와 같은 속성을 메시지에 추가하는 것입니다. 메시지 객체는 다른 코드에 의해 관리되므로 일반적으로 권장되지 않지만 충돌을 피하기 위해 기호 속성을 사용할 수 있습니다.
이와 같이:
// 기호 속성은 우리 코드에만 알려져 있습니다. let isRead = Symbol("isRead"); 메시지[0][isRead] = true;
이제 제3자 코드는 아마도 우리의 추가 속성을 볼 수 없을 것입니다.
기호를 사용하면 문제 발생 가능성을 낮출 수 있지만 구조적 관점에서는 WeakSet
사용하는 것이 더 좋습니다.
중요도: 5
이전 작업과 마찬가지로 일련의 메시지가 있습니다. 상황은 비슷합니다.
메시지를 보자 = [ {텍스트: "안녕하세요", 보낸 사람: "John"}, {텍스트: "어떻게 지내세요?", 보낸 사람: "John"}, {텍스트: "곧 만나요", 보낸 사람: "앨리스"} ];
이제 문제는 "메시지를 읽었을 때" 정보를 저장하기 위해 어떤 데이터 구조를 제안할 것인가입니다.
이전 작업에서는 "예/아니요" 사실만 저장하면 되었습니다. 이제 날짜를 저장해야 하며 메시지가 가비지 수집될 때까지만 메모리에 남아 있어야 합니다.
PS 날짜는 내장 Date
클래스의 객체로 저장될 수 있으며 이에 대해서는 나중에 다루겠습니다.
날짜를 저장하려면 WeakMap
사용할 수 있습니다.
메시지를 보자 = [ {텍스트: "안녕하세요", 보낸 사람: "John"}, {텍스트: "어떻게 지내세요?", 보낸 사람: "John"}, {텍스트: "곧 만나요", 보낸 사람: "앨리스"} ]; readMap = new WeakMap(); readMap.set(messages[0], new Date(2017, 1, 1)); // 나중에 공부할 날짜 객체