react+TS前端面试题集锦(含参考答案与追问)

一、React 核心与 Hooks 相关

(一)基础概念与版本、UI 库

  1. 面试题:React 常用版本有哪些?你熟悉的 React 生态 UI 库有哪些?

  2. 参考答案

    • 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 库使用)。
  3. 针对性追问:在使用 Ant Design 时,是否做过组件二次封装?比如对 Table 或 Form 组件的封装思路是什么?

    • 追问参考答案:做过二次封装。以 Table 组件为例,针对多个页面需重复配置“分页、加载状态、空数据提示、固定列”等共性需求,封装为 BaseTable 组件:
      • 接收 columns(列配置)、dataSource(数据源)、loading(加载状态)等 props,内部默认实现分页逻辑(如 pagination={{ pageSize: 10, showSizeChanger: true }});
      • 统一空数据提示(通过 locale 属性自定义“暂无数据”文案,避免各页面重复写 emptyText);
      • 暴露 onChange 事件,支持页面自定义表格交互(如排序、筛选后的回调),既减少重复代码,又保留灵活性。

(二)Hooks 核心问题

  1. 面试题 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)。
    • 针对性追问:useMemo 和 useCallback 的依赖项为空数组时,是否永远不会更新?为什么?
      • 追问参考答案:不是。若依赖项为空数组,useMemo 缓存的计算结果、useCallback 缓存的函数引用仅在组件初次渲染时生成,后续组件重渲染时不会更新;但如果组件被卸载后重新挂载,会再次生成新的结果/函数引用。例如:父组件传递空依赖的 useCallback 函数给子组件,若父组件卸载后重新渲染,子组件接收的函数会是新的引用,可能触发子组件重渲染(需配合 React.memo 且子组件无其他 props 变化时才不会重渲染)。
  2. 面试题 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 共享用户信息:
        1. 创建上下文:const UserContext = createContext(null);
        2. 编写 Provider 组件:const UserProvider = ({ children }) => { const [userInfo, setUserInfo] = useState(null); return <UserContext.Provider value={{ userInfo, setUserInfo }}>{children}</UserContext.Provider> };
        3. 自定义共享 Hooks:const useUser = () => { const context = useContext(UserContext); if (!context) throw new Error(‘useUser must be used within UserProvider’); return context };
        4. 使用:在根组件包裹 UserProvider,兄弟组件 A 和 B 调用 useUser(),即可共享 userInfo 和 setUserInfo,修改后两者都会同步更新。
  3. 面试题 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 依赖过多变量。

二、TypeScript 相关

(一)TS 项目实践与泛型

  1. 面试题 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. 面试题 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 区别

  1. 面试题:TypeScript 中 type 和 interface 有什么区别?分别在什么场景下使用?

  2. 参考答案

对比维度 interface type(类型别名)
扩展方式 支持 extends 继承,或多个同名 interface 自动合并 支持 &(交叉类型)扩展,不支持同名合并
适用场景 主要用于定义对象/类的结构(如接口、组件 props) 可定义任意类型(对象、基本类型、联合类型等)
类型运算 不支持联合、交叉等复杂类型运算 支持联合、交叉等复杂类型运算
声明合并 支持(同名 interface 会合并属性) 不支持(同名 type 会编译报错)
与类结合 支持 class implements interface 支持 class implements type,但若 type 包含联合/交叉类型则不支持
  1. 使用场景建议

    • 用 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)。
  2. 针对性追问:若 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,保持一致性便于协作。

三、组件设计与共享

(一)组件开发与设计思路

  1. 面试题 1:请举例说明你开发过的一个具体组件(如业务组件或通用组件),说明设计思路和 props 设计。

    • 参考答案:以业务组件 ChatMessageItem(AI 对话项目中的消息项组件)为例:
      • 组件功能:展示用户/AI 的对话消息,支持复制、删除、编辑消息,且支持渲染 Markdown 格式的 AI 回复(如代码块、列表)。
      • 设计思路:
        1. 拆分职责:仅负责“消息展示与基础交互”,不处理消息列表逻辑(如新增/删除消息),通过 props 接收外部数据和回调;
        2. 可复用性:提取通用交互(复制、编辑)为独立方法,支持通过 props 控制功能开关(如某些场景不允许删除);
        3. 兼容性:处理 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. 示例代码:
    // props 新增
    interface ChatMessageItemProps {
    // ... 原有 props
    loading?: boolean; // AI 回复加载中状态
    }
    // 组件内部渲染逻辑修改
    return (
    <div className={`chat-message chat-message--${type}`}>
    {/* 头像不变 */}
    {type === 'user' ? '用户' : 'AI'} 
    {/* 加载中状态 */}
    {loading ? (
    
      {/* 加载动画 */}
     
    ) : (
    // 原有内容和操作按钮
    <>
    {/* ... 原有内容 */} 
    {/* ... 原有按钮 */} 
    </>
    )}
    
    );
    
  2. 面试题 2:组件中能否使用 useContext 中的业务数据?为什么?在什么场景下使用?

    • 参考答案:能使用。useContext 的核心作用是“跨组件传递数据”,业务数据(如用户信息、全局主题、权限状态)可通过 useContext 注入组件,无需通过 props 层层传递。
    • 使用场景:
      • 全局数据共享:如用户登录状态(userInfo)、系统主题(theme: ‘light’ | ‘dark’),多个组件(如 Header、Sidebar、设置页)需使用;
      • 避免 props 透传:如“用户信息”需从根组件传递到深层子组件(如 App -> Layout -> Sidebar -> UserAvatar),用 useContext 可直接在 UserAvatar 中读取,简化层级传递。
    • 注意事项:
      • 避免滥用:若数据仅在父子组件间传递,优先用 props(更清晰);仅当数据需跨多层级或多个组件共享时,才用 useContext;
      • 性能优化:useContext 会导致依赖它的组件在上下文数据变化时重渲染,若上下文数据频繁变化(如实时刷新的计数器),需配合 useMemo 缓存上下文值,或拆分上下文为多个细粒度上下文(如“用户信息上下文”和“主题上下文”分离)。
    • 针对性追问:若 useContext 传递的业务数据需修改(如用户修改昵称),如何设计修改逻辑?
      • 追问参考答案:将“数据”和“修改数据的方法”一起放入上下文,组件通过调用方法修改数据:
        1. 创建上下文时包含数据和方法:
    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>;
    };
    

(二)公共组件共享策略

  1. 面试题:若公共组件需在 3 个产品线(项目)中共享,有哪些好的共享策略?分别说明优缺点。

  2. 参考答案

共享策略 实现方式 优点 缺点 适用场景
私有 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 工具,权限管理需控制 同一团队维护多个项目,组件与项目关联紧密
  1. 针对性追问:若公共组件需支持“按需引入”和“主题定制”,私有 npm 包方案如何实现?
    • 追问参考答案:
      • 按需引入:
        1. 组件库采用“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. 面试题 1:是否听说过模块联邦(Module Federation)?微前端有哪些常见方案?项目中是否用过微前端?

    • 参考答案
      • 模块联邦(Module Federation):是 Webpack 5 推出的跨应用模块共享方案,核心思想是“将应用拆分为‘宿主应用’和‘远程应用’,宿主应用可动态加载远程应用的模块(如组件、函数),远程应用也可加载宿主应用的模块”,无需构建时合并代码,支持跨技术栈(如 React 项目加载 Vue 组件)。
      • 微前端常见方案:
        1. 基于路由分发(如 qiankun):将不同产品线作为独立子应用,通过路由匹配(如 /app1/* 对应子应用 1,/app2/* 对应子应用 2),主应用负责子应用的加载、卸载和通信,优点是接入简单,缺点是子应用切换需刷新页面;
        2. 模块联邦(Webpack Module Federation):动态加载子应用模块,支持子应用嵌入主应用任意位置(无需路由隔离),优点是灵活性高,缺点是配置复杂,依赖 Webpack 5;
        3. 基于 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 命名规范,进一步减少样式冲突。
  2. 面试题 2:微前端中主应用如何给子应用传值?子应用之间如何进行数据交互?

    • 参考答案
      • 主应用给子应用传值(以 qiankun 为例):
        1. 注册子应用时传递参数:通过 props 字段传递静态/动态数据,子应用在 mount 生命周期中接收:
    // 主应用注册子应用
    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 时,子应用会通过监听函数实时接收新值,避免子应用重启。
    

五、适配相关(移动端、大屏)

(一)移动端适配

  1. 面试题:移动端如何做屏幕自适应?请说明至少 2 种常用方案,包括实现细节和优缺点。

  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 或特殊单位 需编写多个媒体查询规则,适配多屏幕时代码量大 组件级适配,或需兼容低版本浏览器的项目
  1. 针对性追问:若设计稿同时包含移动端和 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 个)
      }
    }
    

(二)大屏可视化适配

  1. 面试题:大屏可视化项目(如数据看板)如何做适配?需考虑哪些问题?

  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)兼容。
  3. 针对性追问:若大屏需支持“全屏模式”,如何实现?缩放适配是否会受影响?

    • 追问参考答案:
      • 全屏模式实现:通过浏览器 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);
      };
    }, []);
    

六、性能优化相关

(一)通用性能优化

  1. 面试题:你对前端性能优化的理解是什么?在项目中如何落地这些优化措施?请举例说明。

  2. 参考答案

    • 性能优化核心目标:减少页面加载时间(首屏加载、白屏时间)、提升交互流畅度(减少卡顿、延迟)、降低资源消耗(CPU、内存),最终提升用户体验。
    • 落地措施(按“加载阶段-运行阶段”分类):
      • 一、加载阶段优化(减少资源加载时间):
        1. 资源压缩与合并:
        • 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。
        1. 静态资源 CDN 分发:
        • 将静态资源(JS、CSS、图片、字体)部署到 CDN(如阿里云 CDN),利用 CDN 节点缓存,用户从最近节点获取资源,减少网络延迟;
        • 配置 CDN 缓存策略(如 Cache-Control: max-age=31536000),长期缓存不变的资源(如第三方库、图标)。
        1. 懒加载(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。
      1. 优化 JS 执行效率:
      • 避免长时间同步任务:将耗时操作(如大数据筛选)拆分为微任务(Promise)或使用 requestIdleCallback,避免阻塞主线程;
      • 缓存计算结果:使用 useMemo/useCallback 缓存重复计算(如列表筛选)、localStorage 缓存接口数据(如用户配置);
      • 示例:项目中“大数据表格筛选”(10000 条数据)通过 useMemo 缓存筛选结果,筛选操作从 500ms 缩短至 50ms。
      1. 接口请求优化:
      • 接口合并:将多个独立接口(如“获取用户信息”“获取权限列表”)合并为一个接口,减少 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 打包后的代码包结构,定位体积过大的模块(如第三方库)。

(二)大图片优化

  1. 面试题:若首页有一张大图片(如 2MB、1920×1080px),如何让用户第一时间看见这张图片?请说明具体优化措施。

  2. 参考答案

    • 核心思路:通过“减少图片加载时间”“优先展示关键内容”“优化加载体验”三个方向,确保用户快速看到图片。
    • 具体措施:
      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(网速极慢),如何优化大图片的加载体验?
      • 追问参考答案:
        1. 降级为低质量图片:检测网络类型(通过 navigator.connection.effectiveType),若为 2G,加载超低分辨率图片(如 300×168px,体积 < 50KB),并提供“加载高清图片”按钮,用户可手动触发加载;
        2. 文本占位:在图片加载完成前,展示图片相关文本(如 banner 文案“限时优惠活动”),避免用户长时间看到空白;
        3. 延迟加载非首屏图片:若大图片不在首屏,2G 网络下完全禁用加载,仅当用户滚动到图片位置且网络改善后,再触发加载。

七、JavaScript 核心概念

(一)函数柯里化与伪调用

  1. 面试题 1:什么是 JavaScript 函数柯里化(Currying)?请举例说明其用途和实现方式。

    • 参考答案
      • 定义:柯里化是将“接收多个参数的函数”转换为“接收单个参数(第一个参数),并返回接收剩余参数的新函数”的技术,最终通过多次调用累积参数,直到参数满足函数执行条件。
      • 核心目的:复用参数逻辑、延迟执行、支持部分应用(Partial Application)。
      • 示例(用途):
        1. 复用参数:封装“计算税费”函数,固定税率后复用(如 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); // 9
    
    2. 延迟执行:分步接收参数,直到参数足够后执行(如表单验证,先接收验证规则,再接收表单值)。
    
    • 通用柯里化函数实现(支持任意参数数量):
    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. 面试题 2:什么是 JavaScript 函数伪调用?请举例说明。

    • 参考答案
      • 定义:函数伪调用(Pseudo-invocation)指“不通过函数本身的 call/apply/bind 方法,也不通过对象原型链,而是通过其他方式间接调用函数,且可能改变函数 this 指向”的调用方式,常见场景为“通过 eval 或函数表达式间接调用”。
      • 核心特点:this 指向可能不符合常规(如非严格模式下指向 window,严格模式下指向 undefined),且代码可读性差,不推荐使用。
      • 示例(常见伪调用场景):
        1. 通过 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 指向 undefined
    
    3. 通过定时器/事件回调伪调用:
    
    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)。

(二)节流与防抖

  1. 面试题:什么是 JavaScript 节流(Throttle)和防抖(Debounce)?两者有什么区别?请举例说明使用场景和实现方式。

  2. 参考答案

    • 核心定义:两者均为“控制函数执行频率”的技术,解决高频事件(如滚动、输入、点击)导致的性能问题,但执行逻辑不同。
    • 区别对比:
    对比维度 节流(Throttle) 防抖(Debounce)
    核心逻辑 规定“单位时间内仅执行一次函数”,无论事件触发多少次 规定“事件触发后延迟 N 秒执行函数,若 N 秒内再次触发则重置延迟时间”,仅最后一次触发后执行
    执行时机 单位时间内首次触发或末次触发(可配置) 事件停止触发后延迟 N 秒执行
    适用场景 高频连续事件(如滚动加载、窗口 resize、鼠标移动) 高频触发后需最终结果的事件(如输入搜索、按钮防重复点击)
    示例效果 滚动页面时,每 100ms 执行一次加载函数 输入框输入时,停止输入 500ms 后才发送搜索请求
    • 实现方式(原生 JS):
      1. 节流(时间戳 + 定时器实现,支持“首次触发”和“末次触发”):
    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));
    
    1. 防抖(定时器实现,支持“立即执行”配置):
    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);
      };
    }, []);
    
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