Использование литерала объекта, как простого средства для хранения пар ключ-значение давно стало обычным делом в JavaScript. Тем не менее, литерал объекта всё же не является настоящим ассоциативным массивом и по этому, в некоторых ситуациях, его использование может привести к неожиданным результатам. Пока JS не предоставляет нативную реализацию ассоциативных массивов (не во всех браузерах, по крайней мере), существует отличная альтернатива объектам, с нужной функциональностью и без подводных камней.
Проблема с объектами
Проблема заключается в цепочке прототипов. Любой новый объект наследует свойства и методы от Object.prototype, которые могут помешать нам однозначно определить существование ключа. Возьмем для примера метод
toString
, проверка наличия ключа с таким же именем, с помощью оператора
in
приведет к ложноположительному результату:
var map = {};
'toString' in map;
Это происходит потому что оператор
in
, не найдя свойство в экземпляре объекта, смотрит дальше по цепочке прототипов в поисках унаследованных значений. В нашем случае это метод
toString
. Чтобы решить эту проблему существует метод
hasOwnProperty
, который был задуман специально для того, чтобы проверить наличие свойств только в текущем объекте:
var map = {};
map.hasOwnProperty('toString');
Этот приём отлично работает до тех пор, пока вы не напоретесь на ключ с именем «hasOwnProperty». Перезапись этого метода приведет к тому, что последующие его вызовы будут приводить к непредсказуемым результатам или ошибкам, в зависимости от нового значения:
var map = {};
map.hasOwnProperty = 'foo';
map.hasOwnProperty('hasOwnProperty');
Быстренько чиним и эту проблему. Для этого воспользуемся другим, нетронутым объект и вызовем его метод
hasOwnProperty
в контексте нашего объекта:
var map = {};
map.hasOwnProperty = 'foo';
{}.hasOwnProperty.call(map, 'hasOwnproperty');
Вот, этот способ уже работает без проблем, но всё же он накладывает некоторые ограничения, на то как мы будем его использовать. Например, каждый раз, когда вы захотите перечислить свойства своего объекта с помощью
for … in
, вам придется отфильтровывать всё унаследованное барахло:
var map = {};
var has = {}.hasOwnProperty;
for(var key in map){
if(has.call(map, key)){
}
}
Через какое-то время этот способ вас ужасающе утомит. Слава богу есть вариант получше.
Голые объекты
Секрет создания чистого ассоциативного массива в избавлении от прототипа и всего того багажа, что он тащит с собой. Чтобы это осуществить, воспользуемся методом
Object.create
,
представленного в ES5. Уникальность этого метода в том, что вы можете явно определить прототип нового объекта. Например создадим обычный объект чуть более наглядно:
var obj = {};
var obj = Object.create(Object.prototype);
Помимо того, что вы можете выбрать любой прототип, метод также дает вам возможность не выбирать прототип вовсе, просто передав
null
вместо него:
var map = Object.create(null);
map instanceof Object;
Object.prototype.isPrototypeOf(map);
Object.getPrototypeOf(map);
Эти голые объекты (или словари) идеально подходят для создания ассоциативных массивов, так как отсутствие
[[Prototype]]
убирает риск наткнуться на конфликт имён. И даже лучше! После того, как мы лишили объект всех унаследованных методов и свойств, любые попытки использовать его не по прямому назначению (хранилище), будут приводить к ошибкам:
var map = Object.create(null);
map + "";
Нет ни примитивного значения, ни строкового представления. Голые объекты предназначены лишь для работы в качестве хранилища пар ключ-значение и точка.
Имейте в виду, что метода
hasOwnProperty
тоже больше нет, да он и не нужен, так как оператор
in
теперь прекрасно работает без каких-либо проверок.
var map = Object.create(null);
'toString' in map;
Более того, те утомительные циклы
for … in
теперь стали гораздо проще. Наконец-то мы можем без опаски писать их так, как они и должны выглядеть:
var map = Object.create(null);
for(var key in map){
}
Несмотря на внесенные изменения, мы можем по-прежнему делать с объектами всё что нужно, как то использовать точечную нотацию или квадратные скобочки, превращать их в строку или использовать объект как контекст для любого метода из
Object.prototype
:
var map = Object.create(null);
Object.defineProperties(map, {
'foo': {
value: 1,
enumerable: true
},
'bar': {
value: 2,
enumerable: false
}
});
map.foo;
map['bar'];
JSON.stringify(map);
{}.hasOwnProperty.call(map, 'foo');
{}.propertyIsEnumerable.call(map, 'bar');
Даже различные способы проверки типов по прежнему будут работать:
var map = Object.create(null);
typeof map;
{}.toString.call(map);
{}.valueOf.call(map);
Все это делает возможным без проблем заменить обычные объекты, используемые для создания ассоциативных массивов, на голые объекты, как в новых, так и в любых ранее созданных приложениях.
Заключение
Если говорить о простых хранилищах пар ключ-значение, то голые объекты справятся с этой задачей однозначно лучше обычных объектов, избавив разработчика от всего лишнего. Для более функциональных структур данных придется подождать ES6 (ES2015), который предоставит нам нативные ассоциативные массивы в виде объектов
Map,
Set и других. А пока этот радужный момент не настал, голые объекты — лучший выбор.