Я полагаю, что многие читатели знакомы с картой, представленной в ES6, и некоторые из них, возможно, слышали о WeakMap. Что такое WeakMap Что такое WeakMap? Имея в виду этот вопрос, эта статья подробно познакомит вас со знанием WeakMap в следующих аспектах.
1. Что такое сборка мусора
В компьютерных науках сборка мусора (сокращенно GC) относится к механизму автоматического управления памятью. Когда часть пространства памяти, занятого программой, больше не используется программой, программа возвращает эту часть пространства памяти операционной системе с помощью алгоритма сборки мусора.Мусорные коллекторы могут снизить бремя программистов и уменьшить ошибки в программе.
Сборка мусора возникла на языке LISP и имеет два основных принципа:
- Учтите, что объект не будет доступен в будущих запусках программы;
- Память, занимаемая этими объектами, освобождается.
В JavaScript есть механизм автоматической сборки мусора.Принцип работы этого механизма сборки мусора на самом деле очень прост: узнать те переменные, которые больше не используются, а затем освободить занимаемую ими память.Сборщик мусора будет выполнять это периодически через равные промежутки времени. работать.
(Изображение предоставлено: Сбор мусора: V8’s Orinoco)
Локальные переменные существуют только в процессе выполнения функции, в этом процессе обычно выделяется место для локальных переменных в памяти стека, а затем эти переменные используются в функции до окончания выполнения функции. Сборщик мусора должен отслеживать использование каждой переменной и помечать те переменные, которые больше не используются, чтобы память, занимаемая ими, могла быть вовремя освобождена в будущем.Стратегии, используемые для выявления бесполезных переменных, в основном включают метод подсчета ссылок и метод удаления метки.
1.1 Метод подсчета ссылок
Самый ранний и самый простой способ реализации сборки мусора.Этот метод прикрепляет счетчик к объекту, занимающему физическое пространство.Когда другие объекты ссылаются на этот объект, счетчик увеличивается на единицу, и наоборот, когда ссылка освобождается. Этот алгоритм периодически проверяет счетчик объектов, которые не были освобождены, и если он равен нулю, то физическое пространство, занимаемое им, освобождается, потому что объект в это время уже недоступен.
Метод подсчета ссылок относительно прост в реализации, но он не может восстанавливать объекты хранения циклических ссылок, такие как:
function f() {
var o1 = {};
var o2 = {};
o1.p = o2; // o1引用o2
o2.p = o1; // o2引用o1
}
f();
Чтобы решить эту проблему, сборщик мусора ввел метод пометки-очистки.
1.2 Метод пометки и развертки
Метод маркировки и очистки в основном делит процесс сборки мусора GC на две фазы: фазу маркировки и фазу очистки:
- Фаза маркировки: отметить все активные объекты;
- Фаза очистки: Уничтожайте непомеченные (то есть неактивные объекты).
Наиболее часто используемый метод сборки мусора в JavaScript — пометка и очистка.Когда переменная входит в среду, переменная помечается как «входящая в среду», а когда переменная покидает среду, она помечается как «выходящая из среды». . . .
Конкретный процесс сборки мусора методом пометки и очистки показан на следующем рисунке:
(Изображение предоставлено: Как работает JavaScript: управление памятью + как справиться с 4 распространенными утечками памяти)
В повседневной работе для объектов, которые больше не используются, мы обычно хотим, чтобы они были собраны сборщиком мусора. На этом этапе вы можете использоватьnull
перезаписать ссылку на соответствующий объект, например:
let sem = { name: "Semlinker" };
// 该对象能被访问,sem是它的引用
sem = null; // 覆盖引用
// 该对象将会被从内存中清除
Однако, когда структуры данных, такие как объекты и массивы, находятся в памяти, их дочерние элементы, такие как свойства объекта и элементы массива, доступны. Например, если вы помещаете объект в массив, этот объект существует до тех пор, пока существует массив, даже если на этот объект нет других ссылок. Например:
let sem = { name: "Semlinker" };
let array = [ sem ];
sem = null; // 覆盖引用
// sem 被存储在数组里, 所以它不会被垃圾回收机制回收
// 我们可以通过 array[0] 来获取它
Точно так же, если мы используем объекты как обычныеMap
ключ, то когдаMap
Когда он существует, объект также будет существовать. Он займет память и не будет утилизирован механизмом сборки мусора. Например:
let sem = { name: "Semlinker" };
let map = new Map();
map.set(sem, "全栈修仙之路");
sem = null; // 覆盖引用
// sem被存储在map中
// 我们可以使用map.keys()来获取它
Итак, как решить проблему сбора мусора на приведенной выше карте? На данный момент нам нужно узнать о WeakMap.
2. Зачем нам WeakMap?
2.1 Разница между Map и WeakMap
Я думаю, что многим читателям не привыкать к Map в ES6, Map уже есть, зачем WeakMap и в чем между ними разница? Основное различие между Map и WeakMap:
- Ключи объектов Map могут быть любого типа, но ключи в объектах WeakMap могут быть только ссылками на объекты;
- WeakMap не может содержать объекты, на которые нет ссылок, иначе он будет автоматически удален из коллекции (механизм сборки мусора);
- Объекты WeakMap не являются перечисляемыми и не могут получить размер коллекции.
В JavaScript Map API можно реализовать, заставив четыре его API-метода совместно использовать два массива (один для ключей и один для значений). Установка значения для такой карты добавляет как ключ, так и значение в конец обоих массивов. Таким образом, индекс ключа и значения соответствуют в двух массивах. При извлечении значений из этой Карты вам необходимо перебрать все ключи, а затем использовать индекс для извлечения соответствующего значения из массива сохраненных значений.
Однако у такой реализации есть два основных недостатка: во-первых, операции присваивания и поиска имеют временную сложность O(n) (n — количество пар ключ-значение), потому что обе операции должны пройти по всему массиву, чтобы найти соответствие.Другим недостатком является то, что это может привести к утечке памяти, потому что массив будет продолжать ссылаться на каждый ключ и значение.Такие ссылки делают невозможным их переработку алгоритмом сборки мусора, даже если других ссылок не существует.
Напротив, собственный WeakMap содержит «слабую ссылку» на каждый ключевой объект, что означает, что сборка мусора работает правильно, когда не существует других ссылок.Структура собственного WeakMap особенная и действительная, а ключ, используемый для сопоставления, действителен только в том случае, если он не используется повторно.
Из-за таких слабых ссылок,WeakMap
Ключи не перечислимы (нет метода, который дает все ключи). Если ключ является перечислимым, его список будет подвергнут сборке мусора, что приведет к неопределенным результатам. Поэтому, если вы хотите получить список ключевых значений для объектов этого типа, вы должны использоватьMap
. А если вы хотите добавить данные в объект, не вмешиваясь в механизм сборки мусора, вы можете использовать WeakMap.
Итак, для проблемы со сборкой мусора, с которой мы столкнулись ранее, мы можем использовать WeakMap для ее решения следующим образом:
let sem = { name: "Semlinker" };
let map = new WeakMap();
map.set(sem, "全栈修仙之路");
sem = null; // 覆盖引用
2.2 WeakMap и сборка мусора
Действительно ли WeakMap так волшебен, как его представили? Давайте проверим влияние Map и WeakMap на сборку мусора в том же сценарии. Сначала мы создаем два файла: map.js и weakmap.js.
map.js
//map.js
function usageSize() {
const used = process.memoryUsage().heapUsed;
return Math.round((used / 1024 / 1024) * 100) / 100 + "M";
}
global.gc();
console.log(usageSize()); // ≈ 3.19M
let arr = new Array(10 * 1024 * 1024);
const map = new Map();
map.set(arr, 1);
global.gc();
console.log(usageSize()); // ≈ 83.19M
arr = null;
global.gc();
console.log(usageSize()); // ≈ 83.2M
После создания map.js введите в командной строкеnode --expose-gc map.js
выполнение командыmap.js
код в , где--expose-gc
Параметр указывает, что механизм сборки мусора разрешено выполнять вручную.
weakmap.js
function usageSize() {
const used = process.memoryUsage().heapUsed;
return Math.round((used / 1024 / 1024) * 100) / 100 + "M";
}
global.gc();
console.log(usageSize()); // ≈ 3.19M
let arr = new Array(10 * 1024 * 1024);
const map = new WeakMap();
map.set(arr, 1);
global.gc();
console.log(usageSize()); // ≈ 83.2M
arr = null;
global.gc();
console.log(usageSize()); // ≈ 3.2M
Аналогично после создания weakmap.js введите в командной строкеnode --expose-gc weakmap.js
выполнение командыweakmap.js
код в . по сравнениюmap.js
а такжеweakmap.js
Выход , мы знаем, чтоweakmap.js
определено вarr
После очистки занимаемая им память кучи успешно освобождается сборщиком мусора.
Кратко проанализируем основные причины вышеуказанных различий:
дляmap.js
Другими словами, поскольку сильная ссылка на массив сохраняется и в arr, и в Map, простая очистка памяти переменной arr в Map не освобождает ее, потому что у Map все еще есть счетчик ссылок. В WeakMap его ключи являются слабыми ссылками и не учитываются в счетчике ссылок, поэтому при очистке arr массив будет очищен сборщиком мусора, поскольку счетчик ссылок равен 0.
Разобравшись с приведенным выше содержанием, давайте официально представим WeakMap.
3. Введение в WeakMap
Объект WeakMap — это набор пар ключ/значение, где на ключи слабо ссылаются.Ключ WeakMap может быть только типа Object. Примитивные типы данных нельзя использовать в качестве ключей (например, Symbol).
3.1 Синтаксис
new WeakMap([iterable])
iterable
: представляет собой массив (двоичный массив) или другой итерируемый объект, элементами которого являются пары ключ-значение. Каждая пара ключ-значение будет добавлена в новую WeakMap. null рассматривается как неопределенный.
3.2 Свойства
-
length
: значение атрибута равно 0; -
prototype
:WeakMap
Прототип конструктора. Позволяет добавлять свойства ко всем объектам WeakMap.
3.3 Методы
-
WeakMap.prototype.delete(key)
: удаляет связанный объект ключа. после казниWeakMap.prototype.has(key)
Вернуть ложь. -
WeakMap.prototype.get(key)
:вернутьkey
связанный объект илиundefined
(когда нет связанного с ключом объекта). -
WeakMap.prototype.has(key)
: возвращает логическое значение в зависимости от того, есть ли ключ, связанный с объектом. -
WeakMap.prototype.set(key, value)
: установить набор объектов ключевых ассоциаций в WeakMap, вернуть этоWeakMap
объект.
3.4 Пример
const wm1 = new WeakMap(),
wm2 = new WeakMap(),
wm3 = new WeakMap();
const o1 = {},
o2 = function(){},
o3 = window;
wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // value可以是任意值,包括一个对象或一个函数
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // 键和值可以是任意对象,甚至另外一个WeakMap对象
wm1.get(o2); // "azerty"
wm2.get(o2); // undefined,wm2中没有o2这个键
wm2.get(o3); // undefined,值就是undefined
wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (即使值是undefined)
wm3.set(o1, 37);
wm3.get(o1); // 37
wm1.has(o1); // true
wm1.delete(o1);
wm1.has(o1); // false
Познакомившись с базовыми знаниями о WeakMap, давайте познакомимся с приложением WeakMap.
4. Приложение WeakMap
4.1 Кэширование результатов расчета через WeakMap
С помощью WeakMap вы можете связать результаты предыдущих вычислений с объектами, не беспокоясь об управлении памятью. Следующие функцииcountOwnKeys()
пример: он кэширует предыдущие результаты в WeakMapcache
.
const cache = new WeakMap();
function countOwnKeys(obj) {
if (cache.has(obj)) {
return [cache.get(obj), 'cached'];
} else {
const count = Object.keys(obj).length;
cache.set(obj, count);
return [count, 'computed'];
}
}
созданныйcountOwnKeys
метод, давайте проверим его:
let obj = { name: "kakuqo", age: 30 };
console.log(countOwnKeys(obj));
// [2, 'computed']
console.log(countOwnKeys(obj));
// [2, 'cached']
obj = null; // 当对象不在使用时,设置为null
4.2 Храните личные данные в WeakMap
В следующем коде WeakMap_counter
а также_action
Используется для хранения значения виртуального свойства следующих экземпляров:
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}
}
созданныйCountdown
класс, давайте проверим это:
let invoked = false;
const countDown = new Countdown(3, () => invoked = true);
countDown.dec();
countDown.dec();
countDown.dec();
console.log(`invoked status: ${invoked}`)
Говоря о частных свойствах классов, нельзя не упомянутьECMAScript Private Fields.
5. Приватные поля ECMAScript
5.1 Введение в частные поля ES
Прежде чем представить закрытые поля ECMAScript, давайте взглянем на его «красоту»:
class Counter extends HTMLElement {
#x = 0;
clicked() {
this.#x++;
window.requestAnimationFrame(this.render.bind(this));
}
constructor() {
super();
this.onclick = this.clicked.bind(this);
}
connectedCallback() { this.render(); }
render() {
this.textContent = this.#x.toString();
}
}
window.customElements.define('num-counter', Counter);
первый взгляд#x
Вы чувствуете себя неловко, текущий комитет TC39 достиг консенсуса по этому вопросу, и предложение перешло на этап 3. Так зачем использовать#
символы, но не другие символы?
Комитет TC39 объяснил, что они также долго размышляли и выбрали символ # вместо ключевого слова private. Также обсуждается использование private с символом #. А также намерен зарезервировать ключевое слово @ в качестве защищенного свойства.
Источник от Большого Папочки:Почему частные свойства JavaScript используют символ #
Поддерживается начиная с TypeScript 3.8Частные поля ECMAScript, использовать следующим образом:
class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}
let semlinker = new Person("Semlinker");
semlinker.#name;
// ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.
с обычными свойствами (даже с использованиемprivate
свойства, объявленные модификатором), приватные поля учитывают следующие правила:
- Частные поля начинаются с
#
характер, иногда мы называем его частным именем; - Каждое имя частного поля уникально квалифицировано для содержащего его класса;
- Модификаторы доступности TypeScript (например, public или private) нельзя использовать в закрытых полях;
- К закрытым полям нельзя получить доступ за пределами содержащего класса или даже обнаружить.
Говоря об использовании#
Определенные частные поля сprivate
В чем разница между полями определения модификатора? Теперь давайте посмотрим на одинprivate
Пример:
class Person {
constructor(private name: string){}
}
let person = new Person("Semlinker");
console.log(person.name);
В приведенном выше коде мы создали класс Person, который используетprivate
модификатор определяет частное свойствоname
, затем используйте этот класс для созданияperson
объект, а затем передатьperson.name
посетитьperson
Закрытое свойство объекта, то компилятор TypeScript предложит следующее исключение:
Property 'name' is private and only accessible within class 'Person'.(2341)
Итак, как решить это исключение? Конечно, вы можете использовать утверждение типа для преобразования человека в любой тип:
console.log((person as any).name);
Таким образом, хотя запрос исключения компилятора TypeScript решен, мы все еще можем получить к нему доступ во время выполнения.Person
Частные свойства внутри класса, почему это происходит? Давайте посмотрим на скомпилированный код ES5, возможно, вы знаете ответ:
var Person = /** @class */ (function () {
function Person(name) {
this.name = name;
}
return Person;
}());
var person = new Person("Semlinker");
console.log(person.name);
В настоящее время я считаю, что некоторым небольшим партнерам будет любопытно, и они прошли TypeScript 3.8 и выше.#
Какой код будет сгенерирован после того, как будет скомпилировано приватное поле, определяемое числом:
class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}
Приведенный выше целевой код установлен на ES2015, который скомпилирует и сгенерирует следующий код:
"use strict";
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet)
|| function (receiver, privateMap, value) {
if (!privateMap.has(receiver)) {
throw new TypeError("attempted to set private field on non-instance");
}
privateMap.set(receiver, value);
return value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet)
|| function (receiver, privateMap) {
if (!privateMap.has(receiver)) {
throw new TypeError("attempted to get private field on non-instance");
}
return privateMap.get(receiver);
};
var _name;
class Person {
constructor(name) {
_name.set(this, void 0);
__classPrivateFieldSet(this, _name, name);
}
greet() {
console.log(`Hello, my name is ${__classPrivateFieldGet(this, _name)}!`);
}
}
_name = new WeakMap();
Соблюдая приведенный выше код, используйте#
Частные поля ECMAScript, определенныеWeakMap
объект для хранения, и компилятор сгенерирует__classPrivateFieldSet
а также__classPrivateFieldGet
Эти два метода используются для установки значения и получения значения. После представления соответствующего содержимого закрытых полей в одном классе давайте посмотрим на производительность закрытых полей в случае наследования.
5.2 Наследование частного поля ES
Чтобы сравнить разницу между обычными полями и приватными полями, давайте сначала посмотрим на производительность обычных полей при наследовании:
class C {
foo = 10;
cHelper() {
return this.foo;
}
}
class D extends C {
foo = 20;
dHelper() {
return this.foo;
}
}
let instance = new D();
// 'this.foo' refers to the same property on each instance.
console.log(instance.cHelper()); // prints '20'
console.log(instance.dHelper()); // prints '20'
Очевидно, определен ли он в вызывающем подклассеcHelper()
Метод по-прежнему определен в родительском классе.dHelper()
В конечном итоге метод находится в выходном подклассеfoo
Атрибуты. Далее давайте посмотрим, как приватные поля ведут себя при наследовании:
class C {
#foo = 10;
cHelper() {
return this.#foo;
}
}
class D extends C {
#foo = 20;
dHelper() {
return this.#foo;
}
}
let instance = new D();
// 'this.#foo' refers to a different field within each class.
console.log(instance.cHelper()); // prints '10'
console.log(instance.dHelper()); // prints '20'
Наблюдая за приведенными выше результатами, мы можем знать, чтоcHelper()
Методы иdHelper()
в методеthis.#foo
указывает на разные поля в каждом классе. Что касается другого содержимого закрытых полей ECMAScript, мы больше не будем расширяться, и заинтересованные читатели могут самостоятельно ознакомиться с соответствующими материалами.
6. Резюме
Эта статья в основном знакомит с ролью и сценариями применения WeakMap в JavaScript.Фактически, помимо WeakMap, есть еще WeakSet.Поскольку объект добавляется в WeakMap или WeakSet, сборщик мусора может высвободить занимаемую им память, когда срабатывает условие.
Но на самом деле WeakMap в JavaScript не совсем слабая ссылка: на самом деле она сильно ссылается на свое содержимое, пока ключ еще жив. WeakMap слабо ссылается на свое содержимое только после того, как ключ был удален сборщиком мусора. Чтобы обеспечить настоящие слабые ссылки, TC39 предложил предложение WeakRefs.
WeakRef — это API более высокого уровня, который обеспечиваетистинная слабая ссылкаи вставляет окно во время жизни объекта. В то же время это также может решить проблему, заключающуюся в том, что WeakMap поддерживает только тип объекта в качестве ключа.
7. Справочные ресурсы
- MDN - WeakMap
- exploringjs - ch_weakmaps
- typescriptlang - ecmascript-private-fields
- what-are-the-actual-uses-of-es6-weakmap
- Сборка мусора JavaScript
- What's New in JavaScript
- Простое понимание механизма сборки мусора JavaScript
- javascript.info - weakmap-weakset
- Почему частные свойства JavaScript используют символ #