skip to content

You take a node out of a same-origin iframe with iframe.contentDocument.querySelector('.row') and append it into the parent page. What happens to the node's ownerDocument, and why can `row instanceof HTMLElement` be false in the parent page?

level: seniorimportance: nice to knowfreq 15%

answer

  1. two documents, two sets of constructors
  2. insertion fixes ownership for you
  3. move versus copy: adopt versus import
  4. the prototype chain does not travel
  5. test the shape, not the constructor

basics

~20 s

Inserting the node adopts it: the browser changes its ownerDocument to the parent page's document and the append succeeds. Adoption does not change the object's prototype, which still comes from the iframe's realm, so instanceof against the parent window's HTMLElement fails.

solid answer

~40 s

Every node records the document it was created in as `ownerDocument`. Modern browsers no longer throw when you insert a node from another document — the insertion algorithm **adopts** the node and its whole subtree first, so afterwards `row.ownerDocument === document` in the parent. You can do the same step explicitly with `document.adoptNode(node)`, which *moves* the node, or `document.importNode(node, true)`, which *copies* it and leaves the original in the iframe. What adoption does not touch is the prototype chain: the object was constructed by the iframe's JavaScript realm, so it is an instance of that window's `HTMLElement`, not the parent's, and cross-realm `instanceof` returns false. For nodes that may come from another document, test structurally — `node.nodeType === Node.ELEMENT_NODE`, or `node.matches?.('.row')` — rather than with `instanceof`.

code

javascript · 13 lines
javascript
const frame = document.createElement('iframe');
document.body.append(frame);
frame.contentDocument.body.innerHTML = '<div class="row">hi</div>';

const row = frame.contentDocument.querySelector('.row');
console.log(row.ownerDocument === frame.contentDocument); // true

document.body.append(row);                    // adopted, no error thrown
console.log(row.ownerDocument === document);  // true — ownership moved

console.log(row instanceof HTMLElement);      // false — built in the frame's realm
console.log(row instanceof frame.contentWindow.HTMLElement); // true
console.log(row.nodeType === Node.ELEMENT_NODE);             // true — safe check

go deeper

for a junior

Know that every node remembers the document it came from as ownerDocument, and that a page with an iframe has more than one document in play.

for a middle

Explain that inserting a foreign node adopts it automatically, that adoptNode moves while importNode clones — with deep defaulting to false — and that a clone loses its event listeners.

for a senior

Demonstrate the realm reasoning: constructors are per-window, adoption does not touch prototypes, so cross-document nodes fail instanceof and must be checked structurally. Recognise the bug when a widget rejects an iframe's element.

for a principal

Set the contract for shared components — what counts as a valid node at an API boundary, how foreign-realm values are validated, and whether embedding scenarios with multiple documents are supported at all, so the answer is not rediscovered per library.

