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

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

Что такое псевдомассив (массивоподобный объект или коллекция)?


Псевдомассив — это объект, который структурно похож на массив. То есть у него есть числовые свойства (индексы) и свойство length.

Пример:
{0: 'Значение 1', 1: 'Значение 2', 2: 'Значение 3', length: 3};

Чем псевдомассив отличается от массива?


Главным отличием псевдомассива от массива является его наследование прототипа, то есть свойство __proto__.

Когда мы посмотрим в свойства массива, то увидим, что он наследует прототип Array объекта. То есть, все свойства, которые есть в объекте Array.prototype будут доступны для любого массива. Если же посмотреть в свойства какого-либо псевдомассива, то можно заметить, что он наследует прототип другого объекта вместе с другими свойствами.

Список типов объектов, которые относятся к псевдомассивам


Псевдомассив может наследовать прототипы различных объектов. Вот маленький список типов объектов — псевдомассивов:

  • DOMTokenList
  • NamedNodeMap
  • DOMStringMap
  • HTMLCollection
  • NodeList
  • HTMLAllCollection
  • StyleSheetList
  • DOMStringList
  • HTMLMapElement
  • CSSRuleList

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

Как отличить обычный объект от псевдомассива?


Я дня три разбирал данный вопрос читая различные статьи и в итоге составил всего одно условие: если объект является псевдомассивом, то у него должно быть свойство length, которое должно являться целым числом и быть больше либо равно нулю.

Number.isInteger(Number(object.length)) && Number(object.length) >= 0

Это условие я составил откинув следующие пукнты:

  1. Нельзя равняться на числовые свойства, ведь если не указать их, то это не значит, что их не будет. Они просто будут равны значению undefined
  2. Когда я посмотрел типы псевдомассивов, то увидел, что в их типах содержится слово Collection, Map либо List. Но данная идея сразу развеялась, так как псевдомассив может иметь тип обычного объекта — Object,
    и вообще это глупо, ведь под этот пункт даже обычный массив не попадёт.
  3. Так же нельзя равняться и на нечисловые свойства, ведь нечисловое свойство может быть и в массиве.

Но JavaScript «сказал», что и моё условие слишком жестокое. Когда я проанализировал варианты конвертирования псевдомассива в массив, то понял, что JavaScript «съест» псевдомассив, в котором свойство length равно числу, которое больше либо равно нулю.

typeof object.length === 'number' && Number(object.length) >= 0

И не обязательно, чтобы число было целым (кроме некоторых случаев). JavaScript просто переведёт дробное число в наибольшее целое число, меньшее, либо равное указанному.

Пример:
Array.from({0: 'Значение 1', 1: 'Значение 2', length: 1.6}); // ['Значение 1']
Array.from({0: 'Значение 1', 1: 'Значение 2', 2: 'Значение 3', length: 2.3}); // ['Значение 1', 'Значение 2']

Как конвертировать псевдомассив в массив?


