具有子类的构造函数中的Object.freeze

Object.freeze in constructor with subclasses

本文关键字:Object freeze 构造函数 子类      更新时间:2023-09-26

如果我希望我的类是不可变的,我知道我可以使用Object.freeze()。现在,如果我希望我的对象在构造后是不可变的,我会把Object.freeze(this)作为最后一行放入我的构造函数中。但现在,如果我想将其子类化,我就不能添加更多的参数,因为我不能在调用super之前调用this,而在调用super之后,它是不可变的:

class A {
  constructor(x) {
    this.x = x
    Object.freeze(this)
  }
}
class B extends A {
  constructor(x, y) {
    this.y = y // nope. No "this" before "super"
    super(x)
    this.y = y // nope. "this" is already frozen
    Object.freeze(this)
  }
}

我想弄清楚构造函数是否是通过super调用的,以便跳过冻结并将冻结留给子类,但这将让子类来冻结或不冻结。我该如何最好地处理这个问题?也许有工厂?

首先,您想要混合类似乎很奇怪,因为类的基本原则基本上是有状态的可变性和不变性。我认为,带组合的可组合工厂函数可能更惯用(可能有比以下更好的方法):

function compose(funcs...) {
  return function(...args) {
    const obj = funcs.reduce((obj, func) = >{
      return func(obj, args);
    }, {});
  }
  return Object.freeze(obj);
}
function a(obj, { x }) {
  obj.x = x;
  return obj;
}
function b(obj, { y }) {
  obj.y = y;
  return obj;
}
const ab = compose(a, b);
ab({ x: 1, y: 2 });

或者,如果你想坚持类语法,你可以使用new.target来检查A中的构造函数调用是否是super调用:

class A {
  constructor(x) {
    this.x = x;
    // Not a super call, thus freeze the object
    if (new.target === A) {
      Object.freeze(this);
    }
  }
}
class B extends A {
  constructor(x, y) {
    super(x)
    this.y = y
    if (new.target === B) {
      Object.freeze(this)
    }
  }
}

您可以使用代理的构造陷阱添加此功能,基本上不用调用每个类的构造函数,而是调用一个中间函数,该函数应该添加您需要的功能,并创建实例

让我解释一下如何先在ES5中完成,然后跳到ES6

function A() {
  this.name = 'a'
}
A.prototype.getName = function () {
  return this.name
}
var instance = new A()
instance.getName() // 'a'

