Pull to refresh

Настоящие ассоциативные массивы в JavaScript

Reading time 4 min
Views 58K
Original author: Ryan Morr
Использование литерала объекта, как простого средства для хранения пар ключ-значение давно стало обычным делом в JavaScript. Тем не менее, литерал объекта всё же не является настоящим ассоциативным массивом и по этому, в некоторых ситуациях, его использование может привести к неожиданным результатам. Пока JS не предоставляет нативную реализацию ассоциативных массивов (не во всех браузерах, по крайней мере), существует отличная альтернатива объектам, с нужной функциональностью и без подводных камней.

Проблема с объектами


Проблема заключается в цепочке прототипов. Любой новый объект наследует свойства и методы от Object.prototype, которые могут помешать нам однозначно определить существование ключа. Возьмем для примера метод toString, проверка наличия ключа с таким же именем, с помощью оператора in приведет к ложноположительному результату:

var map = {};
'toString' in map; // true

Это происходит потому что оператор in, не найдя свойство в экземпляре объекта, смотрит дальше по цепочке прототипов в поисках унаследованных значений. В нашем случае это метод toString. Чтобы решить эту проблему существует метод hasOwnProperty , который был задуман специально для того, чтобы проверить наличие свойств только в текущем объекте:

var map = {};
map.hasOwnProperty('toString'); // false

Этот приём отлично работает до тех пор, пока вы не напоретесь на ключ с именем «hasOwnProperty». Перезапись этого метода приведет к тому, что последующие его вызовы будут приводить к непредсказуемым результатам или ошибкам, в зависимости от нового значения:

var map = {};
map.hasOwnProperty = 'foo';
map.hasOwnProperty('hasOwnProperty'); // TypeError

Быстренько чиним и эту проблему. Для этого воспользуемся другим, нетронутым объект и вызовем его метод hasOwnProperty в контексте нашего объекта:

var map = {};
map.hasOwnProperty = 'foo';
{}.hasOwnProperty.call(map, 'hasOwnproperty'); // true

Вот, этот способ уже работает без проблем, но всё же он накладывает некоторые ограничения, на то как мы будем его использовать. Например, каждый раз, когда вы захотите перечислить свойства своего объекта с помощью for ... in, вам придется отфильтровывать всё унаследованное барахло:

var map = {};
var has = {}.hasOwnProperty;

for(var key in map){
    if(has.call(map, key)){
        // do something
    }
}

Через какое-то время этот способ вас ужасающе утомит. Слава богу есть вариант получше.

Голые объекты


Секрет создания чистого ассоциативного массива в избавлении от прототипа и всего того багажа, что он тащит с собой. Чтобы это осуществить, воспользуемся методом Object.create, представленного в ES5. Уникальность этого метода в том, что вы можете явно определить прототип нового объекта. Например создадим обычный объект чуть более наглядно:

var obj = {};
// то же самое:
var obj = Object.create(Object.prototype);

Помимо того, что вы можете выбрать любой прототип, метод также дает вам возможность не выбирать прототип вовсе, просто передав null вместо него:

var map = Object.create(null);

map instanceof Object; // false
Object.prototype.isPrototypeOf(map); // false
Object.getPrototypeOf(map); // null

Эти голые объекты (или словари) идеально подходят для создания ассоциативных массивов, так как отсутствие [[Prototype]] убирает риск наткнуться на конфликт имён. И даже лучше! После того, как мы лишили объект всех унаследованных методов и свойств, любые попытки использовать его не по прямому назначению (хранилище), будут приводить к ошибкам:

var map = Object.create(null);
map + ""; // TypeError: Cannot convert object to primitive value

Нет ни примитивного значения, ни строкового представления. Голые объекты предназначены лишь для работы в качестве хранилища пар ключ-значение и точка.

Имейте в виду, что метода hasOwnProperty тоже больше нет, да он и не нужен, так как оператор in теперь прекрасно работает без каких-либо проверок.

var map = Object.create(null);
'toString' in map; // false

Более того, те утомительные циклы for ... in теперь стали гораздо проще. Наконец-то мы можем без опаски писать их так, как они и должны выглядеть:

var map = Object.create(null);

for(var key in map){
    // do something
}

Несмотря на внесенные изменения, мы можем по-прежнему делать с объектами всё что нужно, как то использовать точечную нотацию или квадратные скобочки, превращать их в строку или использовать объект как контекст для любого метода из Object.prototype:

var map = Object.create(null);

Object.defineProperties(map, {
    'foo': {
        value: 1,
        enumerable: true
    },
    'bar': {
        value: 2,
        enumerable: false
    }
});

map.foo; // 1
map['bar']; // 2

JSON.stringify(map); // {"foo":1}

{}.hasOwnProperty.call(map, 'foo'); // true
{}.propertyIsEnumerable.call(map, 'bar'); // false

Даже различные способы проверки типов по прежнему будут работать:

var map = Object.create(null);

typeof map; // object
{}.toString.call(map); // [object Object]
{}.valueOf.call(map); // Object {}

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

Заключение


Если говорить о простых хранилищах пар ключ-значение, то голые объекты справятся с этой задачей однозначно лучше обычных объектов, избавив разработчика от всего лишнего. Для более функциональных структур данных придется подождать ES6 (ES2015), который предоставит нам нативные ассоциативные массивы в виде объектов Map, Set и других. А пока этот радужный момент не настал, голые объекты — лучший выбор.
Tags:
Hubs:
+45
Comments 38
Comments Comments 38

Articles