【20250924】9月份前端面试复盘

190 阅读9分钟

感觉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))

经验总结

  1. 回答八股问题可以适当结合实际开发场景,以凸显技术深度
  2. 面试前可以提前了解下待面试公司的产品概况,有针对性地在自我介绍中描述项目亮点
  3. 项目难点描述:动机/痛点 -> 措施/解决方案 -> 收益
  4. 如果提前得知面试有手撕代码环节,建议先刷几道题热热身,虽然基本猜不到对方考啥,但是可以让大脑提前转起来,不至于出现“我不会,我也不知道”的尴尬情况