教程雨

OKX新手入门教程导航,收录OKX注册、充值、买币、提现等基础操作教程

前端开发面试题库封面图,展示React、Vue、TypeScript等核心技能面试准备

前端开发面试题库2026版:涵盖React/Vue/TypeScript/工程化的完整面试指南

引言

又到金三银四招聘季,很多前端开发者开始准备跳槽或求职。一份好的面试准备不仅能帮你拿到更好的offer,更能帮你系统性地梳理前端知识体系。

根据我这些年面试和被面试的经验,前端面试的核心考察点其实相对稳定:基础知识是否扎实、框架理解是否深入、工程化能力是否具备、问题解决能力如何。

这篇文章按照前端面试的不同模块,整理了高频考察的知识点和面试题。每个问题都配有详细解析,帮助你真正理解而不是死记硬背。

前端面试知识模块图,呈现JavaScript、React、Vue、工程化六大核心考点

模块一:JavaScript基础

问题1:var、let、const的区别是什么?

这是最基础的JavaScript问题,但很多人只能回答出表层。

核心区别

特性varletconst
作用域函数作用域块级作用域块级作用域
变量提升提升但值是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. 首先执行同步代码,输出1和4
  2. 同步代码执行完后,执行队列中的微任务(Promise的then),输出3
  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
}

主要区别

特性interfacetype
定义对象结构
定义基本类型
定义联合类型
定义元组
声明合并
计算属性

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、副作用等信息
  • 异步可中断的渲染,允许在渲染过程中暂停和恢复

两种工作模式

  1. Render阶段:构建Fiber树,可中断
  2. 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的优势

  1. 更好的逻辑复用:通过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)
  1. 更好的代码组织:相关逻辑放在一起
  2. 更好的类型推断:setup函数的返回值类型明确
  3. 更小的打包体积: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的区别

特性WebpackVite
开发服务器启动慢(需要打包)快(直接服务源文件)
热更新较慢极快(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
}

面试技巧总结

如何回答概念性问题

  1. 先给出简洁定义:用一句话概括概念
  2. 解释核心原理:为什么需要这个特性
  3. 举例说明:用代码或生活实例解释
  4. 实际应用:在项目中如何使用

如何应对手写代码题

  1. 先理解题意:确认输入输出和边界条件
  2. 和面试官沟通:确认不确定的地方
  3. 先写思路:用注释描述步骤
  4. 再写代码:注意代码规范和可读性
  5. 验证测试:用几个测试用例验证

如何回答项目经历类问题

使用STAR法则

  • Situation(情境):项目的背景是什么
  • Task(任务):你负责什么
  • Action(行动):你具体做了什么
  • Result(结果):取得了什么成果

结语

前端面试考察的核心是解决问题的能力和对技术的理解深度。刷题固然重要,但更重要的是真正理解原理,能够举一反三。

建议的学习方式是:

  1. 先理解基础概念
  2. 通过实践加深理解
  3. 定期回顾复习
  4. 模拟面试锻炼表达

希望这份面试题库对你有帮助。祝你面试顺利,拿到理想的offer!

相关资源推荐

延伸阅读

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注