Получить индекс дочернего узла

125

В прямом javascript (т.е. без расширений, таких как jQuery и т. Д.), Есть ли способ определить индекс дочернего узла внутри его родительского узла без повторения и сравнения всех дочерних узлов?

Например,

var child = document.getElementById('my_element');
var parent = child.parentNode;
var childNodes = parent.childNodes;
var count = childNodes.length;
var child_index;
for (var i = 0; i < count; ++i) {
  if (child === childNodes[i]) {
    child_index = i;
    break;
  }
}

Есть ли лучший способ определить индекс ребенка?

Все работники необходимы
источник
2
Извини, я полный болван? Здесь есть много, казалось бы, изученных ответов, но чтобы получить все дочерние узлы, вам не нужно делать это parent.childNodes, а не parent.children?. В последнем перечислены только узлы Elements, исключая отдельные Textузлы ... Некоторые из ответов здесь, например, using previousSibling, основаны на использовании всех дочерних узлов, тогда как другие беспокоятся только о дочерних узлах, которые являются Element... (!)
mike rodent
@mikerodent Я не помню, с какой целью я изначально задал этот вопрос, но это ключевая деталь, о которой я не знал. Если вы не будете осторожны, .childNodesопределенно следует использовать вместо .children. Как вы указали, два первых опубликованных ответа дадут разные результаты.
Все рабочие
Если вы планируете выполнять тысячи поисков по более чем 1000 узлов, затем прикрепите информацию к узлу (например, через child.dataset). Целью было бы преобразовать алгоритм O (n) или O (n ^ 2) в алгоритм O (1). Обратной стороной является то, что если узлы добавляются и удаляются регулярно, связанная информация о местоположении, прикрепленная к узлам, также должна быть обновлена, что может не привести к увеличению производительности. Случайные итерации не имеют большого значения (например, обработчик кликов), но повторные итерации проблематичны (например, перемещение мыши).
CubicleSoft

Ответы:

122

вы можете использовать это previousSiblingсвойство для повторного перебора братьев и сестер, пока не вернетесь, nullи подсчитайте, сколько братьев и сестер вы встретили:

var i = 0;
while( (child = child.previousSibling) != null ) 
  i++;
//at the end i will contain the index.

Обратите внимание, что в таких языках, как Java, есть getPreviousSibling()функция, однако в JS она стала свойством - previousSibling.

Liv
источник
2
Ага. Однако в тексте вы оставили getPreviousSibling ().
Тим Даун
7
этот подход требует того же количества итераций для определения дочернего индекса, поэтому я не вижу, как это будет намного быстрее.
Майкл
31
Однострочная версия:for (var i=0; (node=node.previousSibling); i++);
Скотт Майлз
2
@sfarbota Javascript не знает области видимости блока, поэтому iбудет доступен.
A1rPun
3
@nepdev Это связано с различиями между .previousSiblingи .previousElementSibling. Первый попадает в текстовые узлы, второй - нет.
abluejelly
133

Я стал любить использовать indexOfдля этого. Поскольку indexOfон включен Array.prototypeи parent.childrenесть NodeList, вы должны использовать call();Это некрасиво, но это один лайнер и использует функции, с которыми любой разработчик javascript должен быть знаком в любом случае.

var child = document.getElementById('my_element');
var parent = child.parentNode;
// The equivalent of parent.children.indexOf(child)
var index = Array.prototype.indexOf.call(parent.children, child);
KhalilRavanna
источник
7
var index = [] .indexOf.call (child.parentNode.children, дочерний элемент);
cuixiping
23
Fwiw, using []создает экземпляр массива каждый раз, когда вы запускаете этот код, что менее эффективно для памяти и GC по сравнению с использованием Array.prototype.
Скотт Майлз
@ScottMiles Могу я попросить еще немного объяснить то, что вы сказали? Не []очищается память как значение мусора?
mrReiha
14
Для оценки [].indexOfдвижка необходимо создать экземпляр массива только для доступа к indexOfреализации на прототипе. Сам экземпляр не используется (он выполняет сборку мусора, это не утечка, это просто напрасная трата циклов). Array.prototype.indexOfобращается к этой реализации напрямую, не выделяя анонимный экземпляр. Разница будет незначительной практически при любых обстоятельствах, поэтому, откровенно говоря, о ней не стоит беспокоиться.
Скотт Майлз
3
Остерегайтесь ошибок в IE! Internet Explorer 6, 7 и 8 поддерживает его, но ошибочно включает узлы комментариев. Источник " developer.mozilla.org/en-US/docs/Web/API/ParentNode/…
Luckylooke
102

ES6:

Array.from(element.parentNode.children).indexOf(element)

Пояснение:

  • element.parentNode.children→ Возвращает братьев element, включая этот элемент.

  • Array.from→ Приводит конструктор childrenк Arrayобъекту

  • indexOf→ Вы можете подать заявку, indexOfпотому что теперь у вас есть Arrayобъект.

