Maui 实践:JavaScript 动态生成集合属性的 get/set 代理

Maui 实践:JavaScript 动态生成集合属性的 get/set 代理

原创 夏群林 2025.11.6

一、背景

在我的数独项目的 SudokuFound 类中,需要管理 8 个集合属性,每个集合都需要:

  1. 「读」:返回副本(避免外部直接修改内部数据);
  2. 「写」:校验数组类型 + 检测变化 + 触发事件;
  3. 「统一逻辑」:所有集合的 get/set 行为完全一致。

如果手动为每个集合写 get/set,会出现 3 个问题:

  • 代码冗余:8 个集合需要写 8 组重复代码,后续新增集合还要手动补;
  • 维护成本高:修改逻辑(比如加校验、改事件)需要改 8 个地方,容易漏改;
  • 易出错:手动写代码可能出现语法错误(比如副本创建遗漏、变化检测不一致)。

二、创意

利用 ES6 的 static 代码块(类加载时执行一次)+ Object.defineProperty,自动为所有「合法集合属性」生成统一逻辑的 get/set 代理,完美解决上述痛点。这是一个 静态代码块 + 动态代理 的思路:

  1. 「筛选合法属性」:自动识别数据载体(如 JsonFound)中所有数组类型的属性(即需要管理的集合);
  2. 「动态生成代理」:为每个合法集合自动生成 get/set 方法,内置「副本返回」「类型校验」「变化检测」「事件触发」逻辑;
  3. 「自动化适配」:后续新增/删除集合属性,无需修改代理逻辑,自动适配。

三、代码

我把这部分代码抽出来,可作为模板直接复用。

  1. 数据载体类,保持纯数据结构,便于前后端通讯
// 纯数据载体:定义需要管理的集合属性(数组类型)
class DataCarrier {
  constructor() {
    this.basicProp1 = "默认值"; // 基础属性(非集合)
    this.basicProp2 = 0;
    // 待管理的集合属性(数组类型)
    this.collection1 = [];
    this.collection2 = [];
    this.collection3 = [];
    // 后续新增集合,只需在这里加一行
  }
}
  1. 带集合属性变化通知功能的轻量包装类,动态生成集合属性代理
class ObservableDataWrapper {
  constructor() {
    this.#data = new DataCarrier();
    // 筛选合法集合属性:仅保留数组类型的属性名
    this.#validCollectionNames = Object.keys(this.#data)
      .filter(key => Array.isArray(this.#data[key]));
  }

  // 私有变量
  #data; // 内部数据载体实例
  #validCollectionNames; // 合法集合名称列表(自动筛选)
  #isEqual; // 深比较工具(用于变化检测)
  #triggerChange; // 事件触发方法(统一触发布局/UI更新)

  // 核心:静态代码块(类加载时动态生成 get/set)
  static {
    // 步骤1:筛选 DataCarrier 中所有数组类型的属性(合法集合)
    const validCollections = Object.keys(new DataCarrier())
      .filter(key => Array.isArray(new DataCarrier()[key]));

    // 步骤2:为每个合法集合动态生成 get/set 代理
    validCollections.forEach(collectionName => {
      Object.defineProperty(this.prototype, collectionName, {
        // get 方法:返回副本,避免外部直接修改内部数据
        get() {
          return [...this.#data[collectionName]];
        },
        // set 方法:统一逻辑(校验+变化检测+更新+事件)
        set(newValue) {
          // 1. 类型校验:必须是数组
          if (!Array.isArray(newValue)) return;
          // 2. 变化检测:深比较新旧数据,无变化则跳过
          if (this.#isEqual(newValue, this.#data[collectionName])) return;
          // 3. 安全更新:存储副本,避免外部引用篡改
          this.#data[collectionName] = [...newValue];
          // 4. 统一事件:触发变化通知(UI/其他模块监听)
          this.#triggerChange();
        },
        enumerable: true, // 支持 for...in 遍历
        configurable: true // 允许后续扩展(如重写)
      });
    });
  }

  // 辅助工具:深比较(简化版,可替换为 lodash.isEqual)
  #isEqual(a, b) {
    if (a === b) return true;
    if (a.length !== b.length) return false;
    return a.every((item, idx) => item === b[idx]); // 复杂对象需扩展
  }

  // 辅助工具:触发事件(统一对外通知)
  #triggerChange() {
    // 实际项目中可替换为事件总线。比如,在我的项目: GameUtils.publishEvent。
    console.log(`集合变化:${JSON.stringify(this.#data)}`);
  }
}

四、价值

  1. 极致精简代码。8 个集合只需写 1 套逻辑,代码量减少 87.5%。新增集合时,仅需在 DataCarrier 中加一行数组定义,无需修改 ObservableDataWrapper

  2. 逻辑绝对一致。所有集合的 get/set 逻辑完全统一,避免手动写代码导致的差异化 bug。如某个集合漏写副本、某个集合未加校验。

  3. 内置数据安全。get 自动返回副本,外部无法通过 manager.collection1.push(xxx) 直接修改内部数据。set 自动存储副本,外部传入的数组引用变化,不会影响内部数据。类型校验 + 变化检测,避免非法数据写入,减少无效事件触发。

  4. 低成本,高复用。外部使用方式,如manager.collection1 读、manager.collection1 = [1,2,3] 写,和普通属性完全一致,无需学习新 API。「多集合属性管理」场景,直接复用本模板即可。

五、避坑

  1. 合法属性筛选要精准。确保 filter(key => Array.isArray(this.#data[key])) 只筛选需要管理的集合,避免把基础属性(如字符串、数字)误判为集合。

  2. 深比较需适配复杂对象。若集合元素是复杂对象,需扩展 #isEqual 方法,比较对象的核心属性(如 x/yfirst/last),避免“引用不同但内容相同”被误判为变化。

  3. 兼容外部操作逻辑。动态生成的 get/set 是“代理”,是在外部赋值时触发 set 逻辑,无需额外调用方法。如我的项目中, GameStates.SudokuFound.InferenceChain = inference 直接生效。

  4. 调试技巧:若集合操作异常,可在 static 代码块中加日志,确认合法集合是否正确筛选:

  • static {
      const validCollections = Object.keys(new DataCarrier())
        .filter(key => Array.isArray(new DataCarrier()[key]));
      console.log("合法集合:", validCollections); // 调试用
      // ... 后续生成代理
    }
    

六、适用

这个技巧特别适合以下场景:

  1. 业务类需要管理 多个结构一致的集合属性
  2. 要求所有集合的 读/写行为统一
  3. 希望 减少冗余代码,降低后续维护成本,特别是频繁新增/修改集合的情形。

七、效果

在我的数独项目中,该技巧实现了:

  • 8 个集合属性仅用 20 行代码完成 get/set 代理,后续新增集合无需修改;
  • 外部操作逻辑不变,兼容旧代码;
  • 数据安全问题(如外部篡改内部集合)被彻底解决;
  • 事件触发统一,UI 模块只需监听 sudoku-found-changed 即可感知所有集合变化。

一次编写,多处可用。这个实践,希望对同仁有所助益。

posted @ 2025-11-06 18:07  zhally  阅读(45)  评论(0)    收藏  举报