技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

Map、Set 的区别,以及 Map 和 Object 的区别

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/49-Map和Set的区别,Map和Object的区别
本文目录10 个章节
  1. 一、Set 是什么
  2. 二、Set 的类型转换和集合运算
  3. 三、Map 是什么
  4. 四、Map 的克隆、合并和转换
  5. 五、Set 与数组去重
  6. 六、Object、Map、Set 的区别
  7. 七、Object 常用方法与注意事项
  8. 八、使用场景
  9. 九、总结
  10. 参考资料

Map、Set 的区别,以及 Map 和 Object 的区别

Category(分类): JavaScript Status: 已整理(2026)

本文保留原文关于 Set 去重、集合运算、Map 遍历、Map/Set/Object 转换、数组去重和 API 对比的内容,并删除抓取产生的 123456 行号、重复章节和失效的 so./so 搜索链接。重点补充 SameValueZero、对象键身份、属性顺序、原型污染、浅拷贝和现代 Set 集合运算方法。

一、Set 是什么

Set 是值的集合,每个值最多出现一次。它可以保存原始值,也可以保存对象引用:

const values = new Set([1, 2, 2, NaN, NaN, -0, 0])

console.log(values.size) // 4:1、2、NaN、0
console.log([...values]) // [1, 2, NaN, 0]

Set 的唯一性使用 SameValueZero 判断:

  • NaNNaN 视为相同;
  • +0-0 视为相同;
  • 其他原始值大致遵循 === 的相等语义;
  • 对象、数组和函数按引用身份判断,不做深度比较。
const firstObject = { id: 1 }
const secondObject = { id: 1 }
const set = new Set([firstObject, secondObject, firstObject])

console.log(set.size) // 2:两个对象内容相同但身份不同
console.log(set.has(firstObject)) // true
console.log(set.has({ id: 1 })) // false

Set 的迭代顺序是成功插入的顺序,不是“无序”:

const set = new Set(['red', 'green', 'blue'])
set.add('red') // 已存在,不改变位置
set.delete('green')
set.add('green') // 删除后重新加入,会排到末尾

console.log([...set]) // ['red', 'blue', 'green']

1. Set 常用属性和方法

API作用
new Set(iterable)创建 Set,可接收可迭代对象
set.add(value)添加值并返回当前 Set
set.has(value)判断值是否存在
set.delete(value)删除值并返回是否删除成功
set.clear()删除所有值
set.size获取成员数量
set.keys()返回值迭代器,是 values() 的别名
set.values()返回值迭代器
set.entries()返回 [value, value] 迭代器
set.forEach(callback, thisArg)按插入顺序遍历

Set 没有通过索引读取成员的 API,需要迭代或转换为数组:

const set = new Set(['a', 'b'])

console.log(set.keys() === set.values()) // false:是两个迭代器对象
console.log([...set.keys()]) // ['a', 'b']
console.log([...set.entries()]) // [['a', 'a'], ['b', 'b']]

keys()values() 每次调用都会返回新的迭代器;它们不是同一个对象,但产生相同的值序列。

二、Set 的类型转换和集合运算

2.1 数组、字符串与 Set

const array = ['value1', 'value2', 'value3']
const fromArray = new Set(array)
const backToArray = [...fromArray]

const fromString = new Set('hello')

console.log(backToArray) // ['value1', 'value2', 'value3']
console.log([...fromString]) // ['h', 'e', 'l', 'o']
console.log(String(fromArray)) // [object Set]:Set 不会自动变成成员拼接字符串

new Set(value) 要求参数是可迭代对象;普通对象 {} 没有默认迭代器,不能直接传入。

2.2 并集、交集和差集

原文使用展开语法模拟集合运算,仍然适用于所有较早的现代环境:

const a = new Set([1, 2, 3])
const b = new Set([4, 3, 2])

const union = new Set([...a, ...b])
const intersection = new Set([...a].filter(value => b.has(value)))
const difference = new Set([...a].filter(value => !b.has(value)))

console.log([...union]) // [1, 2, 3, 4]
console.log([...intersection]) // [2, 3]
console.log([...difference]) // [1]

现代 ECMAScript 已提供集合组合方法,但需要确认目标浏览器/Node.js 版本:

const a = new Set([1, 2, 3])
const b = new Set([3, 4])

