引言
又到金三银四招聘季,很多前端开发者开始准备跳槽或求职。一份好的面试准备不仅能帮你拿到更好的offer,更能帮你系统性地梳理前端知识体系。
根据我这些年面试和被面试的经验,前端面试的核心考察点其实相对稳定:基础知识是否扎实、框架理解是否深入、工程化能力是否具备、问题解决能力如何。
这篇文章按照前端面试的不同模块,整理了高频考察的知识点和面试题。每个问题都配有详细解析,帮助你真正理解而不是死记硬背。

模块一:JavaScript基础
问题1:var、let、const的区别是什么?
这是最基础的JavaScript问题,但很多人只能回答出表层。
核心区别:
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 提升但值是undefined | 提升但有暂时性死区 | 提升但有暂时性死区 |
| 重复声明 | 可以 | 不可以 | 不可以 |
| 重新赋值 | 可以 | 可以 | 不可以 |
面试追问:
javascript
// 经典面试题:输出什么?
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100)
}
// 输出:3 3 3
// 为什么?
// var是函数作用域,setTimeout是异步的
// 循环结束后i已经变成3,所以输出3个3
// 改成let会怎样?
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100)
}
// 输出:0 1 2
// 为什么?
// let是块级作用域,每次循环都会创建一个新的i
// setTimeout捕获的是各自块级作用域的i
最佳实践:默认使用const,只在确实需要重新赋值时使用let,避免使用var。
问题2:JavaScript的事件循环机制
事件循环是理解JavaScript异步编程的基础。
javascript
console.log('1')
setTimeout(() => {
console.log('2')
}, 0)
Promise.resolve().then(() => {
console.log('3')
})
console.log('4')
// 输出顺序:1 4 3 2
原理解析:
- 首先执行同步代码,输出1和4
- 同步代码执行完后,执行队列中的微任务(Promise的then),输出3
- 微任务执行完后,执行宏任务(setTimeout),输出2
面试追问:Promise和setTimeout的优先级?
javascript
// 微任务(Promise、async/await、MutationObserver)优先于宏任务
// 宏任务:setTimeout、setInterval、I/O、UI渲染
async function async1() {
console.log('async start') // 2
await async2()
console.log('async end') // 6
}
async function async2() {
console.log('async2') // 3
}
console.log('script start') // 1
setTimeout(() => {
console.log('setTimeout') // 8
}, 0)
async1()
new Promise((resolve) => {
console.log('promise') // 4
resolve()
}).then(() => {
console.log('promise then') // 7
})
console.log('script end') // 5
// 输出顺序:script start -> async start -> async2 -> promise -> script end -> async end -> promise then -> setTimeout
问题3:闭包的理解与应用
闭包是JavaScript最重要的概念之一。
基础理解:函数可以记住并访问其词法作用域,即使函数在其作用域之外执行。
javascript
function createCounter() {
let count = 0 // 私有变量
return {
increment() {
count++
return count
},
decrement() {
count--
return count
},
getCount() {
return count
}
}
}
const counter = createCounter()
console.log(counter.increment()) // 1
console.log(counter.increment()) // 2
console.log(counter.getCount()) // 2
console.log(counter.decrement()) // 1
经典面试题:
javascript
// 实现一个累加器函数
function add(x) {
return function(y) {
return x + y
}
}
const add5 = add(5)
console.log(add5(3)) // 8
console.log(add5(10)) // 15
闭包的应用场景:
- 模块化:创建私有变量
- 函数柯里化:延迟执行
- 防抖/节流:记住状态
- 缓存:存储计算结果
模块二:TypeScript类型系统
问题4:TypeScript的接口和类型别名有什么区别?
typescript
// 接口
interface User {
name: string
age: number
}
// 类型别名
type User = {
name: string
age: number
}
主要区别:
| 特性 | interface | type |
|---|---|---|
| 定义对象结构 | ✅ | ✅ |
| 定义基本类型 | ❌ | ✅ |
| 定义联合类型 | ❌ | ✅ |
| 定义元组 | ❌ | ✅ |
| 声明合并 | ✅ | ❌ |
| 计算属性 | ❌ | ✅ |
typescript
// 接口可以声明合并
interface User {
name: string
}
interface User {
age: number
}
// 最终User同时具有name和age
// type可以定义联合类型
type ID = string | number
type Status = 'pending' | 'success' | 'error'
// type可以定义元组
type Point = [number, number]
// interface可以继承
interface Person {
name: string
}
interface Student extends Person {
grade: number
}
// type可以用&实现交叉类型
type Person = {
name: string
}
type Student = Person & {
grade: number
}
最佳实践:
- 定义对象结构时,优先使用interface
- 需要使用联合类型、元组等复杂类型时,使用type
- 库和公共API倾向使用interface,便于扩展
问题5:泛型约束的理解
泛型让类型像变量一样灵活,泛型约束限定泛型的范围。
typescript
// 基本泛型
function identity<T>(arg: T): T {
return arg
}
// 泛型约束:限制T必须有某些属性
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: 'Tom', age: 25 }
const name = getProperty(user, 'name') // string
const age = getProperty(user, 'age') // number
// getProperty(user, 'email') // 报错,email不在user中
// 约束继承的类必须实现某个接口
interface Printable {
print(): void
}
function printAll<T extends Printable>(items: T[]): void {
items.forEach(item => item.print())
}
问题6:TypeScript的条件类型
条件类型可以根据其他类型推导出新类型。
typescript
// 基本语法
type IsString<T> = T extends string ? 'yes' : 'no'
type A = IsString<string> // 'yes'
type B = IsString<number> // 'no'
// 提取数组元素类型
type ElementType<T> = T extends (infer U)[] ? U : never
type C = ElementType<string[]> // string
type D = ElementType<number[]> // number
type E = ElementType<string> // never
// 实用类型:Parameters
type MyParameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never
function greet(name: string, age: number) {
return `Hello, ${name}, you are ${age}`
}
type GreetParams = MyParameters<typeof greet>
// [string, number]
模块三:React框架
问题7:React Hooks的规则是什么?
React Hooks有两个核心规则:
规则一:只在顶层调用Hook
javascript
// ❌ 错误:在条件语句中调用Hook
function Component() {
const [name, setName] = useState('')
if (condition) {
const [age, setAge] = useState(0) // 错误!
}
}
// ✅ 正确:始终在顶层调用
function Component() {
const [name, setName] = useState('')
const [age, setAge] = useState(0)
if (condition) {
// 使用已有的age状态
}
}
规则二:只在React函数中调用Hook
javascript
// ❌ 错误:在普通函数中调用
function handleClick() {
const [count, setCount] = useState(0) // 错误!
}
// ✅ 正确:在组件或自定义Hook中调用
function Component() {
const [count, setCount] = useState(0)
function handleClick() {
setCount(count + 1)
}
}
问题8:useEffect的依赖数组
useEffect的依赖数组是React面试的高频考点。
jsx
// 每次渲染后执行
useEffect(() => {
console.log('每次渲染都执行')
})
// 只在挂载时执行(类似componentDidMount)
useEffect(() => {
console.log('只在挂载时执行')
}, []) // 空依赖数组
// 依赖name变化时执行
useEffect(() => {
console.log('name变化了', name)
}, [name]) // 依赖name
// 清理副作用
useEffect(() => {
const timer = setInterval(() => {
console.log('tick')
}, 1000)
// 返回清理函数
return () => {
clearInterval(timer)
}
}, [])
// 常见错误:依赖设置为对象
// ❌ 不要这样写
useEffect(() => {
doSomething(obj)
}, [obj]) // 每次渲染obj都是新引用,会导致无限循环
// ✅ 应该这样写
useEffect(() => {
doSomething(obj.name)
}, [obj.name])
问题9:React Fiber架构的理解
React 16引入的Fiber架构是React性能优化的基础。
核心概念:
- Fiber是一个链表结构,每个React元素对应一个Fiber节点
- Fiber节点保存了组件的状态、props、副作用等信息
- 异步可中断的渲染,允许在渲染过程中暂停和恢复
两种工作模式:
- Render阶段:构建Fiber树,可中断
- Commit阶段:提交DOM更新,不可中断
优先级调度:
javascript
// React自动根据任务类型分配优先级
// 同步任务:用户输入、点击 -> 高优先级
// 异步任务:数据获取 -> 低优先级
// 动画 -> 最高优先级
问题10:React性能优化手段
jsx
// 1. React.memo:避免不必要的重渲染
const ExpensiveComponent = React.memo(({ data }) => {
// 只有data变化时才重新渲染
return <div>{/* 复杂渲染 */}</div>
})
// 2. useMemo:缓存计算结果
function Component({ list, filter }) {
const filteredList = useMemo(() => {
return list.filter(item => item.name.includes(filter))
}, [list, filter]) // 只有list或filter变化时才重新计算
return filteredList.map(item => <div key={item.id}>{item.name}</div>)
}
// 3. useCallback:缓存函数引用
const handleClick = useCallback(() => {
console.log('clicked')
}, []) // 空依赖,函数引用始终不变
// 4. 列表渲染使用key
// ✅ 正确:使用稳定的唯一ID
{items.map(item => <Item key={item.id} item={item} />)}
// ❌ 错误:使用index作为key
{items.map((item, index) => <Item key={index} item={item} />)}
// 5. 虚拟列表:长列表优化
import { FixedSizeList } from 'react-window'
function VirtualList({ items }) {
return (
<FixedSizeList
height={400}
itemCount={items.length}
itemSize={50}
>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
)
}
模块四:Vue框架
问题11:Vue3的Composition API相比Options API的优势
javascript
// Options API:按选项组织代码
export default {
data() {
return { count: 0 }
},
methods: {
increment() { this.count++ }
},
computed: {
double() { return this.count * 2 }
},
watch: {
count(newVal) {
console.log('count changed', newVal)
}
}
}
// Composition API:按逻辑功能组织代码
import { ref, computed, watch } from 'vue'
export default {
setup() {
// 响应式状态
const count = ref(0)
// 计算属性
const double = computed(() => count.value * 2)
// 方法
function increment() {
count.value++
}
// 监听
watch(count, (newVal) => {
console.log('count changed', newVal)
})
// 逻辑复用:使用composables
const { fetchUser, user } = useUser()
return { count, double, increment }
}
}
Composition API的优势:
- 更好的逻辑复用:通过composables函数复用逻辑
javascript
// 提取可复用逻辑
function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
return { count, increment, decrement }
}
// 在组件中使用
const { count, increment, decrement } = useCounter(10)
- 更好的代码组织:相关逻辑放在一起
- 更好的类型推断:setup函数的返回值类型明确
- 更小的打包体积:Composition API产生的代码更少
问题12:Vue3响应式原理
javascript
// Vue3使用Proxy实现响应式
// 基本原理
const target = { name: 'Tom', age: 25 }
const handler = {
get(target, key, receiver) {
console.log(`获取 ${key}`)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(`设置 ${key} 为 ${value}`)
return Reflect.set(target, key, value, receiver)
}
}
const proxy = new Proxy(target, handler)
proxy.name // 触发get,输出:获取 name
proxy.age = 30 // 触发set,输出:设置 age 为 30
javascript
// ref和reactive的区别
import { ref, reactive } from 'vue'
// ref:用于基本类型,返回带value属性的响应式对象
const count = ref(0)
console.log(count.value) // 0
count.value++
// reactive:用于对象,返回代理对象
const state = reactive({
count: 0,
name: 'Tom'
})
state.count++
state.name = 'Jerry'
// ref在模板中自动解包
// <div>{{ count }}</div> // 不需要count.value
问题13:Vue Router的实现原理
javascript
// 前端路由的两种模式
// 1. Hash模式:使用URL的hash部分
// URL: example.com/#/user
// 改变hash不会触发页面刷新
window.addEventListener('hashchange', () => {
const hash = window.location.hash
// 根据hash渲染对应组件
})
// 2. History模式:使用HTML5 History API
// URL: example.com/user
// 需要服务器配置支持fallback
// 核心方法
history.pushState(state, title, url) // 添加路由
history.replaceState(state, title, url) // 替换路由
window.addEventListener('popstate', () => {
// 监听浏览器前进/后退
})
// Vue Router使用示例
const routes = [
{ path: '/', component: Home },
{ path: '/user/:id', component: User }
]
const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(),
routes
})
// 在组件中使用
export default {
methods: {
goToUser(id) {
this.$router.push(`/user/${id}`)
// 或者
this.router.push(`/user/${id}`)
}
}
}
模块五:前端工程化
问题14:Webpack和Vite的区别
| 特性 | Webpack | Vite |
|---|---|---|
| 开发服务器启动 | 慢(需要打包) | 快(直接服务源文件) |
| 热更新 | 较慢 | 极快(ESM) |
| 生产构建 | 打包式 | Rollup(原生ESM) |
| 配置复杂度 | 复杂 | 相对简单 |
| 生态 | 丰富 | 快速成长 |
Vite的核心原理:
bash
# 开发环境
vite
# 1. 不打包,直接用ESM服务源文件
# 2. 依赖(node_modules)用esbuild预构建
# 3. 懒加载按需编译
# 生产环境
vite build
# 1. 使用Rollup进行打包
# 2. 代码分割优化
问题15:CommonJS和ES Module的区别
javascript
// CommonJS (CJS)
const fs = require('fs') // 导入
module.exports = { name: 'test' } // 导出
// ES Module (ESM)
import fs from 'fs' // 导入
export const name = 'test' // 导出
// 差异点:
// 1. 静态 vs 动态:CJS require可以在任何位置,ESM import只能在顶部
// 2. 同步 vs 异步:CJS同步,ESM支持异步
// 3. 值拷贝 vs 值引用:CJS导出的是拷贝,ESM是引用
javascript
// 经典面试题
// lib.js
let counter = 3
function inc() { counter++ }
module.exports = { counter, inc }
// main.js
const { counter, inc } = require('./lib')
console.log(counter) // 3(拷贝)
inc()
console.log(counter) // 3(不变!)
// ESM版本
// lib.mjs
export let counter = 3
export function inc() { counter++ }
// main.mjs
import { counter, inc } from './lib.mjs'
console.log(counter) // 3
inc()
console.log(counter) // 4(引用,会变)
模块六:浏览器与网络
问题16:浏览器缓存策略
plaintext
浏览器缓存优先级(从高到低):
1. Service Worker(可编程缓存)
2. Memory Cache(内存缓存,关闭Tab失效)
3. Disk Cache(磁盘缓存,持久化)
4. Push Cache(HTTP/2推送缓存)
HTTP缓存头:
javascript
// 强缓存:Cache-Control
Cache-Control: max-age=3600 // 缓存1小时
Cache-Control: no-cache // 每次都要验证
Cache-Control: no-store // 不缓存
// 协商缓存:Last-Modified / ETag
Last-Modified: Wed, 21 Oct 2026 07:28:00 GMT
If-Modified-Since: Wed, 21 Oct 2026 07:28:00 GMT
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
问题17:跨域解决方案
javascript
// 1. CORS(后端配置)
// 服务端设置响应头
Access-Control-Allow-Origin: * // 或具体域名
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
// 2. JSONP(仅GET)
function jsonp(url, callback) {
const script = document.createElement('script')
script.src = `${url}?callback=${callback}`
document.body.appendChild(script)
}
// 3. 代理(前端使用)
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://api.example.com',
changeOrigin: true
}
}
}
}
// 4. postMessage
window.parent.postMessage(message, 'https://parent.com')
// 5. WebSocket
const ws = new WebSocket('wss://example.com/ws')
模块七:性能优化
问题18:Core Web Vitals指标
| 指标 | 含义 | 优秀标准 |
|---|---|---|
| LCP | 最大内容绘制 | < 2.5s |
| FID/INP | 首次输入延迟/交互延迟 | < 100ms |
| CLS | 累积布局偏移 | < 0.1 |
javascript
// 使用Web Vitals库测量
import { onLCP, onFID, onCLS } from 'web-vitals'
onLCP(metric => {
console.log('LCP:', metric.value)
})
onFID(metric => {
console.log('FID:', metric.value)
})
onCLS(metric => {
console.log('CLS:', metric.value)
})
问题19:图片优化策略
html
<!-- 1. 使用现代格式 -->
<img src="image.avif" alt="..."> <!-- AVIF: 体积最小 -->
<img src="image.webp" alt="..."> <!-- WebP: 兼容性更好 -->
<!-- 2. 响应式图片 -->
<img
srcset="small.jpg 480w, medium.jpg 800w, large.jpg 1200w"
sizes="(max-width: 480px) 100vw, (max-width: 800px) 50vw, 33vw"
src="medium.jpg"
alt="..."
>
<!-- 3. 懒加载 -->
<img loading="lazy" src="image.jpg" alt="...">
<!-- 4. CSS图片 -->
background-image: url('image.webp');
/* 5. 骨架屏:加载时显示占位 */
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
模块八:手写代码题
问题20:实现防抖和节流
javascript
// 防抖:事件触发n秒后执行,n秒内再次触发则重新计时
function debounce(fn, delay) {
let timer = null
return function(...args) {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
// 使用
const debouncedSearch = debounce(search, 300)
// 节流:n秒内只执行一次
function throttle(fn, interval) {
let lastTime = 0
return function(...args) {
const now = Date.now()
if (now - lastTime >= interval) {
lastTime = now
fn.apply(this, args)
}
}
}
// 使用
const throttledScroll = throttle(handleScroll, 100)
问题21:实现深拷贝
javascript
// 基础版本
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj
}
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item))
}
const cloned = {}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key])
}
}
return cloned
}
// 完整版本:处理循环引用、Symbol、Date、RegExp等
function deepCloneAdvanced(obj, hash = new WeakMap()) {
// 处理null
if (obj === null) return obj
// 处理基本类型
if (typeof obj !== 'object') return obj
// 处理Date
if (obj instanceof Date) return new Date(obj)
// 处理RegExp
if (obj instanceof RegExp) return new RegExp(obj)
// 处理循环引用
if (hash.has(obj)) return hash.get(obj)
// 处理对象或数组
const clone = Array.isArray(obj) ? [] : {}
hash.set(obj, clone)
// 复制Symbol keys
const symbolKeys = Object.getOwnPropertySymbols(obj)
for (const key of symbolKeys) {
clone[key] = deepCloneAdvanced(obj[key], hash)
}
// 复制普通属性
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepCloneAdvanced(obj[key], hash)
}
}
return clone
}
面试技巧总结
如何回答概念性问题
- 先给出简洁定义:用一句话概括概念
- 解释核心原理:为什么需要这个特性
- 举例说明:用代码或生活实例解释
- 实际应用:在项目中如何使用
如何应对手写代码题
- 先理解题意:确认输入输出和边界条件
- 和面试官沟通:确认不确定的地方
- 先写思路:用注释描述步骤
- 再写代码:注意代码规范和可读性
- 验证测试:用几个测试用例验证
如何回答项目经历类问题
使用STAR法则:
- Situation(情境):项目的背景是什么
- Task(任务):你负责什么
- Action(行动):你具体做了什么
- Result(结果):取得了什么成果
结语
前端面试考察的核心是解决问题的能力和对技术的理解深度。刷题固然重要,但更重要的是真正理解原理,能够举一反三。
建议的学习方式是:
- 先理解基础概念
- 通过实践加深理解
- 定期回顾复习
- 模拟面试锻炼表达
希望这份面试题库对你有帮助。祝你面试顺利,拿到理想的offer!
相关资源推荐:
延伸阅读:

发表回复