JavaScript数据结构深度对比:Array与Set、Object与Map 
目录 
引言 
在JavaScript开发中,我们经常需要在不同的数据结构中做选择。特别是ES6引入了Set和Map这两种新的数据结构后,我们有了更多的选择。本文将深入探讨Array与Set这一组有序集合,以及Object与Map这一组键值对集合之间的关系、区别和应用场景,帮助开发者在实际项目中做出更明智的选择。
数据结构概览 
在开始详细比较之前,让我们先通过图表了解这四种数据结构的基本定位:
graph TD
    A[JavaScript数据结构] --> B[有序集合]
    A --> C[键值对集合]
    
    B --> D[Array 数组]
    B --> E[Set 集合]
    
    C --> F[Object 对象]
    C --> G[Map 映射]
    
    D --> D1[可重复元素]
    D --> D2[索引访问]
    
    E --> E1[唯一元素]
    E --> E2[值访问]
    
    F --> F1[字符串键]
    F --> F2[原型链]
    
    G --> G1[任意类型键]
    G --> G2[无原型污染]
    
    style D fill:#ffdddd
    style E fill:#ddffdd
    style F fill:#ddddff
    style G fill:#ffffdd
Array与Set:有序集合的两种选择 
Array和Set都属于有序集合,用于存储一系列值。它们都保持元素的插入顺序,但在元素唯一性、访问方式和API设计上有明显差异。
Array基础特性 
数组是JavaScript中最基础的有序集合,它具有以下特点:
- 有序:元素按插入顺序排列,有明确的索引
 - 可重复:可以存储重复的值
 - 动态大小:可以随时添加或删除元素
 - 索引访问:可以通过数字索引直接访问元素
 
const fruits = ['apple', 'banana', 'apple', 'orange'];
console.log(fruits[0]); // 'apple'
console.log(fruits.length); // 4Set基础特性 
Set是ES6引入的新数据结构,它是值的集合,具有以下特点:
- 无重复值:自动去除重复元素
 - 有序:虽然没有索引,但元素按插入顺序排列
 - 高效查找:检查元素存在性的性能较高
 - 简洁API:提供了add、delete、has、clear等方法
 
const fruitsSet = new Set(['apple', 'banana', 'apple', 'orange']);
console.log(fruitsSet.size); // 3 (重复的'apple'被去除)
console.log(fruitsSet.has('banana')); // trueArray vs Set:API详解 
Array和Set提供了不同的API方法用于操作数据。下面详细比较两者的主要API:
classDiagram
    class Array {
        +length: number
        +push(item): number
        +pop(): any
        +shift(): any
        +unshift(...items): number
        +splice(start, deleteCount, ...items): Array
        +slice(start, end): Array
        +indexOf(item): number
        +includes(item): boolean
        +concat(...arrays): Array
        +join(separator): string
        +map(callback): Array
        +filter(callback): Array
        +reduce(callback): any
        +sort(compareFn): Array
        +reverse(): Array
    }
    
    class Set {
        +size: number
        +add(value): Set
        +delete(value): boolean
        +has(value): boolean
        +clear(): void
        +forEach(callback): void
        +values(): Iterator
        +keys(): Iterator
        +entries(): Iterator
    }
