Сегодня я пытался перебрать все элементы DOM с определенным классом на странице, вытащить их атрибуты в объект и скомпилировать эти объекты в массив. Звучит довольно просто, правда? Очевидно нет. У меня возникли проблемы, поэтому я хотел написать это, чтобы помочь другим в подобных ситуациях.

Наивный (неправильный) путь

var elements = document.getElementsByClassName("bgflag"); 
var BgFlags = [] //Array of objects of info about the elements
for (i in elements){ //assemble our info objects
    var flag = {
        height: elements[i].offsetTop,
        bgsrc: elements[i].dataset.bgsrc,
        bgcolor: elements[i].dataset.bgcolor,
        size: elements[i].dataset.size,
        name: elements[i].id,
        image: parseInt(elements[i].dataset.image)
    }
    BgFlags.push(flag); 
    if(flag.name == "defaultFlag"){
        defaltFlag = flag //global variable
    }
}

Оказывается, getElementsByClassName() возвращает HTMLCollection, а не массив.

Это было проблемой, так как в Chrome все работало нормально, но когда я тестировал его в Edge, i взял идентификатор элемента, а не индекс, как должен быть. Хм…

Я связал два соответствующих документа MDN выше. Если вы их прочитаете, то заметите, что в них ничего не говорится об использовании метода getElementsByClassName() или интерфейса HTMLCollection в циклах for. Документ HTMLCollection, однако, ссылается на документ NodeList, в котором упоминается итерация:

Не поддавайтесь соблазну использовать for… in или for each… in для перечисления элементов в списке, так как это также будет перечислять длину и свойства элемента NodeList и вызывать ошибки, если ваш сценарий предполагает, что он имеет дело только с элементом объекты. Кроме того, не гарантируется, что for..in будет посещать объекты в каком-либо определенном порядке.

Получается, что это неопределенное поведение, то есть в языке нет спецификации о том, как обрабатывать использование for… in на HTMLCollection, поэтому браузеры могут делать все, что угодно. Не смешно.

Скучный способ

Согласно документу NodeList, указанному выше:

Можно перебирать элементы в NodeList, используя:

for (var i = 0; i < myNodeList.length; ++i) {
 var item = myNodeList[i]; // Calling myNodeList.item(i) isn’t necessary in JavaScript
}

for… of циклы будут правильно перебирать объекты NodeList:

var list = document.querySelectorAll( ‘input[type=checkbox]’ );
for (var item of list) {
 item.checked = true;
}

Последние браузеры также поддерживают методы итератора forEach (), а также записи (), values ​​() и keys ().

Это была чрезвычайно полезная информация, но после поиска в Интернете я заметил, что похоже, что все в StackOverflow использовали forEach(). Это нормально, но не очень весело.

Веселый (художественный) способ

В колледже я ходил на занятия по функциональному программированию. Это было самое интересное, что мне когда-либо доводилось программировать.

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

Подождите, это звучит знакомо. Передача функций немного необычна для большинства объектно-ориентированных языков, но чрезвычайно распространена в JavaScript. В ECMAScript 5.1 добавлено несколько очень полезных.

  • Array.prototype.map(fn) вызывает fn для каждого элемента в массиве и возвращает новый массив со значениями, которые вернули fn.
  • Array.prototype.filter(fn) вызывает fn для каждого элемента в массиве amd собирает новый массив только из элементов исходного массива, для которых fn вернул true.

Вы понимаете, к чему я клоню? Прежде чем мы туда доберемся, нам понадобятся еще две вещи:

  • Стрелочные функции: в ES6 в 2015 году была добавлена ​​эта замечательная небольшая функция. Они отличаются от обычных функций по многим параметрам, но они действительно крутые, потому что они возврат ручки (и они действительно краткие)
var timesTwo = num => num * 2;
// if you have more than one argument, you need () around them
// equivalent to
var timesTwo = num => { return num * 2 } 
//equivalent to
var double = function(num){ return num * 2;}