Абденнур Туми
источник
2
Самое элегантное решение, безусловно :)
Амит Саксена
1
Но доступно только в Chrome 45 и Firefox 32 и вообще не доступно в Internet Explorer.
Питер
Internet Explorer еще жив? Просто Джок ... Итак, вам нужен полифилл, чтобы Array.fromработать в Internet Explorer
Абденнур ТУМИ
9
Согласно MDN, вызов Array.from () creates a new Array instance from an array-like or iterable object. Создание нового экземпляра массива только для поиска индекса может быть неэффективным с точки зрения памяти или GC, в зависимости от того, насколько часто выполняется операция, и в этом случае итерация, как описано в принятом ответе, будет больше идеально.
TheDarkIn1978,
1
@ TheDarkIn1978 Я знаю, что между элегантностью кода и производительностью приложения есть компромисс 👍🏻
Абденнур ТУМИ,
38

ES-короткие

[...element.parentNode.children].indexOf(element);

Оператор спреда - это ярлык для этого

Philipp
источник
Интересный оператор.
All Workers Are Essential
В чем разница между e.parentElement.childNodesи e.parentNode.children?
mesqueeb 08
4
childNodesвключает также текстовые узлы
Филипп
С Typescript вы получите Type 'NodeListOf<ChildNode>' must have a '[Symbol.iterator]()' method that returns an iterator.ts(2488)
ZenVentzi
9

Добавление (с префиксом для безопасности) element.getParentIndex ():

Element.prototype.PREFIXgetParentIndex = function() {
  return Array.prototype.indexOf.call(this.parentNode.children, this);
}
mikemaccana
источник
1
Причина болезненности веб-разработки: префиксы разработчиков. Почему бы просто не сделать if (!Element.prototype.getParentIndex) Element.prototype.getParentIndex = function(){ /* code here */ }? В любом случае, если это когда-нибудь будет реализовано в стандарте в будущем, то, скорее всего, оно будет реализовано как геттер element.parentIndex. Итак, я бы сказал, что лучший подход был быif(!Element.prototype.getParentIndex) Element.prototype.getParentIndex=Element.prototype.parentIndex?function() {return this.parentIndex}:function() {return Array.prototype.indexOf.call(this.parentNode.children, this)}
Джек Гиффин
3
Потому что будущее getParentIndex()может иметь другую подпись, чем ваша реализация.
mikemaccana 04
7

𝗣𝗿𝗼𝗼𝗳 𝗢𝗳 𝗔 𝗟𝗲𝘀𝘀 𝗘𝗳𝗳𝗶𝗰𝗶𝗲𝗻𝘁 𝗕𝗶𝗻𝗮𝗿𝘆 𝗦𝗲𝗮𝗿𝗰𝗵

Я предполагаю, что с учетом элемента, в котором все его дочерние элементы упорядочены в документе последовательно, самым быстрым способом должен быть двоичный поиск, сравнивая позиции элементов в документе. Однако, как указано в заключении, гипотеза отвергается. Чем больше у вас элементов, тем выше потенциал для повышения производительности. Например, если у вас 256 элементов, то (оптимально) вам нужно будет проверить только 16 из них! Для 65536 всего 256! Производительность выросла до 2! Смотрите больше цифр / статистики. Посетите Википедию

(function(constructor){
   'use strict';
    Object.defineProperty(constructor.prototype, 'parentIndex', {
      get: function() {
        var searchParent = this.parentElement;
        if (!searchParent) return -1;
        var searchArray = searchParent.children,
            thisOffset = this.offsetTop,
            stop = searchArray.length,
            p = 0,
            delta = 0;
        
        while (searchArray[p] !== this) {
            if (searchArray[p] > this)
                stop = p + 1, p -= delta;
            delta = (stop - p) >>> 1;
            p += delta;
        }
        
        return p;
      }
    });
})(window.Element || Node);

Затем вы можете использовать его, получая свойство parentIndex любого элемента. Например, посмотрите следующую демонстрацию.

(function(constructor){
   'use strict';
    Object.defineProperty(constructor.prototype, 'parentIndex', {
      get: function() {
        var searchParent = this.parentNode;
        if (searchParent === null) return -1;
        var childElements = searchParent.children,
            lo = -1, mi, hi = childElements.length;
        while (1 + lo !== hi) {
            mi = (hi + lo) >> 1;
            if (!(this.compareDocumentPosition(childElements[mi]) & 0x2)) {
                hi = mi;
                continue;
            }
            lo = mi;
        }
        return childElements[hi] === this ? hi : -1;
      }
    });
})(window.Element || Node);

output.textContent = document.body.parentIndex;
output2.textContent = document.documentElement.parentIndex;
Body parentIndex is <b id="output"></b><br />
documentElements parentIndex is <b id="output2"></b>

Ограничения

  • Эта реализация решения не будет работать в IE8 и ниже.

Двоичный VS линейный поиск на 200000 элементов (может привести к сбою некоторых мобильных браузеров, ОСТОРОЖНО!):

  • В этом тесте мы увидим, сколько времени занимает линейный поиск, чтобы найти средний элемент, по сравнению с двоичным поиском. Почему средний элемент? Поскольку он находится в среднем месте всех остальных местоположений, поэтому он лучше всего представляет все возможные местоположения.

Бинарный поиск