console.log([...a.union(b)]) // [1, 2, 3, 4]
console.log([...a.intersection(b)]) // [3]
console.log([...a.difference(b)]) // [1, 2]
console.log([...a.symmetricDifference(b)]) // [1, 2, 4]
console.log(a.isDisjointFrom(new Set([5]))) // true
console.log(a.isSubsetOf(new Set([1, 2, 3, 4]))) // true
console.log(a.isSupersetOf(new Set([1, 2]))) // true

若项目仍需兼容不支持这些方法的旧运行环境,使用前面的展开语法或提供 polyfill。Set 组合方法还支持满足 sizehas()keys() 的 set-like 对象作为参数,Map 的键集合就属于这一类。

三、Map 是什么

Map 是键值对集合。键和值都可以是任意 JavaScript 值,键在同一个 Map 中唯一:

const map = new Map()
const objectKey = { id: 1 }

map.set('name', 'Ada')
map.set(1, 'number key')
map.set(objectKey, 'object key')

console.log(map.get('name')) // Ada
console.log(map.get(1)) // number key
console.log(map.get(objectKey)) // object key
console.log(map.get({ id: 1 })) // undefined:不是同一个对象

如果对同一个键重复 set,只更新值,不增加条目;更新已有键不会把它移动到末尾:

const map = new Map([
  ['first', 1],
  ['second', 2]
])

map.set('first', 100)
console.log([...map.keys()]) // ['first', 'second']

map.delete('first')
map.set('first', 100)
console.log([...map.keys()]) // ['second', 'first']

3.1 Map 常用 API

API作用
new Map(iterable)创建 Map,输入通常是 [key, value] 对的可迭代对象
map.set(key, value)写入或更新并返回当前 Map
map.get(key)获取值,找不到时返回 undefined
map.has(key)判断键是否存在
map.delete(key)删除键并返回是否删除成功
map.clear()清空所有条目
map.size获取条目数量
map.keys()按插入顺序返回键迭代器
map.values()按插入顺序返回值迭代器
map.entries()按插入顺序返回 [key, value] 迭代器
map.forEach(callback, thisArg)遍历 (value, key, map)

Map 的规范要求实现平均访问时间为集合元素数量的次线性复杂度,但规范不强制必须是哈希表,也不保证在每个场景都比 Object 快。不要只凭结论选择数据结构,应结合键类型、操作模式和实际测量。

3.2 Map 的遍历

const map = new Map([
  [0, 'zero'],
  [1, 'one']
])

for (const [key, value] of map) {
  console.log(`${key} = ${value}`)
}

for (const key of map.keys()) {
  console.log(key)
}

for (const value of map.values()) {
  console.log(value)
}

for (const [key, value] of map.entries()) {
  console.log(key, value)
}

map.forEach((value, key, currentMap) => {
  console.log(key, value, currentMap === map)
})

Map.prototype.forEach 的回调参数顺序是 (value, key, map),与 Array.prototype.forEach(value, index, array) 不同。Set 的回调参数虽然是 (value, value, set),第二个参数仍然只是为了与 Map 的回调形状保持兼容。

四、Map 的克隆、合并和转换

4.1 Map 与数组互转

const entries = [
  ['key1', 'value1'],
  ['key2', 'value2']
]

const map = new Map(entries)
const array = Array.from(map)
const keys = [...map.keys()]
const values = [...map.values()]
const pairs = [...map.entries()]

console.log(map.get('key1')) // value1
console.log(array) // [['key1', 'value1'], ['key2', 'value2']]
console.log(keys) // ['key1', 'key2']
console.log(values) // ['value1', 'value2']
console.log(pairs) // [['key1', 'value1'], ['key2', 'value2']]

4.2 Map 的克隆是浅拷贝

const original = new Map([
  ['user', { name: 'Ada' }]
])
const clone = new Map(original)

console.log(original === clone) // false
console.log(original.get('user') === clone.get('user')) // true

new Map(original) 只复制条目和键值引用,不会递归复制键和值。需要深拷贝时,应根据类型选择 structuredClone 或领域级复制方案。

4.3 Map 的合并

后面的重复键覆盖前面的值:

const first = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three']
])
const second = new Map([
  [1, 'uno'],
  [2, 'dos']
])

const merged = new Map([...first, ...second])

console.log(merged.get(1)) // uno
console.log(merged.get(2)) // dos
console.log(merged.get(3)) // three

