在 JavaScript 中,Map(ES6 新增的键值对集合)和普通 Object(对象)虽然都能实现"键值对"存储,但设计初衷、特性和适用场景有本质区别。以下是全方位对比,结合示例和场景帮你理解核心差异:
一、核心定位差异
- Object:JS 语言的基础数据类型,设计初衷是「对象」(包含属性、方法、原型链),键值对只是其附带功能,并非专门为"键值存储"优化。
- Map:ES6 专为「键值对集合」设计的内置对象,无原型链干扰,优化了动态键值的增删、迭代、查询等操作。
二、关键特性对比(附示例)
| 特性维度 | Object(普通对象) | Map(映射) |
|---|---|---|
| 键的类型限制 | 键只能是 字符串/Symbol,数字会被自动转为字符串(如 obj[1] === obj['1'])。 |
键可以是任意类型(对象、数组、函数、数字、字符串、Symbol、null/undefined 等),无类型转换。 |
| 键的顺序 | ES6 后规则复杂: 1. 数字键(升序)→ 字符串键(插入序)→ Symbol 键(插入序); 2. 原型链属性不参与顺序。 | 严格遵循插入顺序存储/迭代,所有键(无论类型)都按插入先后排列。 |
| 大小获取 | 无原生 size 属性,需手动计算:Object.keys(obj).length(仅统计自有可枚举键)。 |
内置 size 属性,直接返回键值对总数(无需遍历)。 |
| 迭代方式 | 不能直接迭代,需先转数组(Object.keys/values/entries); for...in 会遍历原型链属性,需配合 hasOwnProperty 过滤。 |
可直接迭代(for...of/forEach),迭代器返回 [key, value] 数组; 无需处理原型链,只遍历自身键。 |
| 原型链干扰 | 继承 Object.prototype,默认包含 toString/hasOwnProperty 等原型键,可能与自定义键冲突(如 obj['toString'] 会覆盖原型方法)。 |
原型是 Map.prototype,无默认键,不会出现"原型键冲突"问题。 |
| 增删/查询方法 | 需结合静态方法/语法: - 新增/修改:obj.key = val 或 obj['key'] = val; - 查询:obj.key; - 判断存在:obj.hasOwnProperty('key')(需排除原型); - 删除:delete obj.key(性能差)。 |
内置语义化方法,无歧义: - 新增/修改:map.set(key, val); - 查询:map.get(key); - 判断存在:map.has(key); - 删除:map.delete(key)(性能优); - 清空:map.clear()(一键清空)。 |
| 性能 | 适合「静态键值」(键名提前确定、少量数据),频繁增删/大量数据时性能差。 | 适合「动态键值」(频繁增删、大量数据),迭代/增删效率远高于 Object(引擎专门优化)。 |
| 序列化支持 | 可直接 JSON.stringify()(但 Symbol 键、函数键会丢失,对象键转为 "[object Object]")。 |
默认 JSON.stringify(map) 得到 {},需手动转换(如 JSON.stringify([...map]))。 |
示例:核心差异可视化
1. 键的类型差异
javascript
// Object:非字符串键被强制转换
const obj = {};
const keyObj = { id: 1 };
obj[keyObj] = "value"; // 键被转为字符串 "[object Object]"
console.log(obj["[object Object]"]); // "value"
console.log(obj[1] === obj["1"]); // true(数字转字符串)
// Map:任意类型键,精准匹配
const map = new Map();
map.set(keyObj, "value");
map.set(1, "num1");
map.set("1", "str1");
console.log(map.get(keyObj)); // "value"(精准匹配对象键)
console.log(map.get(1)); // "num1"(不与 "1" 混淆)
2. 键的顺序差异
javascript
// Object:数字键升序,字符串键插入序
const obj = {
"3": "three",
"1": "one",
"b": "b",
"a": "a"
};
console.log(Object.keys(obj)); // ['1','3','b','a'](数字升序,字符串插入序)
// Map:严格插入序
const map = new Map();
map.set("3", "three");
map.set("1", "one");
map.set("b", "b");
map.set("a", "a");
console.log([...map.keys()]); // ['3','1','b','a'](插入顺序)
3. 迭代与大小差异
javascript
// Object:手动计算大小 + 迭代需转数组
const obj = { name: "张三", age: 18 };
console.log(Object.keys(obj).length); // 2(手动计算大小)
// 迭代需先转 entries
for (const [key, value] of Object.entries(obj)) {
console.log(key, value); // name 张三 → age 18
}
// Map:原生 size + 直接迭代
const map = new Map([["name", "张三"], ["age", 18]]);
console.log(map.size); // 2(原生 size)
// 直接 for...of 迭代
for (const [key, value] of map) {
console.log(key, value); // name 张三 → age 18
}
// forEach 迭代
map.forEach((value, key) => console.log(key, value));
三、适用场景选择
优先用 Object 的场景
- 作为「数据模型」:存储固定结构的业务数据(如用户信息
{ name: '张三', age: 18 }),需结合原型/方法(如user.sayHi())。 - 需要 JSON 序列化:如接口请求/响应、本地存储(
localStorage),Object 可直接序列化。 - 静态键值对:键名提前确定、少量数据,且需简洁的点访问(
obj.key)。 - 需利用原型链:如继承公共方法/属性。
优先用 Map 的场景
- 动态键值对:键名不确定、频繁增删(如缓存、临时数据存储)。
- 键为非字符串类型:如用对象/数组作为键(如
map.set(domElement, domData))。 - 依赖键的插入顺序:如日志记录、时序数据迭代。
- 大量数据/高频迭代:如大数据集的遍历、筛选(Map 迭代性能远优于 Object)。
- 避免原型冲突:如键可能是
toString/hasOwnProperty等原型方法名。
四、补充注意点
-
Map的get/set语法比 Object 的点访问稍繁琐,但语义更清晰(避免obj.key中键名是保留字的问题,如obj.class)。 -
Object的delete操作性能差,若需频繁删除键,优先用 Map。 -
若需将 Map 转为 Object(如序列化),可手动转换:
javascriptconst map = new Map([["name", "张三"], ["age", 18]]); const obj = Object.fromEntries(map); // { name: '张三', age: 18 } -
若需将 Object 转为 Map,可用:
javascriptconst obj = { name: '张三', age: 18 }; const map = new Map(Object.entries(obj));
总结
| 选择依据 | 选 Object | 选 Map |
|---|---|---|
| 存储"对象模型"(有属性/方法) | ✅ | ❌ |
| 键是对象/数组等非字符串类型 | ❌ | ✅ |
| 频繁增删/大量数据 | ❌ | ✅ |
| 依赖插入顺序迭代 | ❌ | ✅ |
| 需要 JSON 序列化 | ✅ | ❌ |
| 静态少量键值、点访问更简洁 | ✅ | ❌ |
简单来说:Object 适合"静态数据模型",Map 适合"动态键值集合"。