【问题标题】:Clone private fields of class to implement immutable pattern克隆类的私有字段以实现不可变模式
【发布时间】:2022-11-11 18:53:19
【问题描述】:

我正在尝试为 React 应用程序使用带有私有字段的 JS 类(因为我仍然觉得到处使用裸 Object 实例很奇怪)。 React 使用不可变状态的概念,因此我必须克隆我的对象才能更改它们。我正在使用私有字段 - 使用 getter 和 setter 来访问它们。我遇到的问题是私有字段不会在 Firefox、Chrome 或 Node.js 中被克隆。令人讨厌的是,我的 React 项目的 Jest 设置出现了误报,该代码在单元测试中按预期工作。

有没有办法解决这个问题?否则,看起来我必须放弃我的一些(感知的)封装安全性并改用下划线前缀的“私有”字段。

这是我的克隆功能:

const objclone = obj => {
  const cloned = Object.assign(
    Object.create(
      Object.getPrototypeOf(obj),
      Object.getOwnPropertyDescriptors(obj),
    ),
    obj,
  );

  return cloned;
};

这会克隆 getter 和 setter 以及对象属性,并且在我使用私有字段之前似乎运行良好。

例子:

class C {
  #priv;

  constructor() {
    this.#priv = 'priv-value';
  }

  get priv() { return this.#priv; }
}

const obj = new C();
console.log("obj.priv", obj.priv);

const cloned = objclone(obj);
console.log("cloned.priv", cloned.priv);

尝试访问 cloned.priv 时显示的错误消息:

火狐:

未捕获的类型错误:无法访问私有字段或方法:对象不是正确的类

铬和节点:

未捕获的类型错误:无法从类未声明的对象中读取私有成员#priv

【问题讨论】:

  • 如果您的所有私有字段都有 getter 和 setter,那么无论如何您都没有任何封装安全性。
  • a) Object.create 不创建私有插槽,您需要为此调用构造函数 b) 您的 Example 类没有 .priv 的设置器 c) Object.assign 不复制继承的、不可枚举的属性
  • 我省略了 setter 以保持代码紧凑。添加时它不会改善事情。您关于封装的陈述对于简单的 getter 和 setter 来说是正确的,但在某些用例中,肯定会想要在其中做更多的事情。根据 IDE,它也可能在代码导航方面有所不同。
  • 私有字段是私有的。如果您可以使用函数复制它们,它们就不会是私有的。
  • 到目前为止,我得到的是:突然 JS 决定实施适当的 OOP,这是我不能(也不应该)使用 JS 工具克隆的一件事。我必须以传统方式做事,使用复制构造函数(这只是一个对 JS 没有特殊意义的类方法) - 编辑:实际上是克隆函数。或者......我将只使用下划线约定,并希望如果有人试图访问它们,它会尖叫“错误代码”。一直以来,class 仍然是下面的对象,除了私有字段之外没有什么特别的......除非转译器没有实现正确的语义。正确的?

标签: javascript clone private es6-class class-fields


【解决方案1】:

我解决了。它不像我想的那么简单——我不知道它是否可以变得更简单,但它对我来说看起来不错。

解决问题的关键:

  • 同一类的对象可以访问彼此的私有字段
  • 让对象定义其私有字段的唯一方法是调用其构造函数。

我创建了一个Cloner 类,它可以克隆普通的 JS 对象,也可以克隆实现以下两个接口之一的对象:cloneMecopyOthercloneMe 接口允许对象创建克隆、填充它并返回它,而copyOther 接口允许克隆器调用new,这会导致克隆代码略少。

一个对象必须实现这些接口之一,并且它负责手动复制私有字段。运气好的话,精神开销是最小的。

我使用 Symbol 来防止标识符冲突。我希望我做对了,因为我以前从未使用过它。

class Cloner {
  static cloneMe = Symbol('clone');
  static copyOther = Symbol('copy');

  static clone(obj, init = []) {
    // reject non-Object input
    if (!(obj instanceof Object)) {
      throw new Error(`Cannot clone non-Object of type ${typeof(obj)}`)
    }

    // classic cloning
    if (!obj[this.cloneMe] && !obj[this.copyOther]) {
      return Object.assign(Object.create(
          Object.getPrototypeOf(obj),
          Object.getOwnPropertyDescriptors(obj),
        ),
        obj,
      );
    }

    const cloned = Object.assign(new obj.constructor(...init), obj);

    // self-cloning object
    if (obj[this.cloneMe]) {
      return obj[this.cloneMe](...init);
    }

    // copier object
    // ask the cloned object to copy the source
    cloned[this.copyOther](obj);

    return cloned;
  }

示例cloneMe 实现:

class MyClonerClass {
  #priv;

  constructor(init) {
    this.#priv = init;
  }

  [Cloner.cloneMe](...init) {
    const cloned = new this.constructor(...init);
    cloned.#priv = this.#priv;
    return cloned;
  }

  get a() {
    return this.#priv;
  }

  set a(value) {
    this.#priv = value;
  }
}

示例copyOther 实现:

class MyCopierClass {
  #priv;

  constructor(init) {
    this.#priv = init;
  }

  [Cloner.copyOther](src) {
    this.#priv = src.#priv;
  }

  get a() {
    return this.#priv;
  }

  set a(value) {
    this.#priv = value;
  }
}

用法:

const copySrc = new MyCopierClass('copySrc.#a');
const copyDst = Cloner.clone(copySrc);
copyDst.a = 'copyDst.#a';
console.log(copySrc.a);
console.log(copyDst.a);

const cloneSrc = new MyClonerClass('cloneSrc.#a');
const cloneDst = Cloner.clone(cloneSrc);
cloneDst.a = 'cloneDst.#a';
console.log(cloneSrc.a);
console.log(cloneDst.a);

此处未显示的是Cloner.cloneinit 参数。如果构造函数期望某些参数存在,并且裸构造函数不起作用,则可以使用它。

cloneMe 接口可以通过克隆器获取init,或者可以根据内部状态提供自己的接口,从而很好地封装和靠近。

额外学分

在弄清楚这一点的同时,我想出了一种方法来简化克隆代码,方法是将私有字段保存在字典中。这粉碎了 TC39 对无法添加或删除的私有字段的固定编译时列表的希望和梦想,但它使事情变得更加 Javascript-y。看看copyOther 的实现——这几乎就是全部了。

class WeirdPrivPattern {
  #priv = {}

  constructor(a, b) {
    this.#priv.a = a;
    this.#priv.b = b;
  }

  get a() {return this.#priv.a;}
  set a(value) {this.#priv.a = value;}

  get b() {return this.#priv.b;}
  set b(value) {this.#priv.b = value;}

  [Cloner.copyOther](src) {
    this.#priv = {...src.#priv}
  }
}

关于深度克隆的说明:它超出了此答案的范围。我不担心深度克隆。如果没有发生突变,我实际上依靠子对象来保持其身份。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-01-31
    • 2023-03-15
    • 2010-11-01
    • 2020-06-26
    • 2016-06-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多