Несколько селекторов потомков, ошибка или недоразумение?

Разве следующие два подхода к выбору узла не должны давать один и тот же результат?

let tmp = fruits.querySelector("ul:first-of-type li:first-of-type");
tmp = tmp.querySelector("span")    

vs.

let tmp = fruits.querySelector("ul:first-of-type li:first-of-type span");

(Посмотрите в действии здесь)

Я тестировал это как в Firefox, так и в Chrome. Разные результаты в обоих случаях. Кто-нибудь может объяснить, почему?

Пример в фрагменте стека:

let fruits = document.querySelector("[data-segment='fruits']");
console.log(fruits);
let tmp = fruits.querySelector("ul:first-of-type li:first-of-type")
tmp = tmp.querySelector("span")
console.log("Works:")
console.log(tmp)
console.log("Does not work:")
console.log(fruits.querySelector("ul:first-of-type li:first-of-type span"))
<main id="app" data-v-app="">
  <section>
    <h2>Tree</h2>
    <ul role="tree">
      <li role="treeitem" data-segment="fruits" aria-level="1" aria-setsize="3" aria-posinset="1" aria-expanded="true"><span tabindex="0">Fruits</span>
        <ul role="group">
          <li role="none" data-segment="oranges" aria-level="2" aria-setsize="5" aria-posinset="1"><span tabindex="-1">Oranges</span>
            <!--v-if-->
          </li>
          <li role="none" data-segment="pineapple" aria-level="2" aria-setsize="5" aria-posinset="2"><span tabindex="-1">Pineapple</span>
            <!--v-if-->
          </li>
          <li role="treeitem" data-segment="apples" aria-level="2" aria-setsize="5" aria-posinset="3" aria-expanded="false"><span tabindex="-1">Apples</span>
            <ul role="group">
              <li role="none" data-segment="macintosh" aria-level="3" aria-setsize="3" aria-posinset="1"><span tabindex="-1">Macintosh</span>
                <!--v-if-->
              </li>
              <li role="none" data-segment="granny_smith" aria-level="3" aria-setsize="3" aria-posinset="2"><span tabindex="-1">Granny Smith</span>
                <!--v-if-->
              </li>
              <li role="none" data-segment="fuji" aria-level="3" aria-setsize="3" aria-posinset="3"><span tabindex="-1">Fuji</span>
                <!--v-if-->
              </li>
            </ul>
          </li>
          <li role="none" data-segment="bananas" aria-level="2" aria-setsize="5" aria-posinset="4"><span tabindex="-1">Bananas</span>
            <!--v-if-->
          </li>
          <li role="none" data-segment="pears" aria-level="2" aria-setsize="5" aria-posinset="5"><span tabindex="-1">Pears</span>
            <!--v-if-->
          </li>
        </ul>
      </li>
      <li role="none" data-segment="vegetables" aria-level="1" aria-setsize="3" aria-posinset="2"><span tabindex="-1">Vegetables</span>
        <!--v-if-->
      </li>
      <li role="none" data-segment="grains" aria-level="1" aria-setsize="3" aria-posinset="3"><span tabindex="-1">Grains</span>
        <!--v-if-->
      </li>
    </ul>
  </section>
</main>


person Michael Lipp    schedule 11.04.2021    source источник
comment
Я запустил его и тот же результат в 3 разных браузерах.   -  person Masood    schedule 11.04.2021
comment
больше всего меня расстраивает то, что fruits.querySelector("ul:first-of-type li:first-of-type") выбирает апельсины <li>, а fruits.querySelector("ul:first-of-type li:first-of-type span") выбирает фрукты <span>   -  person asdru    schedule 11.04.2021


Ответы (2)


Это объясняется в документации:

element = baseElement.querySelector(selectors);

Возвращаемое значение