## ownerDocument: which document a node belongs to Every node carries a reference to the document that created it, exposed as `node.ownerDocument`. `document.createElement('div')` stamps it with that document; the parser stamps every node it builds with the document being parsed. (`Document` itself is the one node whose `ownerDocument` is `null` — it is the owner.) A page that embeds a same-origin iframe therefore has **two** documents in play, and nodes are not interchangeable between them by accident. ## Insertion adopts, it does not throw Old DOM Level 1 required an explicit `importNode` and threw `WrongDocumentError` otherwise. That is history. The current DOM standard specifies that pre-insertion **adopts** the node into the parent's node document, so this simply works: ```js const row = frame.contentDocument.querySelector('.row'); document.body.append(row); // no error row.ownerDocument === document; // true ``` Adoption is a move, not a copy. The node is removed from wherever it was, its `ownerDocument` (and that of every descendant) is rewritten, and it is inserted at the destination. The iframe's document no longer contains it. Two related calls let you do the step by hand: - `document.adoptNode(node)` — the same move, performed without inserting anything. The node comes back detached but now owned by the calling document. - `document.importNode(node, deep)` — a **clone** owned by the calling document; the original stays where it is. The `deep` argument defaults to `false`, which imports the node alone without its subtree, and is a common bug. Because `importNode` clones, it carries the same losses any clone does: the copy has no event listeners and none of the JavaScript state you had associated with the original object. `adoptNode` and plain insertion move the identical object, so listeners registered on it are still there afterwards. ## Realms: why instanceof lies Each window — including each iframe's window — is a separate JavaScript realm with its own set of intrinsic objects and its own DOM constructors. The iframe has an `HTMLElement`, the parent has an `HTMLElement`, and they are **different function objects with different `.prototype` objects**. When the iframe's parser built `.row`, it gave the object the iframe realm's `HTMLDivElement.prototype`. Adoption rewrites the node's owner document; it does not re-link the prototype chain. So in the parent page: ```js row instanceof HTMLElement; // false — parent realm's constructor row instanceof frame.contentWindow.HTMLElement; // true, while the frame lives ``` This is the same reason an array received from an iframe fails `instanceof Array` — a general cross-realm hazard, not something specific to nodes. The practical rules that follow: - **Do not type-test foreign objects with `instanceof`.** For nodes, check `node.nodeType === Node.ELEMENT_NODE`, or duck-type the capability you are about to use (`typeof node.matches === 'function'`). - **Be careful with `node.ownerDocument.defaultView`.** It is the correct way to reach "the window this node belongs to" — but *after* adoption it is the parent window, whose constructors the node is still not an instance of. Capture the source realm before you adopt if you need it. - **Libraries that guard their public API with `instanceof HTMLElement` reject legitimate nodes** from iframes and from documents created by `DOMParser` or `document.implementation.createHTMLDocument`. That is a real bug class in widget code, not a curiosity. ## Other documents you will meet The iframe is the obvious case, but nodes from a foreign document also arrive from `new DOMParser().parseFromString(html, 'text/html')` and from `document.implementation.createHTMLDocument()`. Both produce inert documents in the **same** realm as the caller, so `instanceof` still works for those — the ownership question applies, the realm question does not. That distinction is worth stating precisely: **ownerDocument is about which document; realm is about which window built the object.** They usually move together, and the iframe case is where they come apart. ## Answering well The strong answer separates the two effects. Insertion fixes ownership automatically — no error, no `importNode` needed, and the node is moved out of the frame. Nothing fixes the prototype, because the object's identity was set when it was constructed in another realm. Then name the consequence: structural checks over `instanceof` for any code that may be handed a node it did not create.

  • What is the difference between document.adoptNode and document.importNode?
    `adoptNode` moves the node: it is removed from its old document, its `ownerDocument` is rewritten, and you get the same object back with its listeners intact. `importNode` clones: the original stays put and you receive a copy owned by the calling document, with no event listeners. `importNode`'s `deep` argument defaults to false, so pass `true` to bring the subtree.
  • How should a library validate that a value handed to its API is really an element?
    Structurally, not by constructor. Check `value && value.nodeType === Node.ELEMENT_NODE`, or duck-type the method it is about to call. Guarding with `instanceof HTMLElement` rejects legitimate nodes that came from an iframe, because those carry another realm's prototypes even though they behave identically.
  • Do nodes produced by DOMParser hit the same instanceof problem?
    No. `DOMParser` and `document.implementation.createHTMLDocument()` create a separate *document* in the *same* realm, so the nodes get the calling window's prototypes and `instanceof` works. Only their `ownerDocument` differs, which insertion resolves by adopting. The realm split is specific to a separate window such as an iframe or a popup.

saying these in an interview costs you the question

  • Expects a WrongDocumentError from appending a foreign node
  • Thinks importNode moves the node rather than cloning it
  • Assumes importNode copies the subtree without passing deep
  • Believes adoption re-links the node's prototype chain
  • Guards a public API with instanceof HTMLElement and calls it robust

context