前言
大家好,我是木斯佳。
相信很多人都感受到了,在AI浪潮的席卷之下,前端领域的门槛在变高,纯粹的"增删改查"岗位正在肉眼可见地减少。曾经热闹非凡的面经分享,如今也沉寂了许多。但我们都知道,市场的潮水退去,留下的才是真正在踏实准备、努力沉淀的人。学习的需求,从未消失,只是变得更加务实和深入。
这个专栏的初衷很简单:拒绝过时的、流水线式的PDF引流贴,专注于收集和整理当下最新、最真实的前端面试资料。我会在每一份面经和八股文的基础上,尝试从面试官的角度去拆解问题背后的逻辑,而不仅仅是提供一份静态的背诵答案。无论你是校招还是社招,目标是中大厂还是新兴团队,只要是真实发生、有价值的面试经历,我都会在这个专栏里为你沉淀下来。

温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。
面经原文内容
📍面试公司:拓竹科技
🕐面试时间:近期,用户上传于2026-03-15
💻面试岗位:前端一面
❓面试问题:
-
自我介绍
-
动态主题怎么实现的?CSS变量
除了主题颜色,对黑暗模式兼容怎么设计?
less的作用,为什么选择less?
原子CSS和less这些有什么区别,怎么理解?
-
动态拼装是怎么实现的?这些卡片有层级上的概念吗?
自己去实现一个拖拽怎么实现?怎么判断拖拽时选中的是哪个对象呢?
把addEventListener放在哪呢?e.target和e.currentTarget的区别?
-
useMemo用在哪些场景?
改变父组件的state子组件会跟着更新吗?会渲染?会刷新?会执行?
props变了子组件就会更新吗?有什么方法可以让子组件不更新呢?
-
diff算法的理解?他的更新的时间复杂度是怎样的,讲原因?
是深度优先搜索还是广度优先搜索?
-
useRef和useState的区别?
-
怎么理解闭包?他有什么用呢?怎么销毁掉这个变量?
-
用过JS哪些异步处理方式?(promise、await、generator、定时器)
点击事件算异步吗?
哪些是宏任务,哪些是微任务?宏任务和微任务的区别?
-
跨域是什么东西?为什么要有跨域这东西?
如何定向让某个域名a访问b?(CORS、JSONP、iframe+postmessage)这些一般在什么时候用?
-
某个项目有部署吗?数据库用的什么?
transport层发送是通过什么形式发送?
怎么理解nextjs这个框架?
从aisdk拿到数据我是怎么渲染的,怎么去显示消息?
tanquery在前端吗?
-
手撕代码:防抖
-
平常会怎么用AI?agent用的多吗?
💡 木木有话说
这是一场非常全面的面试,收录的原因是目前我在很多大厂的面经里,css的问题比重比前两年降低了很多。AI时代+各种UI库的加持下,手写复杂css变得越来越少。但实际上在我了解的圈子里,很多前后端、全栈在前端领域差异较大的还真就是css,很多框架或者语法糖,后端掌握的很快,唯独css调试很多后端是比较头疼的。所以说前端同学们不要放下相关的基本功。这在实际项目里也是很重要的。
📝 拓竹科技前端一面·深度解析
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 公司定位 | 拓竹科技 - 3D打印/智能制造领域 |
| 面试风格 | 实战细节型 + 原理追问型 + 场景延伸型 |
| 难度评级 | ⭐⭐⭐⭐(四星,覆盖广且深入) |
| 考察重心 | CSS工程化、拖拽实现、React优化、Diff算法、闭包、异步、跨域、AI使用 |
🔍 逐题深度解析
二、CSS主题与工程化
问题:动态主题怎么实现的?CSS变量
css
/* 1. CSS变量实现动态主题 */
:root {
--primary-color: #1890ff;
--bg-color: #ffffff;
--text-color: #333333;
}
.theme-dark {
--primary-color: #ff4d4f;
--bg-color: #141414;
--text-color: rgba(255, 255, 255, 0.85);
}
.button {
background-color: var(--primary-color);
color: var(--text-color);
}
/* 2. JavaScript切换主题 */
function toggleTheme() {
document.documentElement.classList.toggle('theme-dark')
}
/* 3. 黑暗模式兼容 */
@media (prefers-color-scheme: dark) {
:root {
--primary-color: #ff4d4f;
--bg-color: #141414;
--text-color: rgba(255, 255, 255, 0.85);
}
}
问题:less的作用?为什么选择less?
less
// 1. less的作用
// - 变量定义
@primary-color: #1890ff;
@border-radius: 4px;
// - 嵌套
.nav {
ul { list-style: none; }
li { display: inline-block; }
}
// - 混合(Mixin)
.border-radius(@radius) {
-webkit-border-radius: @radius;
-moz-border-radius: @radius;
border-radius: @radius;
}
.button {
.border-radius(4px);
}
// - 函数
@base: 5%;
@filler: @base * 2;
// 2. 为什么选择less
// - 学习曲线平缓,接近CSS语法
// - 功能丰富,满足大部分场景
// - 生态成熟,工具支持好
// - 可与CSS-in-JS方案结合
问题:原子CSS和less的区别
css
/* 1. 原子CSS(如Tailwind) */
/* HTML中直接使用工具类 */
<div class="bg-blue-500 text-white p-4 rounded">
按钮
</div>
/* 2. 传统CSS/Less */
/* 定义样式类 */
.button {
background-color: #1890ff;
color: white;
padding: 1rem;
border-radius: 4px;
}
/* 3. 区别对比 */
| 维度 | 原子CSS | Less/Sass |
|------|---------|-----------|
| 思想 | 组合原子类 | 抽象组件样式 |
| 体积 | 固定,可purge | 随组件增长 |
| 学习成本 | 需记忆类名 | 熟悉CSS即可 |
| 灵活性 | 受限于预设 | 完全自由 |
| 维护性 | 无需命名 | 需规划命名 |
| 适用场景 | 快速开发 | 复杂定制 |
三、拖拽实现
问题:动态拼装怎么实现?卡片有层级吗?
javascript
// 1. 拖拽实现
function Draggable() {
const [position, setPosition] = useState({ x: 0, y: 0 })
const [dragging, setDragging] = useState(false)
const dragRef = useRef()
// 事件监听放在document上,保证拖动流畅
useEffect(() => {
const handleDrag = (e) => {
if (!dragging) return
setPosition({
x: e.clientX - offset.x,
y: e.clientY - offset.y
})
}
const handleDragEnd = () => {
setDragging(false)
}
document.addEventListener('mousemove', handleDrag)
document.addEventListener('mouseup', handleDragEnd)
return () => {
document.removeEventListener('mousemove', handleDrag)
document.removeEventListener('mouseup', handleDragEnd)
}
}, [dragging])
const handleMouseDown = (e) => {
const offset = {
x: e.clientX - position.x,
y: e.clientY - position.y
}
setDragging(true)
}
return (
<div
ref={dragRef}
onMouseDown={handleMouseDown}
style={{
position: 'absolute',
left: position.x,
top: position.y,
zIndex: dragging ? 100 : 1 // 拖拽时提高层级
}}
>
可拖拽卡片
</div>
)
}
问题:怎么判断拖拽时选中哪个对象?事件监听放哪?target和currentTarget区别?
javascript
// 1. 判断选中对象
// 方案1:记录所有可拖拽元素
document.querySelectorAll('.draggable').forEach(el => {
el.addEventListener('mousedown', (e) => {
currentDragElement = e.target.closest('.draggable')
})
})
// 方案2:事件委托
container.addEventListener('mousedown', (e) => {
const dragElement = e.target.closest('.draggable')
if (dragElement) {
currentDragElement = dragElement
}
})
// 2. e.target vs e.currentTarget
// e.target:触发事件的实际元素
// e.currentTarget:绑定事件监听的元素
container.addEventListener('click', (e) => {
console.log('target:', e.target) // 点击的button
console.log('currentTarget:', e.currentTarget) // container
})
// 3. 事件监听位置
// - 具体元素:每个元素单独监听(性能差,动态元素需重新绑定)
// - 父容器委托:性能好,动态元素自动支持
// - document:适合全局拖拽
// 推荐:用父容器委托
dragContainer.addEventListener('mousedown', (e) => {
const item = e.target.closest('.drag-item')
if (item) {
startDrag(item, e)
}
})
四、React性能优化
问题:useMemo用在哪些场景?
javascript
// 1. useMemo适用场景
// 1.1 复杂计算
const expensiveValue = useMemo(() => {
return data.filter(item => item.value > 100)
.map(item => item.name)
.join(', ')
}, [data])
// 1.2 保持引用稳定(配合memo)
const userInfo = useMemo(() => ({
name: user.name,
age: user.age
}), [user])
// 1.3 避免子组件不必要的渲染
<Child data={useMemo(() => ({ id, name }), [id, name])} />
// 2. 错误用法
// ❌ 简单计算不需要
const value = useMemo(() => a + b, [a, b]) // 没必要
// ❌ 依赖频繁变化
const value = useMemo(() => compute(), [Date.now()]) // 每次都会重新计算
问题:父组件state变化,子组件会更新吗?如何阻止?
javascript
// 1. 默认行为
function Parent() {
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setCount(c => c + 1)}>点击</button>
<Child /> {/* 每次count变化,Child都会重新渲染 */}
</div>
)
}
// 2. props不变,子组件也会更新
// 因为父组件重新渲染,默认会重新渲染所有子组件
// 3. 阻止子组件更新的方法
// 3.1 React.memo
const Child = React.memo(function Child() {
console.log('Child渲染')
return <div>Child</div>
})
// 3.2 useMemo缓存组件
const child = useMemo(() => <Child />, [])
// 3.3 使用key
<Child key={stableId} /> // key变化才重新渲染
// 3.4 组件拆分
// 将频繁变化的state隔离到子树
// 4. 特别注意
// 即使Child用了memo,如果传入的函数/对象每次变化,也会重新渲染
// 需要用useCallback/useMemo保持引用稳定
五、Diff算法
问题:diff算法的时间复杂度?深度优先还是广度优先?
javascript
// 1. 传统diff(树的最小编辑距离)
// 时间复杂度:O(n^3)(不可接受)
// 2. React优化后的diff
// 时间复杂度:O(n)
// 基于三个假设:
// - 不同类型的元素产生不同树(直接替换)
// - 通过key标识稳定节点
// - 同层比较,不跨层
// 3. 比较策略
// 3.1 广度优先?深度优先?
// React使用深度优先,但做了优化:
// - 同层比较
// - 有key时复用节点
// 3.2 比较流程
function diff(oldNode, newNode) {
if (!oldNode) return insert(newNode)
if (!newNode) return delete(oldNode)
if (oldNode.type !== newNode.type) {
// 类型不同,直接替换
return replace(oldNode, newNode)
}
// 类型相同,比较属性
updateProps(oldNode, newNode)
// 比较子节点(核心)
if (newNode.props.children) {
diffChildren(oldNode.props.children, newNode.props.children)
}
}
// 3.3 diffChildren算法
function diffChildren(oldChildren, newChildren) {
// 双指针遍历 + key匹配
let oldStart = 0, oldEnd = oldChildren.length - 1
let newStart = 0, newEnd = newChildren.length - 1
while (oldStart <= oldEnd && newStart <= newEnd) {
// 比较节点,移动指针
}
// 时间复杂度 O(n) 因为假设了key的存在
}
六、useRef vs useState
问题:useRef和useState的区别?
javascript
// 1. 核心区别
// useState:变化触发重新渲染
// useRef:变化不触发重新渲染
// 2. useState
const [count, setCount] = useState(0)
// 每次setCount都会重新渲染组件
// 3. useRef
const countRef = useRef(0)
// 修改countRef.current不会重新渲染
countRef.current++ // 不触发渲染
// 4. 使用场景
// useState:需要在UI上显示的状态
const [name, setName] = useState('') // 需要显示在input中
// useRef:不需要显示的数据
const timerRef = useRef() // 定时器ID
const prevCountRef = useRef() // 上一次的值
const inputRef = useRef() // DOM引用
// 5. 读取时机
// useState:只能在渲染时读取
// useRef:可以在任何时候读取
function Component() {
const [count, setCount] = useState(0)
const countRef = useRef(0)
const handleClick = () => {
setCount(c => c + 1)
countRef.current++
console.log(count) // 0(闭包中的旧值)
console.log(countRef.current) // 1(最新值)
}
}
七、闭包
问题:怎么理解闭包?有什么用?怎么销毁?
javascript
// 1. 闭包定义
// 函数 + 词法环境的引用
function createCounter() {
let count = 0 // 被内部函数引用
return function() {
count++ // 内部函数引用外部变量
return count
}
}
const counter = createCounter()
console.log(counter()) // 1
console.log(counter()) // 2
// 2. 闭包的用途
// 2.1 私有变量
function createUser(name) {
let _name = name
return {
getName: () => _name,
setName: (newName) => { _name = newName }
}
}
// 2.2 防抖节流
function debounce(fn, delay) {
let timer
return function(...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
// 2.3 函数柯里化
function add(x) {
return function(y) {
return x + y
}
}
// 3. 如何销毁闭包
// 3.1 解除引用
counter = null // 不再引用内部函数,闭包被垃圾回收
// 3.2 避免意外闭包
// 在useEffect中及时清理
useEffect(() => {
const timer = setInterval(() => {
console.log(count) // 闭包捕获count
}, 1000)
return () => clearInterval(timer) // 清理
}, [count])
// 3.3 内存泄漏场景
// 在事件监听中未移除
element.addEventListener('click', function handler() {
// 引用了外部大对象
})
// 需要removeEventListener
八、异步处理
问题:JS异步处理方式?点击事件算异步吗?宏任务微任务?
javascript
// 1. JS异步处理方式
// 1.1 回调函数
setTimeout(() => {}, 1000)
fs.readFile('file.txt', (err, data) => {})
// 1.2 Promise
fetch('/api/data')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.log(err))
// 1.3 async/await
async function fetchData() {
try {
const res = await fetch('/api/data')
const data = await res.json()
console.log(data)
} catch (err) {
console.log(err)
}
}
// 1.4 Generator
function* generator() {
const data = yield fetch('/api/data')
console.log(data)
}
// 1.5 定时器
setTimeout(() => {}, 1000)
setInterval(() => {}, 1000)
// 2. 点击事件算异步吗?
// 点击事件属于异步,但不是宏任务/微任务
// 它属于Web API,被放入任务队列
// 3. 宏任务 vs 微任务
// 宏任务:setTimeout、setInterval、I/O、UI渲染
// 微任务:Promise.then、MutationObserver、queueMicrotask
// 4. 执行顺序
console.log('1') // 同步
setTimeout(() => {
console.log('2') // 宏任务
}, 0)
Promise.resolve().then(() => {
console.log('3') // 微任务
})
console.log('4') // 同步
// 输出:1, 4, 3, 2
// 5. 点击事件顺序
button.addEventListener('click', () => {
console.log('click')
})
// 当用户点击时,回调会进入任务队列
// 在所有同步代码和微任务之后执行
九、跨域
问题:跨域是什么?为什么要有跨域?如何解决?
javascript
// 1. 跨域是什么?
// 浏览器同源策略:协议、域名、端口不同
// 同源:https://example.com:443/page1 和 https://example.com:443/page2
// 跨域:https://api.example.com 和 https://www.example.com
// 2. 为什么要有跨域?
// - 安全隔离:防止恶意网站读取另一个网站的数据
// - 保护用户隐私:限制访问Cookie等敏感信息
// - CSRF防护:限制恶意请求
// 3. 解决方案
// 3.1 CORS(最常用)
// 后端设置响应头
res.setHeader('Access-Control-Allow-Origin', 'https://example.com')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
res.setHeader('Access-Control-Allow-Credentials', 'true')
// 3.2 JSONP(只支持GET)
function jsonp(url, callback) {
const script = document.createElement('script')
const callbackName = 'callback_' + Date.now()
window[callbackName] = (data) => {
callback(data)
delete window[callbackName]
document.body.removeChild(script)
}
script.src = `${url}?callback=${callbackName}`
document.body.appendChild(script)
}
// 3.3 iframe + postMessage
// 父页面
iframe.contentWindow.postMessage(data, 'https://child.com')
// 子页面
window.addEventListener('message', (e) => {
if (e.origin === 'https://parent.com') {
console.log(e.data)
}
})
// 3.4 代理服务器
// webpack devServer
proxy: {
'/api': 'http://localhost:3000'
}
// 3.5 适用场景
// - CORS:最通用,后端配合
// - JSONP:老旧项目,只支持GET
// - iframe+postMessage:跨域窗口通信
// - 代理:开发环境,绕过跨域
十、项目部署与Next.js
问题:项目部署、数据库、transport层、Next.js、数据渲染
javascript
// 1. 部署方式
// - Vercel(Next.js最佳搭档)
// - 自建服务器(Nginx + PM2)
// - 云服务(AWS、阿里云)
// 2. 数据库
// - MySQL/PostgreSQL(关系型)
// - MongoDB(文档型)
// - Redis(缓存)
// 3. transport层
// - HTTP/REST
// - GraphQL
// - WebSocket
// - gRPC
// 4. Next.js理解
// 4.1 核心特性
// - SSR/SSG/ISR多种渲染模式
// - 文件路由系统
// - API Routes
// - 图片优化
// - 零配置
// 4.2 优势
// - SEO友好
// - 首屏加载快
// - 开发体验好
// 5. 从aisdk获取数据渲染
import { useChat } from 'ai/react'
function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat()
return (
<div>
{messages.map(m => (
<div key={m.id}>
<div>{m.role}: {m.content}</div>
</div>
))}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
placeholder="输入消息..."
/>
</form>
</div>
)
}
// 6. TanQuery在前端
import { useQuery } from '@tanstack/react-query'
function Posts() {
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: () => fetch('/api/posts').then(res => res.json())
})
if (isLoading) return <div>加载中...</div>
if (error) return <div>错误:{error.message}</div>
return (
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
十一、手撕:防抖
问题:实现防抖函数
javascript
// 1. 基础版
function debounce(func, wait) {
let timer
return function(...args) {
clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
// 2. 支持immediate
function debounce(func, wait, immediate = false) {
let timer
return function(...args) {
const callNow = immediate && !timer
clearTimeout(timer)
timer = setTimeout(() => {
timer = null
if (!immediate) {
func.apply(this, args)
}
}, wait)
if (callNow) {
func.apply(this, args)
}
}
}
// 3. 带取消功能
function debounce(func, wait, immediate = false) {
let timer
const debounced = function(...args) {
const callNow = immediate && !timer
clearTimeout(timer)
timer = setTimeout(() => {
timer = null
if (!immediate) {
func.apply(this, args)
}
}, wait)
if (callNow) {
func.apply(this, args)
}
}
debounced.cancel = function() {
clearTimeout(timer)
timer = null
}
return debounced
}
// 4. 使用示例
const search = debounce((keyword) => {
console.log('搜索:', keyword)
}, 500)
search('a')
search('ab')
search('abc') // 只有这个会执行
// 5. React中使用
function SearchInput() {
const [value, setValue] = useState('')
const debouncedSearch = useMemo(
() => debounce((val) => console.log('搜索:', val), 500),
[]
)
const handleChange = (e) => {
setValue(e.target.value)
debouncedSearch(e.target.value)
}
return <input value={value} onChange={handleChange} />
}
十二、AI使用
问题:平常会怎么用AI?agent用的多吗?
javascript
// 1. AI使用场景
// 1.1 代码生成
// - 生成重复性代码(表单、表格)
// - 生成单元测试
// - 生成文档注释
// 1.2 问题排查
// - 复制错误信息让AI分析
// - 代码审查建议
// 1.3 学习辅助
// - 解释复杂概念
// - 生成学习大纲
// 1.4 原型开发
// - 用自然语言生成初始代码
// - 快速搭建Demo
// 2. 常用AI工具
// - GitHub Copilot(代码补全)
// - Cursor(AI编辑器)
// - ChatGPT(问答)
// - Claude(长上下文)
// 3. Agent使用
// 3.1 什么是Agent
// - 能自主规划、调用工具的AI
// - 可执行复杂任务
// 3.2 Agent应用场景
// - 自动化测试生成
// - 代码重构建议
// - 项目文档生成
// 3.3 如何用好AI
// - 提供清晰上下文
// - 分步拆解复杂任务
// - 验证AI输出
// - 沉淀prompt模板
// 4. 注意事项
// - 不依赖AI,要理解代码
// - 敏感代码不直接贴给AI
// - 验证AI输出的准确性
📚 知识点速查表
| 知识点 | 核心要点 |
|---|---|
| CSS变量 | 动态主题、黑暗模式、prefers-color-scheme |
| Less | 变量、嵌套、混合、函数 |
| 原子CSS | 工具类、与传统CSS对比 |
| 拖拽实现 | 事件委托、target/currentTarget、层级管理 |
| useMemo | 复杂计算、引用稳定、优化子组件 |
| 组件更新 | React.memo、useMemo、useCallback |
| Diff算法 | O(n)、同层比较、key的作用 |
| useRef/useState | 渲染触发、引用稳定 |
| 闭包 | 词法环境、私有变量、内存释放 |
| 异步 | 宏任务/微任务、执行顺序 |
| 跨域 | 同源策略、CORS、JSONP、postMessage |
| Next.js | SSR/SSG、文件路由、API Routes |
| 防抖 | 定时器、immediate、cancel |
| AI使用 | Copilot、代码生成、问题排查 |
📌 最后一句:
拓竹科技的这场面试,覆盖了前端开发的方方面面,从CSS工程化到React性能优化,从拖拽实现到跨域原理,再到AI使用。每个问题都在考察原理理解和实战经验的结合。能答好这些,说明你已经具备了独立应对复杂前端开发的能力。