跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
JavaScript大前端算法

数组与哈希表(Map/Object)核心区别及实战

对比了 JavaScript 中数组与哈希表(Map/Object)的本质区别,涵盖索引方式、查找逻辑、有序性及空间特性。通过初始化方法、去重、词频统计、缓存等实战场景,展示了如何根据需求选择数据结构。同时整理了面试常见考点,包括 Map 与 Object 的区别、引用类型键的处理,以及手动实现简易 Map 和数组哈希化的代码示例,旨在帮助开发者优化算法复杂度并提升编码效率。

女王发布于 2026/3/23更新于 2026/8/2225K 浏览

数组与哈希表(Map/Object)核心区别及实战

数组和哈希表(前端常用 Object/Map 实现)的核心区别,我会从本质特征、核心操作、适用场景三个维度,用更通俗的方式帮你梳理,避免重复且突出关键差异。

核心区别

维度数组哈希表(Object/Map)
索引本质「位置索引」:只能用连续数字(0,1,2...),索引和元素的物理位置强绑定「键值映射」:可用任意类型键(字符串 / 数字 / 对象),通过哈希函数映射到存储位置,键与位置无直接关联
查找逻辑1. 按下标查:O(1)(直接定位)2. 按值查:O(n)(必须遍历)1. 按键查:平均 O(1)(哈希函数直接映射)2. 按值查:O(n)(仍需遍历)
有序性天然有序(按下标顺序排列)1. Object:ES6 前无序,ES6 后按插入顺序 2. Map:严格保持插入顺序
唯一性元素可重复(允许多个相同值)键 / 属性名唯一(重复赋值会覆盖),值可重复
插入 / 删除1. 尾部操作:O(1) 2. 中间操作:O(n)(需移动后续元素)无'头尾 / 中间'概念,增删平均 O(1)(仅需修改映射关系),受哈希冲突影响可能退化到 O(n)
空间特性连续内存,空间利用率高;静态数组长度固定,动态数组扩容需复制离散存储,需额外空间存储哈希结构(如冲突链表),空间利用率略低;长度可动态调整,无需提前分配

JavaScript Map 初始化与转换

JS 的 Map 构造函数支持接收一个二维数组作为参数,二维数组的每一个子数组都是 [key, value] 的形式,这是官方原生、最推荐的初始化方式,无需额外操作。

基础写法(最常用)

// 定义时直接初始化键值对,key 可以是任意类型(数字、字符串、对象等)
const map = new Map([
  ['name', '张三'], // 字符串 key
  [100, '满分'], // 数字 key
  [true, '布尔值'] // 布尔值 key
]);
// 验证:直接获取值,无需 set
console.log(map.get('name')); // 输出:张三
console.log(map.get(100)); // 输出:满分

特殊场景:key 是对象 / 数组(JS Map 独有的特性)

const objKey = { id: 1 };
const arrKey = [2];
const map = new Map([
  [objKey, '对象作为 key'],
  [arrKey, '数组作为 key']
]);
console.log(map.get(objKey)); // 输出:对象作为 key

Map 和二维数组的转换

const map = new Map([
  ['name', '张三'],
  [100, '满分'],
  [true, '布尔值']
]);
console.log(map);
console.log(Array.from(map.entries()));

适用场景与实战案例

1. 数组:'按位置排好队的储物柜'

  • 通俗比喻:就像超市的储物柜,每个柜子有固定编号(0 号、1 号、2 号),你知道编号就能直接打开,但想找'放了水杯的柜子',就得一个个看。

前端例子:

// 数组:按下标快速访问,按值查找需遍历
const arr = ["苹果", "香蕉", "橙子"];
console.log(arr[1]); // O(1) 直接取'香蕉'
console.log(arr.find(item => item === "橙子")); // O(n) 遍历找值

2. 哈希表:'按名字贴标签的储物柜'

  • 通俗比喻:储物柜没有固定编号,而是贴标签(比如'小明的水杯''小红的伞'),你报标签名,系统能快速找到对应的柜子,不用管柜子的物理位置。

前端例子:

// Map:按键快速查找,键可以是任意类型
const map = new Map();
map.set("水果 1", "苹果");
map.set(2, "香蕉");
map.set({ id: 3 }, "橙子");
console.log(map.get(2)); // O(1) 直接取'香蕉'
console.log(map.get({ id: 3 })); // undefined(对象是引用类型,需用同一个引用)

典型场景与优化方案

场景 1:数组去重(高频面试 / 业务场景)

