2025年|前端面试练习题
你在长列表优化中做了哪些具体的事情?
核心思路:避免同时渲染大量的DOM节点,因为DOM节点的创建、渲染和内存占用都非常消耗性能。
1. 虚拟滚动
- 计算可视区域:监听容器的滚动事件,获取
scrollTop和scrollTop + clientHeight - 计算渲染区间:根据可视区域和每个列表项的高度,计算出需要渲染的起始索引
startIndex和结束索引endIndex。通常会上下多渲染 5-10 个项,防止滚动时出现白屏 - 动态渲染和定位:
- 只渲染
[startIndex, endIndex]区间的列表项 - 容器高度设置为所有列表项的总高度,通过”撑开容器”实现
- 对渲染的列表项使用
position: absolute或transform: translateY(...),设置top属性为索引 × 列表项高度
- 只渲染
- 性能优化:
- 使用
requestAnimationFrame优化动画或Intersection Observer优化滚动监听 - 缓存已计算的位置,对于动态高度的列表需要在渲染后测量并缓存高度
- 使用
- 使用的库/技术:
- React:
react-window,react-virtualized - Vue:
vue-virtual-scroller - 也可根据业务需求使用
Intersection Observer API自己实现简化版
- React:
2. 分页和无限滚动
分页
- 实现方式:明确将数据分成多页,用户通过点击页码或”加载更多”按钮请求新数据
- 优点:实现简单,
SEO友好,内存压力小 - 缺点:交互不连贯,需要用户主动操作
无限滚动
- 实现方式:当用户滚动到底部附近时,自动加载下一页数据并追加到当前列表
- 优点:用户体验流畅,适合内容流(社交媒体、商品列表)
- 缺点:实现复杂,需要管理滚动位置和加载状态;性能随滚动逐渐下降;页脚可能无法到达
- 优化点:可与虚拟滚动结合,即使加载很多数据,
DOM节点数也保持恒定
3. 微观优化:必须渲染长列表时
优化
key属性:- 使用唯一且稳定的
key(如数据中的 id),而不是数组的index - 帮助框架准确识别哪些节点可被复用、移动或销毁
- 使用唯一且稳定的
避免内联对象和函数:
- 将内联样式对象和函数提取到组件外部,或使用
useCallback/useMemo进行记忆化 - 避免因引用不同导致的不必要重渲染
- 将内联样式对象和函数提取到组件外部,或使用
简化子组件:
- 将列表项拆分成独立的轻量子组件
- 使用
React.memo(函数组件)或PureComponent(类组件)进行包裹
不可变数据与结构共享:
- 使用不可变数据的方式更新列表数据(ES6 扩展运算符、Immer.js 等)
- 让
React.memo的浅比较快速失效,精确重渲染发生变化的项
控制更新粒度:
- 将频繁更新的状态(如点赞按钮)下放到每个列表项组件自身
- 使用状态管理库避免触发整个列表的根组件更新
4. 其他辅助优化
图片懒加载:
- 使用
loading="lazy"属性或Intersection Observer - 减少页面初始化时的 HTTP 请求数和带宽占用
- 使用
使用 CSS 属性
contain: content:- 告诉浏览器元素的子树独立于页面其余部分
- 减少重绘和重排时的计算量,提升滚动性能
数组扁平化、去重、排序
1 | var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10]; |
LeetCode:LRU 缓存机制
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作:获取数据 get 和写入数据 put。
- 获取数据
get(key)- 如果密钥存在于缓存中,则获取密钥的值(总是正数),否则返回 -1 - 写入数据
put(key, value)- 如果密钥不存在,则写入数据。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据,从而为新数据留出空间
进阶:你是否可以在 O(1) 时间复杂度内完成这两种操作?
示例:
1 | LRUCache cache = new LRUCache(2); // 缓存容量 |
实现:
1 | class LRUCache { |
数组扁平化与去重
数组扁平化(数组降维)
Array.prototype.flat() 方法
MDN:flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
1 | const test = ["a", ["b", "c"], ["d", ["e", ["f"]], "g"]] |
Array.prototype.flat() 特性总结
- 用于将嵌套的数组扁平化,变成一维的数组
- 该方法返回一个新数组,对原数据没有影响
- 不传参数时,默认扁平化一层
- 可以传入一个整数,表示想要扁平化的层数
- 传入 <=0 的整数将返回原数组,不扁平化
Infinity关键字作为参数时,无论多少层嵌套,都会转为一维数组- 如果原数组有空位,
Array.prototype.flat()会跳过空位
手动实现扁平化方法
方法一:使用 reduce 方法
一次性扁平化所有
1 | function flattenDeep(arr) { |
实现 flat 函数
- 作业帮一面
1 | function flat(arr, depth = 1) { |
方法二:使用栈
一次性降维所有
1 | function flattenDeep(arr) { |
数组去重
方式一:Set(ES6)
1 | function unique(arr) { |
方式二:reduce
1 | function unique (arr) { |
方法三:filter
1 | function unique(arr) { |
数组原地去重方法
方法一:排序去重
1 | const removeDuplicates = (nums) => { |
特点:
- 使用
sort()方法进行原地排序 - 通过双指针技巧在排序后的数组中去除重复项
- 最后使用
splice()删除多余的重复元素
方法二:优化版本
1 | const removeDuplicates = (nums) => { |
特点:
- 从后向前遍历数组
- 使用
indexOf检查当前元素是否为重复项 - 将重复元素与数组末尾元素交换,然后缩短数组长度
- 最后使用
splice()删除末尾的重复元素
两种方法的比较
| 方法 | 优点 | 缺点 | 是否保持原顺序 |
|---|---|---|---|
| 排序去重 | 效率较高 | 改变元素原始顺序 | 否 |
| 优化版本 | 保持元素原始顺序 | 效率相对较低 | 是 |
数组去重函数(支持对象和数组元素)
需求说明
实现一个数组去重函数,能够处理包含对象、数组等复杂类型的元素:
输入:
[123, "meili", "123", "mogu", 123]输出:
[123, "meili", "123", "mogu"]输入:
[123, [1, 2, 3], [1, "2", 3], [1, 2, 3], "meili"]输出:
[123, [1, 2, 3], [1, "2", 3], "meili"]输入:
[123, {a: 1}, {a: {b: 1}}, {a: "1"}, {a: {b: 1}}, "meili"]输出:
[123, {a: 1}, {a: {b: 1}}, {a: "1"}, "meili"]
基础解法:使用 JSON.stringify
1 | const removeDuplicates = (arr) => { |
问题分析
使用 JSON.stringify 的局限性:
1 | let o1 = {a:1, b:2} |
问题:对象键顺序不同会被认为是不同的元素。
解决方案
解决思路
一个数组(包含对象等类型元素)去重函数,需要在基础类型判断相等条件下满足以下条件:
- 如果元素是数组类型,则需要数组中的每一项相等
- 如果元素是对象类型,则需要对象中的每个键值对相等
去重本身就是遍历数组,然后比较数组中的每一项是否相等而已,所以关键步骤有两步:比较、去重
比较:
- 首先判断类型是否一致,类型不一致则认为两个数组元素是不同的
- 如果是数组类型,则递归比较数组中的每个元素是否相等
- 如果是对象类型,则递归比较对象中的每个键值对是否相等
- 否则,直接
===比较
去重:
- 采用
reduce去重,初始accumulator为[] - 采用
findIndex找到accumulator是否包含相同元素 - 如果不包含则加入,否则不加入
- 返回最终的
accumulator,则为去重后的数组
代码实现
1 | // 获取类型 |
总结
这种方法解决了 JSON.stringify 在面对对象键顺序不同时的局限性,通过深度比较实现了真正的对象内容去重。
1 |
|
1 | // 模拟多个异步操作 |
1 | // Node.js 文件系统示例 |
1 |
|
手写aysnc和await
1 |
|
合并两个有序数组
1 |
|
js判断数据类型
typeof 判断Array.isArray()instanceofObject.prototype.toString.call()
1 | typeof 123 // "number" |
map实现reduce
1 | function myReduceMap(arr, callback, initialValue) { |
Jsx和React.createElement()
1 |
|
1 | +------------------------------------------------------+ |
低代码平台的核心就是“配置转代码”和“代码回流配置”。
用户拖拽产生的是 JSON Schema,我会把这个 Schema 转成 AST,使用 Babel Types 生成 JSX,再用 Generator 输出 React/Vue 代码。
用户修改代码时,我反向用Babel Parser把代码解析成AST,再还原成JSON Schema,这样可视化和代码能双向同步。
事件表达式、安全校验、自动 import、属性补全、跨端转换(React → H5/Native)也都是基于AST的遍历和重写完成的。
所以AST是低代码的“编译器基础设施”.
AST转换
为什么前端AST转换用DFS?
因为Babel、ESLint都是基于DFS preOrder + postOrder的。
1 | const ast = { |
预加载
❓“preload 和 prefetch 区别是什么?”
完美回答:
preload 是为当前页面做的高优先级加载;
prefetch 是为未来页面做的低优先级加载。
preload 立即加载并参与渲染;
prefetch 在空闲时间加载,不阻塞当前渲染。
❓“preload 字体为什么要加 crossorigin?”
因为字体是跨域资源,浏览器需要使用 CORS 才能缓存。
否则会重复下载两次。
❓“什么时候用 preconnect?”
当你要请求第三方域名(CDN、API、字体)时,提前建好连接,可以减少 50~300ms 的延迟。
- 最佳实践(实际项目中这样写)
🌟 推荐的 HTML 头部1
2
3
4
5
6
7
8
9
10
11
12<!-- 提前解析 DNS -->
<link rel="dns-prefetch" href="//cdn.example.com">
<!-- 提前建立连接 -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<!-- 首屏关键 CSS / JS -->
<link rel="preload" href="/static/main.css" as="style">
<link rel="preload" href="/static/main.js" as="script">
<!-- 字体 -->
<link rel="preload" href="/fonts/myfont.woff2" as="font" crossorigin>
浅拷贝和深拷贝
1 | //浅拷贝 |
1 | //深拷贝 |
JSON方法深拷贝的局限性
1 | const obj = { |
DFS,二叉树,前中后序,以及插入部分BFS的理论知识
DFS只有先序(pre-order)和后序(post-order),没有“中序”。
而“前序/中序/后序”是二叉树独有的概念。
👉 DFS 先序 / 后序
是泛树的概念,只有 “先访问自己 OR 后访问自己”。DFS = Depth First Search(深度优先搜索)
1 | // 伪代码 |
👉 二叉树前中后序
是 在 DFS 基础上 针对二叉树的 根、左、右 三者顺序组合出来的。
1 | // 都是伪代码 |
BFS应用
1 | // 伪代码 |
- React Fiber 架构(核心点)
React Fiber 不再使用 DFS 递归,而是使用 可中断的 BFS 式遍历。
逐层遍历每个节点
并允许中断(时间切片)
做成可恢复的链表结构
面试说法:
Fiber 是一个基于链表的可中断的 BFS 遍历,解决了递归 DFS 不能中断导致的卡顿问题。
- 渲染树(DOM Tree)层级布局计算
浏览器 Layout(布局)阶段是 自上而下 BFS 派发。
- 聊天 / 评论 / 社交 App 的层级展开
例如查看评论时,要按“楼层”展示。
- 微前端路由调度、依赖图分析(拓扑排序)
BFS 用于:
构建依赖图
层序加载微应用
构建拓扑排序
面试官:DFS和BFS的区别?在前端有哪些场景应用?
你可以这么回答(非常强的那种👇):
DFS 是深度优先,一条路走到底;BFS 是广度优先,按层遍历。
两者的区别在于遍历策略、数据结构(DFS 用栈,BFS 用队列)、内存占用和是否适合求最短路径。
在前端工程中 DFS 常用于:
虚拟DOM深度遍历(Vdom Diff)
路由树、菜单树、权限树的处理
AST的遍历(Babel、ESLint、Webpack Loader)
查找节点路径、深克隆
BFS 常用于:
React Fiber的遍历机制
按层级渲染数据(评论、留言、树组件)
依赖图、拓扑排序
查找最近的节点(最近可见元素)
十大排序算法
我记不住怎么办,每次从冒泡开始,就像是学习英语,每次只背了个abandon。