You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何不依赖外部库,用不可变数据类型降低竞态条件风险?

用原生JavaScript实现不可变数据,规避异步竞态条件

不可变数据类型能有效降低JavaScript异步代码里的竞态条件风险——这类问题大多源于全局可变状态。虽说函数式编程能规避可变状态,但JavaScript不是纯函数式语言,自带的Array、Map都是可变引用类型,复制这类结构成本不低,很容易把可变引用泄漏到全局作用域,进而引发各种不可预期的问题。

比如下面这个示例,多个异步操作同时读写同一个数组,就会跑出各种混乱的结果:

const delay = ms =>
  new Promise(resolve => setTimeout(resolve, ms));

async function pushStack(ys, x, ms) {
  await delay(ms)
  ys.push(x);
  return ys;
};

async function popStack(ys, ms) {
  await delay(ms)
  const y = ys.pop();
  return y;
};

async function getLastStack(ys, ms) {
  await delay(ms)
  return ys.length - 1;
};

async function readStack(ys, ms) {
  await delay(ms)
  return ys[await getLastStack(ys, Math.random()*10)];
}

const xs = [1,2,3,4];

Promise.all([
  pushStack(xs, 5, Math.random()*10),
  readStack(xs, Math.random()*10),
  popStack(xs, Math.random()*10)
]).then(([a,b,c]) => {
  console.log(JSON.stringify(a), b, c);
});

/* 可能的输出包括:
[1,2,3,5], 3, 4
[1,2,3,5], 5, 4
[1,2,3,4], 4, 5
[1,2,3,4], 5, 5
[1,2,3,4], 4, 4
*/

下面是不依赖复杂外部库的解决办法:

1. 用原生操作实现不可变读写

核心思路就是绝不直接修改原数据,每次操作都返回新的副本:

数组操作改造

  • 替换push:用扩展运算符[...原数组, 新元素]或者原数组.concat(新元素)生成新数组
  • 替换pop:用原数组.slice(0, -1)拿到去掉最后一项的新数组,单独取最后一项用原数组[原数组.length - 1]
  • 替换splice:结合slice和扩展运算符实现局部修改
  • 读取操作直接基于原数组,不用动它

把上面的示例改成不可变版本:

const delay = ms =>
  new Promise(resolve => setTimeout(resolve, ms));

// 不可变push:返回新数组,原数组不动
async function pushStack(ys, x, ms) {
  await delay(ms)
  return [...ys, x];
};

// 不可变pop:返回新数组和弹出的元素
async function popStack(ys, ms) {
  await delay(ms)
  const popped = ys[ys.length - 1];
  return { newStack: ys.slice(0, -1), popped };
};

async function getLastStack(ys, ms) {
  await delay(ms)
  return ys.length - 1;
};

async function readStack(ys, ms) {
  await delay(ms)
  return ys[await getLastStack(ys, Math.random()*10)];
}

const xs = [1,2,3,4];

Promise.all([
  pushStack(xs, 5, Math.random()*10),
  readStack(xs, Math.random()*10),
  popStack(xs, Math.random()*10)
]).then(([newPushStack, readVal, {newPopStack, poppedVal}]) => {
  console.log(JSON.stringify(newPushStack), readVal, poppedVal);
});

/* 现在输出会稳定符合预期:
[1,2,3,4,5], 4, 4
*/

对象操作改造

  • 替换直接修改属性:用Object.assign({}, 原对象, {要改的键: 新值})或者扩展运算符{...原对象, 要改的键: 新值}生成新对象
  • 嵌套对象修改:逐层生成新副本,比如{...原对象, 嵌套属性: {...原对象.嵌套属性, 要改的键: 新值}}

2. 用闭包封装可变状态,对外暴露安全接口

如果完全用副本的性能开销太大,可以把可变数据藏在闭包里,只对外提供返回新状态的方法,不让外部直接碰原数据:

function createImmutableStack(initialItems) {
  let stack = [...initialItems]; // 闭包里的可变状态,外部拿不到

  return {
    async push(x, ms) {
      await delay(ms);
      stack = [...stack, x];
      return [...stack]; // 只返回副本
    },
    async pop(ms) {
      await delay(ms);
      const popped = stack[stack.length - 1];
      stack = stack.slice(0, -1);
      return { stack: [...stack], popped };
    },
    async getLast(ms) {
      await delay(ms);
      return stack.length - 1;
    },
    async readLast(ms) {
      await delay(ms);
      const lastIdx = await this.getLast(Math.random()*10);
      return stack[lastIdx];
    }
  }
}

// 使用示例
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const stack = createImmutableStack([1,2,3,4]);

Promise.all([
  stack.push(5, Math.random()*10),
  stack.readLast(Math.random()*10),
  stack.pop(Math.random()*10)
]).then(([newPushStack, readVal, {stack: newPopStack, poppedVal}]) => {
  console.log(JSON.stringify(newPushStack), readVal, poppedVal);
});

这种方式既利用了闭包内可变状态的性能优势,又保证外部没法直接修改状态,从根源上避免竞态问题。

3. 浅拷贝与深拷贝的选择

如果是简单数据结构(没有嵌套的引用类型),用浅拷贝(扩展运算符、Object.assign、slice)就够;如果是嵌套结构,需要深拷贝的话,可以用原生的JSON.parse(JSON.stringify(对象))(注意它处理不了函数、Symbol、循环引用),或者自己写个递归深拷贝函数:

function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (Array.isArray(obj)) return obj.map(item => deepClone(item));
  const cloned = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloned[key] = deepClone(obj[key]);
    }
  }
  return cloned;
}

内容的提问来源于stack exchange,提问作者user5536315

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 20:42:22