需求:将 [1, 2, 2, 3, 3, 3] 转为无重复数组。

❷ 哈希表优化(高效版) 思路:用 Map/Set 记录已出现的元素,将'查找是否存在'的操作从 O(n) 降到 O(1)。时间复杂度:O(n)(仅一次遍历)。

function uniqueArrayOptimize(arr) {
  const map = new Map();
  const result = [];
  for (const item of arr) {
    // 哈希表查找:O(1)
    if (!map.has(item)) {
      map.set(item, true);
      result.push(item);
    }
  }
  return result; // 极简版:return [...new Set(arr)](底层也是哈希表)
}

❶ 纯数组实现(基础版,效率低) 思路:遍历数组,用 indexOf 检查元素是否已存在(本质是遍历)。时间复杂度:O(n²)(嵌套遍历)。

function uniqueArrayBasic(arr) {
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    // indexOf 会遍历 result,嵌套 O(n)
    if (result.indexOf(arr[i]) === -1) {
      result.push(arr[i]);
    }
  }
  return result;
}

场景 2:高频词统计(数据处理场景)

需求:统计字符串中每个单词出现的次数,如 "a b a c b a" → {a:3, b:2, c:1}。

❷ 哈希表实现(最优解) 思路:直接用 Object/Map 做键值映射,单词为键、次数为值,查找 / 更新都是 O(1)。时间复杂度:O(n),代码简洁。

function countWordsOptimize(str) {
  const words = str.split(" ");
  const countMap = {};
  for (const word of words) {
    // 哈希表更新:O(1)
    countMap[word] = (countMap[word] || 0) + 1;
  }
  return countMap;
}

❶ 纯数组实现(不可行 + 效率低) 思路:用两个数组分别存'单词'和'次数',每次统计需遍历数组找单词。时间复杂度:O(n²),且代码冗余。

function countWordsBasic(str) {
  const words = str.split(" ");
  const keys = [];
  const counts = [];
  for (const word of words) {
    const index = keys.indexOf(word); // O(n) 查找
    if (index === -1) {
      keys.push(word);
      counts.push(1);
    } else {
      counts[index]++;
    }
  }
  // 转成对象(额外操作)
  return keys.reduce((obj, key, i) => ({ ...obj, [key]: counts[i] }), {});
}

场景 3:有序列表的快速访问(UI 渲染场景)

需求:渲染一个商品列表,支持'点击第 N 个商品'快速定位详情。

❷ 哈希表实现(没必要 + 冗余) 思路:用哈希表存 {0:商品 1, 1:商品 2...},虽然访问也是 O(1),但丢失了'有序'特性,且无法快速做排序 / 切片。

const goodsMap = new Map([[0, "商品 1"], [1, "商品 2"], [2, "商品 3"], [3, "商品 4"]]);
// 虽能访问,但无法直接做 goodsList.slice(0,2) 这类有序操作

❶ 数组实现(最优解) 思路:数组天然有序,按下标访问是 O(1),适合按位置操作。

const goodsList = ["商品 1", "商品 2", "商品 3", "商品 4"];
// 点击第 2 个商品(下标 1),直接获取:O(1)
function getGoodsByIndex(index) {
  return goodsList[index];
}

场景 4:缓存数据(性能优化场景)

需求:缓存接口返回的用户信息,避免重复请求,支持'按用户 ID 快速查找'。

❷ 哈希表实现(最优解) 思路:用 Map 存 {1:用户 1, 2:用户 2},按 ID 查找是 O(1),适合高频键值查询。

const userMap = new Map([[1, { id: 1, name: "小明" }], [2, { id: 2, name: "小红" }]]);
function getUserByIdOptimize(id) {
  return userMap.get(id); // O(1)
}

❶ 数组实现(效率低) 思路:用数组存用户列表,按 ID 查找需遍历,O(n)。

const userList = [{ id: 1, name: "小明" }, { id: 2, name: "小红" }];
function getUserById(id) {
  return userList.find(user => user.id === id); // O(n)
}

算法优化核心思路(通用规则)

优化方向用数组优化用哈希表优化
降低时间复杂度利用'下标 O(1) 访问'特性,适合有序、按位置操作的场景利用'键 O(1) 查找'特性,将遍历查找(O(n))转为哈希映射(O(1))
简化代码逻辑有序遍历、切片、排序等场景,数组 API(forEach/slice/sort)更简洁键值映射、去重、统计等场景,Map/Object 的 API(set/get/has)更直接
空间换时间动态数组扩容(牺牲少量空间,保证访问效率)哈希表的冲突链表(牺牲少量空间,保证查找效率)