Array主要API详解:
| API | 描述 | 返回值 | 示例 | 
|---|---|---|---|
push() | 在数组末尾添加一个或多个元素 | 新数组长度 | arr.push('apple') | 
pop() | 移除并返回数组的最后一个元素 | 被移除的元素 | arr.pop() | 
shift() | 移除并返回数组的第一个元素 | 被移除的元素 | arr.shift() | 
unshift() | 在数组开头添加一个或多个元素 | 新数组长度 | arr.unshift('apple') | 
splice() | 从数组中添加/删除元素 | 被删除元素的数组 | arr.splice(2, 1, 'new') | 
slice() | 返回数组的一部分的浅拷贝 | 新数组 | arr.slice(1, 3) | 
indexOf() | 查找元素的索引 | 索引或-1 | arr.indexOf('apple') | 
includes() | 判断数组是否包含某元素 | 布尔值 | arr.includes('apple') | 
map() | 对每个元素执行函数并返回新数组 | 新数组 | arr.map(x => x * 2) | 
filter() | 过滤符合条件的元素到新数组 | 新数组 | arr.filter(x => x > 3) | 
reduce() | 将数组元素计算为一个值 | 累积结果 | arr.reduce((a, b) => a + b) | 
sort() | 对数组元素进行排序 | 排序后的数组 | arr.sort((a, b) => a - b) | 
forEach() | 为每个元素执行函数 | undefined | arr.forEach(x => console.log(x)) | 
Set主要API详解:
| API | 描述 | 返回值 | 示例 | 
|---|---|---|---|
add() | 添加一个新元素 | Set对象本身 | set.add('apple') | 
delete() | 删除一个元素 | 布尔值,成功为true | set.delete('apple') | 
has() | 是否存在某元素 | 布尔值 | set.has('apple') | 
clear() | 清空所有元素 | undefined | set.clear() | 
forEach() | 对每个元素执行函数 | undefined | set.forEach(x => console.log(x)) | 
values() | 返回值的迭代器 | 迭代器 | for (let v of set.values()) {...} | 
keys() | 与values()相同(为兼容Map) | 迭代器 | for (let k of set.keys()) {...} | 
entries() | 返回[值,值]的迭代器 | 迭代器 | for (let [k,v] of set.entries()) {...} | 
graph LR
    A[Array操作类型] --> B[增删元素]
    A --> C[查找元素]
    A --> D[迭代操作]
    A --> E[变形操作]
    
    B --> B1[push/pop]
    B --> B2[shift/unshift]
    B --> B3[splice]
    
    C --> C1[indexOf]
    C --> C2[includes]
    C --> C3[find/findIndex]
    
    D --> D1[forEach]
    D --> D2[map]
    D --> D3[filter]
    D --> D4[reduce]
    
    E --> E1[sort]
    E --> E2[reverse]
    E --> E3[concat]
    
    F[Set操作类型] --> G[增删元素]
    F --> H[查找元素]
    F --> I[迭代操作]
    
    G --> G1[add]
    G --> G2[delete]
    G --> G3[clear]
    
    H --> H1[has]
    
    I --> I1[forEach]
    I --> I2[values/keys]
    I --> I3[entries]
使用场景对比 
下面是Array和Set在不同场景下的适用性比较:
mindmap
  root((Array vs Set))
    Array适用场景
      需要通过索引访问元素
      元素顺序重要且需维护
      可能包含重复元素
      需要频繁执行数组方法(map, filter, reduce等)
      需要处理大量连续数据
      需要对数据进行排序
      需要使用栈或队列的数据结构
    Set适用场景
      需要保证元素唯一性
      频繁检查元素是否存在
      需要快速去重
      需要高效的交集、并集、差集操作
      不需要通过索引访问
      只关心值是否存在,不需要存储额外数据
      需要避免重复添加相同数据
性能对比 
| 操作 | Array | Set | 
|---|---|---|
| 查找元素 | O(n) | O(1) | 
| 添加元素 | O(1)* | O(1) | 
| 删除元素 | O(n) | O(1) | 
| 检查包含 | O(n) | O(1) | 
| 迭代元素 | O(n) | O(n) | 
*注:数组在末尾添加元素为O(1),在开头或中间添加元素为O(n)
在实际的性能测试中,Set在查找、添加和删除操作上通常比Array快,特别是数据量较大时:
// 示例:比较Array和Set查找性能
const arr = Array.from({length: 1000000}, (_, i) => i);
const set = new Set(arr);
const target = 999999;
console.time('Array查找');
arr.includes(target);
console.timeEnd('Array查找');  // 通常在几十毫秒
console.time('Set查找');
set.has(target);
console.timeEnd('Set查找');  // 通常在微秒级别Object与Map:键值对集合的两种选择 
Object和Map都属于键值对集合,用于存储"键->值"的映射关系。但它们在键的类型、顺序保证和API设计上存在显著差异。
Object基础特性 
对象是JavaScript中最常用的键值对集合,它具有以下特点:
- 键限制:键只能是字符串或Symbol
 - 原型链:继承原型链上的属性和方法
 - 内置方法:如hasOwnProperty、toString等
 - 直接量语法:可以使用{}直接创建
 
