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


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.


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);

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);

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


Интересный оператор.
— 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);
}

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;
}

Не работает. Я вынужден отметить, что версия 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());
     });

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>

Здесь вам не нужен jQuery.
— Виталий Зданевич
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.