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

数组、对象与不可变更新

数组、对象与不可变更新 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

这一课对一份课程清单做筛选、分组和更新,目标是:函数执行完,原始数据一个字节都不能被改坏(不可变更新,immutable update,生成新数据而不修改原数据的做法)。读完你应该会用 map/filter/reduce 完成常见加工,理解浅拷贝(只复制第一层)的能力边界,并识别「嵌套对象被顺手改掉」的坑。

问题场景

课程列表页有两个需求:按方向筛选展示,给某个课程改名。第一版直接在原始数组上 sort()、courses[0].title = '新名字',结果切走再切回来,排序乱了、名字也变了——原始数据被函数顺手改掉了,调用方毫不知情。数据一旦被多处使用,这类「暗中修改」就是最难查的 bug。

核心解释

数组的常用方法分两类:会改原数组(push、splice、sort、reverse),和返回新数组不动原数组(map、filter、slice、concat)。加工数据优先用后者:

const courses = [
  { id: 1, title: '网页基础', track: 'foundation', hours: 8 },
  { id: 2, title: 'React 入门', track: 'react', hours: 12 },
  { id: 3, title: '状态管理', track: 'react', hours: 6 },
];

// 筛选:filter 返回新数组
const reactOnly = courses.filter((c) => c.track === 'react');

// 更新:map + 展开运算符(spread,... 把对象展开复制一层)
const renamed = courses.map((c) =>
  c.id === 2 ? { ...c, title: 'React 基础' } : c
);

renamed 里的 id 2 是一个新对象,原数组里那个没被碰过。但注意 { ...c } 是浅拷贝:只复制第一层。如果课程对象里还有 tags: ['前端'] 这样的嵌套引用,新旧对象会共享同一个 tags 数组——改一个,另一个跟着变。这就是浅拷贝的边界。

分组统计用 reduce(把数组折叠成一个任意结果的方法):

const byTrack = courses.reduce((acc, c) => {
  (acc[c.track] ??= []).push(c.title); // ??= :该分组不存在时先建空数组
  return acc;
}, {}); // { foundation: ['网页基础'], react: ['React 入门', '状态管理'] }

示例拆解

用「前后快照对比」验证不可变:加工前把原始数据拍个快照,加工完再对比:

const snapshot = JSON.stringify(courses);
const result = renameCourse(courses, 2, 'React 基础');
console.log(JSON.stringify(courses) === snapshot); // true:原数据没变
console.log(result); // 新数组,只有 id 2 换了名字
  • 这也是排查「谁改了我的数据」的土办法:在可疑函数前后各拍一次快照,一比就知道。
  • 真需要深拷贝(每一层都复制)时,现代浏览器直接用 structuredClone(courses),不用再手写递归。
  • 验证浅拷贝边界:const copy = { ...courses[1] }; copy.tags.push('新'); 然后打印 courses[1].tags——它也被改了,因为 tags 是共享引用。

动手任务

打开本课实验区:

  1. 写 groupHours(courses):按 track 汇总学时,返回 { foundation: 8, react: 18 } 形状的对象,用快照对比确认原数组没变。
  2. 写 updateCourse(courses, id, patch):返回更新后的新数组,要求连嵌套的 tags 也不与原数据共享(提示:展开两层)。
  3. 实验区最后有一行直接 sort 原数组的错误示范,改成不动原数组的写法(提示:toSorted(),或先 slice() 再 sort)。

常见错误

  • 直接 sort 原数组:courses.sort() 返回的就是原数组且已改序;先复制再排。
  • 以为展开就是深拷贝:{ ...obj } 只复制一层,嵌套的对象和数组仍然是共享引用。
  • map 里顺手改原对象:map((c) => { c.hours += 1; return c; }) 改的是原对象;要返回 { ...c, hours: c.hours + 1 }。
  • 用 JSON.parse(JSON.stringify(x)) 深拷贝:会丢函数、丢 undefined、把日期变成字符串;现在应该用 structuredClone。

检查与复盘

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

  • 能列出哪些数组方法改原数组、哪些返回新数组
  • 能说清浅拷贝复制到哪一层,并举出嵌套共享引用的例子
  • 加工数据前有「拍快照验证不可变」的习惯

参考资料

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

执行以下代码后,original[0].id 的值是多少? const original = [{ id: 1 }, { id: 2 }]; const copy = [...original]; copy[0].id = 99;

Q2多选中等(多选,全部选对才算正确)

todos 是一个对象数组,需求:把 id 为 2 的项标记 done: true,其余完全不动。下列哪些写法符合不可变更新?(多选)

读完这一课了?

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

Command Palette

Search for a command to run...