const user = {
    name: 'Alice',
    age: 30,
    'user-id': 12345
};
console.log(user.name); // 'Alice'
console.log(user['user-id']); // 12345Map基础特性 
Map是ES6引入的新数据结构,专门用于存储键值对,具有以下特点:
- 任意键类型:键可以是任何类型,包括对象、函数等
 - 有序:键值对按插入顺序排列
 - 大小获取:可以直接通过size属性获取长度
 - 专用方法:提供了set、get、has、delete、clear等专用方法
 
const userMap = new Map();
const userIdKey = { id: 1 };
userMap.set(userIdKey, { name: 'Alice', age: 30 });
userMap.set('role', 'admin');
console.log(userMap.get(userIdKey)); // { name: 'Alice', age: 30 }
console.log(userMap.size); // 2Object vs Map:API详解 
Object和Map提供了不同的API方法用于操作键值对数据。下面详细比较两者的主要API:
classDiagram
    class Object {
        +Object.keys(obj): Array
        +Object.values(obj): Array
        +Object.entries(obj): Array
        +Object.assign(target, ...sources): Object
        +Object.create(proto): Object
        +Object.defineProperty(obj, prop, descriptor): Object
        +obj.hasOwnProperty(prop): boolean
        +obj.propertyIsEnumerable(prop): boolean
        +obj.toString(): string
        +obj.valueOf(): primitive
    }
    
    class Map {
        +size: number
        +set(key, value): Map
        +get(key): any
        +has(key): boolean
        +delete(key): boolean
        +clear(): void
        +forEach(callback): void
        +keys(): Iterator
        +values(): Iterator
        +entries(): Iterator
    }
Object主要API详解:
| API | 描述 | 返回值 | 示例 | 
|---|---|---|---|
Object.keys() | 返回对象自身可枚举属性名数组 | 数组 | Object.keys(obj) | 
Object.values() | 返回对象自身可枚举属性值数组 | 数组 | Object.values(obj) | 
Object.entries() | 返回对象自身可枚举属性[键,值]数组 | 数组 | Object.entries(obj) | 
Object.assign() | 将源对象属性复制到目标对象 | 目标对象 | Object.assign({}, obj) | 
Object.create() | 创建指定原型的新对象 | 新对象 | Object.create(proto) | 
hasOwnProperty() | 检查是否有自己的属性(非原型) | 布尔值 | obj.hasOwnProperty('prop') | 
in操作符 | 检查属性是否存在(含原型链) | 布尔值 | 'prop' in obj | 
| 属性访问 | 通过.或[]访问属性 | 属性值 | obj.prop或obj['prop'] | 
| 属性赋值 | 通过.或[]设置属性 | 赋值 | obj.prop = value | 
| 删除属性 | 使用delete操作符删除属性 | 布尔值 | delete obj.prop | 
Map主要API详解:
| API | 描述 | 返回值 | 示例 | 
|---|---|---|---|
set() | 设置键值对 | Map对象本身 | map.set(key, value) | 
get() | 获取指定键的值 | 值或undefined | map.get(key) | 
has() | 检查键是否存在 | 布尔值 | map.has(key) | 
delete() | 删除指定键的键值对 | 布尔值,成功为true | map.delete(key) | 
clear() | 清空所有键值对 | undefined | map.clear() | 
size | 获取键值对数量 | 数值 | map.size | 
forEach() | 对每个键值对执行函数 | undefined | map.forEach((v, k) => console.log(k, v)) | 
keys() | 返回键的迭代器 | 迭代器 | for (let k of map.keys()) {...} | 
values() | 返回值的迭代器 | 迭代器 | for (let v of map.values()) {...} | 
entries() | 返回[键,值]的迭代器 | 迭代器 | for (let [k, v] of map.entries()) {...} | 
graph LR
    A[Object操作类型] --> B[属性访问]
    A --> C[属性检查]
    A --> D[遍历属性]
    A --> E[对象操作]
    
    B --> B1["obj.prop / obj['prop']"]
    B --> B2["obj.prop = value"]
    B --> B3["delete obj.prop"]
    
    C --> C1["'prop' in obj"]
    C --> C2["obj.hasOwnProperty('prop')"]
    
    D --> D1["Object.keys(obj)"]
    D --> D2["Object.values(obj)"]
    D --> D3["Object.entries(obj)"]
    D --> D4["for...in循环"]
    
    E --> E1["Object.assign()"]
    E --> E2["Object.create()"]
    E --> E3["Object.freeze()"]
    
    F[Map操作类型] --> G[键值操作]
    F --> H[检查操作]
    F --> I[遍历操作]
    F --> J[整体操作]
    
    G --> G1["map.set(key, value)"]
    G --> G2["map.get(key)"]
    G --> G3["map.delete(key)"]
    
    H --> H1["map.has(key)"]
    H --> H2["map.size"]
    
    I --> I1["map.forEach()"]
    I --> I2["map.keys()"]
    I --> I3["map.values()"]
    I --> I4["map.entries()"]
    
    J --> J1["map.clear()"]
    J --> J2["new Map([...])"]
    J --> J3["[...map]"]
