在 JavaScript 中混合本身就是对象的对象属性

Mixin object properties that are objects itself in JavaScript

我已经通读了很多 JavaScript 混合设计模式,但没有找到适合我需要的。我所见的大部分内容都使用某种程度的函数,将一个对象的方法复制到另一个对象的原型。对于值属性,这就像使用方法一样工作,但对于本身是对象的属性,它会失败,因为仅复制它们意味着我的 class 的每个实例都将指向此 属性 的同一个对象。 =17=]

我做了一个简单点的代码示例class:

var Point = function ( x, y ) {
    this.x = x;
    this.y = y;
};

Point.prototype = {
    constructor: Point,
    print: function () {
        console.log( this.x + this.offset[0], this.y + this.offset[1] );
    }
};

打印函数使用了一个我还没有声明的偏移量属性,因为它来自一个可转换的class我想混入我的点class:

var Transformable = {
    offset: [ 0, 0 ],
    setOffset: function ( x, y ) {
        this.offset[ 0 ] = x;
        this.offset[ 1 ] = y;
    }
};

为了制作 mixin,我使用了一些像这样的扩展函数:

var extent = function ( target, mixin ) {
    for ( var property in mixin ) {
        if ( !target.prototype[ property ] ) {
            target.prototype[ property ] = mixin[ property ];
        }
    }
};
extent( Point, Transformable );

现在代码可以编译并可以使用了:

var myPoint1 = new Point( 1, 2 );
myPoint1.setOffset( 5, 5 );
var myPoint2 = new Point( 1, 2 );
myPoint1.print();
myPoint2.print();

但它两次都打印“6 7”,因为 myPoint2 中使用的偏移数组与 myPoint1 中使用的相同。

那么我怎样才能实现每个点都有自己的偏移量数组,同时仍然来自混合?它也应该适用于数组以外的每个对象,因为如果不是为了简单的简短示例,我会在这里使用向量 class。

实际上 this answer 完成了我想要的,但作为进一步的要求,我希望仍然能够使用对象文字语法来添加 Point class:[=17= 的方法]

Point.prototype = {
    constructor: Point,
    print: function () {...},
    ...
};

不过,对于 mixin 对象的语法,我是灵活的。

在 ECMAScript6 中,我会使用符号来存储内部数据,并将 public 属性定义为使用该数据的访问器:

var Transformable = (function() {
  var defaultOffset = [0, 0],
      offset = Symbol('offset');
  return {
    get offset() {
      if(!this[offset]) this[offset] = Object.create(defaultOffset);
      return this[offset];
    },
    setOffset: function ( x, y ) {
      this.offset[0] = x;
      this.offset[1] = y;
    }
  };
})();

为了复制描述符,您将需要更复杂的 extend:

var extent = function(target, mixin) {
  Object.getOwnPropertyNames(mixin).forEach(function(prop) {
    var desc = Object.getOwnPropertyDescriptor(mixin, prop);
    Object.defineProperty(target, prop, desc);
  });
};
extent(Point.prototype, Transformable);

那一点你也可以考虑摆脱 setOffset 并为 offset 定义一个 setter 以便你可以使用 myPoint1.offset = [5, 5] 代替。

我有一个可行的解决方案,但我自己认为它有争议。基本上每次我混音时,我都会使用我的 mixin class 的新实例进行复制。所以我实际上不能扩展我的 Point class,而是在构造函数中进行混合。 (使用与问题中相同的范围函数。)

var Transformable = function () {
    this.offset = [ 0, 0 ];
    this.setOffset = function ( x, y ) {
        this.offset[ 0 ] = x;
        this.offset[ 1 ] = y;
    };
};

var Point = function ( x, y ) {
    this.x = x;
    this.y = y;
    extent( this, new Transformable() );
};

Point.prototype = {
    constructor: Point,
    print: function () {
        console.log( this.x + this.offset[0], this.y + this.offset[1] );
    }
};

var myPoint1 = new Point( 1, 2 );
myPoint1.setOffset( 5, 5 );
var myPoint2 = new Point( 1, 2 );

myPoint1.print();
myPoint2.print();

与我在问题中链接到的多重继承答案相比,我认为它更具可读性,因为我可以使用 Point class 的对象文字语法,而且我只需要声明一次从哪里到混合。

但是性能可能是个问题,因为我没有混合到 class 中,而是混合到每个实例中,尽​​管我不知道这有多严重。

我欢迎任何明确的论证为什么这可能是一个糟糕的解决方案。

正在回答您的第 2 个 post;不,这个解决方案没有什么不好的,除了 Bergi 已经提到的——一个基于函数的 Mixin 永远不应该被实例化,而是总是通过 callapply 应用于一个对象。 性能不会成为问题。 JS 在处理对象、函数委托和处理闭包方面快如闪电。 你的胜利是关注点分离、代码重用、仍然坚持 ES3 语言核心(不需要第 3 方库)、能够引入额外的状态,同时控制如何公开或隐藏这些额外的状态。

重构后的例子随原文提供post:

var Transformable = function () {       // a function based mixin approach
    var offset = [0, 0];                // is library agnostic and already
                                        // at ES3 language core level enables
    this.setOffset = function (x, y) {  // composition as well as creation of
        offset[0] = x;                  // and/or passsing around additional
        offset[1] = y;                  // state.
    };
    this.getOffsetX = function () {
        return offset[0];
    };
    this.getOffsetY = function () {
        return offset[1];
    };
};

var Point = function (x, y) {

    this.x = x;
    this.y = y;
                                // applying the `Transformable` mixin
    Transformable.call(this);   // onto the new `Point` instance.
};
Point.prototype = {
    constructor: Point,
    print: function () {
        console.log(this.x + this.getOffsetX(), this.y + this.getOffsetY());
    }
};


var myPoint1 = new Point( 1, 2 );
myPoint1.setOffset( 5, 5 );

var myPoint2 = new Point( 1, 2 );

myPoint1.print(); // 6 7
myPoint2.print(); // 1 2

仍然以 OP 的示例为起点,对于 Point 原型 print 方法,下一个方法可能更清晰,不会对既不属于 [=] 的方法做出任何假设16=] 也不是 prototype ...

var Transformable = function () {
    var offset = [0, 0];

    this.setOffset = function (x, y) {
        offset[0] = x;
        offset[1] = y;
    };
    this.getOffsetX = function () {
        return offset[0];
    };
    this.getOffsetY = function () {
        return offset[1];
    };

    return this;
};


var Point = function (x, y) {

    this.x = x;
    this.y = y;

    return this;
};
Point.prototype.print = function () {

    console.log(this.x, this.y);
};


var TransformablePoint = function () {

    return Transformable.call(Point.apply(this, arguments));
};
TransformablePoint.prototype.print = function () {

    console.log(this.x + this.getOffsetX(), this.y + this.getOffsetY());
};


var myPoint1 = new TransformablePoint( 1, 2 );
myPoint1.setOffset( 5, 5 );

var myPoint2 = new TransformablePoint( 1, 2 );

myPoint1.print(); // 6 7
myPoint2.print(); // 1 2