感觉9月份过的挺快的,近两周基本上都是忙于面试/准备面试,直到今天才有空停下来复盘。
本文把近两周的面经进行记录,并且把一些回答的比较差的点/写不出来的代码题进行回溯。
个人情况
3-5年前端开发,React + Vue,偏b端。
面试情况
某外包公司自研岗(项目外包、终面没去)
一面(技术面)
-
介绍上一份工作主要负责项目概况
-
大数据列表渲染处理
-
虚拟列表
-
减少列表元素初次加载渲染开销(如:tooltips组件实例监听鼠标mouseover事件按需创建)
-
-
Echarts 使用场景,如何进行数据处理
-
使用什么框架多(Vue),对比下Vue和React
-
Vue的diff算法工作流程
-
平常使用到哪些数组API
-
使用的构建工具(Vite),开发模式下初始加载白屏时间长怎么优化
-
gzip压缩
-
如何基于ElementPlus进行样式定制化开发
-
反问:
- 进一步了解项目的类型(信创、项目外包模式)
二面(领导面)
-
自我介绍
-
介绍过往项目概况
-
离职原因
-
如何看待加班
-
工作中是否遇到不舒服的事情,怎么解决
-
平时有参与公司的什么活动
-
之前的绩效评估方式是怎样的
因为岗位急招,自己感觉也对项目不是很感兴趣,终面拒绝了。
某科技公司自研岗(offer)
base深圳,线下面试,面试前先填表,之后两轮面试一次进行。
技术面
-
vue响应式原理
-
双向绑定更新过程
-
vite常用配置,编写的插件
-
promise实现,用哪些设计模式
-
闭包和应用
-
防抖和节流
-
xss,csrf
-
平时用到哪些设计模式
-
如何从0设计一个前端后台管理系统,考虑哪些方面
-
平时在工作中怎样去使用ts
-
项目的难点,怎么解决
-
对AI工具的使用
HR面
-
离职原因
-
职业规划是怎样的,最近学了哪些东西
-
找工作关注哪些方面
-
如何看待加班
-
之前的薪资情况、绩效怎么算的
其实本来是有三轮面试的,因为主管比较忙,我也着急赶车,就提前结束了。本来以为没啥戏了,不过面试完第二周就说通过了。
薪资较原来base稍微降了一点,可以接受,不过公司附近没有地铁口,交通略麻烦(来回两个钟),因此后面拒绝了offer。
文远知行子公司(挂)
base广州,boss上联系过后交换简历,之后让笔试。
按照HR的说法,四轮面试:机试 + 技术面 x 2 + HR面。
两轮技术面大致都是:问项目 + 问八股 + 给到题让现场写代码。
机试题
用的牛客网的考试平台进行笔试,三道题选两道,只记得两道题了,不过题目算不上难:
-
判断点是否在矩形内
-
判断给定矩阵是否为对角矩阵
技术一面
-
自我介绍 + 项目介绍
-
在项目组做了哪些贡献
-
有没有了解过MCP
-
AI编程工具有用过哪些
-
代码提交规范约束怎么做的
-
vite构建优化怎么做
-
css预编译语言有哪些,有没有用过tailwind,tailwind在构建做了什么优化(不知道),有没有了解过purgecss(没有)
-
React的Fiber架构(调度器、协调器(新的虚拟dom、diff)、渲染器)
-
对React列表渲染key的理解
-
React的心智模型、函数式编程
-
手撕代码:对象扁平化(嵌套对象 -》 一维对象(对象key为‘xxx.xxx.xxx’))
-
反问:
- 业务类型:AI视频语料的一些处理(标注等)
面完第二天直接让技术二面,处理效率还算高。
技术二面(挂)
-
自我介绍 + 项目介绍
-
细化问项目的贡献(monorepo改造、构建优化)
-
Webpack和Vite区别
-
大文件上传如何处理(分片上传),鉴权怎么做
-
HTTP1.1和HTTP2.0区别(首部压缩、多路复用、服务器推送)
-
进程和线程(答得不太好),js为什么设计成单线程,EventLoop
-
密集计算怎么处理,webWorker和主进程怎么通信,传输的数据是怎样的
-
React hook(useMemo/useCallback区别、useLayoutEffect/useEffect区别)
-
手撕代码:给定一个数组,有n - 1 个数,分别表示第i + 1个节点的父节点,在此基础上构建树。给定两个节点u、v,求解节点间距离(没撕出来)
-
反问:
- 项目组情况(AI数据采集/处理业务,基于React,涉及2维/3维可视化,团队内暂时没有专门前端所以需要招人)
说起来也是可惜,让手撕的代码实际不是很难,不过当时脑子卡壳了做不出来,那就这样吧...
错漏面试题复盘
进程和线程概念
当时问这块卡壳了,语言组织也不是很好,只答出来了多线程并发这个要点。
进程是资源分配的基本单位,线程是程序执行的基本单位。
线程的主要优点在于:能极大提升程序的并发性能和响应能力,同时创建、切换和通信的开销远小于进程。
解释
进程好比一个公司,它拥有独立的办公空间和资源(如内存、文件);线程则是公司里的员工。 线程的优点在于:
高效协作: 员工们(线程)共享公司的公共资源(进程资源),沟通协作(线程通信)效率极高,远高于两个公司(进程)之间的合作。
节省成本: 招聘一个员工(创建线程)或让员工切换任务(线程切换)比开一家新公司(创建进程)或让两个公司协调(进程切换)要快得多、省资源得多。
并发工作: 多个员工可以同时处理不同的任务(如一个负责接待客户,一个负责处理数据),极大地提高了公司的整体效率(程序并发性)和响应客户(用户交互)的速度。
tailwind在构建做了什么优化
-
JIT Mode:
- Tailwind 3.0+ 默认使用 JIT 模式,只生成项目中实际使用的 CSS 类
- 扫描源代码文件,识别使用的 Tailwind 类名
- 动态生成对应的 CSS 规则,而不是预先生成所有可能的类
-
PurgeCss集成:CSS层面的tree-shaking,自动移除未使用的 CSS 类,减小最终生成的 CSS 文件大小
WebWorker相关
之前面试问了密集数据处理的场景,回答了WebWorker,但我确实不了解这个东西,只是知道它是用来处理密集计算的,避免阻塞主线程。所以回答的不是很好。
webworker基本使用
主线程初始化worker后,通过postMessage方法发送消息给worker:
// 主线程
const worker = new Worker('worker.js');
// 发送消息
worker.postMessage('Hello from main thread!');
worker.postMessage({ type: 'calculate', data: [1, 2, 3] });
// 接收消息
worker.onmessage = function(event) {
console.log('Received from worker:', event.data);
};
// 或者使用 addEventListener
worker.addEventListener('message', (event) => {
console.log('Received:', event.data);
});
worker.js通过注册onmessage事件来接收主线程发送的消息,处理完成后通过postMessage方法发送消息回主线程。
// worker.js
// 接收消息
self.onmessage = function(event) {
console.log('Worker received:', event.data);
// 处理数据
const result = event.data * 2;
// 发送回主线程
self.postMessage(result);
};
// 或者使用 addEventListener
self.addEventListener('message', (event) => {
// 处理逻辑
self.postMessage({ result: 'processed', data: event.data });
});
Worker传输的数据
Worker线程可以传输的数据类型包括:
- 原始类型(如字符串、数字、布尔值)
- 对象(如数组、对象字面量)
- 二进制数据(如ArrayBuffer、Blob、File、ImageBitmap)
- 结构化克隆算法支持的类型(如Date、RegExp、Map、Set、WeakMap、WeakSet)
- 循环引用的对象会被自动处理,避免无限循环
代码题(一)
对象扁平化(嵌套对象 -> 一维对象(对象key为‘xxx.xxx.xxx’))。难度不大,算是比较常规的js手撕代码题目。
const obj = {
a: {
b: {
c: [1, 2, 3],
d: 'aaa',
f: null,
},
e: 'bbb'
},
g: '111'
}
function flattenObj(obj, rootKey = '', map = new Map()) {
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
map.set(rootKey, obj)
return obj
}
Object.entries(obj).forEach(([k, v]) => {
const key = rootKey ? `${rootKey}.${k}` : k
flattenObj(v, key, map)
})
return [...map.entries()].reduce((pre, [k, v]) => ({ ...pre, [k]: v, }), {})
}
console.log(flattenObj(obj))
代码题(二)
给定一个数组,有n - 1 个数,分别表示第i + 1个节点的父节点,在此基础上构建树(默认第一个节点为根节点,没有父节点)。给定两个节点u、v,求解节点间距离。
题目可以拆解成四个部分:
- 构建树
- 求解节点u到根节点的距离
- 求解节点v到根节点的距离
- 节点u到节点v的距离 = 节点u到根节点的距离 + 节点v到根节点的距离 - 2 * 节点u和节点v的最近公共祖先的深度
其中2、3步可用一个函数实现,用深度优先算法求解。
之前卡壳的步骤主要是在于如何找到公共祖先,后面想了下,完全可以把root到u、v的路径直接求出来,然后用指针找到第一个不同的节点,它的前一个节点就是最近公共祖先。只能说当时没想到有点可惜了...
// 给定一个数组,有n - 1 个数,分别表示第i + 1个节点的父节点,在此基础上构建树(默认第一个节点为根节点,没有父节点)。给定两个节点u、v,求解节点间距离。
// 示例1: parents = [1, 1, 2, 2]
// 树结构:
// 1
// / \
// 2 3
// / \
// 4 5
const parents = [1, 1, 2, 2, 3, 3, 7]
// 1. 构建树
function buildTree(parents) {
const nodeMap = new Map()
const rootNode = {
id: 1,
children: []
}
nodeMap.set(1, rootNode)
const nodeArr = [
rootNode,
...parents.map((item, index) => {
const _node = {
id: index + 2,
parentId: item,
children: []
}
nodeMap.set(index + 2, _node)
return _node
})
]
nodeArr.forEach(item => {
if (item.parentId) {
nodeMap.get(item.parentId).children.push(item)
}
})
return rootNode
}
// 2. 求根节点到指定节点的距离
function dfs(root, nodeId, route = []) {
if (!root) {
return
}
route.push(root.id)
if (root.id === nodeId) {
return route
}
for (let i = 0; i < root.children.length; i++) {
const res = dfs(root.children[i], nodeId, route)
if (res) {
return res
}
}
route.pop()
return
}
// 3. 求解u,v间距离
function getDistance(root, u, v) {
if (u === v) return 0
const rootToURoute = dfs(root, u)
const rootToVRoute = dfs(root, v)
let pt = 0
while(rootToURoute[pt] === rootToVRoute[pt]) {
pt++
}
return rootToURoute.length + rootToVRoute.length - 2 * pt
}
function handleInput(parents, u, v) {
return getDistance(buildTree(parents), u, v)
}
console.log(handleInput(parents,4, 8))
经验总结
- 回答八股问题可以适当结合实际开发场景,以凸显技术深度
- 面试前可以提前了解下待面试公司的产品概况,有针对性地在自我介绍中描述项目亮点
- 项目难点描述:动机/痛点 -> 措施/解决方案 -> 收益
- 如果提前得知面试有手撕代码环节,建议先刷几道题热热身,虽然基本猜不到对方考啥,但是可以让大脑提前转起来,不至于出现“我不会,我也不知道”的尴尬情况