面试考点与解析

1. 简答题:Map 和普通 Object 有哪些核心区别?(高频)

参考答案:

特性MapObject
键的类型支持任意类型(字符串 / 数字 / 对象 / 函数 / Symbol)仅支持字符串、Symbol(数字会转为字符串)
有序性严格保持键的插入顺序ES6 前无序,ES6 后仅保留字符串键的插入顺序
长度获取直接通过 size 属性获取需手动遍历(Object.keys(obj).length)
迭代方式原生支持迭代(for...of)需先转数组(Object.keys/values/entries)
原型链干扰无(键不会继承原型链属性)可能被原型链属性干扰(如 obj.hasOwnProperty('toString'))
性能频繁增删键值对时性能更优增删频繁时性能略差

2. 选择题:以下代码执行结果是什么?

const map = new Map();
const objKey = { id: 1 };
map.set(objKey, "test");
console.log(map.get({ id: 1 })); // ?
console.log(map.get(objKey)); // ?

答案:undefined、test

思路:Map 的键是引用类型时,判断相等的依据是「引用地址是否相同」,而非值是否相同。{id:1} 是两个不同的对象,引用地址不同,因此第一个 get 返回 undefined。

3. 简答题:Map 的 set 方法返回值是什么?这一特性有什么用?

参考答案:

  • set 方法返回 Map 实例本身(支持链式调用);
  • 用途:可以简化代码,比如 map.set('a',1).set('b',2).set('c',3),无需多次写 map.set。

4. 编程题:用 Map 实现数组去重(要求保留原顺序,支持引用类型元素)

需求:将 [1, 2, 2, {id:1}, {id:1}, {id:2}] 去重,输出 [1, 2, {id:1}, {id:2}]。

参考答案:

function uniqueArray(arr) {
  const map = new Map();
  const result = [];
  for (const item of arr) {
    // 引用类型用自身当键,基本类型直接当键
    if (!map.has(item)) {
      map.set(item, true);
      result.push(item);
    }
  }
  return result;
}
// 测试
const arr = [1, 2, 2, {id:1}, {id:1}, {id:2}];
console.log(uniqueArray(arr)); // [1,2,{id:1},{id:2}]

思路:利用 Map 键的唯一性,且支持引用类型作为键,相比 Set 更直观(Set 本质是特殊的 Map),同时保留插入顺序。

5. 编程题:用 Map 优化'两数之和'算法(高频算法题)

需求:给定数组 [2,7,11,15] 和目标值 9,找出两个数的下标,使它们的和等于目标值(要求时间复杂度 O(n))。

参考答案:

function twoSum(nums, target) {
  const map = new Map();
  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i];
    // 检查是否已存在互补数
    if (map.has(complement)) {
      return [map.get(complement), i];
    }
    // 存储'数值 - 下标'映射
    map.set(nums[i], i);
  }
  return [];
}
// 测试
console.log(twoSum([2,7,11,15], 9)); // [0,1]

思路:用 Map 替代嵌套遍历,将'查找互补数'的操作从 O(n) 降到 O(1),整体时间复杂度从 O(n²) 优化为 O(n)。

6. 分析题:为什么在频繁增删键值对的场景下,Map 比 Object 更高效?

  • Object 是为静态属性设计的,频繁增删会触发 JS 引擎的'隐藏类'重排,导致性能损耗;
  • Map 是为动态键值对场景优化的底层数据结构(哈希表实现),增删操作不会触发隐藏类重排,性能更稳定。

手写简易版 Map

要求:模拟 Map 的核心逻辑,支持任意类型键,保留插入顺序。

参考答案:

class MyMap {
  constructor() {
    // 用两个数组分别存键和值,保证顺序一致
    this.keys = [];
    this.values = [];
    this.size = 0;
  }

  // 核心:判断两个键是否相等(处理引用类型)
  isEqual(key1, key2) {
    if (key1 === key2) return true;
    // 处理 NaN(NaN === NaN 为 false,需特殊判断)
    if (key1 !== key1 && key2 !== key2) return true;
    return false;
  }

  set(key, value) {
    // 先查找是否已存在该键
    const index = this.keys.findIndex(k => this.isEqual(k, key));
    if (index !== -1) {
      this.values[index] = value; // 存在则更新值
    } else {
      this.keys.push(key);
      this.values.push(value);
      this.size++; // 新增则更新 size
    }
    return this; // 支持链式调用
  }

