The currently accepted answer somehow provides a valid logical explanation as to what happens, but they are factually wrong.
Element.querySelector
triggers the match a selector against tree algorithm, which goes from the root element and checks if its descendants do match the selector.
The selector itself is absolute, it doesn't have any knowledge of a Document and doesn't even require that your Element be appended to any. And apart from the :scope
attribute, it doesn't either care with which root you called the querySelector
method.
If we wanted to rewrite it ourselves, it would be more like
const walker = document.createTreeWalker(element, {
NodeFilter.SHOW_ELEMENT,
{ acceptNode: (node) => return node.matches(selector) && NodeFilter.FILTER_ACCEPT }
});
return walker.nextNode();
const rootDiv = document.getElementById('test');
console.log(querySelector(rootDiv, 'div>div').innerHTML);
function querySelector(element, selector) {
const walker = document.createTreeWalker(element,
NodeFilter.SHOW_ELEMENT,
{
acceptNode: (node) => node.matches(selector) && NodeFilter.FILTER_ACCEPT
});
return walker.nextNode();
};
<div>
<div>
<div id="test">
<div>
<div>
This is content
</div>
</div>
</div>
</div>
</div>
With the big difference that this implementation doesn't support the special :scope
selector.
You may think it's the same going from the document or going from the root element, but not only will it make a difference in terms of performances, it will also allow for using this method while the element is not appended to any document.
const div = document.createElement('div');
div.insertAdjacentHTML('beforeend', '<div id="test"><div class="bar"></div></div>')
console.log(div.querySelector('div>.bar')); // found
console.log(document.querySelector('div>.bar')); // null
In the same way, matching elements in the Shadow-DOM would not be possible if we only had Document.querySelector.