本文整理前端高频面试题,按照 JavaScript / ES6+、Vue3、React、TypeScript、Webpack / Vite / 工程化、前端性能优化、CSS / HTML 等模块分类。
推荐学习方式:先理解原理,再记忆标准回答,最后结合项目经验进行扩展。
📚 目录
- [一、JavaScript / ES6+](#一、JavaScript / ES6+)
- 二、Vue3
- 三、React
- 四、TypeScript
- [五、Webpack / Vite / 前端工程化](#五、Webpack / Vite / 前端工程化)
- 六、前端性能优化
- [七、CSS / HTML 高频基础](#七、CSS / HTML 高频基础)
- 八、面试快速背诵总结
一、JavaScript / ES6+
1. JavaScript 事件循环:代码输出顺序
题目
js
console.log(1)
async function fn() {
await 100
console.log(4)
}
fn()
console.log(5)
Promise.resolve().then(() => {
console.log(3)
})
setTimeout(() => {
console.log(2)
}, 0)
输出结果
text
1 → 5 → 3 → 4 → 2
原因
JavaScript 的执行顺序可以简单理解为:
text
同步任务
↓
微任务
↓
宏任务
执行过程:
console.log(1),输出1。- 执行
fn(),遇到await,后续代码进入微任务队列。 - 继续执行同步代码,输出
5。 - 同步代码执行完毕,执行微任务:
Promise.then()输出3await后面的代码输出4
- 最后执行宏任务
setTimeout,输出2。
面试回答
JavaScript 会优先执行同步任务,同步任务执行完成后清空微任务队列,最后执行宏任务。
await后面的代码会被放入微任务队列,因此最终输出顺序是1 → 5 → 3 → 4 → 2。
2. 循环绑定点击事件,为什么输出最后一个索引?
错误示例
js
for (var i = 0; i < 3; i++) {
buttons[i].onclick = function () {
console.log(i)
}
}
点击按钮时可能全部输出:
text
3
3
3
原因
var 没有块级作用域,整个循环共享同一个 i。
循环结束后:
text
i = 3
点击事件是之后才触发的,因此所有回调访问的都是同一个变量 i,最终拿到的都是 3。
解决方案一:使用 let
js
for (let i = 0; i < 3; i++) {
buttons[i].onclick = function () {
console.log(i)
}
}
let 具有块级作用域,每一轮循环都会创建独立的变量。
解决方案二:使用 IIFE
js
for (var i = 0; i < 3; i++) {
;(function (index) {
buttons[index].onclick = function () {
console.log(index)
}
})(i)
}
面试回答
var没有块级作用域,循环中的所有回调共享同一个i。事件触发时循环已经结束,i已经变成最终值。可以使用let的块级作用域,或者通过 IIFE 创建闭包保存每一轮的索引。
3. 不同场景下的 this 指向
| 调用方式 | this 指向 |
|---|---|
| 普通函数直接调用 | 非严格模式:window;严格模式:undefined |
| 箭头函数 | 没有自己的 this,继承外层作用域 |
| 对象方法调用 | 调用该方法的对象 |
new 调用 |
新创建的实例对象 |
call/apply/bind |
手动传入的对象 |
普通函数
js
function fn() {
console.log(this)
}
fn()
对象方法
js
const user = {
name: 'Tom',
say() {
console.log(this.name)
}
}
user.say()
此时:
text
this === user
箭头函数
js
const user = {
name: 'Tom',
say: () => {
console.log(this.name)
}
}
箭头函数没有自己的 this,会继承定义时外层作用域的 this。
面试重点
this的指向主要取决于函数的调用方式,而箭头函数是特殊情况,它没有自己的this,也不能通过call/apply/bind修改。
4. 防抖和节流的区别
防抖 Debounce
连续触发时不断重新计时,停止触发一段时间后只执行一次。
适用场景:
- 输入框搜索联想
- 搜索请求
- 窗口
resize
js
function debounce(fn, delay) {
let timer
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
节流 Throttle
固定时间内最多执行一次。
适用场景:
- 滚动事件
- 拖拽
- 按钮防重复提交
一句话记忆
text
防抖:等你停止操作后执行
节流:固定时间执行一次
5. ES5 寄生组合继承
js
function Parent(name) {
this.name = name
}
Parent.prototype.say = function () {
console.log('hello')
}
function Child(age) {
Parent.call(this, 'parentName')
this.age = age
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
核心逻辑
text
Parent.call(this)
↓
继承父类实例属性
Object.create(Parent.prototype)
↓
继承父类原型方法
优点
- 避免重复调用父类构造函数
- 避免子类原型上出现多余的父类实例属性
- 解决普通组合继承的问题
6. 如何判断一个变量是否为数组?
instanceof
js
arr instanceof Array
缺点:跨 iframe 场景可能失效。
Array.isArray
js
Array.isArray(arr)
最推荐。
Object.prototype.toString
js
Object.prototype.toString.call(arr) === '[object Array]'
兼容性好,可以准确判断多种内置类型。
constructor
js
arr.constructor === Array
缺点是 constructor 可以被手动修改。
7. 浅拷贝和深拷贝
浅拷贝
只复制第一层属性。
js
const obj = {
name: 'Tom',
info: {
age: 18
}
}
const copy = { ...obj }
copy.info.age = 20
console.log(obj.info.age) // 20
因为:
text
obj.info
↓
同一个引用地址
↑
copy.info
深拷贝
递归复制所有层级,两个对象完全独立。
实现深拷贝时需要考虑:
- 循环引用
DateRegExpFunctionSetMap
循环引用处理
可以使用:
js
WeakMap
记录已经拷贝过的对象,避免无限递归。
面试回答
浅拷贝只复制第一层,引用类型仍然共享内存地址;深拷贝会递归复制所有层级,使两个对象完全独立。实现完整深拷贝时需要使用
WeakMap处理循环引用,并单独处理Date、RegExp、Map、Set等特殊类型。
8. let、var、const 的区别
| 特性 | var |
let |
const |
|---|---|---|---|
| 变量提升 | 有 | 有暂时性死区 | 有暂时性死区 |
| 块级作用域 | ❌ | ✅ | ✅ |
| 重复声明 | ✅ | ❌ | ❌ |
| 重新赋值 | ✅ | ✅ | ❌ |
| 必须初始化 | ❌ | ❌ | ✅ |
const 的注意事项
js
const user = {
name: 'Tom'
}
user.name = 'Jack'
这是允许的。
因为:
text
const 锁定的是引用地址
不是对象内部属性
9. 什么是闭包?
闭包指:
一个函数可以访问并保留自己词法作用域中的变量,即使外层函数已经执行完毕。
js
function outer() {
let count = 0
return function () {
count++
console.log(count)
}
}
const fn = outer()
fn() // 1
fn() // 2
应用场景
- 函数柯里化
- 封装私有变量
- 防抖
- 节流
- 循环保存变量
潜在问题
如果闭包长期持有不需要的变量:
text
变量无法被垃圾回收
↓
可能造成内存泄漏
10. 数组高阶方法
map
返回一个新数组,长度通常不变。
js
const result = [1, 2, 3].map(item => item * 2)
// [2, 4, 6]
适合数据转换。
filter
过滤数据。
js
const result = [1, 2, 3].filter(item => item > 1)
// [2, 3]
reduce
累计计算。
js
const total = [1, 2, 3].reduce((sum, item) => {
return sum + item
}, 0)
适合:
- 求和
- 分组
- 扁平化
- 复杂聚合
forEach
只遍历,不返回新数组。
ES6+ 高频场景题
11. 嵌套解构赋值默认值为什么失效?
错误写法
js
const { user: { name = '匿名' } } = res
如果 res.user 是 undefined,会直接报错。
正确写法
js
const {
user: {
name = '匿名'
} = {}
} = res.data || {}
原因
解构默认值只有在对应属性值为 undefined 时才生效。
如果父级对象本身是:
js
null
或:
js
undefined
就无法继续解构。
12. 手写 Promise 的核心原理
Promise 的核心包括:
text
pending
↓
fulfilled
pending
↓
rejected
状态只能改变一次。
核心代码
js
class MyPromise {
constructor(executor) {
this.state = 'pending'
this.result = undefined
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
const resolve = value => {
if (this.state !== 'pending') return
this.state = 'fulfilled'
this.result = value
this.onFulfilledCallbacks.forEach(fn => fn())
}
const reject = reason => {
if (this.state !== 'pending') return
this.state = 'rejected'
this.result = reason
this.onRejectedCallbacks.forEach(fn => fn())
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
then(onFulfilled, onRejected) {
onFulfilled =
typeof onFulfilled === 'function'
? onFulfilled
: value => value
onRejected =
typeof onRejected === 'function'
? onRejected
: error => {
throw error
}
return new MyPromise((resolve, reject) => {
const runMicroTask = callback => {
queueMicrotask(() => {
try {
const result = callback(this.result)
resolve(result)
} catch (error) {
reject(error)
}
})
}
if (this.state === 'fulfilled') {
runMicroTask(onFulfilled)
}
if (this.state === 'rejected') {
runMicroTask(onRejected)
}
if (this.state === 'pending') {
this.onFulfilledCallbacks.push(() => {
runMicroTask(onFulfilled)
})
this.onRejectedCallbacks.push(() => {
runMicroTask(onRejected)
})
}
})
}
catch(onRejected) {
return this.then(undefined, onRejected)
}
}
面试重点
手写 Promise 主要考察:
- 状态不可逆
- 异步执行
- 微任务调度
- 链式调用
- 错误捕获
then返回新的 Promise
13. Promise.all、race、allSettled、any
| 方法 | 核心规则 | 适用场景 |
|---|---|---|
Promise.all |
全部成功才成功,一个失败则失败 | 多个接口并行请求 |
Promise.race |
谁先改变状态就返回谁 | 请求超时控制 |
Promise.allSettled |
等待所有任务完成 | 批量上传统计成功失败 |
Promise.any |
任意一个成功即可 | 多 CDN 资源兜底 |
Promise.all
js
const result = await Promise.all([
getUser(),
getOrder(),
getPermission()
])
14. CommonJS 和 ES Module 的区别
CommonJS
js
const module = require('./module')
module.exports = {}
特点:
- 运行时加载
- 主要用于 Node.js
- 导出通常是值的拷贝
- 支持动态
require
ES Module
js
import { name } from './module.js'
export const name = 'Tom'
特点:
- 静态编译时解析
- 支持 Tree-Shaking
- 导出的是实时绑定
- 浏览器原生支持
浏览器
html
<script type="module" src="./main.js"></script>
Node.js
可以通过:
text
.mjs
或者:
json
{
"type": "module"
}
开启 ES Module。
15. ES Module 和 CommonJS 循环依赖
CommonJS
执行 require 时会缓存当前模块已经导出的内容。
循环依赖时,另一方可能拿到:
尚未完全初始化的导出对象。
ES Module
模块会先进行静态分析,导入的是实时绑定。
只要变量在真正执行到使用位置之前已经初始化,就可以正常使用。
16. 箭头函数和普通函数的区别
箭头函数特点
- 没有自己的
this - 没有自己的
arguments - 没有
prototype - 不能作为构造函数
- 不能使用
yield
js
const fn = () => {}
不能:
js
new fn()
为什么不能作为构造函数?
new 的过程需要:
text
创建新对象
↓
绑定 prototype
↓
绑定 this
↓
执行构造函数
箭头函数没有自己的 this 和 prototype,因此不能完成构造函数流程。
17. async/await 的执行机制
async 函数本质上可以理解为 Promise 的语法糖。
执行到:
js
await promise
时:
text
暂停 async 函数后续代码
↓
让出主线程
↓
执行同步代码
↓
Promise 状态完成
↓
后续代码进入微任务队列
↓
继续执行
所以:
await后面的代码会被放入微任务队列。
18. 模板字符串
使用反引号:
js
const name = '前端开发者'
const str = `你好,我是${name}
今天学习 ES6 面试题`
优势:
- 支持变量插值
- 支持多行字符串
- 不需要大量
+拼接
19. Set 和 Map 的区别
Set
特点:
- 元素唯一
- 没有键值对
- 适合去重
js
const arr = [1, 2, 2, 3]
const result = [...new Set(arr)]
Map
特点:
- 键值对集合
- key 可以是任意类型
- 适合缓存和映射
js
const map = new Map()
map.set('name', 'Tom')
map.set({ id: 1 }, 'user')
20. class 和 ES5 原型继承
ES5
需要手动处理:
text
构造函数
原型链
constructor
class
js
class Child extends Parent {
constructor() {
super()
}
}
class 本质上仍然基于原型,但语法更加清晰。
多重继承
JavaScript 不支持原生多继承,可以使用 Mixin:
js
const Flyable = Base => class extends Base {
fly() {}
}
const Swimable = Base => class extends Base {
swim() {}
}
class Animal {}
class Duck extends Swimable(Flyable(Animal)) {}
二、Vue3
1. Vue3 为什么使用 Proxy 替代 Object.defineProperty?
Object.defineProperty 的问题
只能监听已有属性:
js
const obj = {
name: 'Tom'
}
新增:
js
obj.age = 18
需要额外处理。
数组下标修改:
js
arr[0] = 100
也存在监听问题。
Proxy 的优势
js
const proxy = new Proxy(obj, {
get(target, key) {},
set(target, key, value) {}
})
可以监听:
- 属性读取
- 属性修改
- 属性新增
- 属性删除
- 数组操作
- 深层对象
面试回答
Object.defineProperty只能劫持单个已有属性,Vue2 需要通过递归遍历和重写数组方法实现响应式。Vue3 使用 Proxy 代理整个对象,可以直接监听新增、删除、数组下标等操作,并且是惰性代理,性能更好。
2. Vue3 为什么能监听数组下标和新增属性?
数组下标本质上也是对象属性:
js
arr[0] = 100
Proxy 可以直接拦截:
text
set
因此能够监听。
深层对象
Vue3 的响应式是递归惰性代理:
text
访问对象
↓
get 拦截
↓
发现子对象
↓
再次 reactive
↓
生成新的 Proxy
3. ref 和 reactive 的区别
| 对比 | ref |
reactive |
|---|---|---|
| 基本类型 | 支持 | 不支持 |
| 对象 | 支持 | 支持 |
| 访问方式 | .value |
直接访问 |
| 整体替换 | 支持 | 容易丢失响应式 |
ref
js
const count = ref(0)
count.value++
reactive
js
const user = reactive({
name: 'Tom'
})
user.name = 'Jack'
为什么 reactive 不能完全替代 ref?
因为:
js
reactive(1)
无法代理基本类型。
而:
js
ref(1)
通过 value 属性包裹基本类型,实现响应式。
4. Composition API 的优势
可以把同一业务逻辑封装到:
text
useUserLogin()
useTableList()
usePermission()
例如:
js
function useCounter() {
const count = ref(0)
const increment = () => {
count.value++
}
return {
count,
increment
}
}
优势:
- 按业务逻辑聚合代码
- 逻辑复用方便
- 避免 Mixins 命名冲突
- TypeScript 支持更好
5. Vue3 生命周期
onMounted
适合:
- DOM 操作
- 初始化第三方库
- 首次请求数据
onUpdated
组件 DOM 更新完成后执行。
onUnmounted
适合清理:
js
onUnmounted(() => {
clearInterval(timer)
window.removeEventListener('resize', handleResize)
})
如果不清理:
text
组件销毁
↓
定时器仍然运行
↓
事件监听仍然存在
↓
内存泄漏
6. Pinia 相比 Vuex 的优势
- 移除了 Mutation
- 直接通过 Action 修改状态
- TypeScript 支持更好
- 支持多个 Store
- 更适合按业务拆分
推荐:
text
userStore
orderStore
appStore
permissionStore
7. Vue3 组件通信方式
父子通信
text
父 → 子:props
子 → 父:emit
双向绑定
text
v-model
跨层级
text
provide / inject
全局状态
text
Pinia
事件通知
text
mitt
直接调用子组件
text
ref
8. Vue3 v-model 原理
默认是:
text
modelValue
+
update:modelValue
子组件:
vue
<script setup>
const props = defineProps({
modelValue: String
})
const emit = defineEmits(['update:modelValue'])
function changeValue(value) {
emit('update:modelValue', value)
}
</script>
父组件:
vue
<Child v-model="value" />
本质:
vue
<Child
:modelValue="value"
@update:modelValue="value = $event"
/>
9. Teleport
Teleport 可以把组件内部的 DOM 渲染到指定位置。
vue
<Teleport to="body">
<div class="modal">
弹窗
</div>
</Teleport>
适合:
- 弹窗
- 抽屉
- Tooltip
- 全局浮层
可以避免:
css
overflow: hidden;
z-index;
造成的显示问题。
10. watch 和 watchEffect
watch
js
watch(
() => user.name,
(newValue, oldValue) => {
console.log(newValue, oldValue)
}
)
特点:
- 手动指定监听源
- 默认不立即执行
- 可以获取新旧值
watchEffect
js
watchEffect(() => {
console.log(user.name)
})
特点:
- 自动收集依赖
- 初始化立即执行
- 依赖变化时重新执行
选择
text
精准监听某个值 → watch
自动追踪多个依赖 → watchEffect
11. defineProps、defineEmits 和 Props 校验
vue
<script setup>
const props = defineProps({
name: {
type: String,
required: true
}
})
const emit = defineEmits(['change'])
</script>
TypeScript 写法:
ts
interface Props {
title: string
count?: number
}
const props = defineProps<Props>()
12. keep-alive
作用:
缓存组件实例,切换组件时不销毁重建。
vue
<KeepAlive>
<RouterView />
</KeepAlive>
include
缓存指定组件。
exclude
排除指定组件。
适合:
- 列表页
- 表单页
- Tab 页面
三、React
1. React 虚拟 DOM
虚拟 DOM 是使用 JavaScript 对象描述真实 DOM 结构。
流程:
text
状态变化
↓
生成新的虚拟 DOM
↓
Diff 对比
↓
计算最小更新
↓
更新真实 DOM
优势:
- 减少直接操作 DOM
- 声明式开发
- 跨平台能力强
2. React 生命周期
类组件
挂载:
text
constructor
↓
render
↓
componentDidMount
更新:
text
shouldComponentUpdate
↓
render
↓
componentDidUpdate
卸载:
text
componentWillUnmount
函数组件
使用:
js
useEffect()
模拟生命周期。
js
useEffect(() => {
// 挂载
return () => {
// 卸载
}
}, [])
3. useState 注意事项
js
const [count, setCount] = useState(0)
不要直接修改:
js
count = count + 1
应该:
js
setCount(count + 1)
连续更新推荐:
js
setCount(prev => prev + 1)
注意
Hooks 不能放在:
- 条件语句
- 循环
- 嵌套函数
中调用。
4. useEffect 依赖项
无依赖
js
useEffect(() => {})
每次渲染后执行。
空数组
js
useEffect(() => {}, [])
首次挂载执行。
指定依赖
js
useEffect(() => {}, [count])
count 变化时执行。
5. React 组件通信
父传子
jsx
<Child name="Tom" />
子传父
父组件传递回调:
jsx
<Child onChange={handleChange} />
兄弟组件
通过共同父组件状态提升。
跨层级
使用:
text
Context API
Redux
Zustand
6. Props 和 State 的区别
| Props | State |
|---|---|
| 父组件传入 | 组件内部维护 |
| 外部数据 | 内部状态 |
| 子组件只读 | 可以通过 setter 修改 |
Props 设计为只读,是为了保证:
text
单向数据流
↓
状态来源清晰
↓
组件行为可预测
7. Context API 的性能问题
Context 更新时,消费该 Context 的组件可能重新渲染。
优化方式:
text
拆分 Context
↓
useMemo 缓存 value
↓
React.memo
示例:
jsx
const value = useMemo(() => ({
user,
setUser
}), [user])
8. useMemo 和 useCallback
useMemo
缓存计算结果:
js
const result = useMemo(() => {
return expensiveCalculation(data)
}, [data])
useCallback
缓存函数引用:
js
const handleClick = useCallback(() => {
console.log('click')
}, [])
一句话:
text
useMemo → 缓存结果
useCallback → 缓存函数
不要滥用,因为缓存本身也有开销。
9. React Router v6
路由跳转
js
const navigate = useNavigate()
navigate('/user')
动态参数
js
const { id } = useParams()
查询参数
js
const [searchParams] = useSearchParams()
const keyword = searchParams.get('keyword')
路由守卫
可以封装权限组件:
jsx
function AuthRoute({ children }) {
const token = getToken()
return token ? children : <Navigate to="/login" />
}
10. Redux / Redux Toolkit
核心流程:
text
组件
↓ dispatch(action)
Action
↓
Reducer
↓
新的 State
↓
组件重新渲染
Redux Toolkit 的优势:
- 减少样板代码
- 内置 Immer
- 支持直接修改 state 的写法
11. React Diff 算法
核心原则:
同层比较
React 主要进行同层级节点比较。
key
jsx
list.map(item => (
<div key={item.id}>
{item.name}
</div>
))
key 应该:
- 唯一
- 稳定
不推荐:
jsx
key={index}
特别是列表存在新增、删除、排序时。
12. 函数组件和类组件
函数组件优势:
- 代码更简洁
- 没有
this指向问题 - Hooks 复用逻辑方便
- 更贴近函数式编程
- 逻辑组织更灵活
现在 React 项目通常优先使用函数组件。
13. React 错误边界
错误边界用于捕获子组件渲染过程中出现的错误,避免整个页面白屏。
核心方法:
js
static getDerivedStateFromError(error) {}
以及:
js
componentDidCatch(error, info) {}
注意:
错误边界不能捕获所有错误,例如事件回调和异步代码中的错误需要单独处理。
四、TypeScript
1. TypeScript 基本类型
常见类型:
text
string
number
boolean
null
undefined
symbol
bigint
void
never
any
unknown
void
表示没有有效返回值:
ts
function log(): void {
console.log('hello')
}
never
表示永远不会正常结束:
ts
function error(): never {
throw new Error('error')
}
2. interface 和 type
interface
ts
interface User {
name: string
age: number
}
支持声明合并:
ts
interface User {
id: number
}
type
ts
type User = {
name: string
age: number
}
可以表示:
ts
type Status = 'success' | 'error'
总结
text
interface → 对象结构、面向对象契约
type → 联合类型、交叉类型、元组等复杂类型
3. 泛型
泛型就是:
把类型作为参数传入。
ts
function identity<T>(value: T): T {
return value
}
identity<number>(100)
identity<string>('hello')
泛型组件
tsx
function GenericList<T>({
list,
renderItem
}: {
list: T[]
renderItem: (item: T) => React.ReactNode
}) {
return (
<div>
{list.map(renderItem)}
</div>
)
}
4. 可选链和空值合并
可选链
ts
const name = user?.info?.profile?.name
如果中间为:
text
null
undefined
则返回 undefined。
空值合并
ts
const pageSize = inputSize ?? 10
只有 null 或 undefined 才使用默认值。
以下值不会被替换:
text
0
''
false
5. 联合类型和交叉类型
联合类型
ts
type Status = string | number
表示:
可以是 string,也可以是 number。
交叉类型
ts
type User = BasicUser & Permission
表示:
同时拥有两个类型的属性。
6. 定义数组、对象、函数类型
数组
ts
const arr1: number[] = [1, 2, 3]
const arr2: Array<string> = ['a', 'b']
对象
ts
interface Person {
name: string
age?: number
readonly id: number
}
函数
ts
type AddFunc = (a: number, b: number) => number
const add: AddFunc = (x, y) => x + y
7. 类型断言
类型断言是告诉 TypeScript:
我比编译器更清楚这个值的类型。
as
ts
const str = value as string
尖括号
ts
const str = <string>value
React TSX 项目推荐使用:
ts
as
8. 如何解决属性不存在报错?
例如:
text
类型 xxx 上不存在属性 xxx
解决方式:
- 补充完整类型定义。
- 使用类型断言。
- 扩展第三方模块声明。
- 使用索引签名。
ts
interface Obj {
[key: string]: any
}
9. TypeScript 枚举
ts
enum Status {
Success = 200,
NotFound = 404,
Error = 500
}
const status = Status.Success
注意:
- 数字枚举支持反向映射
- 字符串枚举不支持反向映射
- 需要时可以使用
const enum
10. Vue3 + TS、React + TS 类型定义
Vue3 + TS
ts
interface Props {
title: string
count?: number
}
const props = defineProps<Props>()
React + TS
tsx
interface Props {
title: string
}
function DemoComponent({ title }: Props) {
const [count, setCount] = useState<number>(0)
return (
<div>
{title} {count}
</div>
)
}
五、Webpack / Vite / 前端工程化
1. Webpack 四大核心
Entry
入口。
text
从入口文件开始分析依赖
Output
输出。
text
定义构建产物路径和文件名
Loader
处理非 JavaScript 文件:
text
CSS
图片
Vue
TypeScript
Plugin
扩展 Webpack 能力:
text
压缩代码
生成 HTML
注入环境变量
2. Webpack 如何优化打包速度?
持久化缓存
js
cache: {
type: 'filesystem'
}
多进程
使用:
text
thread-loader
缩小处理范围
text
include
exclude
第三方依赖预打包
使用:
text
DllPlugin
3. Webpack 如何优化打包体积?
- 开启生产模式
- Tree-Shaking
splitChunks- 图片压缩
- 图片懒加载
- 第三方库按需引入
4. Vite 和 Webpack 的区别
Webpack
text
启动
↓
全量打包
↓
启动开发服务器
Vite
text
启动开发服务器
↓
浏览器通过 ESM 按需请求
↓
按需转换模块
Vite 优势
- 冷启动快
- HMR 快
- 大型项目开发体验更好
生产构建
Vite 生产环境会进行完整构建和优化,常见构建流程基于 Rollup 生态。
5. 模块化打包
模块化打包就是:
将项目中分散的模块统一分析、转换和打包成浏览器可以运行的静态资源。
解决:
- 全局变量污染
- 依赖混乱
- 非 JS 资源无法直接运行
- 代码压缩和兼容问题
6. ESLint 和 Prettier
ESLint
负责:
text
代码质量
语法错误
潜在问题
Prettier
负责:
text
缩进
换行
引号
代码格式
一句话:
text
ESLint → 代码有没有问题
Prettier → 代码长什么样
7. Git 常用命令
| 命令 | 作用 |
|---|---|
clone |
克隆仓库 |
add |
添加到暂存区 |
commit |
提交本地 |
push |
推送远程 |
pull |
拉取远程 |
branch |
分支管理 |
merge |
合并分支 |
stash |
暂存未提交代码 |
标准工作流
text
主分支
↓
创建功能分支
↓
开发
↓
提交
↓
代码评审
↓
合并
↓
发布
8. CI/CD
CI/CD:
text
持续集成
+
持续部署
典型流程:
text
提交代码
↓
自动拉取
↓
安装依赖
↓
执行测试
↓
构建
↓
部署
9. npm 和 yarn
早期 Yarn 主要解决:
- npm 安装速度慢
- 依赖版本不确定
现代 npm 也已经支持:
- lock 文件
- 并行安装
- 依赖管理优化
依赖冲突解决:
text
升级冲突依赖
resolutions
npm dedupe
10. 前端工程化
核心包括:
text
模块化
组件化
规范化
自动化
模块化
管理:
text
JS
CSS
资源
组件化
抽取:
text
Button
Table
Dialog
Form
规范化
text
ESLint
Prettier
Git Commit
自动化
text
自动构建
自动测试
自动部署
11. Vue3 + TypeScript + Vite 项目搭建
常见步骤:
bash
npm create vite@latest
选择:
text
Vue
TypeScript
核心配置:
text
vite.config.ts
tsconfig.json
ESLint
Prettier
Vue Router
Pinia
环境变量
常见 Vite 配置:
ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
}
})
六、前端性能优化
1. 页面加载性能优化
网络层
- HTTP/2
- HTTP/3
- CDN
- Gzip
- Brotli
- 浏览器缓存
渲染层
- 首屏优先加载核心资源
- 非关键 JS / CSS 懒加载
- 避免长任务阻塞主线程
- 使用 SSR / SSG
代码层
- Tree-Shaking
- 路由懒加载
- 代码分割
- 减少第三方依赖
- 图片使用 WebP / AVIF
2. 如何减少重排和重绘?
推荐使用:
css
transform
opacity
进行动画。
避免频繁读取:
js
offsetWidth
offsetHeight
getBoundingClientRect()
因为可能导致:
text
强制同步布局
批量修改 DOM,避免:
text
修改一次
触发布局
修改一次
触发布局
3. 万级数据列表如何优化?
使用:
text
虚拟滚动
核心思想:
text
10000 条数据
↓
只渲染可视区域几十条
↓
动态复用 DOM
↓
减少 DOM 数量
↓
提升性能
还可以使用:
css
contain: layout paint size;
隔离渲染影响。
4. 图片性能优化
图片格式
优先:
text
AVIF
WebP
懒加载
html
<img loading="lazy" src="image.jpg" />
预加载
html
<link
rel="preload"
as="image"
href="banner.webp"
/>
5. 浏览器缓存
强缓存
http
Cache-Control: max-age=31536000
适合:
text
带 hash 的 JS
CSS
图片
协商缓存
text
ETag
Last-Modified
服务器返回:
text
304 Not Modified
表示资源没有变化。
6. 首屏加载优化
- 内联首屏关键 CSS
- 非首屏 JS 使用
defer - 非关键资源延迟加载
- 使用骨架屏
- 减少关键资源数量
- 减少关键资源体积
7. JavaScript 性能优化
Tree-Shaking
删除没有使用的代码。
避免内存泄漏
注意清理:
js
clearInterval(timer)
window.removeEventListener(
'resize',
handleResize
)
Web Worker
复杂计算可以放到子线程:
text
主线程
↓
负责页面交互
Worker
↓
负责复杂计算
避免长任务阻塞页面。
8. CSS 性能优化
避免深层选择器
不推荐:
css
.page .content .main .list .item span {}
推荐:
css
.list-item {}
减少重复样式
提取公共类。
避免大量 DOM 使用昂贵属性
例如:
text
box-shadow
filter
大量使用可能增加渲染成本。
9. 路由切换优化
路由懒加载
js
const User = () => import('./views/User.vue')
组件缓存
Vue:
vue
<KeepAlive>
<RouterView />
</KeepAlive>
预加载
提前加载即将访问的路由资源。
10. 前端性能检测工具
Chrome DevTools
使用:
text
Performance
分析:
- 长任务
- 重排
- 重绘
- JS 执行耗时
Lighthouse
可以分析:
- Performance
- Accessibility
- Best Practices
- SEO
Performance API
可以监控:
text
FP
FCP
LCP
FID
CLS
七、CSS / HTML 高频基础
1. CSS 优先级
常见优先级:
text
!important
↓
行内样式
↓
ID 选择器
↓
类 / 属性 / 伪类选择器
↓
标签 / 伪元素选择器
例如:
html
<div id="app" class="box" style="color: red">
内容
</div>
css
div {
color: blue;
}
.box {
color: green;
}
#app {
color: yellow;
}
最终颜色:
text
red
因为:
text
行内样式 > ID > class > 标签
注意
行内样式不是 CSS 选择器,但在完整的 CSS 优先级比较中需要考虑。
2. src 和 href 的区别
src
表示:
加载资源。
常见:
html
<img src="image.png" />
<script src="main.js"></script>
<iframe src="page.html"></iframe>
href
表示:
引用资源或指向地址。
常见:
html
<a href="page.html">页面</a>
<link rel="stylesheet" href="style.css" />
一句话:
text
src → 加载资源
href → 引用资源 / 指向地址
3. AJAX、Fetch、Axios 的区别
AJAX
AJAX 不是一个具体的库,而是一种:
页面不刷新,通过异步请求与服务器交互的技术思想。
Fetch
浏览器原生 API:
js
fetch('/api/user')
.then(res => res.json())
.then(data => {
console.log(data)
})
Axios
第三方 HTTP 请求库:
js
import axios from 'axios'
const res = await axios.get('/api/user')
console.log(res.data)
对比
| 对比 | AJAX | Fetch | Axios |
|---|---|---|---|
| 本质 | 技术思想 | 原生 API | 第三方库 |
| 是否安装 | 否 | 否 | 是 |
| Promise | 可以 | 支持 | 支持 |
| 请求拦截器 | 需要自己实现 | 需要自己封装 | 原生支持 |
| JSON 处理 | 手动 | 通常手动 | 自动 |
| 项目常用 | 概念 | 常用 | 非常常用 |
八、面试快速背诵总结
JavaScript
text
事件循环:
同步任务 → 微任务 → 宏任务
防抖:
停止操作后执行
节流:
固定时间执行一次
闭包:
函数能够访问并保留外部作用域变量
浅拷贝:
复制第一层
深拷贝:
递归复制所有层级
箭头函数:
没有自己的 this、arguments、prototype
Vue3
text
Proxy:
代理整个对象,支持新增、删除、数组下标
ref:
支持基本类型和引用类型
reactive:
主要用于对象和数组
watch:
精准监听
watchEffect:
自动收集依赖
Pinia:
轻量、TS 友好、按业务拆 Store
Teleport:
把 DOM 挂载到指定位置
KeepAlive:
缓存组件实例
React
text
Props:
父组件传入,只读
State:
组件内部状态
useState:
管理状态
useEffect:
处理副作用
useMemo:
缓存计算结果
useCallback:
缓存函数引用
Context:
跨层级传递数据
React.memo:
避免不必要的组件重新渲染
TypeScript
text
interface:
对象结构和面向对象契约
type:
联合、交叉、元组等复杂类型
泛型:
类型参数化
never:
永远不会正常结束
void:
没有有效返回值
unknown:
安全的未知类型
any:
关闭类型检查
工程化
text
Webpack:
Entry
Output
Loader
Plugin
Vite:
开发环境基于 ESM 按需加载
生产环境进行构建优化
ESLint:
代码质量
Prettier:
代码格式
Git:
版本控制
CI/CD:
自动构建、测试、部署
性能优化
text
网络:
CDN、HTTP2、压缩、缓存
代码:
Tree-Shaking、代码分割、懒加载
渲染:
减少重排重绘、避免长任务
列表:
虚拟滚动
图片:
WebP、AVIF、懒加载、预加载
监控:
Performance API、Lighthouse、DevTools
📌 最后总结
前端面试不要只背概念,建议按照下面的方式回答:
text
是什么
↓
为什么
↓
怎么实现
↓
应用场景
↓
项目中怎么用
例如面试官问:
Vue3 为什么使用 Proxy?
可以回答:
Vue3 使用 Proxy 替代 Object.defineProperty,主要是因为 Proxy 可以代理整个对象,能够监听新增、删除属性以及数组下标操作。Object.defineProperty 只能劫持已有属性,Vue2 需要通过递归遍历和重写数组方法来实现响应式。Proxy 采用惰性代理,在访问深层对象时再进行代理,因此在大对象场景下性能和实现复杂度更好。
这样回答就比单纯说:
Proxy 比 Object.defineProperty 好。
更加完整。
前端面试的核心不是背答案,而是理解浏览器、JavaScript、框架和工程化背后的运行机制。