React 面试题:函数式组件 vs 类组件

在 React 面试中,这属于必考题。它不仅考察你对 API 的熟悉程度,更考察你对 React 渲染机制心智模型的理解。


1. 本质区别:心智模型的差异

这是最核心的回答点:类组件是基于面向对象(OOP)的,而函数式组件是基于函数式编程(FP)的。

A. 捕获渲染值(Capture Value)—— 最重要的技术区别
  • 函数组件:具有捕获渲染时状态的特性。由于闭包机制,函数组件在渲染时,propsstate 是快照。即使在异步操作(如 setTimeout)中,它拿到的也是触发渲染那一刻的值。
  • 类组件:通过 this.propsthis.state 获取数据。由于 this可变的(mutable),如果异步任务执行完成时组件已经重新渲染,this 会指向最新的实例引用,从而可能导致逻辑不一致。
B. 状态管理与生命周期
  • 类组件:通过继承 React.Component,使用 state 对象和一系列生命周期钩子(如 componentDidMount, shouldComponentUpdate)。
  • 函数组件:最初是“无状态”的。但在 Hooks(React 16.8+)引入后,通过 useStateuseEffect 等实现了状态管理和副作用处理。

2. 为什么更推荐使用函数式组件?

React 官方和社区目前全面向函数式组件倾斜,主要原因如下:

  • 逻辑复用更高效(Hooks vs HOC/Render Props)
    在类组件中,复用逻辑通常需要高阶组件 (HOC) 或 Render Props,容易导致“嵌套地狱”。Hooks 允许我们将逻辑提取到独立的自定义 Hooks 中,代码更加扁平、易于测试。
  • 代码更简洁、更易读
    类组件充满了样板代码(如 constructorthis.bind)。函数组件代码量更少,且 useEffect 允许我们将相关逻辑(如订阅和取消订阅)聚合在一起,而不是分散在不同的生命周期钩子中。
  • 对编译器更友好(性能潜力)
    函数组件本质是纯 JS 函数,更容易被工具(如 Babel, Terser)进行静态分析、代码压缩和 Tree-shaking。而类组件的方法名通常难以被安全地混淆。
  • 符合 U I = f ( s t a t e ) UI = f(state) UI=f(state) 的本质
    函数组件更加纯粹地体现了“输入 -> 输出”的映射关系,每一轮渲染都是独立的函数调用。

3. 面试总结话术

“类组件和函数组件最本质的区别在于:类组件通过 this 维护实例,而函数组件通过闭包捕获渲染快照。

官方更推荐函数组件,主要是因为 Hooks 的出现彻底解决了逻辑复用的难题,避免了高阶组件带来的嵌套问题。同时,函数组件的代码更简洁、心智负担更轻,也更符合 React 声明式、数据流驱动的设计初衷。此外,从长远来看,函数式组件在性能优化和打包体积上具有更大的优势。”


💡 核心知识点速览

特性 类组件 (Class) 函数组件 (Function)
编程范式 面向对象 (OOP) 函数式编程 (FP)
内存占用 需创建类实例,开销稍大 普通函数调用,开销较小
数据读取 this.props (易受引用变动影响) 闭包快照 (捕获渲染值)
逻辑复用 HOC, Render Props (易嵌套) Hooks (扁平化)
生命周期 显式的生命周期钩子 useEffect 模拟生命周期

React 核心特性:捕获渲染时状态(Capture Value)

“捕获渲染时状态”是 React 函数式组件最迷人也最容易让人困惑的特性。简单来说:函数组件的每一次渲染,都有它自己独立的 Props、State 和事件处理函数。


1. 闭包体现在哪儿?

在 React 中,组件函数每次执行(Render)都会创建一个新的作用域

当你在组件内部定义一个函数(如点击回调)时,这个函数通过 JavaScript 闭包 锁定了当前那次渲染中的变量。即使这个回调函数在几秒钟之后才执行(比如 setTimeout),它访问的依然是“那个特定时刻”的变量值。


2. 代码对比:函数组件 vs 类组件

通过下面这个经典的“社交关注”例子,你可以一眼看出区别:

A. 函数组件:行为一致(捕获状态)
function ProfilePage(props) {
  const showMessage = () => {
    // 闭包锁定了【点击那一刻】的 props.user
    alert('Followed ' + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return <button onClick={handleClick}>Follow</button>;
}
  • 操作流程:初始用户是 A,点击 Follow → \rightarrow 3 秒内,将用户切换到 B。
  • 结果:3 秒后弹出 “Followed A”。
  • 原因:showMessage 闭包捕获了点击瞬间的 props 快照。
B. 类组件:行为不一致(引用丢失)
class ProfilePage extends React.Component {
  showMessage = () => {
    // this.props 是可变的,总是指向最新的实例引用
    alert('Followed ' + this.props.user);
  };

  handleClick = () => {
    setTimeout(this.showMessage, 3000);
  };

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}
  • 操作流程:初始用户是 A,点击 Follow → \rightarrow 3 秒内,将用户切换到 B。
  • 结果:3 秒后弹出 “Followed B”。
  • 原因:this.props 随着组件更新变掉了,showMessage 读到了最新的 this 指向,导致数据不一致。

React 面试题:类组件生命周期 vs. 函数组件 Hooks

在 React 的演进过程中,生命周期管理从“基于类的显式钩子”演变为“基于函数的副作用同步”。以下是针对这一部分的详细补充和深度对比。


1. 类组件的生命周期 (Class Lifecycle)

类组件的生命周期就像一个人的成长过程:挂载(出生)更新(成长)卸载(死亡)

A. 挂载阶段 (Mounting)

这是组件实例被创建并插入 DOM 的过程:

  • constructor():初始化 state,绑定事件处理器。
  • static getDerivedStateFromProps():根据 props 的变化来更新 state(极少使用)。
  • render()核心方法。返回 JSX,定义 UI 结构。
  • componentDidMount()非常常用。组件挂载到 DOM 后立即调用。适合:发送网络请求、订阅事件、操作 DOM。
B. 更新阶段 (Updating)

当组件的 propsstate 发生变化时:

  • shouldComponentUpdate()性能优化关键。返回 truefalse,决定 React 是否重新渲染该组件。
  • render():重新生成虚拟 DOM。
  • getSnapshotBeforeUpdate():在最近一次渲染提交到 DOM 之前调用,可以捕捉 DOM 信息(如滚动位置)。
  • componentDidUpdate()常用。更新完成后调用。适合:根据数据变化执行 DOM 操作或发送新的请求。
C. 卸载阶段 (Unmounting)
  • componentWillUnmount()重要。组件销毁前调用。适合:清理定时器、取消网络请求、解绑事件监听(防止内存泄漏)。

2. 函数式组件的实现 (Hooks)

函数组件使用 useEffect 这一个 Hook 来模拟上述所有的生命周期行为。其核心哲学是**“同步”**(根据状态同步副作用),而不是单一的“生命周期”。

A. 模拟 componentDidMount

只需将 useEffect 的依赖项数组设置为空 []

useEffect(() => {
  console.log('组件挂载了(只执行一次)');
  // 发送 API 请求
}, []);
B. 模拟 componentDidUpdate

不传递依赖项数组(每次渲染都执行),或传递特定的依赖项(仅当该变量改变时执行):

useEffect(() => {
  console.log('数据改变了,执行更新逻辑');
}, [count]); // 仅在 count 变化时运行
C. 模拟 componentWillUnmount

在 useEffect 中返回一个清理函数 (Cleanup function):

useEffect(() => {
  const timer = setInterval(() => {
    console.log('定时器运行中...');
  }, 1000);

  // 返回的函数会在组件卸载前执行
  return () => {
    clearInterval(timer);
    console.log('组件卸载,定时器已清理');
  };
}, []);

3. 深度对比表

概念 类组件 (Class) 函数组件 (Hooks)
初始化状态 constructor 中使用 this.state useState
挂载后 componentDidMount useEffect(fn, [])
更新后 componentDidUpdate useEffect(fn, [deps])
卸载前 componentWillUnmount useEffectreturn 清理函数
性能优化 shouldComponentUpdate / PureComponent React.memo / useMemo / useCallback

4. 为什么函数式的实现更高级?

  • 关注点分离 (Separation of Concerns)
    • 类组件:你可能需要在 componentDidMount 里同时开启定时器和获取数据,这两个无关的逻辑被迫挤在一起。
    • 函数组件:你可以写多个 useEffect,每个 useEffect 只处理一个独立的逻辑块,让代码更具模块化。
  • 代码复用
    • 你可以轻松地把一组 useEffect 逻辑抽离成一个自定义 Hook(如 useWindowSize),在不同组件间共享。而在类组件中,这需要通过高阶组件(HOC)或 Render Props 才能实现,结构非常复杂且容易造成“嵌套地狱”。
  • 心智模型简洁
    • 函数组件让你思考**“这个副作用依赖于哪些数据”**,而不是“我现在处于组件生命周期的哪个时刻”,这更符合声明式编程的直觉。

💡 总结话术(建议背诵)

“函数组件通过 Hooks 实现了关注点分离。它允许我们将同一业务逻辑的‘开始执行’与‘销毁清理’完整地写在同一个函数块中,而不是散落在不同的生命周期钩子函数里。这种高内聚的写法大大降低了代码的维护成本,并有效减少了因忘记在销毁阶段解绑事件而导致的内存泄漏风险。”

什么是高阶组件 (HOC)?

高阶组件(Higher-Order Component) 本质是一个 函数。它接收一个组件作为参数,并返回一个新的组件。
其原理是利用“包装”的思想,将通用的逻辑注入到被包装的组件中。

// 这是一个 HOC
function withLogger(WrappedComponent) {
  return class extends React.Component {
    componentDidMount() {
      console.log(`${WrappedComponent.name} 挂载了`);
    }
    render() {
      // 将原来的 props 传递下去,并增加一些新属性
      return <WrappedComponent {...this.props} />;
    }
  };
}

// 使用
const EnhancedMyComponent = withLogger(MyComponent);

什么是 Render Props?

Render Props 指的是一种技术,通过一个值为函数的 prop,告知组件需要渲染什么内容。
其原理是将逻辑封装在父组件中,通过调用函数并将数据传回,让子组件决定如何渲染。

class MouseTracker extends React.Component {
  state = { x: 0, y: 0 };

  handleMouseMove = (event) => {
    this.setState({ x: event.clientX, y: event.clientY });
  };

  render() {
    return (
      <div onMouseMove={this.handleMouseMove}>
        {/* 调用 props.render 并将 state 传出去 */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

// 使用
<MouseTracker render={({ x, y }) => (
  <h1>当前鼠标位置:{x}, {y}</h1>
)} />

深入理解函数式编程 (Functional Programming)

函数式编程是一种将计算视为数学函数计算的编程范式。在 React 的世界里,理解 FP 是掌握函数式组件、Hooks 以及状态管理的基础。


1. 函数式编程的核心特点

A. 声明式 (Declarative)

它关注 “要做什么” 而不是“怎么做”。

  • 命令式:手动控制过程(循环、计数器、状态变更)。
  • 函数式:描述逻辑(如使用 mapfilter),由引擎处理具体实现。
B. 纯函数 (Pure Function)

这是 FP 的灵魂。纯函数必须满足两个条件:

  1. 确定性:相同的输入永远得到相同的输出。
  2. 无副作用:函数执行过程中不修改外部变量、不操作 DOM、不发送网络请求。
C. 不可变性 (Immutability)

数据一旦创建就不能修改。若需变更,应创建并返回一个包含新数据的新对象/数组,而不是在原数据上直接修改。

D. 函数是一等公民 (First-class Function)

函数可以像普通变量一样被使用:

  • 作为参数传递(回调函数)。
  • 作为返回值(闭包、高阶函数)。
  • 赋值给变量。

2. 代码举例说明

A. 命令式 vs. 函数式

任务:过滤出数组中的偶数,并将它们乘以 2。

  • 命令式写法(关注过程)

    const arr = [1, 2, 3, 4];
    const result = [];
    for (let i = 0; i < arr.length; i++) {
      if (arr[i] % 2 === 0) {
        result.push(arr[i] * 2);
      }
    }
    
  • 函数式写法(关注结果)

    const arr = [1, 2, 3, 4];
    const result = arr
      .filter(num => num % 2 === 0)
      .map(num => num * 2);
    
B. 纯函数 vs. 不纯函数
  • 不纯函数(依赖且修改外部状态)

    let factor = 2;
    function multiply(num) {
      factor = factor + 1; // 副作用:修改了外部变量
      return num * factor; // 不确定性:结果取决于外部变量
    }
    
  • 纯函数(只依赖参数)

    function multiply(num, factor) {
      return num * factor; // 确定的输出,无副作用
    }
    

3. 在 React 中的体现

React 的设计哲学深度拥抱了函数式编程:

  1. U I = f ( s t a t e ) UI = f(state) UI=f(state):UI 是数据的纯映射,组件本质上是一个将状态转换为视图的函数。
  2. 不可变状态:在 React 中禁止直接修改状态(如 state.push),必须使用 setState([...state, item])
  3. 单向数据流:数据像函数的参数一样从上向下传递,保证了链路的可追溯性。
  4. Hooks / 高阶组件:充分利用了函数作为“一等公民”的特性进行逻辑组合与复用。

💡 核心总结

  • 纯函数 (Pure):相同输入 → \rightarrow 相同输出,无副作用。
  • 不可变性 (Immutability):不修改原数据,而是返回新数据。
  • 高阶函数:函数可以作为参数或返回值(如 map, filter, reduce)。
  • 声明式编程:描述“想要什么”,让代码更具可读性。

为什么 React 拥抱 FP?
因为纯函数和不可变性让 UI 的状态变得高度可预测。这极大地方便了调试和自动化测试,同时也为虚拟 DOM 的 Diff 算法提供了性能优化的基础(例如:只需通过简单的引用比较 prevProps === nextProps 即可判断是否需要更新)。

更多推荐