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 总返回 truemap.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 = valobj[key] = val map.set(key, val)
obj.keyobj[key] map.get(key)
delete obj[key] map.delete(key)
检查存在 key in objobj.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。

更多推荐