Первый элемент-потомок baseElement, который соответствует указанной группе selectors. При сопоставлении учитывается вся иерархия элементов, включая те, которые не входят в набор элементов, включая baseElement и его потомков; другими словами, selectors сначала применяется ко всему документу, а не baseElement, для создания начального списка потенциальных элементов. Полученные элементы затем проверяются, являются ли они потомками baseElement. Метод querySelector возвращает первое совпадение этих оставшихся элементов.

(Выделение мое.)

Рассмотрим упрощенный пример:

console.log(document.getElementById("a").querySelector("ul li span"));
<ul>
  <li id="a"><span>A</span>
    <ul>
      <li><span>B</span></li>
    </ul>
  </li>
</ul>

Здесь baseElement равно document.getElementById("a"); selectors это "ul li span".

document.querySelector("ul li span") действительно включает в себя оба <span>, и оба они находятся внутри baseElement. <span>A</span> оказался первым в этом наборе.

Существует довольно новый псевдокласс под названием :scope, который может помогите здесь:

console.log(document.getElementById("a").querySelector(":scope ul li span"));
<ul>
  <li id="a"><span>A</span>
    <ul>
      <li><span>B</span></li>
    </ul>
  </li>
</ul>

person Sebastian Simon    schedule 11.04.2021
comment
Мне просто пришло в голову, что у вас не может быть относительного селектора, который начинается с комбинатора-потомка - единственный экземпляр псевдо :scope является обязательным в селекторе с областью действия. - person BoltClock; 11.04.2021
comment
А, @BoltClock, я помню, когда ты сам написал бы красивый канонический ответ на этот вопрос. Для чего это стоит: я скучаю по тем дням (и, пожалуйста, не стесняйтесь удалить этот ужасно тангенциальный комментарий, как только вы его прочтете) - person David says reinstate Monica; 11.04.2021
comment
@David: Я не ???? Должно быть, это было навсегда. У тебя случайно нет ссылки? (Если это не вот этот.) - person BoltClock; 11.04.2021
comment
@BoltClock, я не столько вспомнил конкретный ответ, который вы написали, просто я вспомнил, как читал несколько ваших ответов, в которых вы обратились к спецификации и написали что-то вроде эссе - в увлекательной манере - чтобы полно и исчерпывающе ответить на поставленный вопрос. - person David says reinstate Monica; 11.04.2021
comment
@David: О, да, ты бы написал, а не написал. Да, я бы это сделал, хотя это, вероятно, будет примерно таким же, как то, что здесь есть у Себастьяна, за исключением того, что я буду цитировать спецификацию DOM, а не MDN (но с MDN здесь все в порядке). - person BoltClock; 12.04.2021

el.querySelector(selector) может возвращать неожиданные результаты, если вы не знаете, как он работает в фоновом режиме.

 el.querySelector(selector)

На самом деле происходит следующее:

 (el, selector) => [...document.querySelectorAll(selector)].filter(node => el !== node && el.contains(node))[0]

См. этот пример:

const el = document.getElementById('outer');
const selector = 'span > span';
console.log(el.querySelector(selector).id); // logs "inner", not "innermost"
// same as if you did
console.log([...document.querySelectorAll(selector)].filter(node => el !== node && el.contains(node))[0].id);
<span id="outermost">
  <span id="outer">
    <span id="inner">
      <span id="innermost"></span>
    </span>
  </span>
</span>

Таким образом, el.querySelector(selector) возвращает первое совпадение, если вы выполнили document.querySelectorAll(selector), которое удовлетворяет условию, согласно которому найденный узел должен быть потомком el.

Что еще более удивительно, el.querySelector находит соответствие даже при передаче selector, у которого, как вы думаете, нет совпадений, потому что части selector даже не находятся внутри el:

const el = document.getElementById('outer');
const selector = '#outermost > span > span';
console.log(el.querySelector(selector).id); // logs "inner"!
// same as if you did
console.log([...document.querySelectorAll(selector)].filter(node => el !== node && el.contains(node))[0].id);
<span id="outermost">
  <span id="outer">
    <span id="inner">
      <span id="innermost"></span>
    </span>
  </span>
</span>

person connexo    schedule 11.04.2021