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

时间:2016-05-07 03:23:55

标签: javascript

如果我希望我的课程是不可变的,我知道我可以使用Object.freeze()。现在如果我希望我的对象在构造之后是不可变的,我会将Object.freeze(this)作为最后一行放入我的构造函数中。但是现在,如果我想要将其子类化,我无法添加更多参数,因为在调用this之前我无法调用super并且在调用super之后调用{39}}&#39} ; s不可变:

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调用的,以便跳过冻结并将冻结留给子类但是这会让它继续冻结到子类。我怎么能最好地接近这个?有工厂吗?

3 个答案:

答案 0 :(得分:5)

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

<new columns as key>

或者,如果您想坚持使用类语法,可以使用 new.target 来检查 A 中的构造函数调用是否为 super < / em>是否打电话:

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 });

答案 1 :(得分:0)

您可以使用proxy's construct trap添加此功能,基本上不是调用您调用中间函数的每个类的构造函数,该函数应该添加您需要的功能并创建实例

让我解释一下如何在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
  4. 我们可以使用以下

    模仿此行为
    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'
    

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

    &#13;
    &#13;
    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'
    &#13;
    &#13;
    &#13;

    在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 demo

    编辑:正如@nils注意到Proxies cannot be transpiled/polyfilled due to limitations of ES5,请参阅compatibility table以获取有关支持此技术的平台的更多信息

答案 2 :(得分:0)

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

它是这样的:不要在构造函数中冻结,只是在实例化中冻结。 E.g:

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

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

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

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

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

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(…);