Чтобы преобразить псевдомассив в массив, есть несколько вариантов:

  1. Перебрать значения псевдомассива в обычный массив
    Первый вариант, который приходит в голову новичкам — с помощью цикла перебрать все значения из псевдомассива в массив.

    var object = {0: 1, 1: 2, 2: 3, length: 3}
    var array = [];
    
    // Преобразуем псевдомассив в массив
    for (var i = 0; i < object.length; i++) {
       array.push(object[i]);
    };
    
    console.log( array ); // [1, 2, 3]

  2. С помощью функции Array.from()
    Данный вариант немного спорный, так как таблицы, поддержки браузерами данной функции, на разных сайтах разные. Но я с уверенностью могу сказать, что во всех современных браузерах данный метод будет работать.

    var object = {0: 1, 1: 2, 2: 3, length: 3}
    // Преобразуем псевдомассив в массив
    var array = Array.from(object);
    
    console.log( array ); // [1, 2, 3]

  3. С помощью функции Array.prototype.slice.call() ( [].slice.call() )
    Этот метод «наших дедушек и бабушек», который работает до сих пор.

    var object = {0: 1, 1: 2, 2: 3, length: 3}
    // Преобразуем псевдомассив в массив
    var array = Array.prototype.slice.call(object); // Или сокращённая форма: [].slice.call(object);
    
    console.log( array ); // [1, 2, 3]

  4. С помощью spread оператора
    Данный метод во время написания данной статьи является довольно спорным, так как поддерживается до сих пор не всеми браузерами и будет работать только с «корневыми» псевдомассивами (NodeList, HTMLCollection и прочее).

    var object = document.querySelectorAll(selector);
    // Преобразуем псевдомассив в массив
    var array = [...object];
    
    console.log( array ); // [element, element, element]

  5. С помощью изменения свойства __proto__
    Про это свойство я упоминал в начале статьи. Если мы изменим свойство __proto__ объекта на Array.prototype, то псевдомассив преобразуется в массив. Но этот метод входит в те самые «кроме некоторых случаев», про которые я писал више, так как, для полного преображения в массив, свойство length должно являться целым числом.

    var object = {0: 'a', 1: 'b', 2: 'c', length: 3}
    // Меняем __proto__ объекта
    object.__proto__ = Array.prototype;
    
    console.log(object); // ['a', 'b', 'c']

    Так же тут есть одна особенность: если мы укажем length число, которое будет меньше чем количество записей в псевдомассиве, то у нас получится массив с количеством записей указанных в length и с дополнительными свойствами из остатка записей.

    var object = {0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e', length: 3}
    // Меняем __proto__ объекта
    object.__proto__ = Array.prototype;
    
    console.log(object); // ['a', 'b', 'c', 3: 'd', 4: 'e']

    И ещё одна заметка: данный метод не сделает объект настоящим массивом, хоть и даст ему нужные параметры. В этом можно убедиться, если проверить объект с помощью функции Array.isArray();.

    var object = {0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e', length: 3}
    // Меняем __proto__ объекта
    object.__proto__ = Array.prototype;
    
    console.log( Array.isArray(object) ); // false


Это самые популярные методы преображения. Так же нужно сказать, что эти все методы можно не использовать, если вам нужно, к примеру, перебрать псевдомассив с помощью forEach или отфильтровать его функцией filter. Для таких целей в функциях есть дополнительная функция .call(), которая даёт возможность работать с псевдомассивами.

Пример:
var object = {0: 'a', 1: 'b', 2: 'c', length: 3}
// Создаём массив из значений псевдомассива
object = Array.prototype.map.call(object, v => 'Буква: ' + v); // Или сокращённо: [].map.call(object, v => 'Буква: ' + v)

console.log(object); // ['Буква: a', 'Буква: b', 'Буква: c']

На этом я закончу данную статью. Рассказал что хотел, а было ли это полезно для вас, решать вам.

Комментарии (19)


  1. mayorovp
    22.08.2017 13:42
    +3

    Spread оператор работает с последовательностями, а не с псевдомассивами. Ему свойство length не требуется, ему Symbol.iterator нужен.


    И да, замена прототипа псевдомассив настоящим массивом не сделает, хоть и даст нужные методы. Это можно заметить если к такому "массиву" применить функцию Array.isArray или JSON.stringify


    1. yuri_spivak Автор
      22.08.2017 13:53
      +1

      Добавил вторую заметку в статью.


  1. vasIvas
    22.08.2017 14:31

    Лично мне кажется что не совсем корректно выбрано название. Почему какой-то псевдомассив, а не хеш? Ведь массив подразумевает числовые значения в качестве ключей, а хеш строковой. И мне почему-то кажется что в js объекты могут только строки в качестве ключей использовать.
    И если считать, что псевдомассив, это каждый объект имеющий свойство length, то было бы проще сказать, что в js все объекты псевдомассивы, так как они все унаследованы от Object, у которого есть свойство length.


    1. yuri_spivak Автор
      22.08.2017 14:45

      Простите, но я не соглашусь. Не у всех производных Object есть стандартное свойство length. Псевдомассив потому, что в объекте в качестве ключей используется числовые значения, но объект не является массивом. И я сказал, что JavaScript может преобразовать любой объект в массив, если тот соответствует условию из статьи.


      1. vasIvas
        22.08.2017 15:39

        А у каких производных от Object типов нет свойства length? А как Вы узнаете что в качестве ключа используется именно число?


        1. yuri_spivak Автор
          22.08.2017 15:47

          К примеру, у того же Element. А зачем мне это узнавать? Я проверю присутствие свойства length и то что значение в нём >= 0. Если всё подойдёт, то объект может быть псевдомассивом.


        1. mayorovp
          22.08.2017 17:08

          У большинства нет такого свойства.


    1. k12th
      22.08.2017 16:32

      Ноги у "псеводомассивов" растут из первой версии языка, в которой собственно массивов не было, а DOM-интерфейс содержал списки элементов.
      Хэш-таблица это просто один из способов организации словаря и собственно называть так любую key-value структуру особо смысла нет — внутри может быть хэш-таблица, а может и не быть.


  1. Bhudh
    23.08.2017 15:12

    Если псевдомассиву не нужны все функции от Array.prototype, их с call использовать не обязательно.
    Можно вместо этого сначала переписать их в псевдомассив.

    myPseudoArray.each = Array.prototype.forEach;
    myPseudoArray.sort = Array.prototype.sort;
    
    myPseudoArray.sort( coolSortFun ).each( myLog );


    Но опять же, не в продакшене. На Тостере мне уже говорили, что это ай-я-яй.


  1. mrzv33
    23.08.2017 15:32
    +1

    как вариант, в добавок к методам можно Object.entries() использовать


    1. yuri_spivak Автор
      23.08.2017 15:32
      -1

      К сожалению, это экспериментальная технология. Может в будущем…


  1. iShatokhin
    23.08.2017 15:56
    +1

    В списке есть Map, но он не подходит под определение


    То есть у него есть числовые свойства (индексы) и свойство length.

    т.к. length всегда равен '1' и индекс могут быть любыми.
    Да и перечисленные методы с ним корректно работать не будут.


    1. yuri_spivak Автор
      23.08.2017 15:59

      Простите, но подходит. Если вы преобразуете его, даже без числовых индексов, то получится массив [undefined]


      1. iShatokhin
        23.08.2017 16:04

        Наличие индексов не спасает.


        Array.prototype.slice.call(new Map([[0, "value1"], [1, "value2"]])) // > []

        В данном случае должно присутствовать свойство length, равное размеру псевдомассива. А его нет.


        1. yuri_spivak Автор
          23.08.2017 16:14

          Простите, но вы, насколько я знаю, у вас просто создаётся объект {0: 'value1', 1: 'value1'}, который не является псевдомассивом… вы его ни в одной функции массивов не сможете использовать


          1. iShatokhin
            23.08.2017 16:19

            не является псевдомассивом

            Я вам и пишу, что Map (который вы перечислили в статье) не подходит на роль псевдомассива. Или вы о каком-то другом Map?


            1. yuri_spivak Автор
              23.08.2017 16:22

              О нём. Интересно конечно. Я внося в список ориентировался документацией, в которой было написано, что это массив. Удалил из списка


              1. mayorovp
                23.08.2017 17:01

                Э… но где в документации было написано что это массив?


                1. yuri_spivak Автор
                  23.08.2017 17:04

                  Древнеегипетской :-)