Skip to content

电商前端面试题

一、前端基础

1. 浏览器渲染流程?reflow 和 repaint 区别?

答案: HTML → DOM 树,CSS → CSSOM 树,合并成渲染树 → 布局(Layout)→ 绘制(Paint)→ 合成(Composite)。

// 重排:改布局属性(宽高、位置)→ 重新计算布局
el.style.width = '100px'    // reflow
// 重绘:改外观属性(颜色、背景)→ 只重新画
el.style.color = 'red'      // repaint
// 重排一定触发重绘,重绘不一定要重排

2. 多端适配方案?

答案: 响应式(媒体查询)+ 动态尺寸(rem/vw)+ 布局弹性(flex/grid)。

/* 方案1:vw 单位 —— 随视口变化 */
.box { width: 50vw; }
/* 方案2:rem —— 根字体基准 */

html { font-size: 13.3333vw; }  /* 375px 设计稿 = 50px */

.box { width: 1rem; }           /* 375px 屏 = 50px */
/* 方案3:媒体查询断点 */
@media (max-width: 768px) { ... }

小程序用 rpx(750 设计稿),uni-app 里可写 rpx 直接多端适配。

3. 闭包、原型链、事件循环?

答案: 闭包 = 函数记住定义时作用域;原型链 = 对象找属性一路沿 proto 向上;事件循环 = 同步先执行,微任务(Promise)在宏任务(setTimeout)前。

// 闭包
function outer() {
let n = 0
return () => ++n
}

// 原型链
arr -> Array.prototype -> Object.prototype -> null
// 事件循环
console.log(1)                 // 同步
Promise.resolve().then(() => console.log(2))  // 微任务
setTimeout(() => console.log(3))              // 宏任务
// 输出:1 2 3

4. 深拷贝实现及 JSON 方案的缺陷?

答案: JSON.parse(JSON.stringify) 丢失 undefined/函数/Symbol、Date 变字符串、循环引用直接报错。

function deepClone(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj
if (map.has(obj)) return map.get(obj)         // 循环引用
const copy = Array.isArray(obj) ? [] : {}


map.set(obj, copy)

for (const key of Object.keys(obj)) {


copy[key] = deepClone(obj[key], map)
}

return copy
}

5. HTTP 缓存?

答案: 强缓存命中不发请求(Cache-Control/Expires),没命中走协商缓存(ETag/Last-Modified),服务器返回 304 则用缓存。

第一次请求 → 200 + 响应头 Cache-Control: max-age=3600 3600秒内 → 200 (from disk cache),不发请求 过期 → 带 If-None-Match 问服务器 → 304 用旧 / 200 用新 6. 跨域解决方案?

答案: CORS(服务端配头)、JSONP(script 标签)、代理(开发 Vite proxy / 生产 Nginx 反向代理)。

// 后端加响应头

Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST

// 预检请求:非简单请求(自定义头/JSON)先发 OPTIONS 问服务器允许吗

7. 防抖节流?

答案: 防抖 = 停止触发后才执行(搜索联想);节流 = 固定间隔执行(滚动加载)。

function debounce(fn, delay) {
let t
return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), delay) }

}

function throttle(fn, wait) {
let last = 0
return (...a) => {
const now = Date.now()
if (now - last >= wait) { last = now; fn(...a) }

} } 8. Promise.all 手写?

答案: 全部成功返回数组,一个失败就 reject。

function promiseAll(promises) {
return new Promise((resolve, reject) => {
const results = []
let count = 0
promises.forEach((p, i) => {

p.then(val => { results[i] = val

if (++count === promises.length) resolve(results)

}, reject) }) }) }

二、Vue3 专项

9. Vue3 响应式原理?比 Vue2 强在哪?

答案: Proxy 拦截对象读写,get 收集依赖,set 触发更新;Vue2 用 defineProperty 只能监听已有属性,新增/删除属性、数组索引都监听不到。

const state = reactive({ count: 0 })
// get → 收集依赖;set → 通知更新
effect(() => console.log(state.count))
state.count++  // 自动执行 effect
// Vue2 痛点:state.newKey = 1 不响应(需 $set)

10. Composition API vs Options API?

