This is the second article in my DOM-handling series.
If you haven’t read the first part – Which Language Handles DOM-Like Models Best? – you might want to start there for context.
JavaScript’s garbage collection frees you from explicit memory frees, but it won’t save you from “dangling” references to semi-destroyed objects, multiparenting, or topology corruption in complex object graphs.
Building a DOM-ish model (Document → Card → CardItem
) with predictable behavior means making manual decisions about ownership, weak references, and deep copying.
Design Decisions
Full JS solution (~330 LOC) here: 🔗 https://jsfiddle.net/0tv5yj37/4/
Ownership and Reference Tracking
-
Parent pointers (
parent
field) enforce single ownership of a node at runtime. -
Registration sets (
inboundButtons
,inboundConnectors
) simulate weak references:- When an object is detached, it actively clears inbound links.
- This prevents dangling references after deletion.
-
Detachment cascade ensures that deleting a node cleans up all references to it before it’s gone.
Handling / Avoidance of Shared Mutable State
- Shared
Style
andBitmap
instances are protected withObject.freeze()
to enforce immutability at runtime. - Copy-on-write is achieved via
mutateStyle()
– editing a shared resource automatically clones it. - No shared mutable structures exist in the graph; all shared data is immutable.
Trade-Offs
-
Manual enforcement: Because JavaScript won’t stop multiparenting or cycles, we must check on every
addItem()
andsetParent()
. - Two-phase deep copy: Required to preserve shared topology while creating new mutable nodes.
-
Cognitive overhead: The model spans ~330 LOC, much of it boilerplate for
deepCopy()
andresolve()
methods.
Safety Guarantees
From the Language
- Garbage collection eliminates leaks from unreferenced objects (eventually).
- No raw memory access, so undefined behavior from freed memory is impossible.
From the Design
-
Ghosting prevention via explicit unlinking (
detach()
): On object removal, cross-references are broken to ensure no part of the remaining object hierarchy can continue accessing the removed object in a “post-detached” non-working state. -
Runtime safety: Invariant checks (
Card already has a parent
,Loop detected
) ensure structural integrity. - Topology safety: Connectors and Buttons x-refs are nulled on detach and rewired on copy.
Code Size & Cognitive Overhead
- Size: ~330 LOC, with most complexity in repetitive deep copy/resolve patterns.
-
Cognitive load: Developers must understand the lifecycle of every object and call
detach()
correctly; missing one leads to “ghost” references until GC eventually runs.
Usage
//// This code throws exception - style is immutable
// doc.cards[0].items[0].style.font = "Helvetica";
// unshare on mutate:
myText.mutateStyle(style => {
style.font = "Helvetica";
});
// Drop X-refs
doc.cards[0].removeItem(doc.cards[0].items[0]);
console.log("after item deleted and link lost", doc);
// Result shows that "hello" text is removed both from card and connector.
// Cross-references are explicitly broken to prevent ghosting -
// no remaining part of the object hierarchy can access the removed item.
// Copy
const newDoc = deepCopy(doc);
console.log("after copy, preserving button and connector links", newDoc);
// Result: newDoc is structurally and data-wise identical to doc,
// though consisting of newly allocated objects.
// Safety net against multiparenting and cycles:
// The following code successfully compiles but crashes
// because of multiparenting attempt
doc.addCard(newDoc.cards[0]);
// This code compiles and runs
// because copy operation prevents multiparenting
doc.addCard(deepCopy(newDoc.cards[0]));
Evaluation Table
Criterion | Description | Verdict |
---|---|---|
Memory safety | Avoids unsafe access patterns | ⚠️ Safe from UAF/null deref by design; GC handles deallocation, though stack references can see partially destructed (detached) objects |
Leak prevention | Avoids memory leaks | ⚠️ GC works eventually, with memory overhead and causes sudden pauses |
Ownership clarity | Are ownership relations clear and enforced? | ⚠️ Implemented in code, enforced only at runtime via exceptions |
Copy semantics | Are copy/clone operations predictable and correct? | ⚠️ Manual |
Weaks handling | Survives partial deletions and dangling refs? | ⚠️ Manual, via registration sets and detach cascade; detached objects remain visible to stack references but in a post-detached, non-working state |
Runtime resilience | Can DOM ops crash app? | ⚠️ Yes, if invariants violated (multiparent) – throws exception |
Code expressiveness | Concise and maintainable? | ⚠️ Verbose; every class needs boilerplate deep copy and detach logic |
Ergonomic trade-offs | Difficulty of enforcing invariants? | ❌ High; language offers no compile-time guarantees |
Verdict
JavaScript can host a reasonably safe DOM-like graph if:
- You manually enforce ownership,
- Treat “weak” links as opt-in and track them explicitly,
- Accept that deep copy logic will be verbose,
- And rely on runtime exceptions rather than compiler errors.
It’s not fully safe, but it’s miles better than a manual memory handling.
These conclusions can be applied to all GC-driven languages, which is why I won’t test Java, Kotlin, C#, Python, Dart, Go, or TypeScript – although I welcome any examples in these languages proving that they have extra features helping with DOM handling.
Next stop – C++