4.4 Map 转 Object

当业务明确要求 JSON 对象或普通对象时,可以使用 Object.fromEntries

const map = new Map([
  ['yes', true],
  ['no', false]
])

const object = Object.fromEntries(map)
console.log(object) // { yes: true, no: false }

这不是对任意 Map 的无损转换:对象属性键只能是字符串或 Symbol,数字键会转成字符串,对象键会经过属性键转换并可能发生碰撞:

const map = new Map([
  [1, 'number'],
  ['1', 'string']
])

const object = Object.fromEntries(map)
console.log(object) // { '1': 'string' }:两个键在 Object 中冲突

如果键类型很重要,应保留 Map,或使用键值数组、带类型标签的序列化格式。

4.5 Object 转 Map

const object = { a: 1, b: 2 }
const map = new Map(Object.entries(object))

console.log(map.get('a')) // 1

Object.entries 只返回对象自身的可枚举字符串键属性,不包含继承属性和 Symbol 键。若要处理 Symbol,需要另行调用 Object.getOwnPropertySymbols

4.6 查询字符串转对象

const params = new URLSearchParams('foo=bar&baz=qux')
const object = Object.fromEntries(params)

console.log(object) // { foo: 'bar', baz: 'qux' }

同名查询参数可能出现多次,Object.fromEntries 会保留最后一个值;需要保留全部值时使用 params.getAll(name) 或直接遍历 URLSearchParams。

4.7 Map 与 JSON

JSON 没有原生 Map 类型。Map 的字符串键可以显式转换成对象:

function mapToObject(map) {
  return Object.fromEntries(map)
}

const map = new Map([
  ['yes', true],
  ['no', false]
])

const json = JSON.stringify(mapToObject(map))
console.log(json) // {"yes":true,"no":false}

键可能是任意类型时,常见方式是序列化为条目数组:

const map = new Map([
  [true, 7],
  [{ id: 1 }, ['abc']]
])

const json = JSON.stringify([...map])
console.log(json) // [[true,7],[{"id":1},["abc"]]]

解析回 Map:

const map = new Map(JSON.parse('[["a",1],["b",2]]'))
console.log(map.get('a')) // 1

但 JSON 解析出来的对象键是新对象,不可能恢复原 Map 中的对象引用身份。需要可逆持久化时,应设计明确的 schema,而不是只依靠 JSON.stringify([...map])

五、Set 与数组去重

5.1 Set 去重:现代首选

function uniqueBySet(array) {
  if (!Array.isArray(array)) return array
  return [...new Set(array)]
}

console.log(uniqueBySet([1, 2, 2, 3, NaN, NaN])) // [1, 2, 3, NaN]

Set 去重保留第一次出现的顺序,使用 SameValueZero;它不会把结构相同但身份不同的对象合并。

5.2 双重循环和 splice

这是原文的历史方式,原始写法会原地修改输入数组,并且使用 ===,所以不能正确合并 NaN。下面为了避免示例产生隐藏副作用,先复制数组再执行 splice

function uniqueBySplice(array) {
  if (!Array.isArray(array)) return array

  const result = [...array]
  for (let i = 0; i < result.length; i += 1) {
    for (let j = i + 1; j < result.length; j += 1) {
      if (result[i] === result[j]) {
        result.splice(j, 1)
        j -= 1
      }
    }
  }
  return result
}

它适合展示算法思想,不适合作为大数组的高效实现。

5.3 indexOf

function uniqueByIndexOf(array) {
  if (!Array.isArray(array)) return array

  const result = []
  for (const value of array) {
    if (result.indexOf(value) === -1) result.push(value)
  }
  return result
}

console.log(uniqueByIndexOf([NaN, NaN])) // [NaN, NaN]:indexOf 找不到 NaN

5.4 includes

includes 使用 SameValueZero,因此能处理 NaN

function uniqueByIncludes(array) {
  if (!Array.isArray(array)) return array

  const result = []
  for (const value of array) {
    if (!result.includes(value)) result.push(value)
  }
  return result
}

console.log(uniqueByIncludes([NaN, NaN, 1, 1])) // [NaN, 1]

5.5 sort 去重

排序后比较相邻元素可以减少比较次数,但默认 sort() 按字符串顺序排序,并且会修改数组;它也不适合保留原顺序:

function uniqueBySort(array) {
  if (!Array.isArray(array)) return array

  const sorted = [...array].sort((a, b) => a - b)
  const result = []

  for (const value of sorted) {
    const previous = result.at(-1)
    const sameValueZero = value === previous ||
      (value !== value && previous !== previous)

    if (result.length === 0 || !sameValueZero) {
      result.push(value)
    }
  }
  return result
}

console.log(uniqueBySort([10, 2, 2, 1])) // [1, 2, 10]

即使复制数组后排序,结果的顺序语义也已经改变。若要求稳定地保留首次出现顺序,使用 Set 或 Map。

5.6 用普通对象作为哈希表的历史写法

原文使用对象键名不可重复来去重,但普通对象会把键转换成字符串,且可能遇到 __proto__constructor 和对象键碰撞。若只是字符串键,至少使用 null-prototype 对象和 Object.hasOwn

function uniqueByNullObject(array) {
  if (!Array.isArray(array)) return array

  const seen = Object.create(null)
  return array.filter(value => {
    const key = `${typeof value}:${String(value)}`
    if (Object.hasOwn(seen, key)) return false
    seen[key] = true
    return true
  })
}

这个版本仍然不能区分 String({ id: 1 }) 与另一个相同字符串化结果的对象,也不能完整表达 Symbol 和复杂对象身份,因此一般不如 Set/Map。

原文中下面这种写法有两个问题:if (!table[key]) 会把已经存在但值为假值的条目当成不存在,普通对象还会继承原型属性:

// 历史示例:不推荐
function legacyUnique(array) {
  const table = {}
  const result = []

  for (const value of array) {
    if (!table[value]) {
      table[value] = true
      result.push(value)
    }
  }
  return result
}

5.7 用 Map 去重

Map 可以按值身份保存键,适合需要同时记录额外信息的场景:

function uniqueByMap(array) {
  if (!Array.isArray(array)) return array

  const seen = new Map()
  const result = []

  for (const value of array) {
    if (!seen.has(value)) {
      seen.set(value, true)
      result.push(value)
    }
  }
  return result
}

只需要唯一值时 Set 更直接;需要把值映射到索引、计数或元数据时 Map 更合适。

六、Object、Map、Set 的区别

6.1 数据模型

  • Object:属性键值集合,属性键是 String 或 Symbol;适合表示记录、配置和 JSON 对象;
  • Map:任意值到任意值的映射;适合频繁增删、非字符串键、需要明确集合语义的场景;
  • Set:唯一值集合;适合成员存在性判断、去重和集合运算。

6.2 对比表

对比项ObjectMapSet
存储形式属性键 → 值键 → 值
键/值类型键为 String 或 Symbol键和值均可为任意值值可为任意值
唯一性属性键唯一键唯一值唯一
默认条目普通对象有原型继承属性;null-prototype 对象没有没有用户数据条目没有用户数据条目
顺序自有属性有规范顺序,但数字索引键会先排序,遍历 API 范围不同按首次插入键的顺序按插入值的顺序
直接迭代默认不可 for...offor...offor...of
数量Object.keys(obj).lengthmap.sizeset.size
JSON原生支持对象 JSON需自定义转换需自定义转换
对象身份对象只能先转换成属性键对象键按引用身份对象值按引用身份
原型普通对象有 Object.prototype实例继承 Map.prototype,但条目不在对象属性上实例继承 Set.prototype,但成员不在对象属性上

“Object 没有顺序、Map 有顺序”是入门时期的简化。现代 ECMAScript 已规定对象自有属性的键顺序,但规则比 Map 的插入顺序复杂:整数索引键通常按数值升序,其他字符串键和 Symbol 键按创建顺序;不同反射 API 还会筛选不同的属性集合。若数据本身是有序键值集合,Map 的语义更清晰。

6.3 安全性和原型污染

普通对象接收外部键时要注意原型属性和原型污染:

const unsafe = {}
unsafe['__proto__'] = { polluted: true }

const safeRecord = Object.create(null)
safeRecord['__proto__'] = 'ordinary data'

const map = new Map()
map.set('__proto__', 'ordinary map value')

console.log(Object.hasOwn(safeRecord, '__proto__')) // true
console.log(map.get('__proto__')) // ordinary map value