魔术关键字new执行以下

  1. 创建一个空对象,其[[Prototype]]指向构造函数的原型,即Object.getPrototype(emptyObject) === A.prototypeemptyObject.__proto__ === A.prototype
  2. 调用函数A,将空对象设置为其上下文,即函数this内部为空对象
  3. 为我们返回对象(我们不必编写return this

我们可以用下面的来模拟这种行为

function freezeInstance(T) {
  function proxy () {
    // 1.
    var instance = Object.create(T.prototype)
    // 2.
    T.apply(instance, arguments)
    // this check is added so that constructors up
    // in the constructor chain don't freeze the object
    if (this instanceof proxy) {
      Object.freeze(instance)
    }
    // 3.
    return instance
  }
  // mimic prototype
  proxy.prototype = T.prototype
  return proxy
}

如果我们用一个函数调用这个函数,并将内部函数分配给参数T,我们已经有效地创建了与new相同的行为,而且我们还可以在代理中添加我们的自定义功能,即

A = freezeInstance(A)
var instance = new A()
instance.getName() // 'a'
a.name = 'b'
instance.getName() // 'a'

如果你有一个子类,冻结行为不会影响超类,因为我们只做初始调用的特殊功能

function freezeInstance(T) {
  function proxy() {
    var instance = Object.create(T.prototype)
    T.apply(instance, arguments)
    if (this instanceof proxy) {
      Object.freeze(instance)
    }
    return instance
  }
  // mimic prototype
  proxy.prototype = T.prototype
  return proxy
}
function A() {
  this.name = 'a'
}
A.prototype.getName = function() {
  return this.name
}
A = freezeInstance(A)
function B() {
  A.call(this)
  this.name = 'b'
}
B.prototype = Object.create(A.prototype)
B = freezeInstance(B)
var instance = new B()
document.write(instance.getName()) // 'b'
instance.name = 'a'
document.write(instance.getName()) // 'b'

在ES6中,你可以用做同样的事情

function freezeInstance(T) {
  return new Proxy(T, {
    construct: function (target, argumentLists, newTarget) {
      var instance = new target(...argumentLists)
      Object.freeze(instance)
      return instance
    }
  })
}
class A {
  constructor() {
    this.name = 'a'
  }
  getName() {
    return this.name
  }
}
A = freezeInstance(A)
class B extends A {
  constructor() {
    super()
    this.name = 'b'
  }
}
B = freezeInstance(B)
var a = new A()
console.log(a.getName()) // a
a.name = 'b'
console.log(a.getName()) // a
var b = new B()
console.log(b.getName()) // b
b.name = 'a'
console.log(b.getName()) // b

代理的好处是,您不必更改实现,只需将实现封装在其他中即可

ES6演示

编辑:正如@nils所指出的,由于ES5的限制,代理无法被传输/聚合填充,请参阅兼容性表以了解支持该技术的平台的更多信息

我有一个建议的答案,它可能具有某种简单性的优点。

它是这样的:不要在构造函数中冻结,只需在实例化中冻结即可。例如:

class A {
  constructor(…) { … }
}
…
class B extends A {
  constructor(…) { super(…); … }
}
…
let my_b = Object.freeze(new B(…));

这种方法具有额外的优点,即它强烈宣传对象(例如my_b)是冻结的(并且可能是不可变的),并且不仅提供了从A派生类的灵活性,而且还提供了在方便时偶尔对对象执行恶意突变的灵活性。

事实上,提供一个显式地使类的对象不可变的成员函数可能是明智的。例如:

class A {
  constructor(…) { this.m1 = new X(…); … }      
  deepFreeze() { Object.freeze(this); this.m1.deepFreeze(); return this; }
}
…
class B extends A {
  constructor(…) { super(…); this.m2 = new Y(…); … }
  deepFreeze() { super.deepFreeze(); this.m2.deepFreeze(); return this; }
}
…
let my_b = (new B(…)).deepFreeze();

这里的想法是,类A知道如何深度冻结其成员对象(例如m1)以及冻结自身,以便适当地增强其实例的不变性。子类可以以可组合的方式覆盖deepFreeze

为了方便起见,您可以添加一个静态成员函数,为您构造一个实例。例如:

class A {
  constructor(…) { this.m1 = new X(…); … }      
  deepFreeze() { Object.freeze(this); this.m1.deepFreeze(); return this; }
  static deepFrozen(…) { return new this(…).deepFreeze(); }
}
…
class B extends A {
  constructor(…) { super(…); this.m2 = new Y(…); … }
  deepFreeze() { super.deepFreeze(); this.m2.deepFreeze(); return this; }
}
…
let my_b = B.deepFrozen(…);

另一个非常简单的选项是只拆分层次结构中每个类的冻结版本。

因此,如果你有扩展A的类A、B和扩展B的C,只需添加分别继承自A、B、C的类FrozenA、FrozenB、Frozen C,它们本身没有子类。

如果你使用纯javascript,并且没有在元编程方面做任何非常疯狂的事情(例如,为它们的类互操作对象,然后询问另一个对象是否是该类的实例),那么FrozenB在技术上不是FrozenA的子类就无关紧要了,因为该语言不是静态类型的。

我不知道这是否是您的情况,但您可以执行以下操作:

class A{
    constructor(){} 
}
class B extends A{
    constructor(){
        super();
        Object.freeze(this.__proto__);
    }
}

这是最简单的解决方案,它阻止了类属性的添加,注意你不能使用B.prototype;等等;如果你这样做。