  get(key) {
    const index = this.keys.findIndex(k => this.isEqual(k, key));
    return index !== -1 ? this.values[index] : undefined;
  }

  has(key) {
    return this.keys.some(k => this.isEqual(k, key));
  }

  delete(key) {
    const index = this.keys.findIndex(k => this.isEqual(k, key));
    if (index !== -1) {
      this.keys.splice(index, 1);
      this.values.splice(index, 1);
      this.size--;
      return true;
    }
    return false;
  }

  clear() {
    this.keys = [];
    this.values = [];
    this.size = 0;
  }
}
// 测试
const myMap = new MyMap();
myMap.set(NaN, "test").set({id:1}, "obj");
console.log(myMap.get(NaN)); // test
console.log(myMap.has({id:1})); // false(引用不同)
console.log(myMap.size); // 2
myMap.delete(NaN);
console.log(myMap.size); // 1

核心考点:

  • 处理 NaN 的特殊情况(NaN 作为键时,Map 认为是同一个键);
  • 引用类型键的相等判断(引用地址);
  • set 方法返回自身以支持链式调用;
  • size 属性的动态更新。

手动哈希化实现

手动将数组哈希化,核心是遍历数组,构建键值对映射结构(用 Object 或自定义结构),核心逻辑是「以数组元素为键,以需要的信息(存在性、次数、下标)为值」,下面分三种常见场景给出纯手动实现代码(不依赖 Map,更底层)。

一、手动实现「存在性哈希化」(判断元素是否存在)

需求:将数组转为哈希对象,快速判断元素是否存在,替代 indexOf。核心逻辑:元素作为 key,值设为 true,遍历数组逐个赋值。

function arrayToExistHash(arr) {
  const hash = {};
  for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    // 处理基础类型:数字、字符串、布尔值
    // 注意:对象/数组等引用类型会被转为 "[object Object]",不适合用 Object 存储
    hash[item] = true;
  }
  return hash;
}
// 测试
const arr = [1, 2, 3, 2, 1];
const existHash = arrayToExistHash(arr);
console.log(existHash[2]); // true
console.log(existHash[4]); // undefined

二、手动实现「统计型哈希化」(统计元素出现次数)

需求:统计数组中每个元素的出现次数,核心是「元素为键,次数为值」。核心逻辑:遍历数组时,若元素已在哈希中则计数 +1,否则初始化为 1。

function arrayToCountHash(arr) {
  const countHash = {};
  for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    // 三元表达式简化:存在则 +1,不存在则设为 1
    countHash[item] = countHash[item] ? countHash[item] + 1 : 1;
  }
  return countHash;
}
// 测试
const strArr = ["a", "b", "a", "c", "a"];
const countHash = arrayToCountHash(strArr);
console.log(countHash); // {a: 3, b: 1, c: 1}

三、手动实现「索引型哈希化」(记录元素对应下标)

需求:记录每个元素在数组中的所有下标,核心是「元素为键,下标数组为值」。核心逻辑:遍历数组时,若元素不在哈希中则初始化空数组,再将当前下标推入数组。

function arrayToIndexHash(arr) {
  const indexHash = {};
  for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    // 初始化:不存在则创建空数组
    if (!indexHash[item]) {
      indexHash[item] = [];
    }
    // 存入当前下标
    indexHash[item].push(i);
  }
  return indexHash;
}
// 测试
const numArr = [10, 20, 30, 20, 10];
const indexHash = arrayToIndexHash(numArr);
console.log(indexHash[10]); // [0, 4]
console.log(indexHash[20]); // [1, 3]

四、手动哈希化的注意事项(前端开发避坑)

  1. 引用类型无法正确存储 用 Object 做哈希时,对象 / 数组等引用类型会被转为字符串 [object Object],导致不同引用的元素被当成同一个键:

    const arr = [{id:1}, {id:2}];
    const hash = arrayToExistHash(arr);
    console.log(hash); // {[object Object]: true} → 两个对象被覆盖
    
  2. 特殊值的键名转换问题

    • NaN、undefined、null 作为键时,会被转为字符串 "NaN"、"undefined"、"null";
    • 数字键 1 和字符串键 "1" 会被视为同一个键(因为 hash[1] 和 hash["1"] 指向同一个位置)。
  3. 空间换时间的本质

    • 手动哈希化会额外创建一个对象存储映射关系,内存占用比原数组高,但能将「查找操作」的时间复杂度从 O(n) 降到 O(1)。

若要支持引用类型,需手动实现基于引用地址的哈希(模拟 Map 逻辑):

