前端数据结构与算法进阶|Map/Set模拟哈希表、Proxy链表、数组实操、函数记忆化、手写instanceof

前端算法题真正考的往往不是刷题量,而是你能不能用 JavaScript 原生能力把哈希表、链表、数组操作和底层判断逻辑写得清晰、稳定、可解释。

前端进阶与面试核心算法考点,不再局限于基础语法,更侧重JS原生API实现数据结构、手撕底层逻辑、工程性能优化算法。本文全覆盖指定核心知识点:Map/Set模拟哈希表、Proxy实现链表、数组扁平化/去重/乱序、函数记忆化、手写instanceof,所有案例均为可直接运行的手撕源码,搭配原理解析、坑点总结、面试标准答案。

一、Map/Set 模拟哈希表(前端哈希结构核心)

哈希表(Hash Table)是键值对快速存取的数据结构,核心优势为 O(1) 增删查改 时间复杂度。JS 中 Object/Map/Set 底层均基于哈希表实现,我们可通过 Map/Set 模拟原生哈希表,同时理解哈希冲突、扩容、去重底层逻辑。

1.1 哈希表核心原理

  • 哈希函数:将任意长度的 key 转换为固定哈希下标,映射到哈希表存储空间

  • 哈希冲突:不同 key 计算出相同哈希下标,常用解决方式:链地址法

  • 自动扩容:负载因子(数据量/存储空间)过高时,扩容重构哈希表,保证查询效率

1.2 基于 Map 手写简易哈希表

利用 Map 天然哈希特性,实现完整的增删改查、冲突处理、扩容逻辑,贴合原生哈希表机制。

class HashTable {
  // 初始化哈希表容量与负载因子
  constructor(size = 8) {
    this.size = size // 哈希表容量
    this.count = 0 // 存储数据数量
    this.table = new Map() // 依托Map实现哈希存储
  }

  // 哈希函数:计算key对应的下标
  hash(key) {
    let code = 0
    // 简单字符哈希算法
    for (let i = 0; i < key.length; i++) {
      code += key.charCodeAt(i)
    }
    return code % this.size
  }

  // 新增/修改数据
  set(key, value) {
    const index = this.hash(key)
    // 链地址法处理哈希冲突:同一下标存储数组
    if (!this.table.has(index)) {
      this.table.set(index, [])
    }
    // 遍历判断是否存在key,存在则覆盖
    const list = this.table.get(index)
    for (let item of list) {
      if (item.key === key) {
        item.value = value
        return
      }
    }
    // 不存在则新增
    list.push({ key, value })
    this.count++

    // 负载因子超过0.75,自动扩容(原生哈希表规则)
    if (this.count / this.size > 0.75) {
      this.resize(this.size * 2)
    }
  }

  // 查询数据
  get(key) {
    const index = this.hash(key)
    if (!this.table.has(index)) return null
    const list = this.table.get(index)
    const target = list.find(item => item.key === key)
    return target ? target.value : null
  }

  // 删除数据
  delete(key) {
    const index = this.hash(key)
    if (!this.table.has(index)) return false
    const list = this.table.get(index)
    const idx = list.findIndex(item => item.key === key)
    if (idx === -1) return false
    list.splice(idx, 1)
    this.count--
    return true
  }

  // 哈希表扩容重构
  resize(newSize) {
    const oldTable = this.table
    this.size = newSize
    this.count = 0
    this.table = new Map()
    // 遍历旧数据重新哈希映射
    oldTable.forEach(list => {
      list.forEach(item => {
        this.set(item.key, item.value)
      })
    })
  }
}

// 测试
const ht = new HashTable()
ht.set('name', '前端算法')
ht.set('age', 18)
console.log(ht.get('name')) // 前端算法

1.3 Set 哈希去重核心原理

Set 是无重复、无序的哈希集合,底层通过哈希比对实现自动去重,是前端最高效的基础去重方案。核心特性:严格匹配 ===,可去重基础数据类型,可存储引用类型(无法自动去重引用地址不同的对象)。

二、Proxy 实现响应式链表(高阶手写)

链表是经典线性数据结构,区别于数组,链表非连续存储、插入删除O(1)、查询O(n)。普通链表仅能存储数据,通过 Proxy 劫持可实现响应式链表,监听节点增删改查变化,适配可视化、状态监控等工程场景,是前端独有高阶链表实现方案。

2.1 链表基础结构

单向链表:每个节点包含 value(值)、next(指向下一节点),首尾节点串联形成链式结构。

2.2 Proxy 响应式链表完整实现

// 链表节点类
class Node {
  constructor(value) {
    this.value = value
    this.next = null
  }
}

// Proxy响应式链表
class ProxyLinkList {
  constructor() {
    this.head = null // 头节点
    this.tail = null // 尾节点
    this.watchCallback = null // 监听回调
    // 劫持链表实例,实现全局响应
    this.proxy = new Proxy(this, {
      set: (target, prop, value) => {
        target[prop] = value
        // 触发更新监听
        target.watchCallback && target.watchCallback()
        return true
      }
    })
    return this.proxy
  }

