A NodeIterator flattens the nodes under a root, root first, into depth-first document order; nextNode() and previousNode() move through that list and return null at the ends.
<main hidden><p>A</p><span>Z <i>z</i></span><p>B<b>1</b></p><!-- hi --><p>C</p></main>
<pre id="log"></pre>
<script type="module">
const out = [];
const iterator = document.createNodeIterator(document.querySelector('main'),
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT,
(node) => (node.nodeName === 'SPAN' ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT));
for (let node; (node = iterator.nextNode()); ) {
out.push(`${node.nodeName.padEnd(9)} ${JSON.stringify(node.textContent)}`);
if (node.nodeName === 'B') node.parentNode.remove(); // removing nodes mid-walk is safe
}
out.push(`previousNode() at the end: ${JSON.stringify(iterator.previousNode().textContent)}`);
document.querySelector('#log').textContent = out.join('\n');
</script>
For an iterator, FILTER_REJECT means the same as FILTER_SKIP: the <span> is dropped but its <i> is still visited. Removing nodes does not break the loop, because the iterator re-anchors its referenceNode. Stepping back at the end returns the last node again, since the pointer sits after it.