# 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` 带来的类型丢失。