  // 监听链表变化
  watch(fn) {
    this.watchCallback = fn
  }

  // 尾部插入节点
  append(value) {
    const node = new Node(value)
    if (!this.head) {
      // 空链表,首尾节点归一
      this.head = node
      this.tail = node
    } else {
      this.tail.next = node
      this.tail = node
    }
  }

  // 根据值删除节点
  remove(value) {
    if (!this.head) return false
    // 删除头节点
    if (this.head.value === value) {
      this.head = this.head.next
      return true
    }
    // 遍历查找节点
    let cur = this.head
    while (cur.next) {
      if (cur.next.value === value) {
        cur.next = cur.next.next
        // 更新尾节点
        if (!cur.next) this.tail = cur
        return true
      }
      cur = cur.next
    }
    return false
  }

  // 遍历链表
  traverse() {
    const res = []
    let cur = this.head
    while (cur) {
      res.push(cur.value)
      cur = cur.next
    }
    return res
  }
}

// 测试响应式链表
const list = new ProxyLinkList()
// 监听链表变化
list.watch(() => {
  console.log('链表发生变更,最新数据:', list.traverse())
})

list.append(1)
list.append(2)
list.remove(1)

2.3 核心优势与面试考点

  • 相比普通链表,可响应式监听节点增删改,无需手动触发更新

  • 规避数组插入删除的性能损耗,大数据频繁操作场景更高效

  • 面试高频:链表 vs 数组 差异、Proxy 劫持数据结构的创新应用

三、数组高频算法:扁平化 / 去重 / 乱序(工程常用)

数组三大高频算法是前端笔试、工程优化刚需,摒弃原生API极简写法,提供底层手撕源码,适配多层嵌套、复杂数据、真实业务场景。

3.1 数组扁平化(手撕多层扁平化)

数组扁平化:将多维嵌套数组转为一维数组,提供 递归版、栈迭代版(最优),支持指定扁平化深度。

1. 递归实现(简单易懂)

function flattenArr(arr) {
  let res = []
  arr.forEach(item => {
    // 递归判断是否为数组
    res = res.concat(Array.isArray(item) ? flattenArr(item) : item)
  })
  return res
}

console.log(flattenArr([1, [2, [3, 4], 5]])) // [1,2,3,4,5]

2. 栈迭代实现(性能最优,无递归爆栈风险)

function flattenByStack(arr) {
  const stack = [...arr]
  const res = []
  while (stack.length) {
    const item = stack.pop()
    if (Array.isArray(item)) {
      stack.push(...item)
    } else {
      res.push(item)
    }
  }
  return res.reverse()
}

3. 指定深度扁平化

function flattenDepth(arr, depth = 1) {
  return depth > 0 
    ? arr.reduce((pre, cur) => pre.concat(Array.isArray(cur) ? flattenDepth(cur, depth - 1) : cur), [])
    : arr.slice()
}

3.2 数组去重(全场景方案)

1. 基础类型去重(Set 最简)

function uniqueBasic(arr) {
  return [...new Set(arr)]
}

2. 复杂引用类型去重(对象数组去重,工程常用)

// 根据唯一key去重
function uniqueObjArr(arr, key) {
  const map = new Map()
  return arr.filter(item => {
    if (!map.has(item[key])) {
      map.set(item[key], true)
      return true
    }
    return false
  })
}

// 测试
const list = [{id:1}, {id:2}, {id:1}]
console.log(uniqueObjArr(list, 'id'))

3.3 数组乱序(Fisher-Yates 洗牌算法)

重点坑点arr\.sort\(\(\) =\&gt; Math\.random\(\) \- 0\.5\)不均匀乱序,概率偏差极大,生产环境禁用!

业界标准:Fisher-Yates 洗牌算法,均匀随机乱序,时间复杂度 O(n)

function shuffleArr(arr) {
  const newArr = [...arr]
  for (let i = newArr.length - 1; i > 0; i--) {
    // 随机生成下标
    const randomIndex = Math.floor(Math.random() * (i + 1))
    // 交换位置
    [newArr[i], newArr[randomIndex]] = [newArr[randomIndex], newArr[i]]
  }
  return newArr
}

console.log(shuffleArr([1,2,3,4,5]))

四、函数记忆化(缓存优化算法)

函数记忆化(Memoization)是性能优化经典算法,通过缓存函数入参对应的返回结果,避免重复计算,大幅提升递归、复杂计算、重复请求的执行效率。

4.1 核心原理

  • 基于闭包持久化缓存容器

  • 以函数入参为 key,执行结果为 value

  • 重复入参直接读取缓存,无需重新执行逻辑

4.2 基础版函数记忆化(手撕)

