Отримайте індекс дочірнього вузла


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вузли ... Деякі відповіді, наприклад, використання previousSibling, базуються на використанні всіх дочірніх вузлів, тоді як інші турбують лише дітей, які мають Element... (!)
гризуна Майка

@mikerodent Я не пам’ятаю, якою була моя мета, коли я спочатку задавав це питання, але це ключова деталь, про яку я не знав. Якщо ви не обережні, .childNodesїх обов'язково потрібно використовувати замість цього .children. 2 найкращі опубліковані відповіді дадуть різні результати, як ви вказали.
Усі працівники найважливіші

Плануючи робити тисячі пошукових запитів на понад 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

132

Я захопився використовувати 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, child);
cuixiping

23
Fwiw, використовуючи, []створює екземпляр Array щоразу, коли ви запускаєте цей код, який є менш ефективним для пам'яті та GC vs використання Array.prototype.
Скотт Майлз

@ScottMiles Чи можу я попросити пояснити, що ви сказали трохи більше? Чи не []залишається чистим у пам'яті як значення сміття?
mrReiha

14
Для оцінки [].indexOfдвигуна необхідно створити екземпляр масиву просто для доступу до indexOfреалізації на прототипі. Сам екземпляр залишається невикористаним (це робить GC, це не витік, це просто витрачає цикли). Array.prototype.indexOfотримує доступ до цієї реалізації безпосередньо, не виділяючи анонімний примірник. Різниця буде майже незначною майже за будь-яких обставин, тому, чесно кажучи, це може бути не варто піклуватися.
Скотт Майлз

3
Остерігайтеся помилок в IE! Internet Explorer 6, 7 і 8 підтримував його, але помилково включає вузли коментарів. Джерело " developer.mozilla.org/en-US/docs/Web/API/ParentNode/…
Luckylooke

101

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 Я знаю, що є торги між елегантністю коду та продуктивністю додатків 👍🏻
Abdennour TOUMI

38

ES - коротше

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

Оператор розповсюдження - це ярлик для цього



Яка різниця між e.parentElement.childNodesі e.parentNode.children?
mesqueeb

4
childNodesтакож включає текстові вузли
Філіпп

З Typescript ви отримуєте Type 'NodeListOf<ChildNode>' must have a '[Symbol.iterator]()' method that returns an iterator.ts(2488)
ZenVentzi

9

Додавання (префікса для безпеки) елемента.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

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 лінійний пошук На 200 тис. Елементів (може вийти з ладу деякі веб-переглядачі для мобільних пристроїв, ЗАБЕЖАЙТЕ!):

  • У цьому тесті ми побачимо, скільки часу потрібно для лінійного пошуку, щоб знайти середній елемент VS двійкового пошуку. Чому середній елемент? Оскільки він знаходиться в середньому місці всіх інших локацій, тому він найкраще представляє всі можливі місця.

Двійковий пошук

Лінійний пошук назад (`lastIndexOf`)

Лінійний пошук вперед (`indexOf`)

ПопереднійElementSibling Пошук лічильника

Підраховує кількість PreviousElementSiblings, щоб отримати parentIndex.

Немає пошуку

Для порівняльного оцінювання, яким буде результат тесту, якби браузер оптимізував пошук.

Висновок

Однак після перегляду результатів у Chrome, результати є протилежними до очікуваного. Більш тупий лінійний пошук вперед був дивовижнішим на 187 мс, на 3850%, швидшим, ніж двійковий пошук. Очевидно, що Chrome якось магічно перехитрив console.assertта оптимізував його, або (що більш оптимістично) Chrome внутрішньо використовує систему цифрової індексації для 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

@ extement Element.prototype причиною є подібність ... 4 колишніх ele.children, element.parentNode і т. д. ... таким же чином ви звертаєтесь до element.siblings .... груповий метод трохи складніший причину, яку я хочу поширити трохи близкий підхід до слонів однаковим тим самим nodeType і має однакові атрибути, навіть не маючи того самого предка
Bortunac

Я знаю, що таке розширення прототипу, але мені подобається знати, як використовується ваш код. el.group.value()??. Мій перший коментар є для покращення якості вашої відповіді.
A1rPun

методи групи та братів та сестер повертають масив із основоположними елементами дому .. .... дякую за ваш коментар та за коментар
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.