使用场景对比 
下面是Object和Map在不同场景下的适用性比较:
mindmap
  root((Object vs Map))
    Object适用场景
      简单的键值存储
      JSON兼容性要求
      需要使用原型链
      键都是字符串或符号
      需要使用对象字面量
      需要使用JSON.stringify/parse
      作为类的实例或原型
      配置对象和参数对象
    Map适用场景
      键可能是非字符串
      需要频繁增删键值对
      需要维护键值对顺序
      需要高效遍历所有键值
      需要获取键值对数量
      需要以对象作为键
      需要保证键不会被意外覆盖
      对性能有较高要求的场景
性能对比 
| 操作 | Object | Map | 
|---|---|---|
| 查找键 | 70 | 85 | 
| 添加键值 | 80 | 85 | 
| 删除键值 | 65 | 85 | 
| 迭代 | 60 | 90 | 
| 获取大小 | 50 | 100 | 
*注:数值表示相对性能,越高越好(满分100)
在实际的性能测试中,Map在频繁添加和删除键值对时通常比Object更高效,特别是处理大量数据时:
// 示例:比较Object和Map的频繁操作性能
const count = 1000000;
// 测试Object
console.time('Object操作');
const obj = {};
for (let i = 0; i < count; i++) {
  const key = `key${i}`;
  obj[key] = i;  // 添加
}
for (let i = 0; i < count; i++) {
  const key = `key${i}`;
  const val = obj[key];  // 读取
  delete obj[key];  // 删除
}
console.timeEnd('Object操作');
// 测试Map
console.time('Map操作');
const map = new Map();
for (let i = 0; i < count; i++) {
  const key = `key${i}`;
  map.set(key, i);  // 添加
}
for (let i = 0; i < count; i++) {
  const key = `key${i}`;
  const val = map.get(key);  // 读取
  map.delete(key);  // 删除
}
console.timeEnd('Map操作');
// Map通常快10-30%内部实现原理 
以下是这四种数据结构在JavaScript引擎中的简化实现原理:
| 数据结构 | 内部实现特点 | 
|---|---|
| Array | • 连续内存区域 • 动态调整大小 • 稀疏数组优化  | 
| Set | • 基于哈希表 • 引用值的Hash处理 • 高效查找算法  | 
| Object | • 基于哈希表 • 字符串键优化 • 隐藏类优化 • 原型链查找  | 
| Map | • 哈希表 + 链表 • 保持插入顺序 • 键的引用存储 • 高效迭代实现  | 
各数据结构的内部实现详解:
Array实现原理:
- 在大多数JavaScript引擎中,数组是使用连续内存块实现的
 - 对于稀疏数组(有空洞的数组),引擎会使用哈希表结构
 - 当数组长度变化时,引擎会重新分配内存并复制元素
 - 数组索引实际上是转换为字符串的特殊属性
 
