Почему не стоит использовать объекты в роли карт в JavaScript?

Java Script

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

Объект позволяет нам отображать строки на значения. Однако, если учесть изъяны объектов в JavaScript и вспомнить, что есть конструктор map, то можно уверенно перестать применять их в качестве карт или словарей.

Наследование и чтение свойств

Обычно, если не определен конкретный прототип, то объекты в JavaScript наследуются от объекта object. Это означает, что мы получаем методы, содержащиеся в прототипе объекта.

Чтобы проверить, содержится свойство в самом объекте или же в его прототипе, нам необходимо использовать метод hasOwnProperty объекта. Это проблематично и об этом легко забыть.

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

let obj = {}

Затем напишем:

'toLocaleString' in obj;

Нам вернется значение true. Так произойдет, потому что оператор in определяет свойства внутри прототипа объекта как часть самого объекта, что нам совсем не нужно при работе со словарями или картами. Чтобы создать чистый объект без прототипа, мы пишем:

let obj = Object.create(null);

Метод create принимает прототип создаваемого объекта как аргумент, и мы получаем объект, не наследованный от прототипа. Встроенные методы вроде toString или toLocaleString не являются перечисляемыми, поэтому не могут быть включены в цикл for…in.

Как бы то ни было, если мы создадим объект с перечисляемыми свойствами:

let obj = Object.create({
  a: 1
});
for (const prop in obj) {
  console.log(prop);
}

То получим логирование свойства a в цикле for…in, проходящем через все собственные и наследованные свойства объекта.

Для игнорирования наследованных свойств мы можем использовать метод объекта hasOwnProperty. Например, написать так:

let obj = Object.create({
  a: 1
});for (const prop in obj) {
  if (Object.hasOwnProperty(prop)) {
    console.log(prop);
  }
}

Теперь ничего не логируется.

Как вы видите, в случае с обычными JavaScript объектами, обращение к значениям при помощи ключей свойств может быть достаточно запутанным.

Переопределение значений свойств

Для простых объектов мы легко можем переопределять и удалять существующие перезаписываемые свойства.

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

let obj = {};
obj.toString = null;

Теперь, если мы запустим:

obj.toString();

То получим ошибку Uncaught TypeError: obj.toString is not a function.

Это серьезная проблема, потому что мы можем легко менять значения любого изначального или наследованного свойства объекта. Как вы видите, мы изменили встроенный метод toString на null при помощи всего одной операции.

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

Прототип объекта

Обращение к прототипу объекта возможно через его свойство _proto_. Это свойство мы можем и получать, и определять. Например, напишем:

let obj = Object.create({
  a: 1
});
obj.__proto__ = {
  b: 1
};

Таким образом мы получим прототип объекта (b: 1). Это означает, что мы изменили прототип obj из изначального (a: 1) в (b1:) простым определением свойства _proto_, принадлежащего obj.

Когда проходим по объекту, используя цикл for…in:

for (const prop in obj) {
  console.log(prop);
}

Логируется b.

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

Решение — собственные перечисляемые свойства

Чтобы избежать получения свойств, наследованных от других объектов, мы можем получать собственные имена свойств, используя object.keys. Этот метод возвращает массив с ключами заданного объекта, но при этом не касается наследованных имен свойств. Например:

let obj = Object.create({
  a: 1
});
console.log(Object.keys(obj));

Здесь при логировании мы получим пустой массив.

Похожим образом Object.entries принимает объект как аргумент и возвращает массив с массивами, имеющими ключ в качестве первого элемента и значение в качестве второго. Например:

let obj = Object.create({
  a: 1
});
console.log(Object.entries(obj));

Здесь мы также получаем пустой лог.

Map в ES6

Лучшее решение  —  использовать объекты ES6 Map, служащие специально для создания карт и словарей.

Объекты Map имеют метод set, который позволяет добавлять ключи и значения, указываемые в этом методе в виде первого и второго аргумента соответственно. Пример определения карт:

let objMap = new Map();
objMap.set('foo', 'bar');
objMap.set('a', 1);
objMap.set('b', 2);

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

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

let objMap = new Map();
objMap.set(1, 'a');

Мы также можем использовать вложенные массивы для определения Map. Например, вместо многоразового указания метода set, мы можем написать так:

const arrayOfKeysAndValues = [
  ['foo', 'bar'],
  ['a', 1],
  ['b', 2]
]
let objMap = new Map(arrayOfKeysAndValues);
console.log(objMap)

Существуют и другие методы: получение записей по ключам, получение всех записей, перебор каждой записи или их удаление.

Для получения записи по ее ключу используйте get:

objMap.get('foo'); // 'bar'

Мы также можем получить значение через нестрочный ключ, что невозможно в случае с объектами. К примеру, у нас имеется:

let objMap = new Map();
objMap.set(true, 'a');

В результате console.log(objMap.get(true)); выведет ‘a’.

Очистить все записи объекта Map мы можем с помощью метода clear:

objMap.clear();

Получить же все записи можно методом objMaps.entries, а перебор мы можем реализовать циклом for…in, поскольку в нем есть итератор.

Заключение

Нам следует перестать применять объекты в качестве словарей. В таком методе заключено слишком много ловушек, поскольку объекты наследуются от object по умолчанию и других объектов в зависимости от установок. Такой способ также допускает переопределение значений методов вроде toString, что нам в большинстве случаев не нужно.

Во избежание всех этих проблем следует использовать объекты Map, которые представлены в ES6. Они имеют специальные методы для получения и внесения записей, а также позволяют нам перебирать эти записи посредством for…of или преобразовывать объекты в массивы.

Специально для сайта ITWORLD.UZ. Новость взята с сайта NOP::Nuances of programming