Object.create(null) 没有 Object.prototype,适合做纯字典,但它也没有 hasOwnProperty 等方法;使用 Object.hasOwn(obj, key)Object.prototype.hasOwnProperty.call。Map 对不可信键天然不会把键当作原型属性,但仍需校验值和业务权限。

6.4 性能选择

Map 和 Set 通常为查找、添加、删除提供平均次线性复杂度;Set 的 has 在大集合中通常比线性扫描数组更合适。Object 在固定结构、JSON 互操作、简单记录和静态配置中更自然。

不要写成绝对结论:

  • Map 不保证在所有浏览器、所有操作中都更快;
  • Object 也不是“只能用于小数据”;
  • 数组适合有序列表和按索引访问;
  • 选择应由数据模型、键类型、序列化要求和实测结果决定。

七、Object 常用方法与注意事项

7.1 Object.is

Object.is=== 大部分相同,但区分 +0/-0,并把 NaN 与自身视为相等:

console.log(+0 === -0) // true
console.log(Object.is(+0, -0)) // false
console.log(NaN === NaN) // false
console.log(Object.is(NaN, NaN)) // true

7.2 Object.assign

Object.assign 是浅拷贝/合并操作,只复制源对象自身可枚举的 String/Symbol 键属性,并通过普通赋值写入目标对象:

const source = { nested: { value: 1 } }
const copy = Object.assign({}, source)

console.log(copy !== source) // true
console.log(copy.nested === source.nested) // true

它会读取源属性,因此源对象的 getter 可能执行;目标对象已有 setter 时也可能触发 setter。需要完整复制描述符时考虑 Object.getOwnPropertyDescriptorsObject.defineProperties,需要结构化数据复制时考虑 structuredClone

7.3 原型和属性枚举

const object = { a: 1, b: 2 }

console.log(Object.keys(object)) // ['a', 'b']
console.log(Object.values(object)) // [1, 2]
console.log(Object.entries(object)) // [['a', 1], ['b', 2]]
console.log(Object.fromEntries([['a', 1]])) // { a: 1 }

const prototype = { inherited: true }
const child = Object.create(prototype, {
  own: { value: true, enumerable: true }
})

console.log(Object.keys(child)) // ['own']
console.log('inherited' in child) // true
console.log(Object.hasOwn(child, 'inherited')) // false

__proto__ 是历史访问器属性,不应作为通用原型操作 API。新代码使用 Object.getPrototypeOfObject.setPrototypeOf 或创建时使用 Object.create;接收外部键时要防范原型污染。

八、使用场景

8.1 Set

  • 数组去重和成员存在性判断;
  • 去除字符串重复字符;
  • 并集、交集、差集和集合关系判断;
  • 跟踪已处理 ID、已访问节点和已选标签;
  • 需要唯一值但不需要值到数据的映射。
const visited = new Set()

function visit(id) {
  if (visited.has(id)) return false
  visited.add(id)
  return true
}

8.2 Map

  • 对象、函数或其他任意值作为键;
  • 请求 ID 到请求状态的映射;
  • 频繁增加、删除、查询的键值集合;
  • 需要保持键插入顺序;
  • 需要把值与计数、缓存、元数据关联。
const counts = new Map()

for (const word of ['js', 'map', 'js']) {
  counts.set(word, (counts.get(word) ?? 0) + 1)
}

console.log(counts.get('js')) // 2

8.3 Object

  • 表示固定字段的实体或配置;
  • 与 JSON、表单、HTTP 数据交互;
  • 键确定为 String/Symbol,且对象语义比集合语义更重要;
  • 需要原型方法或 class 实例行为。

九、总结

  • Set 保存唯一值,Map 保存唯一键到值的映射,Object 保存属性键到值的记录;
  • Set 和 Map 使用 SameValueZero,对象键/值按引用身份比较;
  • Set、Map 都按插入顺序迭代,Set 的 keys() 返回值迭代器;
  • Map 克隆和 Set/Map 展开都是浅拷贝;
  • Map 与 Object 的转换可能丢失非字符串键和对象身份,JSON 不能原生表示 Map/Set;
  • Set 是现代数组去重的首选,但 indexOfincludes、排序和对象哈希仍有教学或特殊场景价值;
  • 不要把 Map 一概说成“性能更好”,也不要把普通 Object 键顺序简单说成“无序”;
  • 处理外部键时优先考虑 Map 或 null-prototype 对象,并使用 Object.hasOwn

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS