Всё только о JavaScript

/ Статьи / Массивы в JavaScript

Немассивы в JavaScript

Как это ни странно, не всякий объект, имеющий числовые свойства и свойство length является массивом. Не являются массивами, например, объект arguments и коллекции возвращаемые DOM методами. Это означает, что у этих объектов нет методов, имеющихся у массивов, а также другое, не такое как у массивов, поведение при манипулировании числовыми свойствами и свойством length.

function f() {
    var a = [1, 2, 3];
    alert(arguments.join);                                                      // undefined
    alert([a.length, arguments.length]);                                        // 3,3
    alert([a[0] == arguments[0], a[1] == arguments[1], a[2] == arguments[2]]);  // true,true,true
    a.length = 0;
    arguments.length = 0;
    alert([a.length, arguments.length]);                                        // 0,0
    alert([a[0] == arguments[0], a[1] == arguments[1], a[2] == arguments[2]]);  // false,false,false
    alert(a);                                                                   // пустое сообщение
    alert([arguments[0], arguments[1], arguments[2]]);                          // 1,2,3
}
f(1, 2, 3);

Как же отличить массивы от других похожих объектов? Если интересующий вас объект может быть как массивом, так и любым другим объектом, а вам всего лишь нужно перебрать его элементы, то можно смело, без всяких проверок, перебирать элементы циклом for от 0 до length - 1. Если же нужно точно знать, имеется ли у нас массив или что-то другое, то будем проверять.

Самая логичная и, в принципе, самая правильная проверка — это instanceof.

var a = [], s = '';
alert(a instanceof Array);   // true
alert(s instanceof Array);   // false

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

Есть более надёжный и, что немаловажно, тоже компактный способ определения массива, как впрочем и любого другого native-объекта, — использование Object.prototype.toString. Заглянем в спецификацию

When the toString method is called, the following steps are taken:
1. Get the [[Class]] property of this object.
2. Compute a string value by concatenating the three strings "[object ", Result(1), and "]".
3. Return Result(2).

Т.е. метод toString у любого объекта должен возвращать строку [object [[Class]]], где [[Class]] — внутреннее свойство объекта, недоступное из пользовательских скриптов. У массивов оно равно Array, у строк — String, у чисел — Number, у регулярных выражений — RegExp. Таким образом функция Object.prototype.toString, вызванная в контексте массива, должна вернуть строку [object Array]. Поэтому можно написать такой isArray.

function isArray(o) {
    return Object.prototype.toString.call(o) == '[object Array]';
}
alert(isArray([]));   // true
alert(isArray(3));    // false
alert(isArray({}));   // false

А можно и ещё короче.

function isArray(o) {
    return {}.toString.call(o) == '[object Array]';
}

Такой метод не сможет верно определить массив только если переопределить Object.prototype.toString, а такая ситуация, к счастью, случается крайне редко.

Приведение к массиву

Для начала нужно понять, а зачем объект приводить к массиву. Как правило, это нужно для того, чтобы стали доступны соответствующие методы, но в большинстве случаев для использования этих методов не обязательно использовать массив. Практически все методы у массивов реализованы так, чтобы работать в тех случаях, если они были вызваны не в контексте массива.

function f() {
    return [].slice.call(arguments, 1, 4);
}
alert(f(1, 2, 3, 4, 5));     // 2,3,4

К сожалению в Internet Explorer младше 9-й версии мир DOM — это особый мир, который плохо уживается с миром JavaScript. Поэтому вызов в контексте DOM-объекта метода, взятого у массива, вызовет ошибку.

function f() {
    return [].slice.call(document.getElementsByTagName('div'), 0, 2);
}
f();   // Во всех браузерах вернёт массив из первых двух элементов div
       // В IE вызовет ошибку

Если вы пишете кроссбраузерный скрипт, то вам придётся избегать таких конструкций. Если же IE не входит в круг поддерживаемых браузеров, то скорее всего приведение к массиву вам и не понадобится.

Однако, в IE нет нативной реализации итераторов и indexOf/lastIndexOf, поэтому, если их правильно реализовать, то ими можно кроссбраузерно пользоваться не только для массивов.

function f() {
    return [].filter.call(document.getElementsByTagName('div'), function(el) {
        return el.offsetWidth;
    });
}
f();   // Вернёт массив элементов div, у которых положительная ширина.

Если же всё-таки требуется, преобразовать кроссбраузерно объект в массив возможно несколькими способами. Для объекта arguments достаточно вызвать метод slice.

function f() {
    return [].slice.call(arguments, 0);
}
alert(typeof f(1, 2, 3));  // true

Если для IE реализованы методы map или filter, то ими можно преобразовать любой объект в массив.

// В обоих случаях получим массив элементов
[].map.call(document.getElementsByTagName('div'), function(e) { return e; });
[].filter.call(document.getElementsByTagName('div'), function() { return true; });

Если ни один из способов не подходит, придётся создавать новый пустой массив и вручную добавлять туда элементы, чем впрочем и занимаются методы map и filter.

var a = [];
var divs = document.getElementsByTagName('div');
for (var i = 0; i < divs.length; i++) {
    a.push(divs[i]);
}