ХабрПостыНовостиМегапосты?

Как JavaScript справляется с DOM-подобными структурами данных

Это вторая статья в серии, посвященной сравнению языков программирования по необычному критерию — как они справляются с организацией DOM-подобных структур данных.


Вводная часть тут: Настоящий тест для языков программирования. В ней рассказывается:

Сегодня мы рассмотрим как JavaScript справляется с задачей Card DOM.

Код

Полный пример кода слишком большой (~330 строк), чтобы опубликовать его тут.
Он доступен на JSFiddle:https://jsfiddle.net/bq5gah7r/1/.

Реализация Card DOM показала ряд сильных и слабых сторон JavaScript применительно к DOM-подобным структурам данных.

Где JS хорош из коробки

Где JS не справляется

Особенности реализации CardDOM с учетом ограничений

Что не получилось

Примеры использования

Создание документа

const doc = new Document();
const normal = makeSharedResource(new Style("Times", 16.5, 600));
const card = new Card();
doc.addCard(card);
const helloText = new TextItem("Hello", normal);
card.addItem(helloText);
const buttonOk = new ButtonItem("Click me", card);
card.addItem(buttonOk);
card.addItem(new ConnectorItem(helloText, buttonOk));
Новый документ
Новый документ

Копирование

const newDoc = deepCopy(doc);
// Неизменяемые ресурсы - шарятся,
// Перекрестные ссылки - сохраняют топологию
// Владеющие - определяют дерево и задают область копирования.
Результат копирования
Результат копирования

Стили: копирование при изменениях

// Стиль нельзя изменять - он замороженный
// doc.cards[0].items[0].style.font = "Helvetica";  // Throws: Immutable!

// Но можно изменять его копию.
doc.cards[0].items[0].mutateStyle(style => {
  style.font = "Helvetica";
});

Вставка с проверками на циклы и multiparenting

// Детектирование циклов
const group = new GroupItem();
const subgroup = new GroupItem();
group.addItem(subgroup);
subgroup.addItem(group);  // Throws: Loop detected!
// Нельзя вставлять один элемент в два места дерева
// doc.addCard(newDoc.cards[0]);  // Throws: Multiparenting attempt!

// Но копию вставлять можно:
doc.addCard(deepCopy(newDoc.cards[0]));

Удаление элемента с обрывом перекрестных связей

newDoc.cards[0].removeItem(newDoc.cards[0].items[0]);
// "Hello" удалился из карточки, и на него больше не ссылается коннектор.
Результат удаления: connector.from==null
Результат удаления: connector.from==null

Оценка: как JavaScript справился с DOM-подобными структурами

Критерий

Да

Но

Безопасность памяти

Безопасность памяти гарантирована: нет указателей на мусор, двойных освобождений и падений.

Однако после отсоединения узлов от DOM и выполнения detach() эти логически «уничтоженные» объекты могут оставаться достижимыми из стека, частично разрушенные с удаленными перекрестными ссылками.

Предотвращение утечек

Освобождением занимается GC.

GC = непредсказуемость моментов освобождения, отсутствие гарантии что любой конкретный объект будет удален раньше чем завершится программа, невозможность привязывать внешние ресурсы к времени жизни объектов (отсюда требование ручной очистки). Освобождение объектов привязано к достижимости по графу ссылок, что требует аккуратно следить, чтобы логически не-владеющие ссылки или аккуратно разрывались вручную или реализовывалось через WeakRef иначе будет утечка.

Ясность владения

—

Нет различий между агрегатами, композитами и не владеющими ссылками. Всё владение реализуется вручную — через подписки и очистку, проверки при вставке и ручное копирование. Частичные обходные решения есть (например, structuredClone с transfer), но они фрагментарны.

Копирование

—

Пишется вручную.

Слабые ссылки

Есть WeakRef.

WeakRef разрывается только при отложенном удалении объекта, когда/если до него доберется GC. А если нужно отписываться при логическом удалении объекта (а это почти всегда так) WeakRef бесполезен.

Устойчивость: Переживает ли система произвольные изменения?

Не падает.

Логическую целостность данных приходится обеспечивать вручную.

Выразительность: Можно ли это описать без боли и избыточности?

Использование Card DOM относительно просто, если вручную следить за вставкой, удалением и присваиванием перекрестных ссылок.

Реализация не декларативна, сложна и требует обширных тестов. Любое изменение структуры классов влечет переработку кода.

Момент обнаружения ошибок

—

Все ошибки целостности структур данных выявляются только во время исполнения, даже при использовании TypeScript. Тесты обязательны и обширны.

Итог: JavaScript справляется, но требует немалых усилий

JavaScript позволяет создать DOM-подобный граф, если:

Это решение лучше ручного управления памятью, но далеко от идеала.

Парадоксально, что сборщик мусора, несмотря на свою цену —

дает взамен сравнительно немного:

Эти выводы справедливы и для других языков со сборкой мусора: Java, Kotlin, C#, Python, Dart, Go, TypeScript. Все они сталкиваются с теми же ограничениями при работе со сложными изменяемыми графами объектов.

Следующий — C++.

@kotan-11
14.10.2025 21:01 UTC
Первоисточник

Комментарии

@vita55555
14.10.2025 16:51 UTC
+2

Приветствую,
ссылка на вводную статью правильная ?
https://jsfiddle.net/bq5gah7r/1/

14.10.2025 18:01 UTC
0

Огромное спасибо, исправил.