2025年最新react+TS前端面试题集锦(含参考答案与追问)
React+TS前端面试题集锦摘要: React核心与Hooks部分: 介绍React 18.x关键特性及常用UI库(Ant Design、Material-UI等) 详解useState、useEffect等基础Hooks及useMemo、useCallback优化技巧 分析自定义Hooks数据隔离问题及共享解决方案 解释"闭包陷阱"现象及其防范措施 附带组件封装思路、性能优
react+TS前端面试题集锦(含参考答案与追问)
一、React 核心与 Hooks 相关
(一)基础概念与版本、UI 库
-
面试题:React 常用版本有哪些?你熟悉的 React 生态 UI 库有哪些?
-
参考答案:
- React 常用版本:目前主流稳定版本为 18.x(如 18.2.0),关键版本迭代包括 16.x(引入 Fiber 架构、Hooks)、17.x(无新特性,主要做底层重构以支持后续版本升级)、18.x(新增 Concurrent Mode、自动批处理、useId/useTransition 等新 Hooks)。
- 熟悉的 UI 库:
- 企业级中后台:Ant Design(React 生态最常用,组件丰富,支持 TS,适配 PC 端)、Material-UI(Google 官方风格,设计规范严谨,自定义性强);
- 轻量型:Ant Design Mobile(适配移动端,体积小)、Chakra UI(支持响应式,无障碍友好);
- 特定场景:React Table(表格组件,支持排序、筛选、分页等复杂功能)、React Query(数据请求与缓存,常配合 UI 库使用)。
-
针对性追问:在使用 Ant Design 时,是否做过组件二次封装?比如对 Table 或 Form 组件的封装思路是什么?
- 追问参考答案:做过二次封装。以 Table 组件为例,针对多个页面需重复配置“分页、加载状态、空数据提示、固定列”等共性需求,封装为 BaseTable 组件:
- 接收 columns(列配置)、dataSource(数据源)、loading(加载状态)等 props,内部默认实现分页逻辑(如 pagination={{ pageSize: 10, showSizeChanger: true }});
- 统一空数据提示(通过 locale 属性自定义“暂无数据”文案,避免各页面重复写 emptyText);
- 暴露 onChange 事件,支持页面自定义表格交互(如排序、筛选后的回调),既减少重复代码,又保留灵活性。
- 追问参考答案:做过二次封装。以 Table 组件为例,针对多个页面需重复配置“分页、加载状态、空数据提示、固定列”等共性需求,封装为 BaseTable 组件:
(二)Hooks 核心问题
-
面试题 1:React Hooks 中你常用的有哪些?请举例说明使用场景。
- 参考答案:
- 基础 Hooks:
- useState:管理组件内部状态,如表单输入值(const [inputVal, setInputVal] = useState(‘’))、弹窗显示隐藏(const [visible, setVisible] = useState(false));
- useEffect:处理副作用,如组件挂载时请求接口(useEffect(() => { fetchData() }, []))、监听窗口大小变化(useEffect(() => { window.addEventListener(‘resize’, handleResize); return () => window.removeEventListener(‘resize’, handleResize) }, []));
- useContext:跨组件传递数据,如全局主题、用户信息(无需层层 props 传递,通过 createContext 创建上下文,子组件用 useContext 读取)。
- 优化类 Hooks:
- useMemo:缓存计算结果,如复杂列表筛选(const filteredList = useMemo(() => list.filter(item => item.status === 1), [list]),避免组件重渲染时重复计算);
- useCallback:缓存函数引用,如传递给子组件的回调函数(const handleClick = useCallback(() => { setCount(c => c + 1) }, []),配合子组件 React.memo 避免不必要重渲染);
- useRef:获取 DOM 元素(const inputRef = useRef(null); useEffect(() => inputRef.current.focus(), []))或保存跨渲染周期的变量(如定时器 ID)。
- 基础 Hooks:
- 针对性追问:useMemo 和 useCallback 的依赖项为空数组时,是否永远不会更新?为什么?
- 追问参考答案:不是。若依赖项为空数组,useMemo 缓存的计算结果、useCallback 缓存的函数引用仅在组件初次渲染时生成,后续组件重渲染时不会更新;但如果组件被卸载后重新挂载,会再次生成新的结果/函数引用。例如:父组件传递空依赖的 useCallback 函数给子组件,若父组件卸载后重新渲染,子组件接收的函数会是新的引用,可能触发子组件重渲染(需配合 React.memo 且子组件无其他 props 变化时才不会重渲染)。
- 参考答案:
-
面试题 2:自定义 Hooks 被两个兄弟组件调用时,Hooks 内部的数据能否共享?为什么?
- 参考答案:不能共享。自定义 Hooks 的本质是“可复用的 Hooks 逻辑抽取”,每个组件调用自定义 Hooks 时,都会创建独立的 Hooks 执行上下文:
- 例如自定义 useCounter Hooks(内部用 useState 管理 count),组件 A 和组件 B 分别调用 useCounter(),两者的 count 是独立的,组件 A 修改 count 不会影响组件 B 的 count;
- 若需共享数据,需通过 useContext(跨组件共享)、状态管理库(如 Redux、Zustand)或父组件传递 props 实现。
- 针对性追问:如何改造自定义 Hooks,让其支持数据共享?举一个具体案例。
- 追问参考答案:可结合 createContext 和自定义 Hooks 实现共享。例如实现 useUser Hooks 共享用户信息:
- 创建上下文:const UserContext = createContext(null);
- 编写 Provider 组件:const UserProvider = ({ children }) => { const [userInfo, setUserInfo] = useState(null); return <UserContext.Provider value={{ userInfo, setUserInfo }}>{children}</UserContext.Provider> };
- 自定义共享 Hooks:const useUser = () => { const context = useContext(UserContext); if (!context) throw new Error(‘useUser must be used within UserProvider’); return context };
- 使用:在根组件包裹 UserProvider,兄弟组件 A 和 B 调用 useUser(),即可共享 userInfo 和 setUserInfo,修改后两者都会同步更新。
- 追问参考答案:可结合 createContext 和自定义 Hooks 实现共享。例如实现 useUser Hooks 共享用户信息:
- 参考答案:不能共享。自定义 Hooks 的本质是“可复用的 Hooks 逻辑抽取”,每个组件调用自定义 Hooks 时,都会创建独立的 Hooks 执行上下文:
-
面试题 3:什么是 React Hooks 的“闭包陷阱”?如何避免?
- 参考答案:
- 闭包陷阱定义:Hooks 依赖闭包特性,若函数(如 useEffect 回调、定时器回调)捕获的 Hooks 状态/变量是“旧值”,导致执行结果不符合预期,即为闭包陷阱。例如:
const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { console.log(count); // 始终打印 0,而非实时 count 值 }, 1000); return () => clearInterval(timer); }, []); // 依赖项为空,count 始终是初始值 0- 避免方案:
- 补充依赖项:将回调中使用的状态/变量加入 useEffect 依赖数组(如上述案例添加 [count]),确保回调捕获最新值;
- 使用函数式更新:若状态更新依赖旧状态(如 setCount(c => c + 1)),函数参数 c 始终是最新状态,无需依赖数组;
- 使用 useRef 保存最新值:若需在不触发重渲染的情况下保存最新值,可通过 useRef 存储(如 const countRef = useRef(count); useEffect(() => { countRef.current = count; }, [count]),回调中读取 countRef.current)。
- 针对性追问:若 useEffect 依赖项过多,可能导致频繁执行,如何平衡“闭包陷阱”和“频繁执行”的问题?
- 追问参考答案:可通过“合并依赖”或“拆分 useEffect”解决:
- 合并依赖:若多个依赖项关联同一逻辑(如表单多字段验证),可将其合并为一个对象(如 const formValues = { name, age, email };),依赖数组仅需加入 [formValues](需注意对象引用变化问题,可配合 useMemo 缓存 formValues);
- 拆分 useEffect:将不同逻辑拆分到多个 useEffect 中,每个 useEffect 仅依赖自身相关的变量(如“监听窗口大小”和“请求接口”拆分到两个 useEffect,分别依赖 [] 和 [userId]),避免一个 useEffect 依赖过多变量。
- 追问参考答案:可通过“合并依赖”或“拆分 useEffect”解决:
- 参考答案:
二、TypeScript 相关
(一)TS 项目实践与泛型
-
面试题 1:你是否使用过 TypeScript?参与的项目中 TS 覆盖率能达到多少?如何保障覆盖率?
- 参考答案:使用过 TypeScript,参与的中后台项目 TS 覆盖率约 90% 以上(通过 tsconfig.json 配置 strict: true 开启严格模式,避免 any 类型滥用)。
- 保障覆盖率的措施:
- 禁用 any:通过 tsconfig 配置 noImplicitAny: true,强制为所有变量、函数参数/返回值指定类型,仅在特殊场景(如第三方库无类型定义)使用 unknown 并配合类型断言;
- 类型定义复用:封装公共类型(如 interface ApiResponse { code: number; data: T; message: string }),避免重复定义;
- 工具链检查:使用 eslint 配合 @typescript-eslint 插件(如规则 no-explicit-any),提交代码前通过 lint 检查类型问题;
- 定期review:代码评审时重点检查类型合理性(如是否过度使用 any、泛型是否正确)。
- 针对性追问:若第三方库无 TS 类型定义(如老旧 jQuery 插件),如何处理?
- 追问参考答案:
- 方案1:安装社区维护的类型包(如 @types/jquery),优先使用官方或高下载量的类型包;
- 方案2:自定义类型声明文件(如创建 xxx.d.ts),通过 declare module ‘xxx’ { export default xxx; } 声明模块,若需使用具体方法,可补充接口定义(如 declare interface JQuery { myPlugin: () => void; });
- 方案3:临时使用 // @ts-ignore 忽略类型检查(仅在紧急场景使用,需注明原因,后续优先补充类型定义)。
- 追问参考答案:
-
面试题 2:TS 中如何声明泛型?泛型的运作机制是什么?请举一个具体例子。
- 参考答案:
- 泛型声明:泛型是“类型参数化”,通过 (T 为类型变量,可自定义名称如 )声明,支持在函数、接口、类中使用,用于复用类型逻辑并保持类型安全。
- 运作机制:泛型在编译时不指定具体类型,而是在使用时(如调用函数、创建实例)传入具体类型,TS 编译器根据传入的类型进行类型检查,避免 any 导致的类型丢失。
- 具体例子:封装一个“数组取值”函数,支持返回数组指定索引的元素,且类型与数组元素类型一致:
// 泛型函数声明:<T> 为泛型参数,代表数组元素类型 function getArrayItem<T>(arr: T[], index: number): T | undefined { if (index < 0 || index >= arr.length) return undefined; return arr[index]; } // 使用时传入具体类型(可显式指定或 TS 自动推导) const strArr = ['a', 'b', 'c']; const numArr = [1, 2, 3]; // 自动推导 T 为 string,返回值类型为 string | undefined const strItem = getArrayItem(strArr, 1); // 显式指定 T 为 number,返回值类型为 number | undefined const numItem = getArrayItem<number>(numArr, 2);- 针对性追问:泛型能否约束类型范围?如何实现?举一个例子。
- 追问参考答案:可以通过“泛型约束”(extends 关键字)限制类型范围。例如封装一个“获取对象属性”函数,要求传入的属性必须是对象自身的键:
// 泛型约束:T 必须是对象类型,K 必须是 T 的键(keyof T) function getObjectProp<T extends object, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const user = { name: '张三', age: 20 }; getObjectProp(user, 'name'); // 正确:'name' 是 user 的键,返回值类型为 string getObjectProp(user, 'gender'); // 错误:TS 编译报错,'gender' 不是 user 的键 - 参考答案:
(二)type 与 interface 区别
-
面试题:TypeScript 中 type 和 interface 有什么区别?分别在什么场景下使用?
-
参考答案:
| 对比维度 | interface | type(类型别名) |
|---|---|---|
| 扩展方式 | 支持 extends 继承,或多个同名 interface 自动合并 | 支持 &(交叉类型)扩展,不支持同名合并 |
| 适用场景 | 主要用于定义对象/类的结构(如接口、组件 props) | 可定义任意类型(对象、基本类型、联合类型等) |
| 类型运算 | 不支持联合、交叉等复杂类型运算 | 支持联合、交叉等复杂类型运算 |
| 声明合并 | 支持(同名 interface 会合并属性) | 不支持(同名 type 会编译报错) |
| 与类结合 | 支持 class implements interface | 支持 class implements type,但若 type 包含联合/交叉类型则不支持 |
-
使用场景建议:
- 用 interface:定义组件 props(如 interface ButtonProps { type: ‘primary’ | ‘default’; onClick: () => void })、类的接口(如 interface IUser { getName(): string })、需继承或合并的类型;
- 用 type:定义基本类型别名(如 type ID = string | number)、联合类型(如 type Status = ‘success’ | ‘error’ | ‘pending’)、交叉类型(如 type MergedType = TypeA & TypeB)、函数类型(如 type Fn = (a: number) => string)。
-
针对性追问:若 interface 和 type 都能定义对象类型,在组件 props 定义中优先选哪个?为什么?
- 追问参考答案:优先选 interface。原因:
- 支持继承:若多个组件 props 有共性(如 disabled: boolean),可定义 interface BaseProps { disabled: boolean },其他 props 接口通过 extends 继承(如 interface ButtonProps extends BaseProps { type: string }),减少重复代码;
- 自动合并:若需扩展第三方组件 props(如 Ant Design 的 ButtonProps),可通过同名 interface 合并(如 interface ButtonProps { customProp: string }),无需修改原类型定义;
- 更符合 React 生态习惯:社区多数组件库(如 Ant Design)的 props 定义使用 interface,保持一致性便于协作。
- 追问参考答案:优先选 interface。原因:
三、组件设计与共享
(一)组件开发与设计思路
-
面试题 1:请举例说明你开发过的一个具体组件(如业务组件或通用组件),说明设计思路和 props 设计。
- 参考答案:以业务组件 ChatMessageItem(AI 对话项目中的消息项组件)为例:
- 组件功能:展示用户/AI 的对话消息,支持复制、删除、编辑消息,且支持渲染 Markdown 格式的 AI 回复(如代码块、列表)。
- 设计思路:
- 拆分职责:仅负责“消息展示与基础交互”,不处理消息列表逻辑(如新增/删除消息),通过 props 接收外部数据和回调;
- 可复用性:提取通用交互(复制、编辑)为独立方法,支持通过 props 控制功能开关(如某些场景不允许删除);
- 兼容性:处理 Markdown 渲染(引入 react-markdown 库),对 AI 回复中的代码块添加语法高亮(配合 react-syntax-highlighter)。
- props 设计(结合 TS 接口):
import React from 'react'; import ReactMarkdown from 'react-markdown'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { dracula } from 'react-syntax-highlighter/dist/esm/styles/prism'; // props 接口定义 interface ChatMessageItemProps { // 消息类型:用户/AI type: 'user' | 'ai'; // 消息内容 content: string; // 消息 ID(用于删除/编辑的唯一标识) id: string; // 复制回调(参数为消息内容) onCopy: (content: string) => void; // 删除回调(参数为消息 ID) onDelete: (id: string) => void; // 编辑回调(参数为消息 ID 和新内容) onEdit?: (id: string, newContent: string) => void; // 是否禁用编辑(可选,默认 false) disableEdit?: boolean; } const ChatMessageItem: React.FC<ChatMessageItemProps> = ({ type, content, id, onCopy, onDelete, onEdit, disableEdit = false, }) => { // 复制逻辑 const handleCopy = () => onCopy(content); // 删除逻辑 const handleDelete = () => onDelete(id); // 编辑逻辑(简化,实际需结合输入框) const handleEdit = () => { if (onEdit) { const newContent = prompt('编辑消息', content); if (newContent) onEdit(id, newContent); } }; return ( <div className={`chat-message chat-message--${type}`}> {/* 消息头像 */} <div className="chat-message__avatar"> {type === 'user' ? '用户' : 'AI'} </div> {/* 消息内容(AI 消息渲染 Markdown) */} <div className="chat-message__content"> {type === 'ai' ? ( <ReactMarkdown components={{ code({ node, inline, className, children, ...props }) { const match = /language-(\w+)/.exec(className || ''); return !inline && match ? ( <SyntaxHighlighter style={dracula} language={match[1]} PreTag="div" {...props} > {String(children).replace(/\n$/, '')} </SyntaxHighlighter> ) : ( <code className={className} {...props}> {children} </code> ); }, }} > {content} </ReactMarkdown> ) : ( <span>{content}</span> )} </div> {/* 操作按钮 */} <div className="chat-message__actions"> <button onClick={handleCopy}>复制</button> <button onClick={handleDelete}>删除</button> {!disableEdit && onEdit && ( <button onClick={handleEdit}>编辑</button> )} </div> </div> ); }; export default ChatMessageItem;- 针对性追问:若该组件需支持“消息加载中”状态(如 AI 回复生成中),如何扩展设计?
- 追问参考答案:通过添加 loading props 扩展:
4. 在 ChatMessageItemProps 中添加 loading?: boolean(默认 false);
5. 组件内部判断 loading:若为 true,渲染“加载中”动画(如骨架屏或 spinner),隐藏内容和操作按钮;
6. 示例代码:
- 追问参考答案:通过添加 loading props 扩展:
// props 新增 interface ChatMessageItemProps { // ... 原有 props loading?: boolean; // AI 回复加载中状态 } // 组件内部渲染逻辑修改 return ( <div className={`chat-message chat-message--${type}`}> {/* 头像不变 */} {type === 'user' ? '用户' : 'AI'} {/* 加载中状态 */} {loading ? ( {/* 加载动画 */} ) : ( // 原有内容和操作按钮 <> {/* ... 原有内容 */} {/* ... 原有按钮 */} </> )} ); - 参考答案:以业务组件 ChatMessageItem(AI 对话项目中的消息项组件)为例:
-
面试题 2:组件中能否使用 useContext 中的业务数据?为什么?在什么场景下使用?
- 参考答案:能使用。useContext 的核心作用是“跨组件传递数据”,业务数据(如用户信息、全局主题、权限状态)可通过 useContext 注入组件,无需通过 props 层层传递。
- 使用场景:
- 全局数据共享:如用户登录状态(userInfo)、系统主题(theme: ‘light’ | ‘dark’),多个组件(如 Header、Sidebar、设置页)需使用;
- 避免 props 透传:如“用户信息”需从根组件传递到深层子组件(如 App -> Layout -> Sidebar -> UserAvatar),用 useContext 可直接在 UserAvatar 中读取,简化层级传递。
- 注意事项:
- 避免滥用:若数据仅在父子组件间传递,优先用 props(更清晰);仅当数据需跨多层级或多个组件共享时,才用 useContext;
- 性能优化:useContext 会导致依赖它的组件在上下文数据变化时重渲染,若上下文数据频繁变化(如实时刷新的计数器),需配合 useMemo 缓存上下文值,或拆分上下文为多个细粒度上下文(如“用户信息上下文”和“主题上下文”分离)。
- 针对性追问:若 useContext 传递的业务数据需修改(如用户修改昵称),如何设计修改逻辑?
- 追问参考答案:将“数据”和“修改数据的方法”一起放入上下文,组件通过调用方法修改数据:
- 创建上下文时包含数据和方法:
- 追问参考答案:将“数据”和“修改数据的方法”一起放入上下文,组件通过调用方法修改数据:
const UserContext = createContext<{ userInfo: UserInfo | null; setUserInfo: (info: UserInfo | null) => void; }>({ userInfo: null, setUserInfo: () => {} });2. Provider 组件中管理状态和方法:const UserProvider = ({ children }) => { const [userInfo, setUserInfo] = useState<UserInfo | null>(null); return ( <UserContext.Provider value={{ userInfo, setUserInfo }}> {children} </UserContext.Provider> ); };3. 子组件中修改数据:const UserProfile = () => { const { userInfo, setUserInfo } = useContext(UserContext); const handleUpdateNickname = (newNickname: string) => { if (userInfo) { setUserInfo({ ...userInfo, nickname: newNickname }); } }; return <button onClick={() => handleUpdateNickname('新昵称')}>修改昵称</button>; };
(二)公共组件共享策略
-
面试题:若公共组件需在 3 个产品线(项目)中共享,有哪些好的共享策略?分别说明优缺点。
-
参考答案:
| 共享策略 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 私有 npm 包 | 将公共组件封装为 npm 包(如私有仓库 @company/common-components),各项目通过 npm install 引入 | 版本管理清晰,支持按需引入,更新方便 | 需维护 npm 包(发布、版本控制),调试需 link 本地包 | 组件稳定、迭代频率低,多个项目复用 |
| Git Submodule | 在各项目中通过 Git Submodule 引入公共组件的 Git 仓库 | 无需维护 npm 包,组件修改可实时同步到各项目 | 子模块依赖管理复杂,各项目需手动更新子模块,易出现版本冲突 | 组件迭代频繁,需快速同步修改 |
| 微前端模块联邦 | 通过 Webpack Module Federation,将公共组件作为“远程模块”,各项目动态加载 | 支持跨项目动态共享,无需本地安装,组件更新无需重启项目 | 依赖微前端架构,需配置 Module Federation,兼容性依赖构建工具 | 大型前端架构,支持跨技术栈共享 |
| 本地组件库(Monorepo) | 使用 Monorepo 管理(如 Lerna、PNPM Workspace),公共组件作为独立包 | 组件与项目在同一仓库,调试方便,依赖管理统一 | 仓库体积较大,需熟悉 Monorepo 工具,权限管理需控制 | 同一团队维护多个项目,组件与项目关联紧密 |
- 针对性追问:若公共组件需支持“按需引入”和“主题定制”,私有 npm 包方案如何实现?
- 追问参考答案:
- 按需引入:
- 组件库采用“Tree Shaking 友好”的目录结构(如每个组件单独一个目录,导出独立入口):
- 按需引入:
src/ components/ Button/ index.tsx(导出 Button 组件) style.ts(组件样式) Table/ index.tsx style.ts index.ts(导出所有组件,供全量引入)2. 配置 package.json 的 module 字段指向 ES 模块入口(如 dist/esm/index.js),确保 Webpack/Rollup 可 Tree Shaking; 3. 各项目使用时按需引入:import { Button } from '@company/common-components';(而非全量引入 import * as Common from '@company/common-components')。- 主题定制:
4. 组件样式使用 CSS 变量(如 --primary-color: #1890ff)定义主题色、字体大小等;
5. 组件库导出“主题配置文件”(如 src/theme/default.ts),包含默认主题变量;
6. 各项目可通过覆盖 CSS 变量定制主题(如在项目入口 CSS 中:body { --primary-color: #00b42a; }),或通过组件库提供的 ThemeProvider 组件注入自定义主题(如基于 useContext 实现)。
- 追问参考答案:
四、微前端相关
(一)基础概念与实践
-
面试题 1:是否听说过模块联邦(Module Federation)?微前端有哪些常见方案?项目中是否用过微前端?
- 参考答案:
- 模块联邦(Module Federation):是 Webpack 5 推出的跨应用模块共享方案,核心思想是“将应用拆分为‘宿主应用’和‘远程应用’,宿主应用可动态加载远程应用的模块(如组件、函数),远程应用也可加载宿主应用的模块”,无需构建时合并代码,支持跨技术栈(如 React 项目加载 Vue 组件)。
- 微前端常见方案:
- 基于路由分发(如 qiankun):将不同产品线作为独立子应用,通过路由匹配(如 /app1/* 对应子应用 1,/app2/* 对应子应用 2),主应用负责子应用的加载、卸载和通信,优点是接入简单,缺点是子应用切换需刷新页面;
- 模块联邦(Webpack Module Federation):动态加载子应用模块,支持子应用嵌入主应用任意位置(无需路由隔离),优点是灵活性高,缺点是配置复杂,依赖 Webpack 5;
- 基于 iframe:主应用通过 iframe 嵌入子应用,优点是完全隔离(样式、JS 环境),缺点是通信复杂(需 postMessage),性能较差(页面加载多份资源),仅适用于简单场景。
- 项目实践:在中后台项目中使用过 qiankun 微前端,主应用为 React 技术栈,子应用包含 React(旧项目)和 Vue(新业务),实现了子应用路由隔离、主应用向子应用传值、子应用间通信等功能。
- 针对性追问:qiankun 微前端中,主应用如何实现子应用的“样式隔离”?
- 追问参考答案:qiankun 提供两种样式隔离方案,可在注册子应用时配置:
4. sandbox: { strictStyleIsolation: true }(严格样式隔离):通过 Shadow DOM 实现,子应用的样式被包裹在 Shadow DOM 中,仅作用于子应用内部,不影响主应用和其他子应用,优点是隔离彻底,缺点是部分 CSS 特性(如 :root、@keyframes)在 Shadow DOM 中可能失效;
5. sandbox: { experimentalStyleIsolation: true }(实验性样式隔离):通过 CSS 选择器前缀自动添加子应用唯一标识(如 [data-qiankun-subapp-name=“app1”]),子应用样式被改写为带前缀的选择器,优点是兼容性好,缺点是隔离不彻底(若子应用使用全局样式如 body { margin: 0 },仍可能影响主应用)。- 补充:实际项目中常用第二种方案,配合子应用内部使用 CSS Modules 或 BEM 命名规范,进一步减少样式冲突。
- 追问参考答案:qiankun 提供两种样式隔离方案,可在注册子应用时配置:
- 参考答案:
-
面试题 2:微前端中主应用如何给子应用传值?子应用之间如何进行数据交互?
- 参考答案:
- 主应用给子应用传值(以 qiankun 为例):
- 注册子应用时传递参数:通过 props 字段传递静态/动态数据,子应用在 mount 生命周期中接收:
- 主应用给子应用传值(以 qiankun 为例):
// 主应用注册子应用 registerMicroApps([ { name: 'app1', entry: '//localhost:8081', container: '#appContainer', activeRule: '/app1', props: { userInfo: { name: '张三', role: 'admin' }, // 静态数据 getGlobalConfig: () => fetch('/api/global-config') // 动态方法 } } ]); // 子应用(React)接收参数:在 src/micro-app-interface.ts 中 export function mount(props) { console.log('主应用传递的 userInfo', props.userInfo); props.getGlobalConfig().then(config => console.log('全局配置', config)); ReactDOM.render(<App {...props} />, document.getElementById('root')); }2. 全局状态管理:主应用通过 initGlobalState 创建全局状态,子应用通过 onGlobalStateChange 监听、setGlobalState 修改:// 主应用初始化全局状态 const { setGlobalState } = initGlobalState({ theme: 'light' }); // 主应用修改状态 setGlobalState({ theme: 'dark' }); // 子应用监听并修改状态 export function mount(props) { // 监听状态变化 props.onGlobalStateChange((state, prevState) => { console.log('主题变化', state.theme); }, true); // 修改状态 props.setGlobalState({ theme: 'light' }); }- 子应用之间数据交互:
3. 间接交互(通过主应用中转):子应用 A 调用 setGlobalState 修改全局状态,子应用 B 通过 onGlobalStateChange 监听状态变化,实现数据传递;
4. 直接交互(postMessage):若子应用通过 iframe 嵌入,可通过 window.parent.postMessage 或 iframe.contentWindow.postMessage 传递数据,需处理跨域问题(通过 targetOrigin 限制);
5. 模块联邦直接引用:若使用 Module Federation,子应用 A 可将数据暴露为远程模块,子应用 B 直接加载 A 的模块获取数据(需配置远程模块地址)。 - 针对性追问:若主应用传递的 userInfo 发生变化(如用户切换账号),如何让子应用实时更新?
- 追问参考答案:通过“全局状态+监听”实现:
6. 主应用将 userInfo 放入 qiankun 全局状态(而非仅在注册时传递 props):
- 追问参考答案:通过“全局状态+监听”实现:
// 主应用初始化全局状态 const { setGlobalState } = initGlobalState({ userInfo: null }); // 用户切换账号时更新全局状态 const handleUserChange = (newUserInfo) => { setGlobalState({ userInfo: newUserInfo }); };7. 子应用在 mount 时监听全局状态中的 userInfo 变化:export function mount(props) { // 监听 userInfo 变化,实时更新子应用 props.onGlobalStateChange((state) => { if (state.userInfo) { // 子应用更新用户信息(如重新请求权限接口、更新页面渲染) updateUserInfo(state.userInfo); } }, true); // 第二个参数为 true,初始触发一次 }8. 若子应用已挂载,主应用更新 userInfo 时,子应用会通过监听函数实时接收新值,避免子应用重启。 - 参考答案:
五、适配相关(移动端、大屏)
(一)移动端适配
-
面试题:移动端如何做屏幕自适应?请说明至少 2 种常用方案,包括实现细节和优缺点。
-
参考答案:
| 适配方案 | 实现细节 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| REM 适配 | 1. 动态设置 html 标签的 font-size(如设计稿宽度 375px,设置 html { font-size: 100px },则 1rem = 100px,设计稿中 100px 对应 1rem); 2. 通过 JS 计算 font-size:document.documentElement.style.fontSize = (window.innerWidth / 375) * 100 + ‘px’; 3. 组件样式使用 rem 单位 | 适配精度高,支持任意屏幕宽度,样式书写简单 | 需动态计算 font-size(依赖 JS),部分第三方组件可能不支持 rem | 设计稿基于固定宽度(如 375px),需全屏幕自适应的页面 |
| VW/VH 适配 | 1. 基于视口宽度(VW)和高度(VH):1vw = 视口宽度的 1%,1vh = 视口高度的 1%; 2. 设计稿宽度 375px,某元素宽度 75px,对应 VW 为 (75 / 375) * 100 = 20vw; 3. 通过 PostCSS 插件自动将 px 转换为 VW | 无需 JS 依赖,纯 CSS 实现,适配逻辑简单 | VH 适配易受导航栏/状态栏影响,部分低版本浏览器对 VW 支持不佳 | 简单页面,无需兼容低版本浏览器 |
| Flex + 媒体查询 | 1. 使用 Flex 布局实现弹性布局(如 display: flex; flex-wrap: wrap); 2. 通过媒体查询(@media)针对特定屏幕宽度调整样式(如 @media (max-width: 320px) { .btn { font-size: 12px; } }) | 兼容性好,布局灵活,无需依赖 JS 或特殊单位 | 需编写多个媒体查询规则,适配多屏幕时代码量大 | 组件级适配,或需兼容低版本浏览器的项目 |
- 针对性追问:若设计稿同时包含移动端和 Pad 端(如 768px 宽度),REM 适配方案如何扩展?
- 追问参考答案:通过媒体查询限制 html 的 font-size 最大值,避免 Pad 端元素过大:
// JS 动态计算(基础逻辑不变) function setRem() { const designWidth = 375; // 移动端设计稿宽度 const maxWidth = 768; // Pad 端最大宽度(超过后按 768px 计算) const screenWidth = Math.min(window.innerWidth, maxWidth); const fontSize = (screenWidth / designWidth) * 100; // 1rem = 100px(设计稿) document.documentElement.style.fontSize = fontSize + 'px'; } window.addEventListener('resize', setRem); setRem(); // 配合媒体查询调整 Pad 端特殊样式 @media (min-width: 768px) { .container { max-width: 720px; // 限制容器最大宽度,避免内容过宽 margin: 0 auto; // 居中显示 } .card { width: 33.33%; // Pad 端一行显示 3 个卡片(移动端一行 2 个) } }
(二)大屏可视化适配
-
面试题:大屏可视化项目(如数据看板)如何做适配?需考虑哪些问题?
-
参考答案:
- 核心适配思路:大屏可视化需适配不同尺寸的显示器(如 1920×1080、2560×1440、3840×2160),核心是“保持设计稿比例,避免拉伸变形”,常用方案为“Scale 缩放适配”:
i. 固定设计稿比例(如 16:9,常见大屏比例);
ii. 计算当前屏幕与设计稿的缩放比例(scaleX = 屏幕宽度 / 设计稿宽度; scaleY = 屏幕高度 / 设计稿高度);
iii. 取 scale = Math.min(scaleX, scaleY)(保证内容完全显示,不超出屏幕),通过 CSS transform: scale(scale) 缩放大屏容器;
iv. 示例代码:
import React, { useEffect, useRef } from 'react'; const BigScreen = () => { const screenRef = useRef(null); // 设计稿尺寸(16:9) const designWidth = 1920; const designHeight = 1080; const resizeScreen = () => { if (!screenRef.current) return; const screen = screenRef.current; // 当前屏幕尺寸 const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; // 计算缩放比例(取最小值,避免超出屏幕) const scaleX = windowWidth / designWidth; const scaleY = windowHeight / designHeight; const scale = Math.min(scaleX, scaleY); // 应用缩放(居中显示) screen.style.transform = `scale(${scale})`; screen.style.transformOrigin = 'left top'; // 缩放原点(避免偏移) // 设置容器尺寸(设计稿尺寸) screen.style.width = `${designWidth}px`; screen.style.height = `${designHeight}px`; // 居中定位(若缩放后有空白) screen.style.marginLeft = `${(windowWidth - designWidth * scale) / 2}px`; screen.style.marginTop = `${(windowHeight - designHeight * scale) / 2}px`; }; useEffect(() => { resizeScreen(); window.addEventListener('resize', resizeScreen); return () => window.removeEventListener('resize', resizeScreen); }, []); return ( <div style={{ width: '100vw', height: '100vh', overflow: 'hidden' }}> <div ref={screenRef}> {/* 大屏内容(如 ECharts 图表、数据卡片,样式按设计稿 1920×1080 编写) */} ... </div> </div> ); }; export default BigScreen;- 需考虑的问题:
v. 字体适配:避免缩放后字体模糊,优先使用矢量字体(如 SVG 图标、iconfont),或通过媒体查询调整字体大小(如大屏宽度 > 2560px 时增大字体);
vi. 图表适配:ECharts 图表需设置 responsive: true,并在缩放后调用 chart.resize() 重绘(如在 resizeScreen 函数中触发图表重绘);
vii. 性能优化:大屏元素较多(如多图表、动画),需避免过度使用 transform 或 animation,减少重绘重排;
viii. 浏览器兼容性:大屏通常在 Chrome 浏览器运行,需确保 CSS transform: scale 和 JS API(如 window.innerWidth)兼容。
- 核心适配思路:大屏可视化需适配不同尺寸的显示器(如 1920×1080、2560×1440、3840×2160),核心是“保持设计稿比例,避免拉伸变形”,常用方案为“Scale 缩放适配”:
-
针对性追问:若大屏需支持“全屏模式”,如何实现?缩放适配是否会受影响?
- 追问参考答案:
- 全屏模式实现:通过浏览器 Fullscreen API 实现:
const toggleFullscreen = () => { const docEl = document.documentElement; if (!document.fullscreenElement) { // 进入全屏 docEl.requestFullscreen().catch(err => console.error('无法进入全屏', err)); } else { // 退出全屏 document.exitFullscreen(); } }; // 组件中添加全屏按钮 <button onClick={toggleFullscreen}>全屏模式</button>- 缩放适配影响:进入全屏后,window.innerWidth 和 window.innerHeight 变为屏幕实际分辨率(无浏览器工具栏),需重新计算缩放比例。解决方案:在 resizeScreen 函数中,同时监听 fullscreenchange 事件,触发重新缩放:
useEffect(() => { resizeScreen(); window.addEventListener('resize', resizeScreen); document.addEventListener('fullscreenchange', resizeScreen); // 监听全屏变化 return () => { window.removeEventListener('resize', resizeScreen); document.removeEventListener('fullscreenchange', resizeScreen); }; }, []); - 追问参考答案:
六、性能优化相关
(一)通用性能优化
-
面试题:你对前端性能优化的理解是什么?在项目中如何落地这些优化措施?请举例说明。
-
参考答案:
- 性能优化核心目标:减少页面加载时间(首屏加载、白屏时间)、提升交互流畅度(减少卡顿、延迟)、降低资源消耗(CPU、内存),最终提升用户体验。
- 落地措施(按“加载阶段-运行阶段”分类):
- 一、加载阶段优化(减少资源加载时间):
- 资源压缩与合并:
- JS/CSS 压缩:使用 Webpack terser-webpack-plugin(压缩 JS)、css-minimizer-webpack-plugin(压缩 CSS),移除注释、空格和未使用代码;
- 图片压缩:使用 image-webpack-loader 压缩 PNG/JPG(如压缩率 60%~80%),小图片(< 8KB)转为 Base64 嵌入 CSS/JS(减少 HTTP 请求);
- 示例:项目中首页 banner 图从 2MB 压缩至 300KB,加载时间从 3s 缩短至 500ms。
- 静态资源 CDN 分发:
- 将静态资源(JS、CSS、图片、字体)部署到 CDN(如阿里云 CDN),利用 CDN 节点缓存,用户从最近节点获取资源,减少网络延迟;
- 配置 CDN 缓存策略(如 Cache-Control: max-age=31536000),长期缓存不变的资源(如第三方库、图标)。
- 懒加载(Lazy Loading):
- 图片懒加载:使用 loading=“lazy” 属性(原生)或 react-lazyload 库,仅当图片进入视口时加载;
- 组件懒加载:React 项目使用 React.lazy + Suspense,拆分代码包(如路由级懒加载):
- 一、加载阶段优化(减少资源加载时间):
const Home = React.lazy(() => import('./pages/Home')); const About = React.lazy(() => import('./pages/About')); function App() { return ( <Router> <Suspense fallback={<div>加载中...</div>}> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> </Routes> </Suspense> </Router> ); }- 示例:项目中路由懒加载后,首屏 JS 包体积从 1.2MB 减少至 400KB,首屏加载时间缩短 1.5s。- 二、运行阶段优化(提升交互流畅度):
4. 减少重绘重排:- 批量修改 DOM:避免频繁修改 DOM 样式(如 element.style.width = ‘100px’; element.style.height = ‘200px’),改为修改 class 或使用 DocumentFragment;
- 使用 CSS 硬件加速:对动画元素使用 transform 和 opacity(仅触发复合层,不触发重绘重排),避免使用 width/height/top 等属性;
- 示例:项目中按钮hover动画从 width: 100px → 120px 改为 transform: scale(1.2),动画帧率从 30fps 提升至 60fps。
- 优化 JS 执行效率:
- 避免长时间同步任务:将耗时操作(如大数据筛选)拆分为微任务(Promise)或使用 requestIdleCallback,避免阻塞主线程;
- 缓存计算结果:使用 useMemo/useCallback 缓存重复计算(如列表筛选)、localStorage 缓存接口数据(如用户配置);
- 示例:项目中“大数据表格筛选”(10000 条数据)通过 useMemo 缓存筛选结果,筛选操作从 500ms 缩短至 50ms。
- 接口请求优化:
- 接口合并:将多个独立接口(如“获取用户信息”“获取权限列表”)合并为一个接口,减少 HTTP 请求次数;
- 数据缓存:使用 react-query/swr 缓存接口响应,支持 stale-while-revalidate(过期数据展示,后台刷新),减少重复请求;
- 示例:项目中用户信息接口通过 react-query 缓存,页面切换后无需重新请求,交互延迟从 300ms 缩短至 50ms。
- 针对性追问:如何量化性能优化效果?使用哪些工具进行性能分析?
- 追问参考答案:
- 量化指标:
a. 加载指标:首屏加载时间(FCP,First Contentful Paint)、LCP(Largest Contentful Paint,最大内容绘制,目标 < 2.5s)、TTI(Time to Interactive,可交互时间,目标 < 3.8s);
b. 运行指标:FID(First Input Delay,首次输入延迟,目标 < 100ms)、CLS(Cumulative Layout Shift,累积布局偏移,目标 < 0.1);
c. 资源指标:JS/CSS 包体积、HTTP 请求数、图片资源大小。 - 分析工具:
d. Lighthouse:Chrome 开发者工具内置,生成性能报告(含 FCP、LCP、CLS 等指标),提供优化建议;
e. Chrome DevTools:- Network 面板:分析资源加载时间、请求数、缓存情况;
- Performance 面板:录制页面运行过程,分析 JS 执行时间、重绘重排、主线程阻塞情况;
f. Webpack Bundle Analyzer:分析 Webpack 打包后的代码包结构,定位体积过大的模块(如第三方库)。
- 量化指标:
- 追问参考答案:
(二)大图片优化
-
面试题:若首页有一张大图片(如 2MB、1920×1080px),如何让用户第一时间看见这张图片?请说明具体优化措施。
-
参考答案:
- 核心思路:通过“减少图片加载时间”“优先展示关键内容”“优化加载体验”三个方向,确保用户快速看到图片。
- 具体措施:
i. 图片压缩与格式优化:- 格式选择:优先使用 WebP 格式(比 JPG 小 25%~35%,比 PNG 小 26%),兼容低版本浏览器(如 IE)时降级为 JPG/PNG;
- 压缩工具:使用 TinyPNG、Squoosh 等工具压缩,保持视觉质量(如压缩率 70%~80%),2MB 图片可压缩至 500KB 以内;
- 示例:1920×1080px 的 JPG 图片(2MB)转为 WebP 后体积约 600KB,加载时间从 3s(3G 网络)缩短至 1s。
ii. 渐进式加载(Progressive Loading): - 先加载“缩略图/模糊图”:生成低分辨率缩略图(如 100×56px,体积 < 10KB),优先加载并展示,再异步加载原图;
- 模糊效果:使用“模糊占位符”(如通过 react-blurhash 库生成模糊哈希值,体积极小),加载原图后替换模糊图;
- 示例代码(React):
import React, { useState } from 'react'; import { decode } from 'blurhash'; const LargeImage = () => { const [isLoaded, setIsLoaded] = useState(false); // 模糊哈希值(对应原图的低分辨率模糊图) const blurHash = 'L5H2EC=PM+yV0g-mq.wG9c010J}I'; return ( <div style={{ position: 'relative', width: '100%', height: 'auto' }}> {/* 模糊占位符(优先加载) */} <canvas style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', display: isLoaded ? 'none' : 'block', }} ref={(canvas) => { if (canvas) { const ctx = canvas.getContext('2d'); const pixels = decode(blurHash, 32, 32); const imageData = ctx.createImageData(32, 32); imageData.data.set(pixels); ctx.putImageData(imageData, 0, 0); // 拉伸画布至容器大小 canvas.style.width = '100%'; canvas.style.height = '100%'; } }} /> {/* 原图(异步加载) */} <img src="banner.webp" alt="首页 banner" style={{ width: '100%', height: 'auto', opacity: isLoaded ? 1 : 0, transition: 'opacity 0.3s ease', }} onLoad={() => setIsLoaded(true)} loading="lazy" // 仅当图片进入视口时加载(若图片在首屏,可移除该属性) /> </div> ); }; export default LargeImage;iii. 预加载与缓存:
- 预加载:若图片在首屏,使用<link rel="preload" as="image" href="banner.webp" imagesrcset="banner.webp" imagesizes="100vw">,告诉浏览器优先加载该图片;
- 缓存策略:配置 CDN 或服务器的 Cache-Control 头(如 max-age=31536000),图片加载一次后长期缓存,后续访问无需重新下载。
iv. 响应式图片:
- 根据屏幕宽度加载不同分辨率的图片:使用 srcset 和 sizes 属性,移动端加载小分辨率图片(如 750×422px),PC 端加载大分辨率图片(如 1920×1080px);
- 示例代码:<img src="banner-750.webp" // 默认加载移动端图片 srcset=" banner-750.webp 750w, // 750px 宽度图片 banner-1920.webp 1920w // 1920px 宽度图片 " sizes="100vw" // 图片占满视口宽度 alt="首页 banner" onLoad={() => setIsLoaded(true)} />- 针对性追问:若用户网络为 2G(网速极慢),如何优化大图片的加载体验?
- 追问参考答案:
- 降级为低质量图片:检测网络类型(通过 navigator.connection.effectiveType),若为 2G,加载超低分辨率图片(如 300×168px,体积 < 50KB),并提供“加载高清图片”按钮,用户可手动触发加载;
- 文本占位:在图片加载完成前,展示图片相关文本(如 banner 文案“限时优惠活动”),避免用户长时间看到空白;
- 延迟加载非首屏图片:若大图片不在首屏,2G 网络下完全禁用加载,仅当用户滚动到图片位置且网络改善后,再触发加载。
- 追问参考答案:
七、JavaScript 核心概念
(一)函数柯里化与伪调用
-
面试题 1:什么是 JavaScript 函数柯里化(Currying)?请举例说明其用途和实现方式。
- 参考答案:
- 定义:柯里化是将“接收多个参数的函数”转换为“接收单个参数(第一个参数),并返回接收剩余参数的新函数”的技术,最终通过多次调用累积参数,直到参数满足函数执行条件。
- 核心目的:复用参数逻辑、延迟执行、支持部分应用(Partial Application)。
- 示例(用途):
- 复用参数:封装“计算税费”函数,固定税率后复用(如 13% 税率的税费计算);
// 普通函数:计算税费(金额 * 税率) function calculateTax(amount, rate) { return amount * rate; } // 柯里化后:先固定税率,返回计算金额的函数 function curryCalculateTax(rate) { return function(amount) { return amount * rate; }; } // 复用 13% 税率的税费计算 const calculateTax13 = curryCalculateTax(0.13); calculateTax13(100); // 13(100 * 0.13) calculateTax13(200); // 26(200 * 0.13) // 复用 9% 税率的税费计算 const calculateTax9 = curryCalculateTax(0.09); calculateTax9(100); // 92. 延迟执行:分步接收参数,直到参数足够后执行(如表单验证,先接收验证规则,再接收表单值)。- 通用柯里化函数实现(支持任意参数数量):
function curry(fn) { // 获取函数需要的参数总数 const arity = fn.length; // 递归收集参数 return function curried(...args) { // 若收集的参数数量 >= 函数所需参数,执行函数 if (args.length >= arity) { return fn.apply(this, args); } // 否则返回新函数,继续收集剩余参数 return function(...nextArgs) { return curried.apply(this, args.concat(nextArgs)); }; }; } // 使用示例:柯里化 add 函数(接收 3 个参数) const add = (a, b, c) => a + b + c; const curriedAdd = curry(add); curriedAdd(1)(2)(3); // 6(分步传递参数) curriedAdd(1, 2)(3); // 6(先传递 2 个参数,再传递 1 个) curriedAdd(1, 2, 3); // 6(直接传递所有参数)- 针对性追问:柯里化与部分应用(Partial Application)有什么区别?
- 追问参考答案:
- 柯里化:将多参数函数转换为“单参数函数链”,每次调用仅接收一个参数(或部分参数),必须通过多次调用累积所有参数后才执行(如 add(1)(2)(3));
- 部分应用:固定函数的“部分参数”,返回接收剩余参数的函数,无需严格按“单参数”调用,可一次性传递多个剩余参数(如 partialAdd = add.bind(null, 1, 2); partialAdd(3));
- 示例区别:
- 追问参考答案:
// 柯里化:每次调用接收 1 个参数 curriedAdd(1)(2)(3); // 6 // 部分应用:固定前 2 个参数,剩余参数一次性传递 const partialAdd = (a, b) => (c) => a + b + c; partialAdd(1, 2)(3); // 6 - 参考答案:
-
面试题 2:什么是 JavaScript 函数伪调用?请举例说明。
- 参考答案:
- 定义:函数伪调用(Pseudo-invocation)指“不通过函数本身的 call/apply/bind 方法,也不通过对象原型链,而是通过其他方式间接调用函数,且可能改变函数 this 指向”的调用方式,常见场景为“通过 eval 或函数表达式间接调用”。
- 核心特点:this 指向可能不符合常规(如非严格模式下指向 window,严格模式下指向 undefined),且代码可读性差,不推荐使用。
- 示例(常见伪调用场景):
- 通过 eval 调用函数:
function foo() { console.log(this); // 非严格模式下指向 window,严格模式下指向 undefined console.log(arguments); } // 伪调用:通过 eval 执行函数字符串 const fnName = 'foo'; eval(`${fnName}('a', 'b')`); // 等同于 foo('a', 'b'),this 指向 window(非严格模式)2. 通过函数表达式间接调用:const obj = { name: 'obj', foo: function() { console.log(this.name); } }; // 伪调用:将函数赋值给变量,丢失 this 指向 const bar = obj.foo; bar(); // 非严格模式下 this 指向 window(name 为 undefined),严格模式下 this 指向 undefined3. 通过定时器/事件回调伪调用:const obj = { name: 'obj', foo: function() { console.log(this.name); } }; // 伪调用:定时器回调中调用,this 指向 window setTimeout(obj.foo, 1000); // undefined(非严格模式)- 注意事项:
- 伪调用的核心问题是 this 指向异常,易导致 bugs;
- 避免方式:若需保留 this 指向,使用 bind(如 setTimeout(obj.foo.bind(obj), 1000))或箭头函数(setTimeout(() => obj.foo(), 1000))。
- 针对性追问:如何区分“函数伪调用”和“正常调用”?正常调用的 this 指向规则是什么?
- 追问参考答案:
- 区分标准:是否通过“函数直接调用(fn())、对象调用(obj.fn())、call/apply/bind 调用”之外的方式调用,且 this 指向不符合预期(如非严格模式下指向 window);
- 正常调用的 this 指向规则:
a. 普通函数调用(fn()):非严格模式下 this 指向 window,严格模式下指向 undefined;
b. 对象方法调用(obj.fn()):this 指向调用函数的对象(obj);
c. 构造函数调用(new Fn()):this 指向新创建的实例对象;
d. call/apply/bind 调用(fn.call(obj)):this 指向第一个参数(obj,若为 null/undefined 则指向 window);
e. 箭头函数调用:this 指向定义时的外部上下文(不绑定自身 this)。
- 追问参考答案:
- 参考答案:
(二)节流与防抖
-
面试题:什么是 JavaScript 节流(Throttle)和防抖(Debounce)?两者有什么区别?请举例说明使用场景和实现方式。
-
参考答案:
- 核心定义:两者均为“控制函数执行频率”的技术,解决高频事件(如滚动、输入、点击)导致的性能问题,但执行逻辑不同。
- 区别对比:
对比维度 节流(Throttle) 防抖(Debounce) 核心逻辑 规定“单位时间内仅执行一次函数”,无论事件触发多少次 规定“事件触发后延迟 N 秒执行函数,若 N 秒内再次触发则重置延迟时间”,仅最后一次触发后执行 执行时机 单位时间内首次触发或末次触发(可配置) 事件停止触发后延迟 N 秒执行 适用场景 高频连续事件(如滚动加载、窗口 resize、鼠标移动) 高频触发后需最终结果的事件(如输入搜索、按钮防重复点击) 示例效果 滚动页面时,每 100ms 执行一次加载函数 输入框输入时,停止输入 500ms 后才发送搜索请求 - 实现方式(原生 JS):
- 节流(时间戳 + 定时器实现,支持“首次触发”和“末次触发”):
function throttle(fn, delay) { let lastTime = 0; // 上次执行时间 let timer = null; // 定时器 return function(...args) { const now = Date.now(); // 若距离上次执行时间超过 delay,立即执行(首次触发) if (now - lastTime >= delay) { clearTimeout(timer); fn.apply(this, args); lastTime = now; } else if (!timer) { // 否则设置定时器,确保末次触发后执行(可选,根据需求决定是否保留) timer = setTimeout(() => { fn.apply(this, args); lastTime = Date.now(); timer = null; }, delay - (now - lastTime)); } }; } // 使用示例:滚动页面时每 200ms 执行一次 window.addEventListener('scroll', throttle(() => { console.log('滚动加载:', window.scrollY); }, 200));- 防抖(定时器实现,支持“立即执行”配置):
function debounce(fn, delay, immediate = false) { let timer = null; return function(...args) { // 清除之前的定时器,重置延迟时间 clearTimeout(timer); // 立即执行(首次触发时执行,后续触发需等待延迟) if (immediate && !timer) { fn.apply(this, args); } // 设置新定时器,延迟执行 timer = setTimeout(() => { if (!immediate) { fn.apply(this, args); } timer = null; // 执行后清空定时器 }, delay); }; } // 使用示例 1:输入搜索(停止输入 500ms 后发送请求,非立即执行) const input = document.querySelector('input'); input.addEventListener('input', debounce((e) => { console.log('搜索请求:', e.target.value); // fetch(`/api/search?keyword=${e.target.value}`); }, 500)); // 使用示例 2:按钮防重复点击(点击立即执行,300ms 内不可重复点击) const btn = document.querySelector('button'); btn.addEventListener('click', debounce(() => { console.log('按钮点击执行'); }, 300, true));- 框架中使用(React):
- 在 React 组件中使用时,需注意“函数重新创建导致节流/防抖失效”,需配合 useCallback 缓存函数:
import React, { useCallback } from 'react'; import { throttle } from './utils'; const ScrollComponent = () => { // 用 useCallback 缓存节流函数,避免组件重渲染时重新创建 const handleScroll = useCallback( throttle(() => { console.log('滚动位置:', window.scrollY); }, 200), [] // 依赖项为空,节流函数仅创建一次 ); useEffect(() => { window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, [handleScroll]); return <div>滚动页面查看控制台</div>; }; export default ScrollComponent;- 针对性追问:若节流函数需支持“取消”功能(如组件卸载前取消未执行的节流函数),如何实现?
- 追问参考答案:在节流函数中暴露 cancel 方法,用于清除定时器和重置状态:
function throttle(fn, delay) { let lastTime = 0; let timer = null; const throttled = function(...args) { const now = Date.now(); if (now - lastTime >= delay) { clearTimeout(timer); fn.apply(this, args); lastTime = now; } else if (!timer) { timer = setTimeout(() => { fn.apply(this, args); lastTime = Date.now(); timer = null; }, delay - (now - lastTime)); } }; // 暴露取消方法 throttled.cancel = function() { clearTimeout(timer); timer = null; lastTime = 0; }; return throttled; } // 使用示例:组件卸载时取消 useEffect(() => { const handleScroll = throttle(() => { /* ... */ }, 200); window.addEventListener('scroll', handleScroll); return () => { handleScroll.cancel(); // 取消节流函数 window.removeEventListener('scroll', handleScroll); }; }, []);
更多推荐
所有评论(0)