предисловие
В ежедневных проектах JavaScript наиболее часто используемые структуры данных представляют собой различные формы пар ключ-значение. В JavaScript, кроме самых основныхObjectВ дополнение к этому формату ES6 добавилMapТо же самое верно и для пар ключ-значение. Их использование очень близко во многих случаях. Я не знаю, был ли кто-нибудь так же запутан, как я, о том, какой из них выбрать для использования? В недавнем проекте Bencai я снова столкнулся с такими неприятностями, поэтому просто не стал делать оба, чтобы сравнить, какой из них использовать.
В этой статье речь пойдетObjectа такжеMapразные, сравните их с разных точек зренияObjectа такжеMap:
- Различия в использовании: использование в некоторых случаях может сильно отличаться
- Синтаксическая разница: синтаксическая разница между созданием и добавлением, удалением и модификацией.
- Разница в производительности: скорость и объем памяти
Я надеюсь, что после прочтения этой статьи вы сможете сделать более правильный выбор в своих будущих проектах.
Сравнение использования
-
для
ObjectС точки зрения своего ключа (ключа) тип может быть только строкой, числом илиSymbol; и дляMap, это может бытьлюбой тип. (включая дату, карту или пользовательские объекты) -
Mapэлементы волисохранить порядок, в котором они были вставлены;а такжеObjectПорядок размещения не будет полностью сохранен, а будет отсортирован в соответствии со следующими правилами:- неотрицательное целое числобудут перечислены первыми в порядке возрастания от наименьшего к наибольшему
- Затем будут перечислены все строки, отрицательные целые числа и числа с плавающей запятой в том порядке, в котором они были вставлены.
- будет указан последним
Symbol,SymbolТакже отсортированы по порядку размещения
-
читать
MapДлина проста, просто назовите ее.size()метод; и читатьObjectДлина требует дополнительных расчетов:Object.keys(obj).length -
Картаповторяемый объект, поэтому пары ключ-значение можно передать через
for ofпетля или.forEach()метод для итерации; обычные пары ключ-значение объекта не являются итерируемыми по умолчанию и могут использоваться толькоfor inцикл для доступа (или использованияObject.keys(o)、Object.values(o)、Object.entries(o)чтобы получить число, представляющее ключ или значение) порядок, в котором он повторяется, указан выше.const o = {}; const m = new Map(); o[Symbol.iterator] !== undefined; // false m[Symbol.iterator] !== undefined; // true -
существует
MapКогда добавляется новый ключ, он не перезаписывает ключ своего прототипа;ObjectКогда новый ключ добавляется в середине, можно перезаписать ключ на его прототипе:Object.prototype.x = 1; const o = {x:2}; const m = new Map([[x,2]]); o.x; // 2,x = 1 被覆盖了 m.x; // 1,x = 1 不会被覆盖 -
JSONПоддержка по умолчаниюObjectне поддерживаетсяMap. Если вы хотите пройтиJSONкоробка передачMapвам нужно использовать.toJSON()метод, то вJSON.parse()входящийФункция восстановлениявосстановить его.для
JSONЯ не буду вдаваться в подробности здесь, заинтересованные друзья могут взглянуть на это:Сериализация и парсинг JSONconst o = {x:1}; const m = new Map([['x', 1]]); const o2 = JSON.parse(JSON.stringify(o)); // {x:1} const m2 = JSON.parse(JSON.stringify(m)) // {}
синтаксический контраст
Разница при создании
Obejct
const o = {}; // 对象字面量
const o = new Object(); // 调用构造函数
const o = Object.create(null); // 调用静态方法 Object.create
дляObjectНапример, мы бы выбрали в 95%+ случаевлитерал объекта, который не только проще всего написать, но и более эффективен с точки зрения скорости, чем приведенный ниже вызов функции. Для конструкторов единственным возможным вариантом использования является явная инкапсуляция примитивного типа; иObject.createОбъекты могут быть прототипированы.
Map
const m = new Map(); // 调用构造函数
а такжеObjectразные,MapЕсть не так много наворотов для создания, обычно просто используйте ихКонструкторсоздавать.
Помимо вышеперечисленных методов, мы также можемFunction.prototype.apply()、Function.prototype.call()、reflect.apply()、Reflect.construct()способ вызоваObjectа такжеMapконструктор илиObject.create()метод, который не будет здесь распространяться.
Разница при добавлении/чтении/удалении элементов
Obejct
const o = {};
//新增/修改
o.x = 1;
o['y'] = 2;
//读取
o.x; // 1
o['y']; // 2
//或者使用 ES2020 新增的条件属性访问表达式来读取
o?.x; // 1
o?.['y']; // 2
//删除
delete o.b;
Для новых элементов первый способ кажется проще, но он имеет некоторые ограничения:
- Имя свойстване может содержать пробелы и знаки препинания
- Имя свойстване может начинаться с цифры
Подробнее о выражениях условного доступа к свойствам см. здесь:Выражения условного доступа к свойствам
Map
const m = new Map();
//新增/修改
m.set('x', 1);
//读取
map.get('x');
//删除
map.delete('b');
Для простых добавлений, удалений и измененийMapОписанный выше метод также очень удобен в использовании, однако при выполнении операций связыванияMapИспользование в немного раздуто:
const m = new Map([['x',1]]);
// 若想要将 x 的值在原有基础上加一,我们需要这么做:
m.set('x', m.get('x') + 1);
m.get('x'); // 2
const o = {x: 1};
// 在对象上修改则会简单许多:
o.x++;
o.x // 2
Сравнение производительности
Далее давайте обсудимObjectа такжеMapпредставление. Я не знаю, слышали ли вы, что производительность карты лучше, чем у объекта, я все равно видел это много раз, даже упоминалось в JS Elevation 4.MapВ сравненииObjectОднако обобщения производительности очень общие, поэтому я планирую провести несколько тестов, чтобы сравнить различия.
метод тестирования
Тесты производительности, которые я провел здесь, основаны надвигатель v8из. Скорость придет со стандартной библиотекой JSperformance.now()функция судить, использование памяти будет определятьсяChrome devtoolсерединаmemoryпроверить.
Для тестов скорости, потому что одна операция слишком быстрая, много разperformance.now()вернет 0. Итак, я сделал 10000 циклов и оценил разницу во времени. Поскольку сам цикл займет часть времени, следующий тест можно использовать только в качестве приблизительного ориентира.
производительность при создании
Код теста выглядит следующим образом:
let n, n2 = 5;
// 速度
while (n2--) {
let p1 = performance.now();
n = 10000;
while (n--) { let o = {}; }
let p2 = performance.now();
n = 10000;
while (n--) { let m = new Map(); }
let p3 = performance.now();
console.log(`Object: ${(p2 - p1).toFixed(3)}ms, Map: ${(p3 - p2).toFixed(3)}ms`);
}
// 内存
class Test {}
let test = new Test();
test.o = o;
test.m = m;
Первое сравнение заключается в созданииObjectа такжеMapпроизводительность в то время. Скорость создания следующая:
Мы можем найти, что созданиеObjectбудет быстрее, чемMap. Для использования памяти это выглядит следующим образом:
В основном мы ориентируемся на егоRetained Size, который представляетдля него отведено место. (т.е. объем памяти, освобождаемой при удалении)
Сравнивая, мы можем найти, что пустоеObjectбудет пустMapЗанимает меньше места внутри. Итак, этот раундObjectВыиграй фишку.
Производительность при добавлении элементов
Код теста выглядит следующим образом:
console.clear();
let n, n2 = 5;
let o = {}, m = new Map();
// 速度
while (n2--) {
let p1 = performance.now();
n = 10000;
while (n--) { o[Math.random()] = Math.random(); }
let p2 = performance.now();
n = 10000;
while (n--) { m.set(Math.random(), Math.random()); }
let p3 = performance.now();
console.log(`Object: ${(p2 - p1).toFixed(3)}ms, Map: ${(p3 - p2).toFixed(3)}ms`);
}
// 内存
class Test {}
let test = new Test();
test.o = o;
test.m = m;
Скорость новых элементов следующая:
Мы можем обнаружить, что при создании нового элементаMapбудет быстрее, чемObject. Для использования памяти это выглядит следующим образом:
Для сравнения мы можем обнаружить, что когда у нас есть определенное количество элементов,Objectбыло бы лучше, чемMapЗанять~78% больше памяти. Я также провел несколько тестов и обнаружил, что при достаточном количестве элементов этот процент довольно стабилен. Поэтому, когда нужно выполнить много новых операций и сохранить много данных, используйтеMapбудет более эффективным.
Производительность при чтении элементов
Код теста выглядит следующим образом:
let n;
let o = {}, m = new Map();
n = 10000;
while (n--) { o[Math.random()] = Math.random(); }
n = 10000;
while (n--) { m.set(Math.random(), Math.random()); }
let p1 = performance.now();
for (key in o) { let k = o[key]; }
let p2 = performance.now();
for ([key] of m) { let k = m.get(key); }
let p3 = performance.now();
`Object: ${(p2 - p1).toFixed(3)}ms, Map: ${(p3 - p2).toFixed(3)}ms`
Скорость чтения элементов следующая:
В сравнении можно найтиObjectНебольшое преимущество, но в целом особой разницы нет.
Производительность при удалении элементов
Я не знаю, слышали ли выdeleteПроизводительность оператора низкая, и во многих случаях даже лучше установить значениеundefinedбез использованияdeleteзаявление оператора. Но по фактуv8С недавними оптимизациями его эффективность значительно улучшилась.
Код теста выглядит следующим образом:
let n;
let o = {}, m = new Map();
n = 10000;
while (n--) { o[Math.random()] = Math.random(); }
n = 10000;
while (n--) { m.set(Math.random(), Math.random()); }
let p1 = performance.now();
for (key in o) { delete o[key]; }
let p2 = performance.now();
for ([key] of m) { m.delete(key); }
let p3 = performance.now();
`Object: ${(p2 - p1).toFixed(3)}ms, Map: ${(p3 - p2).toFixed(3)}ms`
Скорость удаления элементов следующая:
Мы можем обнаружить, что при выполнении операции удаленияObjectСкорость будет немного лучше, но общая разница на самом деле невелика.
Особый случай
На самом деле, помимо самого основного случая, есть и частный случай. Помните, что мы упоминали ранееObjectЗаказ ключей? Мы упоминали, что неотрицательные целые числа перечислены первыми. На самом деле длянеотрицательное целое число в качестве ключастоимость иостальные типы как ключиДля значения ,v8будет относиться к ним по-разному. Часть отрицательного целого числа в качестве ключа будет рассматриваться как массив, то есть, когда неотрицательное целое число имеет определенную непрерывность, оно будет рассматриваться как быстрый массив, а если оно слишком разреженное, оно будет рассматриваться как медленный массив.
Для быстрых массивов он имеет непрерывную память, поэтому операции чтения и записи выполняются быстрее и занимают меньше памяти. Для получения дополнительной информации см. это:Изучите базовую реализацию «массива» в движке JS V8.
Когда ключи представляют собой последовательные неотрицательные целые числа, производительность выглядит следующим образом:
Мы видим, чтоObjectне толькоСредняя скорость выше, который занимаетПамять тоже сильно уменьшилась.
Суммировать
Сравнивая, мы можем найти, чтоMapа такжеObjectКаждый из них имеет свои достоинства, и мы должны делать разные выборы для разных ситуаций. Итак, я подытожил то, что я думал использоватьMapа такжеObjectболее подходящее время.
использоватьMap:
- Сохраненный ключ не является строкой/числом/или
Symbol, ВыбратьMap,потому чтоObjectне поддерживается - При хранении больших объемов данных выберите
Map, потому что он занимает меньше памяти - Если требуется много операций по добавлению/удалению элементов, выберите
Map, потому что быстрее - Если вам нужно сохранить порядок вставки, выберите
Map,потому чтоObjectизменит порядок - Если вам нужно выполнить итерацию/обход, выберите
Map, так как по умолчанию это итерируемый объект, итерация более удобна
использоватьObject:
- просто простые структуры данных при выборе
Object, потому что он занимает меньше памяти, когда данных меньше, и более эффективен, когда новые - нужно использовать
JSONПри передаче файла выберитеObject,потому чтоJSONНе поддерживается по умолчаниюMap - Если вам нужно выполнить операции с несколькими ключевыми значениями, выберите
Object, потому что синтаксис более лаконичный - Когда вам нужно переопределить ключи на прототипе, выберите
Object
несмотря на то чтоMapво многих случаях, чемObjectболееэффективный,ноObjectвсегда бытьJSбольшинствобазовый ссылочный тип, это не только для хранения пар ключ-значение.
Ссылаться на
Изучите базовую реализацию «массива» в движке JS V8.
Shallow, Retained, and Deep Size
Slow delete of object properties in JS in V8
ES6 — Карта против объекта — что и когда?
Расширенное программирование с помощью JavaScript (4-е издание)
JavaScript: The Definitive Guide (7th Edition)
Эта статья участвует в «Весенней рекрутинговой кампании Nuggets 2021», нажмите, чтобы просмотреть:сведения о деятельности