历史与确认页:两个小而致命的细节The history and confirmation pages: two small details that decide pass or fail
slice(-3).reverse() 一个字符都不能错;bookedCabDetails?.name 少个问号就白屏。slice(-3).reverse() has to be exact, character for character; drop the question mark in bookedCabDetails?.name and the screen goes blank.
这一页有什么On this page6
- 说清 slice(-3) 和 slice(0, 3) 的区别Explain the difference between slice(-3) and slice(0, 3)
- 知道 reverse() 是原地修改,以及为什么这里安全Know that reverse() changes the array in place, and why it is safe here
- 写出「最新三条、最新在最上」的取法Write the code for the three newest rides with the newest at the top
- 说清为什么 bookedCabDetails 后面必须有可选链Explain why bookedCabDetails needs optional chaining after it
测试 4 是这道题唯一会「看起来做对了但实际全错」的地方:它同时查数量、顺序、和最旧那条真的消失。slice 方向写反、忘了 reverse、或者直接 reverse 到 state 上,三种错法都只在这一条测试里暴露。Test 4 is the one place in this task where your work can look correct and be completely wrong. It checks the count, the order, and that the oldest entry really disappeared, all at the same time. A slice in the wrong direction, a missing reverse, or a reverse applied to the state array: all three show up only in this single test.
cab-booking-context/src/components/Home/RideHistory.jsx取最新三条并反转
提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。Heads up: on disk this project is the finished version — what follows is the answer. Close this if you want to write it yourself first.
cab-booking-context/src/components/Home/RideHistory.jsxcab-booking-context/src/components/CabConfirmation/CabConfirmation.jsx可选链在这里
提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。Heads up: on disk this project is the finished version — what follows is the answer. Close this if you want to write it yourself first.
cab-booking-context/src/components/CabConfirmation/CabConfirmation.jsxslice(-3) 是「最后三个」slice(-3) means the last three
负数从尾巴数起。方向写反,测试 4 直接红A negative number counts from the end. Get the direction wrong and test 4 fails
一句话:arr.slice(-3) 取最后三个,arr.slice(0, 3) 取最前三个。
先看数据是怎么排的:updateBookedCabDetails 用的是[...rideHistory, details] ——新记录追加在尾部。 所以数组是「最旧 → 最新」的顺序。
| 写法 | 拿到什么 | 测试 4 结果 |
|---|---|---|
slice(-3) | Honda Accord / Toyota Highlander / Ford Explorer (第 2、3、4 次) | ✓ 正确 |
slice(0, 3) | Ford Fusion / Honda Accord / Toyota Highlander (第 1、2、3 次) | ✕ 最后一行断言直接炸 ——Ford Fusion 还在 DOM 里 |
slice(3) | 从第 4 个开始到结尾(只剩 1 条) | ✕ toHaveLength(3) 收到 1 |
注意 slice(0, 3) 有多阴:toHaveLength(3) 会过, 因为它确实是 3 条。只有最后那句queryByText(/Ford Fusion/) 抓得住它。
这就是测试 4 为什么要写那一句 ——光查数量不够,得查「该消失的真的消失了」。
会追问:「数组不足三条时 slice(-3) 会怎样?」——安全。只有 1 条时返回那 1 条, 空数组时返回空数组,不会报错也不会补 undefined。 所以测试 1(空历史)和测试 3(1 条)都不用特殊处理。
这一点值得记住:slice 越界不抛错, 这也是它比手写下标循环安全的地方。
In one line: arr.slice(-3) takes the last three; arr.slice(0, 3) takes the first three.
Start from how the data is ordered: updateBookedCabDetails uses [...rideHistory, details], so new records are appended at the tail. The array runs oldest to newest.
| Written as | What you get | Test 4 |
|---|---|---|
slice(-3) | Honda Accord / Toyota Highlander / Ford Explorer (bookings 2, 3, 4) | ✓ correct |
slice(0, 3) | Ford Fusion / Honda Accord / Toyota Highlander (bookings 1, 2, 3) | ✕ the last assertion fails — Ford Fusion is still in the DOM |
slice(3) | From index 3 to the end (only 1 entry) | ✕ toHaveLength(3) receives 1 |
Notice how sneaky slice(0, 3) is: toHaveLength(3) passes, because there really are three rows. Only that final queryByText(/Ford Fusion/) line catches it.
Which is precisely why test 4 has that line — counting is not enough; you have to check that what should be gone is gone.
Follow-up: “What does slice(-3) do with fewer than three entries?” — it is safe. With one entry it returns that one; with an empty array it returns an empty array; no error and no undefined padding. So test 1 (empty) and test 3 (one entry) need no special handling.
Worth remembering: slice never throws on out-of-range, which is one reason it beats a hand-written index loop.
reverse() 原地修改 —— 这里为什么安全reverse() changes the array in place, and why that is safe here
因为 slice 已经给了你一个新数组Because slice has already given you a new array
一句话:reverse() 会改掉它作用的那个数组本身, 但 slice() 返回的是新数组, 所以 slice(-3).reverse() 改的是那个副本,碰不到 state。
直接写 rideHistory.reverse() 会怎样:
- state 数组被就地翻转了 —— 现在它是「最新 → 最旧」;
- React 不知道(引用没变,连重渲染都不会触发);
- 下次
[...rideHistory, details]把新记录追加在已经翻转过的数组尾部 —— 顺序彻底乱了; - 而且每次渲染都翻一次, 顺序在两种排列之间来回跳。
这个 bug 的表现极不稳定, 因为它取决于渲染了几次 ——StrictMode 开发模式下渲染两次,翻两次等于没翻,开发时看着正常,生产环境反而是错的。
JS 里哪些数组方法原地改:
| 原地修改(改原数组) | 返回新数组(安全) |
|---|---|
push / pop / shift / unshift | slice / concat / map / filter |
splice / sort / reverse / fill | [...arr] / toSorted / toReversed |
三个最容易忘的是sort / reverse / splice —— 它们看起来像「计算」,实际是「修改」。
会追问:「有没有不改原数组的版本?」—— 有,toSorted() / toReversed() / toSpliced(),ES2023 加的, Node 20+ 和现代浏览器都支持。但这道题的 slice(-3).reverse() 已经安全了, 没必要换 —— 知道有这么个东西就行。
In one line: reverse() mutates the array it is called on, but slice() hands back a new array — so slice(-3).reverse() reverses the copy and never touches state.
What a bare rideHistory.reverse() would do:
- The state array is flipped in place — it now runs newest to oldest;
- React has no idea (the reference did not change, so not even a re-render is triggered);
- The next
[...rideHistory, details]appends to an already-reversed array, and the ordering is now meaningless; - And it flips again on every render, so the order oscillates between two arrangements.
The bug is wildly unstable because it depends on the render count — StrictMode renders twice in development, two flips cancel out, so it looks right while you develop and is wrong in production.
Which array methods mutate:
| Mutating (changes the original) | Returns a new array (safe) |
|---|---|
push / pop / shift / unshift | slice / concat / map / filter |
splice / sort / reverse / fill | [...arr] / toSorted / toReversed |
The three people forget are sort / reverse / splice — they read like computations and are really mutations.
Follow-up: “Are there non-mutating versions?” — yes: toSorted() / toReversed() / toSpliced(), added in ES2023 and available in Node 20+ and current browsers. But slice(-3).reverse() here is already safe, so there is no need to switch — just know they exist.
cab-booking-context/src/components/Home/RideHistory.jsxbookedCabDetails?.name —— 那个问号不能省bookedCabDetails?.name: you cannot drop that question mark
初始值是 null,而 null 上取属性会抛错The initial value is null, and reading a property of null throws an error
一句话:bookedCabDetails 初始是 null,null.name 会抛TypeError,所以要写 ?.name。
什么时候真的是 null:
- App 刚启动、用户还没选过任何车 —— 这时
CabConfirmation没被渲染,所以撞不到; - 但只要有人把
CabConfirmation单独 render 出来测试(比如写一个组件级单测), 就会立刻炸; - 或者以后加一个「查看上次行程」的入口, 在没有行程时点进去 ——白屏 + 控制台一行
Cannot read properties of null (reading 'name')。
可选链 ?. 做什么:左边是 null 或 undefined 时整个表达式短路成 undefined, 不抛错。 而 React 遇到 {undefined}什么也不渲染(不是渲染字符串 “undefined”), 所以页面只是少一段文字,不会崩。
注意它只挡 null / undefined ——bookedCabDetails 是 {} 时?.name 照样是 undefined, 但那是属性不存在,不是崩溃。?. 防的是「取属性的动作本身炸掉」。
会追问:「那是不是所有地方都该加 ?.?」——不是。到处加 ?. 会把「这里可能没有值」这个信息抹平成噪音, 真正可能为空的地方反而看不出来。
规则是:只在「这个值确实可能不存在」的地方加。这道题里 bookedCabDetails 初始 null, 该加;ride.name 是遍历出来的记录,能进数组就一定有 name,不该加。
In one line: bookedCabDetails starts as null, and null.name throws a TypeError, so you write ?.name.
When it really is null:
- Right after the app starts, before the user has picked anything — but
CabConfirmationis not rendered then, so nothing breaks; - The moment somebody renders
CabConfirmationon its own — say in a component-level unit test — it throws immediately; - Or add a “view your last ride” entry point later and open it with no rides: blank screen and one console line reading
Cannot read properties of null (reading 'name').
What optional chaining does: when the left side is null or undefined, the whole expression short-circuits to undefined instead of throwing. And React renders nothing at all for {undefined} (not the literal string “undefined”), so the page is just missing a phrase rather than crashing.
It only guards null and undefined — with bookedCabDetails as {}, ?.name is still undefined, but that is a missing property, not a crash. ?. protects against the property access itself blowing up.
Follow-up: “Should I add ?. everywhere then?” — no. Sprinkling it everywhere flattens the signal “this might be absent” into noise, and the places that genuinely can be empty stop standing out.
The rule: add it only where the value really can be missing. Here bookedCabDetails starts as null, so it earns one; ride.name comes from iterating records that always have a name to be in the array at all, so it does not.
cab-booking-context/src/components/CabConfirmation/CabConfirmation.jsx动手做Get your hands on it
填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.
[Fusion, Accord, Highlander, Explorer](最旧 → 最新)。 断言要求:3 条、顺序Explorer / Highlander / Accord、Fusion 不在 DOM 里。The history is [Fusion, Accord, Highlander, Explorer] (oldest → newest). The assertions want: 3 entries, in the order Explorer / Highlander / Accord, and Fusion not in the DOM.这题是多选。More than one answer is correct.
- 从 useCabContext() 里读 rideHistoryRead rideHistory out of useCabContext()
- 取最新三条并让最新的排在最上面(slice(-3).reverse() 或等价写法)Take the three newest and put the newest at the top (slice(-3).reverse() or an equivalent)
- 不许在 rideHistory 上直接调 reverse / sort —— 那会原地改 stateNever call reverse or sort on rideHistory directly — that changes the state in place
- 每条记录一个 <li data-testid="history-cabs">,里面有车名和 $价格One <li data-testid="history-cabs"> per record, holding the cab name and the $price
- 空历史显示 <p data-testid="no-ride-title">No ride history yet.</p>,且此时不渲染列表An empty history shows <p data-testid="no-ride-title">No ride history yet.</p> and renders no list
- key 不能只用 ride.id —— 同一辆车可以被订两次The key cannot be ride.id alone — the same cab can be booked twice
看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.
初学者常见的几种写法错误Mistakes beginners actually make
下面每一段都是「能编译、但结果不对」或者「一跑就炸」的真实写法。先自己看出问题在哪,再看解释。Every snippet below either compiles and gives the wrong answer, or blows up on the first run. Spot the problem yourself before reading the explanation.
reverse() 把 state 数组就地翻成「最新 → 最旧」, React 不知道(引用没变)。然后下一次
[...rideHistory, details]把新记录接在已经翻转过的数组尾部 —— 也就是接在「最旧的那一头」,顺序从此彻底乱。更麻烦的是每次渲染都翻一次:
StrictMode 下开发模式渲染两次, 翻两次抵消,你在开发时看不出任何异常。把「读」和「改」分清楚 —— 渲染函数里只许读。The result is right this time and wrong the next time. reverse() turns the state array itself into newest to oldest, and React does not notice, because the reference did not change.The next
[...rideHistory, details] then adds the new entry to the end of an array that is already reversed, which is the oldest end, and from that point the order is broken.Worse, it reverses once on every render. Under
StrictMode development renders twice, the two reversals cancel each other out, and you see nothing wrong while developing. Keep reading and changing apart: a render function may only read.useState(null) 的初始值就是 null。在完整流程里撞不到(走到确认页时一定已经选过车), 所以四个测试都过 ——又一次「测试通过 ≠ 做对了」。但只要有人给
CabConfirmation 写一个组件级单测, 或者以后加一个「查看上次行程」的入口,它就是白屏。 React 没有默认的 error boundary, 渲染时抛错会让整棵树卸载。初始值是 null 的 state,读它的属性就该配
?.。The initial value of useState(null) is null. The complete flow never reaches it, because by the time you are on the confirmation page a cab has been chosen, so all four tests pass. This is another case where passing tests does not mean correct code.But as soon as somebody writes a component test for
CabConfirmation, or a “view last ride” entry point is added later, the screen is blank. React has no default error boundary, and an error thrown during render removes the whole tree.When a state starts as null, reading its properties needs
?..换一道题也能用Works on other problems too
考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.
- slice(-3) 是最后三条,slice(0, 3) 是最前三条 —— 方向写反测试 4 才抓得住。slice(-3) is the last three, slice(0, 3) is the first three. Only test 4 catches the wrong direction.
- reverse() 原地修改;slice(-3).reverse() 安全是因为 slice 先给了新数组。reverse() changes the array in place; slice(-3).reverse() is safe because slice returns a new array first.
- 直接 rideHistory.reverse() 会翻掉 state,且 StrictMode 下开发时看不出来。Calling rideHistory.reverse() reverses the state itself, and under StrictMode you cannot see it while developing.
- sort / reverse / splice / push 都是原地改;slice / map / filter / concat 返回新数组。sort, reverse, splice and push all change the array in place; slice, map, filter and concat return a new array.
- bookedCabDetails 初始 null,所以 ?.name 那个问号不能省。bookedCabDetails starts as null, so the question mark in ?.name cannot be dropped.