前端进阶与面试核心算法考点,不再局限于基础语法,更侧重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\(\(\) =\> 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)。