Обратный (`lastIndexOf`) линейный поиск

Вперед (`indexOf`) Линейный поиск

PreviousElementSibling Counter Search

Подсчитывает количество PreviousElementSiblings для получения parentIndex.

Нет поиска

Для сравнения, каким был бы результат теста, если бы браузер оптимизировал поиск.

Сотрясение

Однако после просмотра результатов в Chrome результаты оказались противоположными ожидаемым. Более тупой прямой линейный поиск оказался на удивление на 187 мс, что на 3850% быстрее, чем двоичный поиск. По- видимому, хром какой - то магическим перехитрить console.assertи оптимизирована ее, или (более оптимистично) Хром внутренне использует числовую систему индексирования для DOM, и эта внутренняя система индексации экспонирует через оптимизацию применительно к Array.prototype.indexOfпри использовании на HTMLCollectionобъекте.

Джек Гиффин
источник
Эффективно, но непрактично.
applemonkey496
3

Используйте алгоритм двоичного поиска, чтобы улучшить производительность, когда узел имеет большое количество братьев и сестер.

function getChildrenIndex(ele){
    //IE use Element.sourceIndex
    if(ele.sourceIndex){
        var eles = ele.parentNode.children;
        var low = 0, high = eles.length-1, mid = 0;
        var esi = ele.sourceIndex, nsi;
        //use binary search algorithm
        while (low <= high) {
            mid = (low + high) >> 1;
            nsi = eles[mid].sourceIndex;
            if (nsi > esi) {
                high = mid - 1;
            } else if (nsi < esi) {
                low = mid + 1;
            } else {
                return mid;
            }
        }
    }
    //other browsers
    var i=0;
    while(ele = ele.previousElementSibling){
        i++;
    }
    return i;
}
cuixiping
источник
Не работает. Я вынужден отметить, что версия IE и версия "другого браузера" будут вычислять разные результаты. Метод «других браузеров» работает так, как ожидалось, занимая n-ю позицию под родительским узлом, однако метод IE «извлекает порядковый номер объекта в исходном порядке, поскольку объект отображается во всей коллекции документа» ( msdn.microsoft .com / en-gb / library / ie / ms534635 (v = vs.85) .aspx ). Например, я получил 126, используя технику "IE", а затем 4, используя другую.
Кристофер Булл,
1

У меня была проблема с текстовыми узлами, и он показывал неправильный индекс. Вот версия, чтобы исправить это.

function getChildNodeIndex(elem)
{   
    let position = 0;
    while ((elem = elem.previousSibling) != null)
    {
        if(elem.nodeType != Node.TEXT_NODE)
            position++;
    }

    return position;
}
Джон хочет знать
источник
0
Object.defineProperties(Element.prototype,{
group : {
    value: function (str, context) {
        // str is valid css selector like :not([attr_name]) or .class_name
        var t = "to_select_siblings___";
        var parent = context ? context : this.parentNode;
        parent.setAttribute(t, '');
        var rez = document.querySelectorAll("[" + t + "] " + (context ? '' : ">") + this.nodeName + (str || "")).toArray();
        parent.removeAttribute(t);            
        return rez;  
    }
},
siblings: {
    value: function (str, context) {
        var rez=this.group(str,context);
        rez.splice(rez.indexOf(this), 1);
        return rez; 
    }
},
nth: {  
    value: function(str,context){
       return this.group(str,context).indexOf(this);
    }
}
}

бывший

/* html */
<ul id="the_ul">   <li></li> ....<li><li>....<li></li>   </ul>

 /*js*/
 the_ul.addEventListener("click",
    function(ev){
       var foo=ev.target;
       foo.setAttribute("active",true);
       foo.siblings().map(function(elm){elm.removeAttribute("active")});
       alert("a click on li" + foo.nth());
     });
bortunac
источник
1
Вы можете объяснить, почему вы расширились Element.prototype? Функции выглядят полезными, но я не знаю, что они делают (даже если названия очевидны).
A1rPun
@ extend Element.prototype причина в сходстве ... 4 ex elemen.children, element.parentNode и т. д ... так же, как вы обращаетесь к element.siblings .... групповой метод немного сложен, потому что я хочу расширить немного похожий подход к элементам с одинаковым типом узла и имеющими одинаковые атрибуты, даже не имея одного и того же предка
Bortunac
Я знаю, что такое расширение прототипа, но мне нравится знать, как используется ваш код. el.group.value()??. Мой первый комментарий предназначен для улучшения качества вашего ответа.
A1rPun
методы group и siblings возвращают массив с найденными элементами dom .. .... спасибо за ваш комментарий и за причину комментария
bortunac
Очень элегантно, но очень медленно.
Джек Гиффин
-1
<body>
    <section>
        <section onclick="childIndex(this)">child a</section>
        <section onclick="childIndex(this)">child b</section>
        <section onclick="childIndex(this)">child c</section>
    </section>
    <script>
        function childIndex(e){
            let i = 0;
            while (e.parentNode.children[i] != e) i++;
            alert('child index '+i);
        }
    </script>
</body>
ofir_aghai
источник
Здесь вам не нужен jQuery.
Виталий Зданевич