Резюмируем:

  • Стрелочные функции всегда анонимны
  • Тело стрелочной функции может быть выражением или блоком.
  • Если тело является выражением, будет возвращен результат выражения.
  • Если тело представляет собой блок, это в основном более компактный синтаксис для анонимной функции *
  • * Здесь это не имеет значения, но стрелочные функции обрабатывают контекст и this очень по-разному. Прочтите документ, если он важен для вас.

Последнее, что нам нужно, это

  • Array.from (): Вся наша проблема в начале этого пути заключалась в том, что мы не могли рассматривать наш HTMLCollection как массив. Array.from() возьмет итерируемый объект и вернет его массив.

Вы можете подумать: «Почему мы просто не сделали это и не сохранили наш исходный код?» Я бы ответил: «Что в этом интересного? Мы учимся! " Кроме того, на самом деле есть веская причина не преобразовывать его в массив, о котором я расскажу позже.

Наконец-то мы здесь. Посмотрим, что мы можем сделать.

var elements = document.getElementsByClassName("bgflag");
elements = Array.from(elements); //convert to array
BgFlags = elements.map(element =>
        ({
            height: element.offsetTop,
            bgsrc: element.dataset.bgsrc,
            bgcolor: element.dataset.bgcolor,
            size: element.dataset.size,
            name: element.id,
            image: parseInt(element.dataset.image)
        })
    );
defaultFlag = BgFlags.filter(flag => flag.name == "defaultFlag")[0]; //we need the [0] because filter() returns an array

Конечно, он похож на наш старый код, но обратите внимание, что нам больше не нужно беспокоиться о циклах! Мы просто говорим: «Вот связь между моими данными и тем, что я хочу, пусть это произойдет». Мне нужно отметить небольшую деталь: скобки вокруг фигурных скобок для литерала объекта необходимы для того, чтобы это работало. Напомним, что стрелочные функции возвращаются неявно, только когда им задано выражение. Без круглых скобок стрелочная функция увидит фигурные скобки, которые определяют литерал объекта как начало и конец блока, и выдаст синтаксическую ошибку. Скобки в Javascript обычно говорят: «сначала сделайте это и рассмотрите результат», тем самым давая нам объект.

Фактический код моего кода выглядит немного иначе:

var elements = document.getElementsByClassName("bgflag");
BgFlags = Array.prototype.map.call(elements,
        element =>
        ({
            height: element.offsetTop,
            bgsrc: element.dataset.bgsrc,
            bgcolor: element.dataset.bgcolor,
            size: element.dataset.size,
            name: element.id,
            image: parseInt(element.dataset.image)
        })
    );
defaultFlag = BgFlags.filter(flag => flag.name == "defaultFlag")[0]; //we need the [0] because filter() returns an array

Я убрал этап преобразования элементов в массив. Зачем беспокоиться, если он вам не нужен? Это просто пустая трата инструкций. Несмотря на то, что map() является функцией Array.prototype, он работает с любым итерируемым массивом. Нам просто нужен способ вызвать его на нашем HTMLCollection. Поскольку elements не является массивом, у него нет методов в Array.prototype. Но Array.prototype делает. Мы можем использовать его и функцию call(thisArg, ...args). У каждой функции есть метод call() (в JavaScript функции являются объектами, что означает, что у них есть методы, помните?). Метод call() позволяет вызывать функцию и переопределять значение this. Обычно this в методе относится к объекту, которому принадлежит метод (вроде… this в javascript странно). Это нормально, когда мы .map() массив, но мы не хотим отображать прототип массива (это даже не сработает), мы хотим map HTMLCollection. Итак, мы передаем elements в качестве первого аргумента. Остальные аргументы будут переданы функции, которую она вызывает, поэтому мы передаем ей нашу анонимную стрелочную функцию, которая обычно была бы единственным аргументом, который мы передали map().