// 用两个数组分别存键和值,基于引用匹配
function arrayToRefHash(arr) {
  const keys = [];
  const values = [];
  for (const item of arr) {
    if (!keys.includes(item)) {
      keys.push(item);
      values.push(true);
    }
  }
  return { keys, values, has: (key) => keys.includes(key) };
}

总结

  1. 数组的核心优势是按位置快速访问 + 天然有序,适合'按顺序存 / 取'的场景;
  2. 哈希表的核心优势是按键快速映射,适合'按标识查 / 存'的场景;
  3. 两者最核心的区别是索引方式:数组是'数字位置索引',哈希表是'任意键映射索引';
  4. 选数组:当需要有序存储、按位置访问 / 操作(如列表渲染、排序、切片)时,数组是最优解;
  5. 选哈希表:当需要快速键值映射、去重 / 统计、缓存数据时,哈希表是最优解;
  6. 优化核心:用哈希表将'数组遍历查找(O(n))'转为'键映射(O(1))',是前端算法优化中最常用的'空间换时间'思路。

目录

  1. 数组与哈希表(Map/Object)核心区别及实战
  2. 核心区别
  3. JavaScript Map 初始化与转换
  4. 基础写法(最常用)
  5. 特殊场景:key 是对象 / 数组(JS Map 独有的特性)
  6. Map 和二维数组的转换
  7. 适用场景与实战案例
  8. 1. 数组:“按位置排好队的储物柜”
  9. 2. 哈希表:“按名字贴标签的储物柜”
  10. 典型场景与优化方案
  11. 场景 1:数组去重(高频面试 / 业务场景)
  12. 场景 2:高频词统计(数据处理场景)
  13. 场景 3:有序列表的快速访问(UI 渲染场景)
  14. 场景 4:缓存数据(性能优化场景)
  15. 算法优化核心思路(通用规则)
  16. 面试考点与解析
  17. 1. 简答题:Map 和普通 Object 有哪些核心区别?(高频)
  18. 2. 选择题:以下代码执行结果是什么?
  19. 3. 简答题:Map 的 set 方法返回值是什么?这一特性有什么用?
  20. 4. 编程题:用 Map 实现数组去重(要求保留原顺序,支持引用类型元素)
  21. 5. 编程题:用 Map 优化“两数之和”算法(高频算法题)
  22. 6. 分析题:为什么在频繁增删键值对的场景下,Map 比 Object 更高效?
  23. 手写简易版 Map
  24. 手动哈希化实现
  25. 一、手动实现「存在性哈希化」(判断元素是否存在)
  26. 二、手动实现「统计型哈希化」(统计元素出现次数)
  27. 三、手动实现「索引型哈希化」(记录元素对应下标)
  28. 四、手动哈希化的注意事项(前端开发避坑)
  29. 总结
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • AtCoder Better 插件安装与 AI 翻译配置教程
  • HarmonyOS 6.0 Network Kit 深度解析:TLS 认证全面支持国密证书
  • 亲测一个月后,我把所有 AI 工具全卸载了:Megick 专业图像视频生成,接入 GPT-Image-2 和 Seedance 后直接封神
  • C++性能优化:提升代码执行效率的核心技巧
  • AI 时代的生产力重构:非技术背景者的产品化路径
  • 命令行大模型交互工具 MCPHost 实战指南
  • VSCode Cline 配置 ChatGPT Plus 与 GitHub Copilot 模型教程
  • Claude Code 提升效率的 Superpower 与记忆插件实战指南
  • 大模型时代:为何传统机器学习仍是 AI 入门最佳路径
  • Llama 3.1 开源模型快速部署教程
  • 美团大模型应用算法工程师岗位职责与技术栈详解
  • 软件工程范式演进:低代码技术逻辑与全栈化趋势
  • AI 智能编码工具深度测评:GitHub Copilot 与国产新秀对比
  • Python uv 工具:安装、升级与卸载指南
  • Flutter 三方库 flutter_dropzone 的鸿蒙化适配指南
  • Python 读取 txt 文件的基础方法与进阶技巧
  • C++ 从零实现 Json-Rpc 框架:服务端模块划分
  • 基于 Python 与 Wan2.2 模型的本地 AI 长视频生成智能体构建教程
  • Flutter 使用 sanitize_html 库防御 XSS 注入风险(OpenHarmony 适配)
  • MS-S1 MAX 与 AI MAX 395 在 Ubuntu 24 下使用 Vulkan llama.cpp 运行 gpt-oss 120b

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online