答案: 组合式按"功能"组织代码,避免同一个功能的逻辑散在 data/methods/lifecycle 三处,逻辑复用靠自定义 hook(useXxx)。

// Options:数据、方法、生命周期分开
export default { data() {}, methods: {}, mounted() {} }
// Composition:一个功能的代码在一起
export default {

setup() {

const count = ref(0)
const inc = () => count.value++

onMounted(() => console.log('mounted'))

return { count, inc }

} } 11. v-if 和 v-for 为什么不能一起用?

答案: v-for 优先级比 v-if 高,同时用会"先循环再判断",每项都白遍历一次;应该用计算属性过滤后再 v-for。

// ❌
<li v-for="item in list" v-if="item.visible">
// ✅
const visibleList = computed(() => list.filter(i => i.visible))
<li v-for="item in visibleList">

12. computed vs watch?

答案: computed 由依赖自动推导、有缓存、模板里用;watch 监听变化做副作用(请求、跳转)。

const total = computed(() => items.value.reduce((s, i) => s + i.price, 0))

watch(() => route.query.id, (val) => { fetchDetail(val) }) 13. nextTick 原理?

答案: Vue 更新是异步的(微任务批处理),DOM 更新完后的回调用 nextTick 包;底层优先 Promise。

const count = ref(0)
count.value = 1
console.log(document.querySelector('p').textContent) // 还是旧值
await nextTick()
console.log(document.querySelector('p').textContent) // 新值

14. 组件通信方式?

答案: props 父传子、emit 子传父、v-model 双向、provide/inject 跨层级、Pinia 全局共享、ref 直接拿子组件实例。

// provide/inject

provide('user', user) // 父

const user = inject('user')   // 任意深层子
// Pinia
const store = useUserStore()  // 任何组件

15. 虚拟 DOM 和 diff?Vue3 优化了什么?

答案: diff 是比对新旧虚拟 DOM,只更新差异;Vue3 优化:静态节点提升(不重复创建)、patch flag 按需更新、双端 diff 改最长递增子序列。

// Vue3 patch flag:标记动态节点,只更新变的
<div>静态内容</div>
<span :class="cls">动态</span>  // 只给这个打 flag

三、React18 专项

16. 并发模式?

答案: 渲染可中断、高优任务插队、低优任务让路;startTransition 标记低优更新,useDeferredValue 延迟值更新。

const [isPending, startTransition] = useTransition()

startTransition(() => setSearchResults(data)) // 低优,不卡输入 17. 渲染流程?Fiber/Scheduler/Reconciler/双缓冲?

答案: Scheduler 调度 → Render 阶段(Fiber 树上 diff,可中断)→ Commit 阶段(改 DOM,不可中断)→ 浏览器绘制。双缓冲 = 两棵 Fiber 树,切换指针上屏,用户永远看不到半成品。

setState → 批处理 → Scheduler → Render(重跑组件 → 新虚拟DOM → reconcile/diff → effectTag) → Commit(Mutation 改DOM → useLayoutEffect → 切指针) → 绘制 → useEffect 18. useEffect/useLayoutEffect?useMemo/useCallback?

答案: useEffect 绘制后异步跑;useLayoutEffect 绘制前同步跑(改布局防闪烁)。useMemo 缓存值防重复计算,useCallback 缓存函数防子组件重渲染。

useLayoutEffect(() => {
// 读布局并调整,用户看不到闪烁

}, [])

useEffect(() => {
// 请求数据、日志,不阻塞绘制

}, []) 19. 防止不必要渲染的手段?

答案: React.memo(函数组件浅比较 props)、PureComponent(类组件)、shouldComponentUpdate(手动控制)、useMemo/useCallback(稳住引用)。

const Child = React.memo(({ name }) => <div>{name}</div>)
// props 引用/值没变就不重渲染

20. 合成事件是什么?

答案: React 把所有事件委托到根容器统一处理(17 前在 document,17+ 在 root),模拟冒泡;好处:跨浏览器统一行为、自动清理、性能好。

<button onClick={handle}>   // 实际绑定在 root 上,事件对象是 SyntheticEvent

21. 受控/非受控组件?