Set实现原理:
- 基于哈希表实现,提供近似O(1)的查找、添加和删除操作
 - 对于对象和数组等引用类型值,通过内存地址进行唯一性判断
 - 内部使用特殊的标记来记录元素的插入顺序
 - 由于要保证唯一性,需要额外的内存来存储哈希信息
 
Object实现原理:
- 基于哈希表实现,键被转换为字符串或符号
 - 使用"隐藏类"(hidden class)优化相同结构对象的访问速度
 - 原型链查找使用指针链接,影响深层属性的查找性能
 - 属性有多种内部标记(数据属性、访问器属性等)
 
Map实现原理:
- 使用哈希表加链表的复合结构
 - 键可以是任意类型,引用类型键通过内存地址区分
 - 内部维护插入顺序,使用链表实现
 - 为了支持高效迭代,维护了额外的数据结构
 
综合对比表 
| 特性 | Array | Set | Object | Map | 
|---|---|---|---|---|
| 存储内容 | 值的有序列表 | 唯一值的集合 | 字符串/Symbol键的值 | 任意类型键的值 | 
| 元素顺序 | 有序 | 有序(插入顺序) | 无序(ES2015前) 基本有序(ES2015后)  | 有序(插入顺序) | 
| 键的类型 | 数字索引 | N/A | 字符串或Symbol | 任意值 | 
| 重复元素 | 允许 | 自动去重 | 键不可重复 | 键不可重复 | 
| 访问元素 | arr[0] | 迭代或转数组 | obj.keyobj['key'] | map.get(key) | 
| 判断大小 | array.length | set.size | 需手动计算 Object.keys(obj).length  | map.size | 
| 添加元素 | push()unshift()等 | add() | obj[key] = value | set(key, value) | 
| 删除元素 | splice()pop()等 | delete() | delete obj[key] | delete(key) | 
| 查找元素 | indexOf()includes() | has() | key in objobj.hasOwnProperty() | has(key) | 
| 迭代性能 | 优秀 | 优秀 | 一般 | 优秀 | 
| 内存占用 | 中等 | 较高 | 较低 | 较高 | 
| 序列化 | 原生支持JSON | 需转为数组 | 原生支持JSON | 需转为数组或对象 | 
| 适用场景 | 有序数据 需要索引访问  | 需要唯一值 频繁查重  | 简单键值对 JSON兼容  | 复杂键值对 频繁增删  | 
实际应用示例 
- 数组去重
 
// 使用Array
function uniqueArray(array) {
  const result = [];
  for (const item of array) {
    if (result.indexOf(item) === -1) {
      result.push(item);
    }
  }
  return result;
}
// 使用Set (更高效)
function uniqueWithSet(array) {
  return [...new Set(array)];
}
// 性能测试
const largeArray = Array.from({length: 10000}, (_, i) => i % 1000);
console.time('Array去重');
uniqueArray(largeArray);
console.timeEnd('Array去重');
console.time('Set去重');
uniqueWithSet(largeArray);
console.timeEnd('Set去重');
// Set方法通常快10-100倍- 频繁查找
 
// 使用Object
function countWordsWithObject(words) {
  const counts = {};
  for (const word of words) {
    counts[word] = (counts[word] || 0) + 1;
  }
  return counts;
}
// 使用Map
function countWordsWithMap(words) {
  const counts = new Map();
  for (const word of words) {
    counts.set(word, (counts.get(word) || 0) + 1);
  }
  return counts;
}
// 当键是简单字符串时,性能相似
// 但当需要使用对象作为键时,Map是唯一选择
const userObj = { id: 1 };
const userMap = new Map();
userMap.set(userObj, "userData");  // 使用对象作为键- 集合操作
 
