R13第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

数组状态与集合操作

数组状态与集合操作 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

数组是 React 状态里最常见的结构,而 JavaScript 数组方法一半复制、一半原地改,混着用就出诡异 bug。这一课把增、删、改、排序四个操作的不可变写法一次配齐,并给出「哪些方法碰不得」的明确清单。

问题场景

任务管理器要四个操作:新增、编辑、删除、上移下移。第一版代码混着写:

function promote(task) {
  tasks.splice(index, 1);        // 原地移除
  tasks.unshift(task);           // 原地插入
  setTasks(tasks);               // 同一引用,React 不渲染
}

更隐蔽的是另一版:tasks.sort() 返回值是同一个数组(原地排序),写 setTasks(tasks.sort()) 看似有「新值」,实际引用没变,界面照样不动。

核心解释

判断一个数组方法能不能用在 React 状态上,只看一条:它返回新数组,还是改原数组? 改原数组(变异方法)一律不碰,改用复制版:

操作 变异方法(禁用) 不可变替代
新增 push / unshift [...tasks, item] / [item, ...tasks]
删除 splice filter / slice 拼接
替换 直接赋值 tasks[i] = x map 命中替换
排序 sort / reverse toSorted / toReversed / [...tasks].sort()
抽取 splice slice

四个核心操作的惯用写法:

// 新增:展开 + 新对象(id 在创建时生成)
setTasks([...tasks, { id: newId(), title, done: false }]);

// 编辑:map 遍历,命中的新建对象,未命中的保持引用
setTasks(tasks.map((t) => (t.id === id ? { ...t, title: nextTitle } : t)));

// 删除:filter 天然返回新数组
setTasks(tasks.filter((t) => t.id !== id));

// 上移:slice 拼出交换后的新数组
setTasks([
  ...tasks.slice(0, i - 1),
  tasks[i],
  tasks[i - 1],
  ...tasks.slice(i + 1),
]);

上移那段值得读三遍:slice(0, i-1) 是前段、两个元素交换、slice(i+1) 是后段,全部拼进一个新数组。原数组从头到尾没被碰过。对象元素的处理原则与 R12 一致:整个数组必然新建,但没被修改的对象保持原引用,不强行深拷贝。

toSorted / toReversed / with 是 ES2023 加入的复制版方法,语义与变异版完全相同,新项目可以直接用;考虑旧环境兼容时用 [...tasks].sort()。

示例拆解

实验区是一个完整任务板,配一个「数据快照」面板,关键点如下:

  • 四类操作按钮齐全:新增(带输入)、编辑(改标题)、删除、上移/下移,全部走不可变路径;
  • 快照面板实时显示当前数组的 JSON 和「数组身份变化次数」——每次操作身份必变,未被编辑的任务对象身份不变;
  • 对照按钮「危险示范」:执行 tasks.push / tasks.sort() 后再 setTasks(tasks),点了没反应,正好复现问题场景;
  • 排序按钮演示 toSorted:随机打乱后重新按名称排正。

验证方式:每点一次操作按钮,快照面板的「身份变化」+1 且数据正确更新;危险示范按钮点了身份不变、界面不动。

动手任务

打开本课实验区,完成三个小改动:

  1. 加「批量删除已完成」:filter 一次删掉所有 done 的任务。
  2. 加「移到末尾」按钮,用 filter + 展开组合实现,不碰 splice。
  3. 把危险示范改成正确写法(先 [...tasks] 复制再排序),让按钮生效。

常见错误

  • push/splice/sort/reverse 直接用于状态数组:原地改 + 同引用 setState,React 不渲染;换成复制版方法。
  • 删除时用下标:filter((t, i) => i !== idx) 在并发操作下会删错行;按业务 id 删,和渲染的 key 保持同一身份依据。
  • 每次更新都深拷贝整个数组:JSON.parse(JSON.stringify(tasks)) 丢方法、丢 undefined,还白白破坏所有对象引用;只需新建被改动的部分。
  • 排序时忘了去变异:tasks.sort((a, b) => ...) 原地生效,即使随后 setState 新引用也已经是排过的,会把「未排序状态」从历史里抹掉。

检查与复盘

完成下方检查题确认理解。自查清单:

  • 能背出「变异方法禁用清单」并说出每个的复制版替代
  • 增删改排序四类操作都能当场写出不可变版本
  • 更新后数组是新引用,未修改的对象保持原引用

参考资料

动手实验
动手实验
预览
点击「运行」在隔离沙箱中预览
检查题共 2 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1读代码中等

阅读代码(React 18+)。初始列表为「苹果、香蕉、樱桃」。点击「处理」按钮后,列表显示什么?

function FruitList() {
  const [fruits, setFruits] = useState(['苹果', '香蕉', '樱桃']);

  function process() {
    setFruits((list) =>
      list
        .filter((f) => f !== '香蕉')
        .map((f) => (f === '樱桃' ? '车厘子' : f))
    );
  }

  return (
    <ul>
      {fruits.map((f) => (
        <li key={f}>{f}</li>
      ))}
    </ul>
  );
}
Q2多选中等(多选,全部选对才算正确)

todos 是 state 数组,每项有唯一 id。下列哪些写法是正确的不可变更新?(多选)

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...