JavaScript 中 Map 和 Object 的区别是什么?
·
JavaScript 中 Map 与 Object 的区别
一、基本概念
// Object:键值对集合,键只能是字符串或 Symbol
let obj = { name: "张三", age: 25 };
obj["name"]; // "张三"
obj[Symbol("id")]; // 也支持 Symbol 键
// Map:键值对集合,键可以是任意类型
let map = new Map();
map.set("name", "张三");
map.set(25, "年龄"); // 数字键
map.set({ a: 1 }, "对象键"); // 对象键
map.set(function(){}, "函数键"); // 函数键
本质区别:Object 是"属性的无序集合",Map 是"键值对的有序映射"。
二、核心区别对比
| 维度 | Object | Map |
|---|---|---|
| 键的类型 | 字符串或 Symbol | 任意类型(对象、函数、基本类型) |
| 键的顺序 | 整数键按升序,其他按插入顺序(ES6+) | 严格按插入顺序 |
| 长度 | Object.keys(obj).length 手动计算 |
map.size 直接获取 |
| 性能 | 大量增删时性能下降 | 频繁增删性能稳定 |
| 迭代 | 需 Object.keys/values/entries |
原生可迭代(for...of) |
| 序列化 | JSON.stringify 原生支持 |
不支持,需手动转换 |
| 原型链 | 有原型,可能继承意外属性 | 无原型,干净无继承 |
| 安全性 | 可被原型污染攻击 | 无原型,天然安全 |
三、逐项详解
3.1 键的类型——最核心的区别
Object 的键会被隐式转为字符串
let obj = {};
obj[1] = "数字1";
obj["1"] = "字符串1";
// 1 被转为 "1",两个赋值指向同一个键
obj; // { "1": "字符串1" } ← 覆盖了!
obj[true] = "布尔值";
obj; // { "1": "字符串1", "true": "布尔值" }
// true 被转为 "true"
obj[{ a: 1 }] = "对象";
obj; // { "1": "字符串1", "true": "布尔值", "[object Object]": "对象" }
// 对象被转为 "[object Object]"
Map 的键保持原始类型
let map = new Map();
let key1 = 1;
let key2 = "1";
map.set(key1, "数字1");
map.set(key2, "字符串1");
map.get(1); // "数字1"
map.get("1"); // "字符串1"
// 1 和 "1" 是不同的键
map.size; // 2
let objKey = { a: 1 };
let objKey2 = { a: 1 };
map.set(objKey, "对象A");
map.set(objKey2, "对象B");
map.get(objKey); // "对象A"
map.get(objKey2); // "对象B"
// 两个内容相同的对象是不同的键(引用不同)
Map 用什么判断键相等?—— SameValueZero 算法
SameValueZero:
- 基本类型:值严格相等(类似 ===,但 NaN === NaN 为 true)
- 引用类型:同一引用
- 0 === -0 为 true(与 Object.is 不同)
let map = new Map();
map.set(NaN, "不是数字");
map.get(NaN); // "不是数字" ← NaN 等于 NaN
map.set(0, "零");
map.get(-0); // "零" ← 0 和 -0 相等
3.2 键的顺序
Object 的键顺序(ES6+ 规范化):
规则:
1. 整数键(如 "1"、"2"、"10")→ 按数值升序排列
2. 其他字符串键 → 按插入顺序排列
3. Symbol 键 → 按插入顺序排列
let obj = {};
obj["b"] = 2;
obj["1"] = 1;
obj["a"] = 3;
obj["2"] = 2;
obj["c"] = 4;
Object.keys(obj);
// ["1", "2", "b", "a", "c"]
// 整数键 "1"、"2" 被提前并按数值升序,其余按插入顺序
Map 的键顺序:始终严格按插入顺序
let map = new Map();
map.set("b", 2);
map.set(1, 1);
map.set("a", 3);
map.set(2, 2);
map.set("c", 4);
[...map.keys()];
// ["b", 1, "a", 2, "c"]
// 严格按插入顺序,没有"整数键提前"的特殊行为
3.3 长度获取
// Object:需要遍历计算
let obj = { a: 1, b: 2, c: 3 };
Object.keys(obj).length; // 3
Reflect.ownKeys(obj).length; // 3
// 每次调用都要创建临时数组,O(n) 复杂度
// Map:直接读取属性
let map = new Map([["a", 1], ["b", 2], ["c", 3]]);
map.size; // 3
// O(1) 复杂度
3.4 增删查操作
// ── Object ──
let obj = { a: 1, b: 2 };
obj.c = 3; // 增
obj.a = 10; // 改
obj.a; // 查 → 10
delete obj.b; // 删
"a" in obj; // 检查存在 → true(含原型链)
obj.hasOwnProperty("a"); // 检查存在(仅自身)
// 删除不存在的键不报错
delete obj.nonexist; // true(静默成功)
// ── Map ──
let map = new Map([["a", 1], ["b", 2]]);
map.set("c", 3); // 增
map.set("a", 10); // 改
map.get("a"); // 查 → 10
map.delete("b"); // 删 → true
map.has("a"); // 检查存在 → true
// 删除不存在的键返回 false
map.delete("nonexist"); // false(明确告诉你没删到)
区别:delete 总返回 true,map.delete() 对不存在的键返回 false,反馈更明确。
3.5 迭代
// ── Object:不可直接迭代 ──
let obj = { a: 1, b: 2, c: 3 };
// 必须用辅助方法
for (let key of Object.keys(obj)) { ... }
for (let [key, val] of Object.entries(obj)) { ... }
Object.values(obj).forEach(val => { ... });
// for...in 会遍历原型链
for (let key in obj) {
if (obj.hasOwnProperty(key)) { ... } // 必须手动过滤
}
// ── Map:原生可迭代 ──
let map = new Map([["a", 1], ["b", 2], ["c", 3]]);
for (let [key, val] of map) { ... } // 直接迭代
for (let key of map.keys()) { ... } // 只迭代键
for (let val of map.values()) { ... } // 只迭代值
for (let [key, val] of map.entries()) { ... }
// 可直接使用解构
let entries = [...map]; // [["a",1], ["b",2], ["c",3]]
// Map 没有原型链,for...in 不会遍历到额外属性
3.6 JSON 序列化
// ── Object:原生支持 ──
let obj = { name: "张三", age: 25 };
JSON.stringify(obj); // '{"name":"张三","age":25}'
JSON.parse('{"name":"张三","age":25}');
// { name: "张三", age: 25 }
// ── Map:不支持 ──
let map = new Map([["name", "张三"], ["age", 25]]);
JSON.stringify(map); // '{}' ← 空对象,数据全部丢失!
// 手动转换
// Map → JSON
function mapToJson(map) {
return JSON.stringify([...map]);
}
mapToJson(map); // '[["name","张三"],["age",25]]'
// JSON → Map
function jsonToMap(json) {
return new Map(JSON.parse(json));
}
jsonToMap('[["name","张三"],["age",25]]'); // Map(2) {"name"=>"张三", "age"=>25}
3.7 原型链与安全性
// ── Object:有原型链 ──
let obj = {};
obj.toString; // [Function: toString] ← 来自 Object.prototype
"toString" in obj; // true ← 自身没有,但原型上有
// 原型污染风险
let malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
// 如果不安全地合并,可能污染整个原型链
// 安全检查
{}.hasOwnProperty("toString"); // false(自身没有)
{}.toString(); // "[object Object]"(但能用)
// ── Map:无原型链 ──
let map = new Map();
map.get("toString"); // undefined ← 没有原型方法
map.has("toString"); // false
// 天然免疫原型污染
3.8 性能特征
| 操作 | Object | Map |
|---|---|---|
| 创建 | 极快 | 略慢(需初始化内部结构) |
| 添加键值对 | 极快 | 极快 |
| 读取 | 极快(V8 有优化) | 极快 |
| 删除键 | 较慢(可能触发去优化) | 极快 |
| 大量增删 | 性能下降 | 性能稳定 |
| 大量数据查找 | 快(引擎优化好) | 快 |
为什么 Map 在频繁增删场景表现更好?
Object 的内部实现:
V8 将 Object 的属性存储分为"快属性"和"慢属性"
- 快属性:线性存储,查找极快,但增删可能需要重排
- 慢属性:哈希表存储,增删快但查找慢
- 大量增删会导致 V8 在快/慢模式间切换,性能不稳定
Map 的内部实现:
始终使用哈希表
- 增删查找性能稳定
- 不会出现模式切换问题
- V8 对 Map 的哈希实现做了专门优化
四、各场景下的选型建议
用 Object 的场景
| 场景 | 原因 |
|---|---|
| 结构固定的配置/选项 | { theme: "dark", lang: "zh" },语义更明确 |
| 传入函数的选项参数 | function fetch(options),Object 天然适合 |
| 需要序列化/反序列化 | JSON 原生支持 |
| 简单的键值映射(键是字符串) | 无需 Map 的额外开销 |
| 类/构造函数实例 | class User { constructor() { this.name = ""; } } |
| 与第三方库交互 | 大多数库期望 Object 格式 |
// 典型应用:配置对象
const config = {
host: "localhost",
port: 3000,
debug: false,
retries: 3
};
// 典型应用:函数选项
function createDB({ host, port, user, password }) {
// ...
}
createDB({ host: "localhost", port: 3306 });
// 典型应用:API 响应
// 后端返回 JSON → 前端自动解析为 Object
用 Map 的场景
| 场景 | 原因 |
|---|---|
| 键不是字符串(对象/函数/数字作键) | Object 的键会被转为字符串 |
| 频繁增删键值对 | Map 性能稳定 |
| 需要知道集合大小 | map.size O(1) |
| 需要保持插入顺序 | Map 严格按插入顺序迭代 |
| 需要快速迭代 | 原生可迭代 |
| 需要避免原型污染 | Map 没有原型链 |
// 典型应用:以对象为键的缓存
const cache = new Map();
let element = document.getElementById("app");
let data = computeData(element);
cache.set(element, data); // 用 DOM 元素作键
cache.get(element); // 快速查找
// 典型应用:计数/分类
const categories = new Map();
categories.set("electronics", new Set());
categories.set("books", new Set());
categories.get("electronics").add("手机");
// 典型应用:关联数据(键-值都是对象)
const userRoles = new Map();
let alice = { name: "Alice" };
let bob = { name: "Bob" };
userRoles.set(alice, ["admin", "editor"]);
userRoles.set(bob, ["viewer"]);
// 典型应用:字符串与字符串的映射(也适合)
const translations = new Map([
["hello", "你好"],
["world", "世界"],
["bye", "再见"]
]);
translations.get("hello"); // "你好"
五、互转方法
// Object → Map
let obj = { a: 1, b: 2 };
let map = new Map(Object.entries(obj));
// Map(2) {"a" => 1, "b" => 2}
// Map → Object
let map2 = new Map([["a", 1], ["b", 2]]);
let obj2 = Object.fromEntries(map2);
// { a: 1, b: 2 }
// Object.entries / Object.fromEntries 是两者互通的桥梁
六、WeakMap——Map 的特殊变体
| 特性 | Map | WeakMap |
|---|---|---|
| 键的类型 | 任意类型 | 只能是对象 |
| 引用方式 | 强引用 | 弱引用 |
| 可迭代 | 是 | 否(没有 keys/values/entries/size) |
| 垃圾回收 | 不自动回收 | 键对象无其他引用时自动回收 |
// WeakMap 的典型用途:给对象附加私有数据
let privateData = new WeakMap();
class User {
constructor(name, password) {
this.name = name;
// 密码存在 WeakMap 中,不暴露在实例上
privateData.set(this, { password: password });
}
checkPassword(input) {
return privateData.get(this).password === input;
}
}
let user = new User("alice", "secret123");
user.name; // "alice"
user.password; // undefined ← 外部无法访问
user.checkPassword("123"); // false
user = null; // user 对象被回收后,WeakMap 中的对应条目自动清除
// 密码数据不会泄漏,也不会造成内存泄漏
七、速查对照表
| 功能 | Object | Map |
|---|---|---|
| 创建 | {} 或 new Object() |
new Map() 或 new Map([[k,v]]) |
| 增 | obj.key = val 或 obj[key] = val |
map.set(key, val) |
| 查 | obj.key 或 obj[key] |
map.get(key) |
| 删 | delete obj[key] |
map.delete(key) |
| 检查存在 | key in obj 或 obj.hasOwnProperty(key) |
map.has(key) |
| 清空 | 遍历 delete 或重新赋值 | map.clear() |
| 获取大小 | Object.keys(obj).length |
map.size |
| 获取所有键 | Object.keys(obj) |
map.keys() |
| 获取所有值 | Object.values(obj) |
map.values() |
| 获取所有条目 | Object.entries(obj) |
map.entries() |
| 迭代 | for...in(含原型链)或 for...of Object.entries() |
for...of(原生) |
| JSON 序列化 | 原生支持 | 不支持 |
八、总结
Object 和 Map 都是键值对集合,但设计目标和适用场景不同。
Object Map 定位 通用的数据容器,语言基石 专门的哈希映射数据结构 键 字符串和 Symbol 任意类型 原型 有(可能被污染) 无(干净) JSON 原生支持 不支持 频繁增删 性能不稳定 性能稳定 简单原则:键是字符串/Symbol、需要 JSON 交互、结构固定的配置——用 Object。键是任意类型、频繁增删、需要迭代或大小统计——用 Map。
更多推荐

所有评论(0)