// 使用Set实现集合操作
const set1 = new Set([1, 2, 3, 4]);
const set2 = new Set([3, 4, 5, 6]);
// 并集
const union = new Set([...set1, ...set2]);
console.log([...union]);  // [1, 2, 3, 4, 5, 6]
// 交集
const intersection = new Set([...set1].filter(x => set2.has(x)));
console.log([...intersection]);  // [3, 4]
// 差集
const difference = new Set([...set1].filter(x => !set2.has(x)));
console.log([...difference]);  // [1, 2]- 使用Map处理复杂键
 
// 使用Map保存用户数据,以DOM元素为键
const userDataMap = new Map();
// 存储DOM元素关联的数据
document.querySelectorAll('.user-item').forEach(element => {
  userDataMap.set(element, {
    id: element.dataset.id,
    name: element.dataset.name,
    score: parseInt(element.dataset.score)
  });
});
// 事件处理中获取数据
document.body.addEventListener('click', event => {
  const userItem = event.target.closest('.user-item');
  if (userItem && userDataMap.has(userItem)) {
    const userData = userDataMap.get(userItem);
    console.log(`用户 ${userData.name} 的分数是 ${userData.score}`);
  }
});我的思考与建议 
在现代JavaScript开发中,我认为对这四种数据结构的选择应该基于以下几个原则:
- 数据的唯一性需求:需要保证唯一性时首选Set
 - 键值对的键类型:非字符串键首选Map
 - 操作频率:频繁查找优先使用Set/Map,频繁整体操作选择Array/Object
 - 序列化需求:需要JSON序列化时使用Array/Object
 - 顺序重要性:顺序重要且需要频繁重排时使用Array,键值对顺序重要时使用Map
 - 内存和性能平衡:大数据量时,谨慎使用Set/Map,可能占用更多内存
 
flowchart TD
    A[选择数据结构] --> B{存储形式?}
    B -->|值列表| C{需要唯一性?}
    B -->|键值对| D{键的类型?}
    
    C -->|是| E[Set]
    C -->|否| F{需要索引访问?}
    F -->|是| G[Array]
    F -->|否| E
    
    D -->|字符串/Symbol| H{需要原型方法?}
    D -->|其他类型| I[Map]
    
    H -->|是| J[Object]
    H -->|否| K{需要频繁操作?}
    K -->|是| I
    K -->|否| J
    
    style E fill:#ddffdd
    style G fill:#ffdddd
    style I fill:#ffffdd
    style J fill:#ddddff
我的建议是学习所有这些数据结构的优缺点,并根据具体场景选择最合适的:
- 新项目中,更多地使用Set和Map来获得更好的语义和性能
 - 需要与旧代码互操作或序列化时,仍然使用Array和Object
 - 对性能关键的代码,进行基准测试后再决定使用哪种数据结构
 - 当处理唯一值集合时,总是优先考虑Set而不是Array加去重
 - 当需要频繁查找、添加、删除键值对时,优先考虑Map而不是Object
 
结论 
JavaScript的这四种核心数据结构各有所长,没有绝对的最佳选择。通过理解它们的异同和适用场景,我们可以在开发中做出更明智的选择:
- Array:有序、可重复、索引访问,适合有序数据集合
 - Set:有序、唯一值、高效查找,适合需要唯一性的集合
 - Object:键值对、字符串键、原型继承,适合简单的结构化数据
 - Map:有序键值对、任意键类型、高效操作,适合复杂的键值对数据
 
这四种数据结构相辅相成,形成了JavaScript完整的数据处理能力。在现代JavaScript开发中,应该充分利用这些数据结构的特点,根据实际需求选择最合适的数据结构,以便编写出更高效、更可维护的代码。