Map 和 Object 都是键值对集合,但在设计和使用上有重要区别:
1. 核心区别对比表
| 特性 | Map | Object |
|---|---|---|
| 键的类型 | 任意类型(对象、函数、NaN等) | String 或 Symbol(其他类型会被转为字符串) |
| 键的顺序 | 保持插入顺序 | ES6 后也保持插入顺序,但有例外 |
| 大小获取 | .size 属性直接获取 |
需要计算:Object.keys(obj).length |
| 默认键 | 无默认键,完全是空的 | 有原型链,可能包含继承的键 |
| 性能 | 频繁增删时性能更好 | 频繁增删时性能较差 |
| 迭代 | 直接可迭代(for...of) |
需要先获取键数组再迭代 |
| 序列化 | 不能直接 JSON 序列化 | 可直接 JSON 序列化 |
| 垃圾回收 | 强引用(除非用 WeakMap) | 强引用 |
2. 详细对比
键的类型
js
// Map:键可以是任意类型
const map = new Map();
const objKey = { id: 1 };
const funcKey = function() {};
const arrKey = [1, 2];
const nanKey = NaN;
map.set(objKey, '对象键');
map.set(funcKey, '函数键');
map.set(arrKey, '数组键');
map.set(nanKey, 'NaN键'); // ✅ NaN 作为键
map.set(null, 'null键');
map.set(undefined, 'undefined键');
// Object:键会被转换为字符串
const obj = {};
obj[objKey] = '对象键'; // 键被转为 "[object Object]"
obj[funcKey] = '函数键'; // 键被转为 "function() {}"
obj[arrKey] = '数组键'; // 键被转为 "1,2"
obj[null] = 'null键'; // 键被转为 "null"
obj[undefined] = 'undefined键'; // 键被转为 "undefined"
顺序保证
js
const map = new Map();
map.set('a', 1);
map.set('b', 2);
map.set('0', 3); // 数字字符串键
map.set('1', 4);
map.set('c', 5);
// Map:严格保持插入顺序
console.log([...map.keys()]); // ['a', 'b', '0', '1', 'c']
const obj = {};
obj['a'] = 1;
obj['b'] = 2;
obj['0'] = 3; // 数字字符串键
obj['1'] = 4;
obj['c'] = 5;
// Object:整数属性(可转为数字的字符串)会按数字顺序排列
console.log(Object.keys(obj)); // ['0', '1', 'a', 'b', 'c']
原型链问题
js
// Object 有原型链继承
const obj = {};
console.log(obj.toString); // ƒ toString() { [native code] } - 来自原型
// 可能导致意外的键冲突
obj['constructor']; // 可能不是你设置的值,而是继承的
obj['__proto__']; // 特殊属性
// Map 完全没有这个问题
const map = new Map();
map.set('constructor', '这是我设置的构造函数');
map.set('__proto__', '这是我设置的属性');
console.log(map.get('constructor')); // '这是我设置的构造函数'
console.log(map.get('__proto__')); // '这是我设置的属性'
性能对比
js
// 频繁增删场景 - Map 性能更好
const map = new Map();
const obj = {};
console.time('Map 添加');
for (let i = 0; i < 100000; i++) {
map.set(i, i);
}
console.timeEnd('Map 添加'); // 通常更快
console.time('Object 添加');
for (let i = 0; i < 100000; i++) {
obj[i] = i;
}
console.timeEnd('Object 添加');
// 频繁删除场景 - Map 优势更明显
console.time('Map 删除');
for (let i = 0; i < 100000; i++) {
map.delete(i);
}
console.timeEnd('Map 删除');
console.time('Object 删除');
for (let i = 0; i < 100000; i++) {
delete obj[i];
}
console.timeEnd('Object 删除'); // 通常更慢
迭代方式
js
const map = new Map([
['name', 'Alice'],
['age', 25]
]);
const obj = {
name: 'Alice',
age: 25
};
// Map:直接可迭代
for (let [key, value] of map) {
console.log(key, value);
}
// Map 的便捷方法
map.forEach((value, key) => console.log(key, value));
// Object:需要先获取键数组
for (let key in obj) {
if (obj.hasOwnProperty(key)) { // 需要过滤原型链
console.log(key, obj[key]);
}
}
// 或使用 Object 方法
Object.keys(obj).forEach(key => console.log(key, obj[key]));
Object.entries(obj).forEach(([key, value]) => console.log(key, value));
3. 使用场景建议
优先使用 Map 的情况:
js
// 1. 键不是字符串或 Symbol
const userRoles = new Map();
userRoles.set(user1, 'admin'); // 对象作为键
userRoles.set(user2, 'editor');
// 2. 频繁增删键值对
const cache = new Map(); // 缓存系统
cache.set(key, value);
cache.delete(key);
// 3. 需要保持严格的插入顺序
const orderedMap = new Map();
orderedMap.set('z', 1);
orderedMap.set('a', 2); // 遍历时保证 z 在 a 之前
// 4. 键值对数量需要频繁获取
if (map.size > 100) { } // 直接获取
// 5. 避免与原型链属性冲突
const safeMap = new Map();
safeMap.set('constructor', '不会冲突');
safeMap.set('__proto__', '安全的');
优先使用 Object 的情况:
js
// 1. 简单的键值对,键都是字符串/Symbol
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
};
// 2. 需要 JSON 序列化/反序列化
const data = { name: 'Alice', age: 25 };
const json = JSON.stringify(data); // 直接支持
const parsed = JSON.parse(json);
// Map 需要转换
const mapData = new Map([['name', 'Alice'], ['age', 25]]);
const mapJson = JSON.stringify([...mapData]); // 需要转为数组
const mapParsed = new Map(JSON.parse(mapJson));
// 3. 需要方法(函数作为值)
const calculator = {
add(a, b) { return a + b; },
multiply(a, b) { return a * b; }
};
// 4. 使用 Object 特殊功能
const obj = Object.create(null); // 创建无原型的对象
Object.defineProperty(obj, 'readonly', {
value: '不可写',
writable: false
});
4. 互相转换
js
// Object → Map
const obj = { a: 1, b: 2 };
const map = new Map(Object.entries(obj));
// Map → Object
const map2 = new Map([['x', 10], ['y', 20]]);
const obj2 = Object.fromEntries(map2);
// Object → Map(处理嵌套)
function objectToMap(obj) {
const map = new Map();
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
map.set(key, typeof value === 'object' && value !== null ?
objectToMap(value) : value);
}
}
return map;
}
5. 特殊注意事项
js
// 1. Map 的相等性判断
const map = new Map();
map.set({}, 'value1');
map.set({}, 'value2'); // 两个空对象是不同的键
console.log(map.size); // 2
// 2. 引用相同的对象是同一个键
const key = { id: 1 };
map.set(key, 'value');
map.set(key, 'new value'); // 更新,不是添加
console.log(map.size); // 3(包含前面的两个空对象)
// 3. NaN 作为键的特殊性
map.set(NaN, 'first');
map.set(NaN, 'second'); // NaN 被视为相同的键
console.log(map.get(NaN)); // 'second'
// 4. 性能权衡
// Object 在已知结构、固定键时,引擎优化更好
// Map 在动态键、频繁增删时表现更好
总结
选择 Map 当:
- 键的类型多样(对象、函数等)
- 需要频繁增删键值对
- 需要保持严格的插入顺序
- 避免与原型链冲突
- 需要直接获取大小
选择 Object 当:
- 键都是字符串/Symbol
- 需要 JSON 序列化
- 需要对象方法(函数作为值)
- 使用 Object 特殊功能(getter/setter、属性描述符)
- 简单的配置对象或数据传输对象
在现代 JavaScript 开发中,对于纯粹的键值对存储,Map 通常是更好的选择,因为它更安全、更灵活且性能更好。Object 更适合表示具有方法和逻辑的实体对象。