答案: 受控 = 值由 state 管(value + onChange),数据可靠但代码多;非受控 = DOM 自己管(defaultValue + ref 读值),代码少但难控制。电商表单(下单地址、优惠券)建议受控。

// 受控
<input value={form.name} onChange={e => setForm({...form, name: e.target.value})} />
// 非受控
<input defaultValue="Tom" ref={inputRef} />

四、工程化与构建

22. Vite vs Webpack?

答案: Vite 开发用原生 ESM 按需编译,启动秒开;Webpack 开发要全量打包,项目大就慢。生产 Vite 也会用 Rollup 打包(浏览器对 ESM 请求太多)。

Vite 开发:浏览器直接请求模块,esbuild 预构建依赖,改哪个编译哪个 Webpack 开发:先全量打包成 bundle,再启动 dev server 23. 前端工程化包含什么?

答案: 模块化(ESM 拆代码)、组件化(复用 UI)、规范化(ESLint/Prettier/commit 规范)、自动化(CI/CD、自动部署、自动化测试)、性能化(构建优化、分包)。

24. 通用组件封装思路?

答案: props 驱动 + 插槽扩展 + 事件通知 + 样式可控(class 透传)。

// 商品卡片组件设计
<GoodsCard

:data="goods" // 数据 @click="onCardClick" // 事件 >

<template #tag>限时购</template>  // 插槽扩展
</GoodsCard>

25. 小程序架构 + 多端方案?

答案: 小程序是双线程架构(逻辑层 JS 线程 + 渲染层 WebView),通信靠 setData;Taro/uni-app 把 Vue/React 代码编译成各端代码。

五、性能优化与兼容性

26. 首页首屏慢怎么排查?

答案: Performance 面板看瀑布图 → 分清网络/JS/渲染瓶颈 → 对症下药。

网络:CDN、HTTP2、gzip、图片压缩、懒加载 JS:分包、按需引入、Tree-shaking、减少主包 渲染:SSR/预渲染、骨架屏、减少重排 缓存:HTML no-cache、静态资源强缓存+hash 27. 大列表渲染?

答案: 虚拟滚动,只渲染可视区域;固定高度用 react-window / vue-virtual-scroller,原理 = 占位撑高 + translateY 偏移。

// 核心:只渲染 start~end
const start = Math.floor(scrollTop / itemHeight)
const end = start + visibleCount

28. 浏览器兼容性?

答案: CSS 加前缀(autoprefixer)、降级写法;JS 用 Babel 转译 + polyfill(core-js);旧浏览器(如低版本安卓 WebView)不用可选链等新语法,或用 esbuild 降级。

user-select: none; /* 现代 / -webkit-user-select: none; / 旧 Safari */ 29. 埋点和性能监控?

答案: 手动埋点(业务事件)+ 自动埋点(PV/点击)+ Web Vitals 采集。

// 性能指标
new PerformanceObserver((list) => {
const lcp = list.getEntries().at(-1)

sendBeacon('/log', { lcp: lcp.startTime }) }).observe({ type: 'largest-contentful-paint', buffered: true })

// 埋点
track('order_submit', { orderId, amount })  // 下单埋点,CPS 归属靠这个

六、电商业务场景

30. 电商下单结算流程 + CPS 追踪 + 支付回调排查?

答案:

下单流程: 商品页 → 购物车 → 确认订单(地址/优惠券) → 提交订单(后端创建) → 拉起支付(微信/支付宝) → 支付成功回调 → 后端改订单状态 → 前端轮询/WebSocket 拿到已支付 → 跳订单详情

// CPS 追踪:推广链接带 pid,下单带上
// https://shop.com/p/123?pid=u_888
localStorage.setItem('cps_pid', params.pid)  // 存到下单请求里
submitOrder({ goodsId: 123, pid: localStorage.getItem('cps_pid') })
// 支付成功页面没跳转排查:
// 1. 看后端订单状态是否已变(轮询接口)
// 2. 看 WebSocket 是否收到推送(网络/重连)
// 3. 看支付回调是否成功(后端日志)
// 4. 前端兜底:定时轮询 + 手动刷新按钮

Released under the MIT License.