数组、对象与不可变更新
数组、对象与不可变更新 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
这一课对一份课程清单做筛选、分组和更新,目标是:函数执行完,原始数据一个字节都不能被改坏(不可变更新,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 是共享引用。
动手任务
打开本课实验区:
- 写
groupHours(courses):按 track 汇总学时,返回{ foundation: 8, react: 18 }形状的对象,用快照对比确认原数组没变。 - 写
updateCourse(courses, id, patch):返回更新后的新数组,要求连嵌套的 tags 也不与原数据共享(提示:展开两层)。 - 实验区最后有一行直接 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。
检查与复盘
完成下方检查题确认理解。自查清单:
- 能列出哪些数组方法改原数组、哪些返回新数组
- 能说清浅拷贝复制到哪一层,并举出嵌套共享引用的例子
- 加工数据前有「拍快照验证不可变」的习惯
参考资料
- MDN:Array —— 每个方法「是否修改原数组」的说明
- MDN:展开语法 —— 展开与浅拷贝
- MDN:structuredClone —— 标准深拷贝
读完这一课了?
标记完成后,工作台会安排接下来的学习。