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

NGRx技术问题:使用扩展运算符时默认值为何被覆盖?

解决NGRx选择器中默认ViewModel被扩展运算符覆盖的问题

这个问题其实是JavaScript引用类型特性导致的典型坑,我来帮你拆解原因和解决办法:

为什么默认值会被覆盖?

首先,你的defaultManagersVM是一个包含引用类型属性的对象:byId是对象,allIds是数组,这两者在JavaScript中都是引用类型,而非值类型。

当你在NGRx选择器中做以下操作时,就会意外修改到全局的defaultManagersVM:

  1. 直接返回默认值的引用:比如空状态时直接return defaultManagersVM,后续如果组件或其他逻辑修改了这个返回的VM(比如vm.allIds.push('new-id')),就会直接修改原默认对象的属性,因为它们指向同一个内存地址。
  2. 浅拷贝未覆盖内部引用类型:即使你用了扩展运算符...defaultManagersVM,这只是浅拷贝——外层对象是新的,但内部的byId和allIds仍然引用着defaultManagersVM里的原对象/数组。如果后续逻辑修改了这些内部属性,同样会污染全局默认值。

举个错误示例:

// 错误写法:直接返回默认值引用 + 浅拷贝内部属性
const selectManagersVM = createSelector(
  selectManagers,
  (managers) => {
    if (!managers.length) return defaultManagersVM; // 直接返回引用
    return {
      ...defaultManagersVM,
      byId: { ...defaultManagersVM.byId, ...managers.reduce((acc, m) => ({...acc, [m.id]: m}), {}) },
      allIds: [...defaultManagersVM.allIds, ...managers.map(m => m.id)]
    };
  }
);

如果此时有代码修改了选择器返回的allIds数组,defaultManagersVM.allIds也会跟着变。

解决办法

1. 始终返回新的ViewModel实例

不管是空状态还是有数据的状态,都不要复用defaultManagersVM的引用,而是每次创建新的对象:

const selectManagersVM = createSelector(
  selectManagers,
  (managers) => {
    if (managers.length === 0) {
      // 返回全新的空VM,而非引用默认值
      return {
        byId: {},
        allIds: []
      };
    }
    // 有数据时直接生成新的VM
    return {
      byId: managers.reduce((acc, manager) => ({ ...acc, [manager.id]: manager }), {}),
      allIds: managers.map(m => m.id)
    };
  }
);

如果想复用默认值的结构,可以封装成一个工厂函数,每次调用都返回新对象:

// 工厂函数:每次调用都生成新的默认VM
const createDefaultManagersVM = (): ManagersVM => ({
  byId: {},
  allIds: []
});

// 在选择器中使用
const selectManagersVM = createSelector(
  selectManagers,
  (managers) => {
    if (!managers.length) return createDefaultManagersVM();
    // ...其他逻辑
  }
);

2. 完整覆盖引用类型属性(如果必须基于默认值扩展)

如果你一定要基于defaultManagersVM来扩展ViewModel,要确保内部的引用类型属性也被重新创建,彻底切断和原默认值的引用:

const selectManagersVM = createSelector(
  selectManagers,
  (managers) => {
    if (!managers.length) {
      // 即使是空状态,也创建新的内部属性
      return { ...defaultManagersVM, byId: {}, allIds: [] };
    }
    return {
      ...defaultManagersVM,
      // 重新创建byId对象,而非复用默认值的引用
      byId: managers.reduce((acc, manager) => ({ ...acc, [manager.id]: manager }), {}),
      // 重新创建allIds数组
      allIds: managers.map(m => m.id)
    };
  }
);

3. 遵循NGRx不可变操作原则

记住,NGRx的状态和衍生的ViewModel都应该是不可变的。永远不要直接修改从选择器返回的VM对象/数组,而是通过触发Action更新状态,再由选择器生成新的VM。这样从根源上避免意外修改默认值的问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.26 09:50:52