解构赋值并不原子化
考虑这样一段用于反转链表的代码:
ts
class ListNode {
val: number;
next: ListNode | null;
constructor(val?: number, next?: ListNode | null) {
this.val = val === undefined ? 0 : val;
this.next = next === undefined ? null : next;
}
}
function reverseList(head: ListNode | null): ListNode | null {
if (!head) return null;
let prev: ListNode | null = null;
let curr: ListNode | null = head;
while (curr) {
[prev, curr, curr.next] = [curr, curr.next, prev];
}
return prev;
}
// Test
const list = [1, 2, 3, 4, 5];
const dummy = new ListNode(NaN);
list.reduce((prev, val) => (prev.next = new ListNode(val)), dummy);
reverseList(dummy.next);在解构赋值的 curr.next 这里报了错:TypeError: Cannot set properties of null (setting 'next')。
奇怪,这里 while 不是要求 curr 不能是 null 吗?说明问题就出在循环体里:解构赋值里的前一对 curr = curr.next 将 curr 变成了 null。
解构赋值总是给我们一种感觉:相比于一个一个赋值,它应该更像是「同时」的
ts
// 不使用解构赋值
const t = a;
a = b;
b = t;
// 使用解构赋值
[a, b] = [b, a];这里向 a 的赋值没有破坏 b = a,不再需要中间变量 t 来临时存储。但是这里的「不破坏」是右值意义的,不是左值意义的!
解构赋值过程中,等号右侧的值确实在复制之前就全部求完了。但是左值引用却是求一个赋值一个,求一个赋值一个。因此,前面的赋值会影响到后面的引用。更糟糕的是,TypeScript 在这里不会报错!
至于为什么不报错:我也没深究,AI 说 TS 的控制流分析(CFA)是「局部启发式」的,不是全局一致的数据流分析。
解决方法有两种:
第一种:会被破坏的放前面。比如:
ts
[prev, curr, curr.next] = [curr, curr.next, prev];
[curr.next, prev, curr] = [prev, curr, curr.next]; 这样 curr.next 先写进去,然后再改动 curr。
第二种:从根源解决,避免在同一个解构赋值里同时操作某个变量和它的属性。直接用中间变量写开来。
ts
const next: ListNode | null = curr.next;
curr.next = prev;
[prev, curr] = [curr, next];