# workflow-practice
**Repository Path**: bronson/workflow-practice
## Basic Information
- **Project Name**: workflow-practice
- **Description**: 前端面试题,包括React Vue Java Postgre SQL等
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 0
- **Created**: 2026-05-18
- **Last Updated**: 2026-05-25
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# React部分
## 1. state 和 props 有什么区别
### 标准答案
`state` 和 `props` 都会影响组件渲染,但它们的职责不同。
- `props` 是组件的外部输入,通常由父组件传给子组件。
- `state` 是组件内部维护的状态,用来描述当前组件会变化的数据。
### 核心区别
1. 数据来源不同
- `props` 来自外部。
- `state` 由组件自身管理。
2. 是否可修改不同
- `props` 对当前组件来说是只读的,组件不应该直接修改 `props`。
- `state` 可以通过 `setState` 或 `useState` 的 setter 更新。
3. 使用场景不同
- `props` 适合组件通信、配置组件行为。
- `state` 适合保存会随交互变化的数据,例如输入框内容、开关状态、请求结果等。
### 面试建议回答
可以补一句:React 提倡单向数据流,数据通常由父组件通过 `props` 向下传递,组件自身变化的数据由 `state` 管理。
---
## 2. 为什么 React 需要 key,使用 index 作为 key 有什么问题
### 标准答案
`key` 的核心作用不是单纯“优化性能”,而是帮助 React 在 diff 过程中识别同一个节点,从而决定是复用、移动、删除还是重新创建对应元素。
### 为什么需要 key
当渲染列表时,React 要比较新旧虚拟 DOM。
如果没有 `key`,React 很难准确判断列表中哪一项对应旧节点中的哪一项。
有了稳定的 `key`,React 才能更准确地复用节点和组件状态。
### 为什么不推荐用 index 作为 key
`index` 在列表不发生变化时通常没问题,但一旦发生插入、删除、排序,`index` 就不稳定了。
这会导致以下问题:
- 节点复用错位
- 输入框内容错乱
- 子组件局部状态错乱
- 本来可以复用的节点被错误更新
### 什么时候可以使用 index
如果列表满足以下条件,使用 `index` 风险较低:
- 列表是静态的
- 不会增删改排序
- 列表项没有本地状态
但在真实项目里,优先使用业务上的唯一 id 作为 `key`。
---
## 3. useEffect 的执行时机,以及它和 useLayoutEffect 的区别
### useEffect 的执行时机
`useEffect` 会在组件渲染完成并提交到 DOM 之后执行。
根据依赖数组不同,执行时机不同:
1. 不传依赖数组
- 每次渲染完成后都会执行。
2. 传空数组 `[]`
- 只在组件首次挂载后执行一次。
3. 传入依赖数组 `[a, b]`
- 首次挂载后执行一次。
- 之后依赖变化时再执行。
### cleanup 的执行时机
`useEffect` 可以返回一个清理函数,清理函数会在以下时机执行:
- 下一次 effect 执行之前
- 组件卸载时
### useEffect 和 useLayoutEffect 的区别
1. `useEffect`
- 在浏览器完成绘制之后执行
- 不会阻塞页面绘制
- 适合数据请求、订阅、日志、副作用处理等
2. `useLayoutEffect`
- 在 DOM 更新后、浏览器绘制前同步执行
- 会阻塞浏览器绘制
- 适合读取布局、同步测量尺寸、避免闪烁
### 面试建议回答
可以总结为:
`useEffect` 更偏异步副作用,`useLayoutEffect` 更偏同步布局处理。
---
## 4. 受控组件和非受控组件有什么区别
### 标准答案
受控组件和非受控组件,本质上是在说“数据的控制权”在哪。
### 受控组件
受控组件的值由 React state 控制。
通常通过 `value` 和 `onChange` 配合实现。
例如:
```jsx
const [value, setValue] = useState("");
setValue(e.target.value)} />;
```
特点:
- 数据源在 React state 中
- 方便做实时校验
- 方便做联动和统一管理
- 更符合 React 单向数据流思想
### 非受控组件
非受控组件的值主要保存在 DOM 本身中,通常通过 `ref` 获取。
例如:
```jsx
const inputRef = useRef(null);
;
```
特点:
- 不必每次输入都更新 React state
- 简单场景更轻量
- 更接近原生表单处理方式
### 如何选择
优先考虑受控组件的场景:
- 需要实时校验
- 需要联动其他字段
- 需要统一管理表单状态
- 需要和业务逻辑强绑定
可以考虑非受控组件的场景:
- 简单输入
- 只在提交时读取值
- 对性能较敏感
- 接第三方表单库或原生表单能力
---
## 5. 为什么不能直接修改 state,而要通过 setState 或 setter 更新
### 标准答案
React 不能直接修改 state,核心原因有三点。
1. React 需要感知状态变化
- 如果你直接改变量,React 不一定知道数据变了。
- 不通过 `setState` 或 setter,React 可能不会触发重新渲染。
2. React 的更新依赖引用变化和调度机制
- 尤其是对象和数组,如果只是修改原对象内部属性,引用没变。
- 浅比较时可能认为值没变,导致组件无法正确更新。
3. setter 不只是赋值
- `setState` / setter 还负责调度更新、批量更新、协调渲染、保持 UI 和数据一致。
- 直接修改会绕开 React 的更新流程。
### 示例
```jsx
const [user, setUser] = useState({ name: "Tom" });
// 错误方式
user.name = "Jack";
// 正确方式
setUser({ ...user, name: "Jack" });
```
---
## 6. 一个组件里有多个 useEffect,它们的执行顺序和 cleanup 顺序是什么
### 执行顺序
多个 `useEffect` 通常按照代码声明顺序执行。
例如:
```jsx
useEffect(() => {
console.log("effect 1");
});
useEffect(() => {
console.log("effect 2");
});
```
执行顺序通常是:
1. effect 1
2. effect 2
### cleanup 时机
当依赖变化时:
- 先执行上一次 effect 的 cleanup
- 再执行新一轮 effect
当组件卸载时:
- 所有 effect 的 cleanup 都会执行
### 面试表达建议
重点记住两点:
- effect 本身按声明顺序执行
- cleanup 发生在下一次执行前和组件卸载时
---
## 7. 父组件每次都新建 user 对象,会对 React.memo 有什么影响
### 标准答案
`React.memo` 默认做的是浅比较。
如果父组件每次渲染都重新创建一个对象,即使对象内容一样,只要引用变了,子组件就会被认为收到了新的 props,从而重新渲染。
例如:
```jsx
```
这段代码里,父组件每次渲染都会创建一个新的对象引用。
因此 `React.memo` 无法命中缓存。
### 为什么会这样
因为浅比较比的是引用,而不是深度比较对象内容。
### 优化方式
1. 把对象拆成基础类型 props
```jsx
```
2. 使用 `useMemo` 缓存对象引用
```jsx
const user = useMemo(() => ({ name, age }), [name, age]);
```
3. 不要过度优化
- 如果子组件渲染开销很小,没有必要到处使用 `memo`。
---
## 8. React 里的闭包陷阱是什么
### 标准答案
闭包陷阱指的是:函数拿到的是某一次渲染时的变量快照,而不是之后自动更新的最新值。
如果异步回调、定时器、事件处理函数等场景中使用了旧值,就会出现“读到旧 state”的问题。
### 常见例子
```jsx
function Demo() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(timer);
}, []);
return ;
}
```
这里 `useEffect` 只在首次挂载时执行一次,因此 `setInterval` 闭包里拿到的是初始的 `count`。
即使后面点击按钮,定时器里打印的仍可能是旧值。
### 常见解决方案
1. 把依赖补全
```jsx
useEffect(() => {
const timer = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(timer);
}, [count]);
```
2. 使用函数式更新
```jsx
setCount((prev) => prev + 1);
```
3. 使用 `useRef` 保存最新值
- 适用于不希望 effect 反复重建的场景。
---
## 9. useMemo 和 useCallback 的区别
### 标准答案
- `useMemo` 用来缓存“计算结果”。
- `useCallback` 用来缓存“函数引用”。
### 示例
```jsx
const total = useMemo(() => computeTotal(list), [list]);
const handleClick = useCallback(() => doSomething(id), [id]);
```
### 它们分别解决什么问题
`useMemo` 适合:
- 缓存昂贵计算
- 稳定对象或数组引用
- 避免重复执行复杂逻辑
`useCallback` 适合:
- 给子组件传函数时稳定引用
- 避免依赖变化导致 effect 反复执行
- 配合 `React.memo` 减少无效渲染
### 注意点
不要把它们当成“默认优化手段”。
它们本身也有维护成本和依赖管理成本。
只有在性能热点或引用稳定性有明确价值时再使用。
---
## 10. React 组件重新渲染时,什么情况下子组件一定会重新渲染,什么情况下可以避免
### 会触发子组件重新渲染的常见情况
1. 父组件重新渲染,且子组件没有做任何渲染优化
2. 子组件自身的 state 变化
3. 子组件接收的 props 变化
4. 子组件消费的 context 值变化
### 可以避免的情况
如果子组件使用了 `React.memo`,并且 props 经过浅比较后没有变化,则子组件可以跳过渲染。
### 常见优化手段
- `React.memo`
- `useMemo`
- `useCallback`
- 合理拆分组件边界
- 避免无意义地创建新对象和新函数
### 注意点
“父组件一更新,子组件一定重新渲染”并不是绝对正确。
默认情况下通常会重新渲染,但在 `memo` 等优化机制下是可以跳过的。
---
## 11. 什么是虚拟 DOM,React 的 diff 在做什么
### 虚拟 DOM 是什么
虚拟 DOM 本质上是用 JavaScript 对象描述真实 UI 结构的一种抽象表示。
它的价值不只是“提升性能”,更重要的是:
- 支持声明式 UI
- 把渲染逻辑和平台实现解耦
- 让 React 可以在不同平台上复用思想,例如 Web 和 Native
### diff 在做什么
当 state 或 props 变化时,React 会生成新的虚拟 DOM 树,并和旧的虚拟 DOM 树进行比较。
然后找出变化的部分,最后只更新必要的真实 DOM。
### diff 的常见策略
1. 同层比较
- React 通常不会做跨层级的复杂全树最优比较。
- 这样可以降低算法复杂度。
2. 节点类型不同,直接替换
- 比如从 `div` 变成 `span`,通常会直接卸载旧节点,创建新节点。
3. 列表依赖 key
- 同一层列表中,React 通过 `key` 识别哪些节点可以复用。
### 面试建议表达
不要只说“虚拟 DOM 提升性能”。
更准确的说法是:React 通过虚拟 DOM 这种抽象,让更新过程可预测、可比较、可跨平台,并借助 diff 把更新范围控制在必要的部分。
---
## 12. 如何设计一个可复用的弹窗组件
### 设计思路
可复用弹窗组件通常要考虑 API 设计、受控与非受控、内容扩展性、关闭行为、挂载位置、无障碍能力等方面。
### 1. 受控与非受控
推荐同时支持两种模式。
受控模式:
```jsx
```
非受控模式:
```jsx
```
这样既方便业务统一管理,也方便简单场景快速使用。
### 2. 内容插槽
常见设计方式:
- `title`
- `children`
- `footer`
- 自定义关闭按钮区域
例如:
```jsx
}
>
确认删除当前数据吗?
```
### 3. 关闭逻辑
需要明确以下交互:
- 点击遮罩是否关闭
- 按 `Esc` 是否关闭
- 点击关闭按钮是否关闭
- 提交成功后是否自动关闭
- 是否允许业务阻止关闭
例如:
- `maskClosable`
- `keyboard`
- `onClose`
- `onConfirm`
- `beforeClose`
### 4. Portal
弹窗一般不建议直接渲染在原组件层级内,通常通过 Portal 挂载到 `body`,避免层级和样式干扰。
### 5. 其他常见能力
- 动画进出场
- 焦点管理
- 防止背景滚动
- z-index 管理
- 销毁时机控制,例如 `destroyOnClose`
- 无障碍支持,例如 `role="dialog"`
### 面试建议表达
这题重点不是写代码,而是体现组件设计能力和边界意识。
---
## 13. 为什么要做状态提升,什么场景适合状态提升
### 标准答案
状态提升是指:把多个组件共同依赖的状态,提升到它们最近的共同父组件中统一管理,再通过 `props` 传递给子组件。
### 为什么要状态提升
因为多个组件如果需要共享同一份数据,各自维护一份 state 容易造成数据不一致。
提升到共同父组件后,可以保证单一数据源。
### 适合状态提升的场景
- 兄弟组件共享数据
- 一个组件修改数据,另一个组件展示数据
- 表单联动
- 筛选条件和列表结果联动
### 不一定要状态提升的场景
如果数据使用范围非常广,跨层级很多,继续往上提升会造成:
- prop drilling 严重
- 父组件变得过重
- 维护成本增加
这时更适合使用 `Context` 或状态管理库。
---
## 14. 多组件共享用户信息、主题信息、权限信息时,怎么设计状态管理方案
### 1. props 传递
适合场景:
- 组件层级不深
- 数据共享范围小
- 数据关系简单
优点:
- 最直接
- 可追踪性强
- 符合 React 单向数据流
缺点:
- 层级深时容易 prop drilling
### 2. Context
适合场景:
- 全局主题
- 当前登录用户信息
- 国际化配置
- 权限上下文
优点:
- 避免层层传参
- 适合共享“全局环境数据”
缺点:
- value 变化会导致相关消费组件重新渲染
- 不适合高频复杂状态更新
### 3. Redux 或 Zustand
适合场景:
- 跨页面、跨模块共享状态
- 业务复杂、更新频繁
- 需要更清晰的数据流和状态拆分
#### Redux
优点:
- 规范强
- 生态成熟
- 中大型项目可维护性较好
缺点:
- 样板代码相对多
#### Zustand
优点:
- 轻量
- 使用简单
- 上手成本低
缺点:
- 在大型项目中需要团队自行约束结构
### 面试建议回答
可以这样总结:
- 小范围共享优先 `props`
- 全局配置类共享适合 `Context`
- 复杂业务状态适合 `Redux` 或 `Zustand`
关键不是“哪个最好”,而是按数据范围、更新频率、复杂度来选。
---
## 15. 受控与非受控思想除了表单,还体现在哪些组件封装里
### 标准答案
受控与非受控不是表单独有,而是一种通用组件设计思想:组件状态到底由外部控制,还是由组件内部控制。
### 常见体现
1. 弹窗组件
受控:
```jsx
```
非受控:
```jsx
```
2. Tabs 组件
受控:
```jsx
```
非受控:
```jsx
```
3. Collapse / Accordion
- 当前展开项可由外部控制
- 也可由组件内部自己维护
4. Pagination
- 当前页码可以外部控制
- 也可以组件内部保存默认页码
### 设计价值
- 受控模式更适合复杂业务编排
- 非受控模式更适合简单易用场景
- 支持双模式能让组件复用性更高
---
## 16. 搜索框输入时请求接口,但不希望每次输入都立刻请求,该怎么做
### 方案一:防抖
用户停止输入一段时间后再发请求。
适合场景:
- 搜索建议
- 输入联想
- 减少接口调用次数
示意:
```jsx
useEffect(() => {
const timer = setTimeout(() => {
fetchData(keyword);
}, 500);
return () => clearTimeout(timer);
}, [keyword]);
```
### 方案二:节流
固定时间内最多触发一次请求。
适合场景:
- 高频输入但允许周期性更新
- 滚动、拖拽、窗口变化等高频事件
### 方案三:使用延迟值或并发能力
在 React 中可以使用 `useDeferredValue`,让输入值更新和昂贵渲染解耦。
它更适合“降低渲染优先级”,不完全等同于传统防抖。
### 方案四:显式触发搜索
不在输入时立刻请求,而是在点击搜索按钮或按回车时请求。
适合场景:
- 结果请求成本高
- 用户需要明确控制查询时机
### 面试建议对比
- 防抖:停止输入后触发,重点是减少无效请求
- 节流:固定周期触发,重点是限制频率
- `useDeferredValue`:偏向优化渲染体验,不完全替代接口层防抖
- 手动触发:更可控,但交互即时性弱一些
---
## 面试总结建议
从以上题目看,React 面试经常会围绕以下几个层次展开:
1. 基础概念
- state
- props
- key
- effect
- 受控组件
2. 渲染机制
- 重新渲染
- 引用稳定性
- `React.memo`
- 闭包陷阱
- 虚拟 DOM 和 diff
3. 工程实践
- 组件封装
- 状态管理
- 状态提升
- 弹窗、表单、列表等常见场景
4. 性能优化
- `useMemo`
- `useCallback`
- 防抖与节流
- 避免无效渲染
## 面试答题建议
1. 不要只给结论,要说原因
- 例如不要只说“不要用 index 做 key”,要继续补充“因为插入删除时 key 不稳定,会导致节点错位和状态复用错误”。
2. 不要把经验说成绝对规则
- 例如“父组件更新,子组件一定更新”这种说法不够严谨。
- 更好的是说“默认情况下通常会更新,但在 `React.memo` 等优化条件下可以跳过”。
3. 回答尽量包含“定义 + 原理 + 场景 + 权衡”
- 这样更像真实工程师,而不是背八股。
4. 开放题重点体现设计思维
- 像弹窗组件这类题,面试官更关注你是否考虑了边界、扩展性、关闭行为、Portal、无障碍,而不是只会说“受控组件”。
# Vue部分
## 1. Vue2 和 Vue3 的核心区别是什么?
### 标准回答
Vue2 和 Vue3 最核心的区别主要体现在响应式原理、代码组织方式、性能优化和生态设计上。
第一,响应式原理不同。Vue2 基于 `Object.defineProperty` 对对象属性进行劫持,所以它无法监听对象新增和删除属性,也不能直接监听数组下标和长度变化。Vue3 改成了 `Proxy`,它是对整个对象做代理,可以天然监听新增、删除、遍历等操作,响应式能力更完整。
第二,组件组织方式不同。Vue2 主要使用 Options API,把 `data`、`methods`、`computed`、`watch` 分散在不同配置项里。Vue3 除了保留 Options API,还主推 Composition API,可以把同一个功能逻辑写在一起,复用性更强,也更适合大型项目。
第三,生命周期名称有变化。比如 Vue2 的 `beforeDestroy` 和 `destroyed`,在 Vue3 中改成了 `beforeUnmount` 和 `unmounted`。同时 Vue3 把组合式逻辑放进了 `setup`。
第四,性能更好。Vue3 在源码层面做了很多优化,比如更高效的 diff 算法、静态提升、Patch Flag、事件缓存、Tree Shaking,所以它在打包体积和运行性能上都优于 Vue2。
第五,新特性更多。Vue3 引入了 `Fragment`、`Teleport`、`Suspense`,并且官方推荐用 Pinia 替代 Vuex,整体开发体验更现代。
### 记忆关键词
`defineProperty`、`Proxy`、Options API、Composition API、Patch Flag、Tree Shaking、Teleport
---
## 2. Vue 的响应式原理是什么?
### 标准回答
Vue 的响应式本质是“数据变化后,自动通知视图更新”。它的核心流程可以概括为:数据劫持 + 依赖收集 + 派发更新。
在 Vue2 里,会通过 `Object.defineProperty` 给对象的每个属性添加 `getter` 和 `setter`。当组件渲染时,模板中用到的数据会触发 `getter`,这时 Vue 会进行依赖收集,把当前 watcher 记录下来。以后当数据修改时,会触发 `setter`,然后通知对应 watcher 更新,最后完成重新渲染。
在 Vue3 里,这套机制升级为 `Proxy + Reflect`。Vue3 会在读取数据时进行依赖追踪,也就是 `track`,在修改数据时触发更新,也就是 `trigger`。这种设计比 Vue2 更灵活,也能支持 Map、Set 这类新数据结构。
所以可以把响应式理解为:在读取时收集依赖,在修改时触发依赖执行。
### 记忆关键词
数据劫持、依赖收集、派发更新、watcher、track、trigger
---
## 3. Vue2 为什么使用 Object.defineProperty?它有什么缺点?
### 标准回答
Vue2 使用 `Object.defineProperty` 的原因是,在当时这是浏览器里比较成熟、兼容性较好的属性拦截方案。它可以通过给对象属性定义 `getter` 和 `setter`,实现在读写数据时进行拦截。
但它的缺点也很明显。
第一,它只能劫持对象已有属性,不能监听新增属性和删除属性,所以 Vue2 中新增属性通常要用 `Vue.set`。
第二,它对数组支持不够自然,因为无法直接监听数组索引和长度变化,所以 Vue2 通过重写数组原型方法,比如 `push`、`pop`、`splice`,来间接实现数组响应式。
第三,它需要递归遍历对象的每个属性去做劫持,如果数据层级很深,初始化成本会比较高。
所以 Vue3 才改成了 `Proxy`,从根本上解决这些问题。
### 记忆关键词
兼容性、已有属性劫持、不能监听新增删除、重写数组方法、递归开销大
---
## 4. Vue3 为什么改用 Proxy?解决了 Vue2 的哪些问题?
### 标准回答
Vue3 改用 `Proxy`,核心原因是它能从对象层级直接做代理,而不是像 `Object.defineProperty` 那样逐个属性劫持,所以能力更强,性能也更好。
它主要解决了 Vue2 的几个问题。
第一,可以监听对象属性的新增、删除和遍历,不再需要像 Vue2 那样依赖 `Vue.set`。
第二,可以直接监听数组索引和长度变化,数组响应式实现更自然。
第三,`Proxy` 是懒代理,访问到哪个属性再处理哪个属性,不需要一开始就深度递归所有字段,所以初始化成本更低。
第四,Vue3 的响应式系统不只支持普通对象,还支持 `Map`、`Set`、`WeakMap` 等复杂数据结构。
所以 Vue3 的响应式不是简单替换 API,而是整体能力和扩展性的一次升级。
### 记忆关键词
整对象代理、监听新增删除、数组更自然、懒代理、支持 Map/Set
---
## 5. ref 和 reactive 的区别是什么?
### 标准回答
`ref` 和 `reactive` 都是 Vue3 里创建响应式数据的方式,但适用场景不同。
`ref` 一般用于基本数据类型,比如字符串、数字、布尔值,也可以包裹对象。它返回的是一个带有 `.value` 的响应式对象,所以在 JavaScript 里访问时需要写 `.value`,但在模板里会自动解包。
`reactive` 主要用于对象、数组这类引用类型,它返回的是对象本身的代理,不需要 `.value`。
实际开发里,如果是单个值,通常用 `ref`;如果是表单对象、配置对象、列表对象,通常用 `reactive`。
另外需要注意,`reactive` 不能直接代理基本类型,而 `ref` 更通用,所以很多团队现在会优先使用 `ref`。
### 记忆关键词
`ref` 适合基本类型、`.value`、模板自动解包、`reactive` 适合对象
---
## 6. computed 和 watch 的区别是什么?
### 标准回答
`computed` 和 `watch` 的区别,核心在于一个偏“计算结果”,一个偏“监听副作用”。
`computed` 用来基于已有响应式数据,派生出一个新值。它有缓存特性,只有依赖发生变化时才会重新计算,所以适合做模板中的复杂表达式、状态组合、格式化展示等。
`watch` 用来监听数据变化后执行逻辑,它没有返回值的概念,更适合处理副作用,比如发请求、操作本地存储、调用第三方库、打印日志等。
所以如果是“一个值依赖另一个值计算得出”,优先用 `computed`;如果是“数据变了我要执行一段动作”,就用 `watch`。
### 记忆关键词
`computed` 有缓存、派生值;`watch` 监听变化、处理副作用
---
## 7. watch 和 watchEffect 的区别是什么?
### 标准回答
`watch` 和 `watchEffect` 都可以监听响应式数据,但它们的关注点不同。
`watch` 需要明确指定监听的数据源,只有这个数据源变化时才会触发回调,所以它的依赖关系更清晰,也能拿到新值和旧值。
`watchEffect` 则是自动收集回调函数里用到的响应式依赖,回调执行时访问了哪些响应式数据,就会追踪哪些数据。它更像“副作用自动执行器”,写法更简洁,但依赖来源没有 `watch` 那么显式。
所以通常来说,依赖明确、需要新旧值时用 `watch`;快速监听、自动收集依赖时用 `watchEffect`。
### 记忆关键词
`watch` 显式依赖、可拿新旧值;`watchEffect` 自动收集依赖
---
## 8. nextTick 的作用是什么?
### 标准回答
`nextTick` 的作用是,在 DOM 更新完成之后执行一段回调逻辑。
原因是 Vue 的数据更新不是同步立刻更新 DOM 的,而是会把同一轮事件循环中的多次数据修改合并,进行异步批量更新。这样做是为了提升性能,避免每改一次数据就立刻渲染一次页面。
所以如果我改完数据,马上就想拿到最新 DOM,比如获取元素高度、设置焦点、操作滚动条,就需要把这段代码放到 `nextTick` 里。
一句话总结就是:`nextTick` 解决的是“数据改了,但 DOM 还没更新完成”的问题。
### 记忆关键词
异步批量更新、DOM 更新后执行、获取最新 DOM
---
## 9. 虚拟 DOM 是什么?
### 标准回答
虚拟 DOM 本质上是对真实 DOM 的 JavaScript 对象描述。Vue 在渲染时,先根据模板生成虚拟 DOM,再通过 diff 算法比较新旧虚拟 DOM 的差异,最后把需要变更的部分更新到真实 DOM 上。
它的优势不是绝对比真实 DOM 快,而是给框架提供了一层抽象,让跨平台和高效更新成为可能。因为直接频繁操作真实 DOM 成本较高,而通过虚拟 DOM 可以先在内存中计算最小变更,再统一更新页面。
所以虚拟 DOM 的核心价值在于:提升框架的可控性、可维护性和更新效率。
### 记忆关键词
JavaScript 对象描述 DOM、diff、最小更新、跨平台
---
## 10. key 为什么很重要?
### 标准回答
`key` 的作用是给虚拟 DOM 节点提供唯一标识,帮助 Vue 在 diff 过程中准确判断节点是复用、移动还是销毁重建。
如果没有 `key`,或者 `key` 不稳定,比如直接用数组下标,在列表发生插入、删除、排序时,Vue 可能会错误复用节点,导致组件状态错乱、输入框内容错位、渲染性能下降。
所以在 `v-for` 中一般要使用稳定且唯一的业务 ID 作为 `key`,这样既能保证渲染正确性,也能提升更新效率。
### 记忆关键词
唯一标识、准确 diff、避免错误复用、不要轻易用 index
---
## 11. v-if 和 v-show 的区别是什么?
### 标准回答
`v-if` 和 `v-show` 都可以控制元素显示隐藏,但实现方式不同。
`v-if` 是“真正的条件渲染”,条件为假时,元素不会渲染到 DOM 中;条件变为真时,会重新创建组件和元素。它的切换成本高,但首次不渲染更省资源。
`v-show` 是通过控制元素的 `display` 样式来实现显示隐藏,元素始终存在于 DOM 中,只是视觉上隐藏了。它的初始渲染成本更高,但切换成本低。
所以如果是频繁切换的场景,用 `v-show`;如果是条件很少变化、初始不一定展示的场景,用 `v-if`。
### 记忆关键词
`v-if` 控制创建销毁;`v-show` 控制 CSS 显示隐藏
---
## 12. Vue 生命周期有哪些?Vue2 和 Vue3 如何对应?
### 标准回答
Vue 生命周期可以理解为组件从创建到销毁的各个阶段。
Vue2 常见生命周期包括:`beforeCreate`、`created`、`beforeMount`、`mounted`、`beforeUpdate`、`updated`、`beforeDestroy`、`destroyed`。
Vue3 对应关系基本一致,只是销毁阶段改名了:`beforeDestroy` 变成 `beforeUnmount`,`destroyed` 变成 `unmounted`。另外在组合式 API 中,这些钩子通常以 `onMounted`、`onUpdated`、`onUnmounted` 这种形式使用。
如果问常见使用场景,通常可以这么回答:
`created` 或 `setup` 适合做数据初始化;`mounted` 适合操作 DOM、发起和页面结构相关的请求;`updated` 可以感知更新完成;`unmounted` 适合清理定时器、事件监听和订阅。
### 记忆关键词
创建、挂载、更新、卸载;`beforeDestroy` = `beforeUnmount`
---
## 13. 父子组件生命周期执行顺序是怎样的?
### 标准回答
父子组件生命周期执行顺序是一个常见面试点。
加载渲染时,一般是:父 `beforeCreate`、父 `created`、父 `beforeMount`、子 `beforeCreate`、子 `created`、子 `beforeMount`、子 `mounted`、父 `mounted`。
也就是说,创建过程先父后子,但挂载完成是先子后父。
更新过程通常是先父 `beforeUpdate`,再子 `beforeUpdate`,然后子 `updated`,最后父 `updated`。
销毁过程通常是先父 `beforeDestroy` 或 `beforeUnmount`,再子销毁,最后父完成销毁。
### 记忆关键词
创建先父后子,挂载先子后父,更新和销毁都要考虑父子嵌套关系
---
## 14. Vue 组件通信有哪些方式?
### 标准回答
Vue 组件通信常见方式主要有以下几种。
第一,父传子用 `props`。父组件通过属性把数据传给子组件。
第二,子传父用自定义事件,也就是子组件通过 `emit` 触发事件,把数据传给父组件。
第三,跨层级通信可以用 `provide/inject`,适合祖先组件和后代组件之间共享依赖。
第四,兄弟组件或者更复杂组件关系,可以通过状态管理工具,比如 Vuex 或 Pinia。
第五,少量简单场景也可以通过事件总线,但 Vue3 已经不推荐这种方式,因为维护性比较差。
如果是面试回答,最好顺带补一句:组件通信的选择要看关系,父子优先 `props + emit`,跨层级考虑 `provide/inject`,全局共享状态用 Pinia 或 Vuex。
### 记忆关键词
`props`、`emit`、`provide/inject`、Pinia、Vuex
---
## 15. props 为什么是单向数据流?为什么不能直接修改 props?
### 标准回答
`props` 是单向数据流,意思是数据只能从父组件流向子组件,子组件只能读取父组件传过来的值,不应该直接修改。
原因是如果子组件随意改 `props`,会破坏组件之间的数据边界,让状态来源变得不清晰。并且一旦父组件重新渲染,父组件传入的新值可能会覆盖子组件的修改,导致数据不可预测。
所以 Vue 会警告不要直接修改 `props`。正确做法通常有两种:
第一,如果子组件只是基于 `props` 做展示,就直接使用。
第二,如果子组件需要基于 `props` 做可编辑状态,可以把它拷贝一份到本地状态,再操作本地状态;或者通过 `emit` 通知父组件更新。
### 记忆关键词
单向数据流、状态边界清晰、父组件可控、不要直接改 `props`
---
## 16. v-model 的原理是什么?
### 标准回答
`v-model` 本质上是语法糖,底层还是“属性绑定 + 事件监听”。
在 Vue2 中,组件上的 `v-model` 默认等价于:父组件给子组件传一个 `value` 属性,再监听子组件触发的 `input` 事件,然后更新这个值。
也就是可以理解为:
`v-model="msg"` 等价于 `:value="msg" @input="msg = $event"`。
在 Vue3 中,默认约定变成了 `modelValue` 和 `update:modelValue`,也就是:
`v-model="msg"` 等价于 `:modelValue="msg" @update:modelValue="msg = $event"`。
所以 `v-model` 的本质没有变,依然是父子通信的一种封装。
### 记忆关键词
语法糖、属性绑定、事件通知、Vue2 是 `value + input`、Vue3 是 `modelValue + update:modelValue`
---
## 17. Vue Router 的 hash 和 history 模式区别是什么?
### 标准回答
Vue Router 常见有两种模式:`hash` 和 `history`。
`hash` 模式的 URL 会带 `#`,比如 `/#/home`。`#` 后面的内容变化不会触发浏览器向服务器重新发请求,它是依赖浏览器的 `hashchange` 事件实现路由切换,所以部署比较简单,刷新一般不会有 404 问题。
`history` 模式的 URL 更美观,比如 `/home`。它依赖 HTML5 的 History API,比如 `pushState` 和 `replaceState`。但它有一个要求,就是服务端必须做路由兜底配置,否则刷新页面时服务器会找不到对应路径,返回 404。
所以简单说,`hash` 部署简单但 URL 不够美观,`history` 更像正常网站路径,但需要服务端配合。
### 记忆关键词
`hash` 带 `#`、部署简单;`history` 更美观、需要服务端兜底
---
## 18. 路由守卫有哪些?分别适合做什么?
### 标准回答
Vue Router 的路由守卫主要分为三类:全局守卫、路由独享守卫、组件内守卫。
全局守卫最常见的是 `beforeEach` 和 `afterEach`。`beforeEach` 一般用于登录鉴权、权限校验、页面跳转拦截;`afterEach` 常用于埋点、修改页面标题这类后置逻辑。
路由独享守卫是写在某个路由配置里的 `beforeEnter`,适合只对单个页面做特殊拦截。
组件内守卫,比如 `beforeRouteEnter`、`beforeRouteUpdate`、`beforeRouteLeave`,适合和组件本身强相关的逻辑,比如离开页面前是否保存、同一路由参数变化时重新拉取数据等。
如果面试官继续追问鉴权方案,可以补充:通常会在全局前置守卫里判断 token 是否存在,以及用户权限是否匹配目标路由。
### 记忆关键词
全局守卫、独享守卫、组件守卫、鉴权、埋点、离开拦截
---
## 19. Vuex 和 Pinia 的区别是什么?
### 标准回答
Vuex 和 Pinia 都是 Vue 的状态管理方案,但 Pinia 是新一代官方推荐方案。
Vuex 的特点是概念相对更多,包括 `state`、`getters`、`mutations`、`actions`、`modules`。其中 Vuex 强调必须通过 `mutation` 去修改状态,结构相对固定。
Pinia 的设计更轻量,去掉了 `mutation`,可以直接在 `action` 里修改状态,API 更简洁,TypeScript 支持也更友好,模块化定义更自然。
另外在 Vue3 生态中,Pinia 的使用体验更符合 Composition API 思维,所以现在新项目通常优先选 Pinia。
如果是老项目、历史包袱较重,可能还会继续使用 Vuex。
### 记忆关键词
Pinia 更轻量、无 mutation、TS 更友好、Vue3 官方推荐
---
## 20. Vue 项目如何做性能优化?
### 标准回答
Vue 项目的性能优化可以从加载性能、渲染性能和代码层面三个方向去看。
在加载性能上,可以做路由懒加载、组件按需加载、图片压缩、资源 CDN、开启 gzip 或 brotli,减少首屏资源体积。
在渲染性能上,可以避免不必要的响应式数据,列表渲染时加稳定 `key`,长列表使用虚拟滚动,合理使用 `v-show`、`v-if`、`keep-alive`,减少频繁销毁和重建。
在代码层面上,可以减少深层嵌套响应式对象,避免滥用 `watch`,把适合缓存的逻辑交给 `computed`,并且及时清理定时器、事件监听,防止内存泄漏。
如果是 Vue3,还可以借助编译优化,比如静态提升和 Patch Flag 自动减少不必要更新。
所以面试里通常可以总结成一句话:优化首屏加载、减少无效渲染、控制响应式开销。
### 记忆关键词
路由懒加载、稳定 key、虚拟列表、`keep-alive`、减少无效渲染
---
## 21. Composition API 相比 Options API 的优势是什么?
### 标准回答
Composition API 相比 Options API,最大的优势是“逻辑聚合”和“更好复用”。
在 Options API 中,一个功能相关的代码可能分散在 `data`、`methods`、`computed`、`watch` 里,组件一大就不好维护。
Composition API 可以把同一个功能的状态、方法、计算属性、监听器都写在一起,这样代码更容易阅读、拆分和复用。
另外,Composition API 对 TypeScript 更友好,逻辑抽离时可以通过组合函数实现复用,能替代部分 mixin 带来的命名冲突和来源不清的问题。
所以在中大型项目中,Composition API 的维护性通常会更好。
### 记忆关键词
逻辑聚合、复用性强、TS 友好、替代 mixin
---
## 22. setup 为什么不能用 this?
### 标准回答
在 Vue3 中,`setup` 执行时机比组件实例创建更早,所以这时候组件实例上的 `this` 还没有建立起来,因此在 `setup` 里不能像 Vue2 那样通过 `this` 访问 `data`、`props` 或方法。
Vue3 的设计思路也不再推荐依赖 `this`,而是鼓励通过函数参数和闭包来组织逻辑,比如通过 `props` 参数获取属性,通过组合函数封装逻辑。
所以 `setup` 不能用 `this`,本质上是因为它是组合式 API 的入口,强调的是显式依赖,而不是实例上下文。
### 记忆关键词
执行更早、实例未创建、显式依赖、弱化 `this`
---
## 23. keep-alive 的作用是什么?
### 标准回答
`keep-alive` 的作用是缓存组件实例,避免组件切换时频繁销毁和重建。
它常用于页面切换后需要保留状态的场景,比如列表页滚动位置、表单页输入内容、Tab 切换内容缓存等。
被 `keep-alive` 包裹的组件不会在切换时真正销毁,而是进入失活状态,对应会触发 `activated` 和 `deactivated` 这两个钩子。
所以它的核心价值是:保留状态、减少重复渲染、提升切换体验。
### 记忆关键词
缓存组件实例、保留状态、`activated`、`deactivated`
---
## 24. 为什么 Vue3 更适合大型项目?
### 标准回答
Vue3 更适合大型项目,主要是因为它在可维护性、类型支持和性能方面都更强。
首先,Composition API 更适合把复杂业务拆成多个可复用的逻辑模块,避免大组件逻辑分散。
其次,Vue3 对 TypeScript 支持更好,在大型项目中更容易做类型约束和团队协作。
再次,Vue3 的响应式系统和编译优化能力更强,在复杂页面和组件树较深时,性能表现通常更稳定。
另外,Vue3 周边生态,比如 Pinia、Vite,也更符合现代前端工程化需求。
所以可以总结为:Vue3 对大型项目更友好,不只是因为语法新,而是因为工程能力更完整。
### 记忆关键词
组合式复用、TS 友好、性能更好、工程化更现代
---
## 25. Vue2 到 Vue3 的迁移重点有哪些?
### 标准回答
Vue2 到 Vue3 的迁移重点主要有四个方面。
第一,响应式 API 变化。Vue3 引入了 `ref`、`reactive`、`computed`、`watchEffect` 等新的组合式写法。
第二,生命周期名称变化。比如 `beforeDestroy` 和 `destroyed` 改成了 `beforeUnmount` 和 `unmounted`。
第三,组件通信和语法有调整。比如 `v-model` 的默认实现从 `value + input` 变成了 `modelValue + update:modelValue`。
第四,生态迁移。状态管理更推荐 Pinia,构建工具很多项目会从 Webpack 迁移到 Vite。
如果项目比较大,迁移时一般建议先兼容运行,再逐步改成 Composition API,而不是一次性全面重构。
### 记忆关键词
响应式 API、新生命周期、`v-model` 变化、Pinia、Vite、渐进迁移
---
## 高频场景题模板
### 1. 页面很卡,你怎么排查?
可以从三个方向排查:
第一,看是不是首屏资源过大,比如包体积过大、图片太大、没有路由懒加载。
第二,看是不是渲染次数过多,比如列表没加稳定 `key`、响应式数据过深、`watch` 使用过多、父组件频繁更新导致子组件重复渲染。
第三,看是不是有内存泄漏,比如定时器、事件监听、订阅没有清理。
排查工具上通常会结合浏览器 Performance、Network、Memory 面板,以及 Vue Devtools 去定位。
### 2. 列表很大,页面渲染慢,怎么优化?
可以从减少渲染节点和减少更新次数两个角度处理。
第一,用虚拟列表,只渲染可视区域。
第二,给列表项加稳定 `key`,避免错误复用。
第三,避免在模板里写复杂计算,能用 `computed` 的提前缓存。
第四,如果列表项组件复杂,可以考虑拆分和缓存,减少重复渲染。
### 3. 父组件更新了,不希望子组件重复渲染,怎么做?
可以先判断子组件是否真的依赖父组件变化的数据。如果没有依赖,就要减少无意义的 props 传递。
另外可以把稳定数据移出响应式、拆分组件边界、合理使用 `computed` 和缓存机制。Vue3 编译层面也会帮助优化静态节点,但前提还是组件设计要合理。
# Typescript
# TypeScript 面试题
## 1. TypeScript 是什么?它和 JavaScript 的关系是什么?
**答案:**
TypeScript 是 JavaScript 的超集,在 JavaScript 的基础上增加了静态类型系统、接口、泛型、枚举等能力。它最终会被编译成 JavaScript 运行,所以可以认为:
- JavaScript 能写的代码,TypeScript 基本都能写
- TypeScript 提供了更好的类型检查和开发体验
- 它不能直接在浏览器中运行,需要编译为 JavaScript
---
## 2. 为什么要使用 TypeScript?
**答案:**
主要原因有:
- 在开发阶段发现类型错误,减少运行时 bug
- 提高代码可读性和可维护性
- 更适合大型项目协作
- IDE 自动补全、跳转、重构能力更强
- 接口定义更清晰,便于约束数据结构
---
## 3. `any`、`unknown`、`never` 的区别是什么?
**答案:**
### `any`
表示任意类型,关闭类型检查。
```ts
let value: any = 123;
value = "hello";
value.foo.bar();
```
特点:
- 可以赋值给任何类型
- 任何类型也可以赋值给它
- 会绕过 TypeScript 类型系统
### `unknown`
表示未知类型,比 `any` 更安全。
```ts
let value: unknown = "hello";
if (typeof value === "string") {
console.log(value.toUpperCase());
}
```
特点:
- 可以接收任何值
- 使用前必须先做类型收窄
### `never`
表示永远不会有值的类型。
```ts
function throwError(message: string): never {
throw new Error(message);
}
```
常见场景:
- 函数永远抛错
- 死循环
- 联合类型穷尽检查
---
## 4. `interface` 和 `type` 的区别是什么?
**答案:**
两者都可以描述对象结构,但有一些区别。
### `interface`
更适合定义对象、类的结构,支持声明合并。
```ts
interface User {
name: string;
age: number;
}
```
### `type`
更灵活,可以定义联合类型、交叉类型、别名等。
```ts
type User = {
name: string;
age: number;
};
type Status = "success" | "error" | "loading";
```
### 区别总结
- `interface` 支持声明合并
- `type` 可表达能力更强
- 定义对象结构时两者都可以
- 团队中通常约定:对象结构优先 `interface`,复杂类型组合优先 `type`
---
## 5. 什么是类型推断?
**答案:**
类型推断是指 TypeScript 根据上下文自动推导变量类型,无需手动声明。
```ts
let name = "Tom";
```
这里 `name` 会被推断为 `string`。
再比如:
```ts
function add(a: number, b: number) {
return a + b;
}
```
返回值会被推断为 `number`。
优点:
- 减少重复代码
- 保持类型信息完整
---
## 6. 什么是联合类型和交叉类型?
**答案:**
### 联合类型 `|`
表示一个值可以是多种类型之一。
```ts
let value: string | number;
value = "hello";
value = 123;
```
### 交叉类型 `&`
表示将多个类型合并成一个类型。
```ts
type A = { name: string };
type B = { age: number };
type C = A & B;
```
此时 `C` 必须同时拥有 `name` 和 `age`。
---
## 7. 什么是泛型?有什么作用?
**答案:**
泛型是指在定义函数、接口、类时,不预先指定具体类型,而在使用时再指定类型。
```ts
function identity(value: T): T {
return value;
}
const a = identity("hello");
const b = identity(123);
```
作用:
- 提高代码复用性
- 保持类型安全
- 避免使用 `any`
---
## 8. `extends` 在 TypeScript 中有哪些用法?
**答案:**
`extends` 常见有以下几种用途:
### 1. 接口继承
```ts
interface Animal {
name: string;
}
interface Dog extends Animal {
bark(): void;
}
```
### 2. 泛型约束
```ts
function getLength(arg: T): number {
return arg.length;
}
```
### 3. 条件类型中使用
```ts
type IsString = T extends string ? true : false;
```
---
## 9. 什么是类型断言?和类型转换有什么区别?
**答案:**
类型断言是告诉编译器“我比你更清楚这个值的类型”。
```ts
const value: unknown = "hello";
const len = (value as string).length;
```
或者:
```ts
const len = (value).length;
```
区别:
- 类型断言只影响编译阶段,不会改变运行时数据
- 它不是强制类型转换
- 如果断言错了,运行时仍可能报错
---
## 10. 什么是类型守卫?
**答案:**
类型守卫用于在运行时缩小变量类型范围。
常见方式:
### `typeof`
```ts
function print(value: string | number) {
if (typeof value === "string") {
console.log(value.toUpperCase());
}
}
```
### `instanceof`
```ts
if (error instanceof Error) {
console.log(error.message);
}
```
### `in`
```ts
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim();
}
}
```
### 自定义类型守卫
```ts
function isString(value: unknown): value is string {
return typeof value === "string";
}
```
---
## 11. `readonly` 和 `const` 的区别是什么?
**答案:**
### `const`
用于声明变量,表示变量不能被重新赋值。
```ts
const name = "Tom";
```
### `readonly`
用于属性级别,表示属性不能被修改。
```ts
interface User {
readonly id: number;
name: string;
}
```
区别:
- `const` 约束变量本身
- `readonly` 约束对象属性
例如:
```ts
const user = { name: "Tom" };
user.name = "Jack";
```
这是允许的,因为 `const` 只是不允许 `user = xxx`,并不限制内部属性变化。
---
## 12. 什么是枚举 `enum`?它适合什么场景?
**答案:**
`enum` 用于定义一组命名常量。
```ts
enum Status {
Pending,
Success,
Failed,
}
```
使用:
```ts
const current = Status.Success;
```
适合场景:
- 状态值固定且可枚举
- 需要语义化常量
注意:现代项目中很多团队更倾向于使用字符串字面量联合类型代替 `enum`,例如:
```ts
type Status = "pending" | "success" | "failed";
```
因为更轻量、兼容性更好。
---
## 13. 什么是 `keyof`?
**答案:**
`keyof` 用于获取某个类型的所有键组成的联合类型。
```ts
interface User {
name: string;
age: number;
}
type UserKeys = keyof User;
```
此时:
```ts
type UserKeys = "name" | "age";
```
常用于泛型约束:
```ts
function getValue(obj: T, key: K) {
return obj[key];
}
```
---
## 14. 什么是 `typeof` 在类型系统中的用法?
**答案:**
在 TypeScript 中,`typeof` 不仅能用于运行时,还能在类型上下文中获取变量的类型。
```ts
const user = {
name: "Tom",
age: 20,
};
type User = typeof user;
```
这里 `User` 的类型是:
```ts
{
name: string;
age: number;
}
```
常用于根据已有对象自动生成类型,减少重复定义。
---
## 15. 什么是映射类型?
**答案:**
映射类型是基于已有类型批量生成新类型的一种方式。
例如手写一个 `Readonly`:
```ts
type MyReadonly = {
readonly [K in keyof T]: T[K];
};
```
例如:
```ts
interface User {
name: string;
age: number;
}
type ReadonlyUser = MyReadonly;
```
常见内置映射类型:
- `Partial`
- `Required`
- `Readonly`
- `Pick`
- `Record`
---
## 16. `Partial`、`Pick`、`Omit` 的作用分别是什么?
**答案:**
### `Partial`
把所有属性变为可选。
```ts
interface User {
name: string;
age: number;
}
type PartialUser = Partial;
```
### `Pick`
从一个类型中挑选部分属性。
```ts
type UserName = Pick;
```
### `Omit`
从一个类型中排除部分属性。
```ts
type UserWithoutAge = Omit;
```
这些工具类型经常用于接口复用和 DTO 定义。
---
## 17. 什么是条件类型?
**答案:**
条件类型的语法类似三元表达式:
```ts
T extends U ? X : Y
```
示例:
```ts
type IsNumber = T extends number ? true : false;
```
使用:
```ts
type A = IsNumber; // true
type B = IsNumber; // false
```
常用于泛型中做类型判断和分发。
---
## 18. 什么是分布式条件类型?
**答案:**
当条件类型作用于联合类型时,会对联合类型的每一个成员分别计算,这叫分布式条件类型。
```ts
type ToArray = T extends any ? T[] : never;
type Result = ToArray;
```
结果是:
```ts
type Result = string[] | number[];
```
这在高级类型编程中非常常见。
---
## 19. `implements` 和 `extends` 的区别是什么?
**答案:**
### `extends`
表示继承。
```ts
class Animal {
move() {}
}
class Dog extends Animal {}
```
### `implements`
表示类实现某个接口。
```ts
interface Flyable {
fly(): void;
}
class Bird implements Flyable {
fly() {}
}
```
区别:
- `extends` 用于类继承类,或接口继承接口
- `implements` 用于类实现接口
- 一个类可以 `implements` 多个接口
---
## 20. TypeScript 中常见的实际开发问题有哪些?
**答案:**
### 1. 过度使用 `any`
问题:失去类型保护。
建议:优先使用明确类型、泛型、`unknown`。
### 2. 类型定义重复
问题:维护成本高。
建议:使用 `typeof`、`keyof`、工具类型复用已有类型。
### 3. 断言滥用
问题:编译能过,运行时报错。
建议:优先使用类型守卫,而不是强行 `as`。
### 4. 联合类型未收窄
问题:访问属性时报错。
建议:使用 `typeof`、`in`、自定义守卫进行收窄。
### 5. 泛型设计过度复杂
问题:代码难维护。
建议:优先简单、可读的类型设计。
---
## 21. 请手写一个泛型函数示例,并说明其意义
**答案:**
```ts
function wrapInArray(value: T): T[] {
return [value];
}
```
使用:
```ts
const a = wrapInArray(1); // number[]
const b = wrapInArray("hello"); // string[]
```
意义:
- 同一套逻辑适用于多种类型
- 返回值类型能和输入值保持一致
- 比 `any` 更安全
---
## 22. 如何理解 TypeScript 的“结构化类型系统”?
**答案:**
TypeScript 采用结构化类型系统,也叫鸭子类型。判断一个值是否属于某个类型,主要看“结构是否匹配”,而不是名字是否一致。
```ts
interface Point {
x: number;
y: number;
}
const p = { x: 1, y: 2, z: 3 };
const point: Point = p;
```
这是允许的,因为 `p` 至少具备 `x` 和 `y`。
核心理解:
- 不关心类型名是否相同
- 只关心所需属性和方法是否满足要求
---
## 23. TypeScript 编译时报错和运行时报错有什么区别?
**答案:**
### 编译时报错
发生在 TypeScript 编译阶段,例如类型不匹配:
```ts
let age: number = "18";
```
### 运行时报错
发生在 JavaScript 实际执行时,例如访问 `undefined` 属性:
```ts
const user = undefined;
console.log(user.name);
```
TypeScript 主要解决的是编译阶段的很多问题,但不能完全替代运行时校验。
---
## 24. `tsconfig.json` 中常见的重要配置有哪些?
**答案:**
### `target`
指定编译后的 JS 版本。
### `module`
指定模块规范,如 `commonjs`、`esnext`。
### `strict`
开启严格模式,是非常重要的配置。
### `noImplicitAny`
禁止隐式 `any`。
### `strictNullChecks`
严格检查 `null` 和 `undefined`。
### `outDir`
指定输出目录。
### `baseUrl` 和 `paths`
用于路径别名配置。
面试里常见结论:实际项目中建议尽量开启 `strict`,否则 TypeScript 的价值会打折。
---
## 25. TypeScript 有哪些优点和局限性?
**答案:**
### 优点
- 提前发现类型问题
- 更适合大型项目
- 开发体验好
- 重构更安全
- 代码可读性更高
### 局限性
- 有学习成本
- 类型系统复杂时理解门槛高
- 不能消除所有运行时问题
- 某些第三方库类型定义可能不完善
---
## 面试加分回答建议
回答 TypeScript 问题时,可以尽量体现以下几点:
- 不只讲概念,还能给出代码示例
- 能说出“为什么这样设计”
- 能结合实际项目场景回答
- 能区分“编译期类型安全”和“运行时行为”
例如回答泛型时,不要只说“泛型是参数化类型”,最好补一句:
> 泛型的核心价值是让同一套逻辑适配多种类型,同时保留类型约束,避免使用 `any` 带来的类型丢失。