Skip to content

解构赋值并不原子化

考虑这样一段用于反转链表的代码:

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.nextcurr 变成了 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];