function memoize(fn) {
  // 闭包缓存
  const cache = new Map()
  return function(...args) {
    // 参数序列化作为key
    const key = args.join(',')
    if (cache.has(key)) {
      return cache.get(key)
    }
    // 执行函数并缓存结果
    const res = fn.apply(this, args)
    cache.set(key, res)
    return res
  }
}

// 测试:斐波那契递归优化
const fib = memoize(function(n) {
  if (n <= 2) return 1
  return fib(n-1) + fib(n-2)
})

console.log(fib(30)) // 极速执行,无重复计算

4.3 进阶优化(支持引用类型参数、过期缓存)

基础版无法缓存对象参数,通过 JSON 序列化解决,同时添加缓存过期机制,适配接口请求缓存场景。

function memoizeAdvance(fn, expire = 5000) {
  const cache = new Map()
  return function(...args) {
    const key = JSON.stringify(args)
    const now = Date.now()
    // 判断缓存是否存在且未过期
    if (cache.has(key)) {
      const { value, time } = cache.get(key)
      if (now - time < expire) return value
    }
    const res = fn.apply(this, args)
    cache.set(key, { value: res, time: now })
    return res
  }
}

4.4 工程应用场景

  • 递归算法优化(斐波那契、阶乘)

  • 高频重复接口请求缓存

  • 复杂计算函数、格式化函数性能优化

五、手写 instanceof(原型链核心考点)

instanceof 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上,是前端原型链面试必手撕考点,核心本质:遍历原型链进行比对

5.1 原生机制原理

  • 遍历实例的 \_\_proto\_\_ 原型链

  • 逐层比对是否等于构造函数的 prototype

  • 找到匹配项返回 true,遍历到原型链顶端 null 返回 false

5.2 完整手撕 instanceof 源码

function myInstanceof(instance, constructor) {
  // 基础类型直接返回false
  if (instance === null || typeof instance !== 'object') return false
  // 获取实例原型
  let proto = Object.getPrototypeOf(instance)
  // 逐层遍历原型链
  while (proto) {
    if (proto === constructor.prototype) {
      return true
    }
    // 向上递归原型
    proto = Object.getPrototypeOf(proto)
  }
  // 遍历到顶层null,匹配失败
  return false
}

// 测试
console.log(myInstanceof([], Array)) // true
console.log(myInstanceof({}, Object)) // true
console.log(myInstanceof('', String)) // false

5.3 核心坑点

  • 基础数据类型 null/undefined/string/number 全部返回 false

  • 所有引用类型最终都能匹配 Object,myInstanceof\(\[\], Object\) // true

  • 可通过修改 prototype 伪造 instanceof 结果

六、全文核心总结(面试必背)

  • 哈希表:基于Map可模拟完整哈希结构,通过哈希函数映射下标,链地址法解决冲突,负载因子0.75自动扩容,存取O(1)

  • Proxy链表:依托Proxy数据劫持实现响应式链表,弥补普通链表无状态监听的缺陷,增删效率优于数组

  • 数组算法:迭代扁平化无爆栈风险,Set去重适配基础类型,Map适配对象去重,Fisher-Yates算法实现均匀乱序

  • 函数记忆化:基于闭包缓存计算结果,解决重复计算性能问题,可拓展过期机制适配工程缓存

  • instanceof:核心是逐层遍历原型链比对构造函数原型,仅对引用类型生效,基础类型直接返回false

七、高频面试简答题

  • 哈希表为什么查询速度快? 通过哈希函数直接映射数据存储下标,无需遍历,增删查改时间复杂度为O(1),远优于数组O(n)。

  • 数组和链表的核心区别? 数组连续存储,查询快、增删慢;链表非连续存储,增删快、查询慢,Proxy链表可实现响应式状态监听。

  • 为什么不推荐sort实现数组乱序? sort随机算法分布不均匀,部分下标概率偏差极大,Fisher-Yates洗牌算法均匀随机、性能更优。

  • 函数记忆化的优缺点? 优点:避免重复计算,提升执行性能;缺点:占用内存,长期使用需清理缓存,适合高频重复调用函数。

  • instanceof 判断的本质是什么? 遍历实例的原型链,判断构造函数的prototype是否存在于实例原型链上,无法判断基础类型。

  • 如何实现对象数组去重? 利用Map存储唯一标识,遍历过滤重复数据,相比双重循环时间复杂度从O(n²)优化为O(n)。

本文总结

  • Map、Set 和 Proxy 这些原生能力,不只是 API 题,它们可以直接映射到哈希表、链表和响应式数据结构的实现思路。
  • 数组扁平化、去重、乱序、记忆化和 instanceof 手写题,本质上都在考你对时间复杂度、引用关系和原型链的理解。
  • 前端算法真正有价值的地方,是把这些手撕题转化成工程里的缓存优化、数据处理和可维护实现。
GYSTACK 文章文末广告 硅云云服务器活动 适合个人项目、轻量建站和出海业务部署。
后浪云移动端信息流广告 后浪云主机服务 适合长期部署、独立站和海外机房需求。