Search code examples
javascriptconstructorprototypemixinscomposition

Mixin object properties that are objects itself in JavaScript


I've read through a lot of JavaScript mixin design patterns, but didn't found one that suits my needs. Most of what I've seen uses some extent function that copies methods of one object to the prototype of another. For value properties this works just like with a method, but it fails for properties which are objects itself, because just copying them means every instance of my class will point to the same object for this property.

I made a code example with a simple point 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] );
    }
};

The print function uses an offset property which I haven't declared yet, because it originates from a Transformable class I want to mixin into my Point class:

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

To make the mixin I use some extent function like this:

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

Now the code compiles and can be used:

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

But it prints "6 7" both of the times, because the offset array used in myPoint2 is the same as in myPoint1.

So how can I achieve that every point gets its own offset array while still originating from the mixin? It should work for every object besides arrays as well, because when not for the simplicity of a short example I would use a vector class here instead.

Actually this answer accomplishes what I want, but as a further requirement I want to be able to still use the object literal syntax for adding the methods of the Point class:

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

I am however flexible as to how the syntax for the mixin object might look like.


Solution

  • Answering your 2nd post; No, there is nothing bad with this solution except from what Bergi already did mention - a function based Mixin should never be instantiated but always be applied to an object by either call or apply. Performance won't be an issue. JS is lightning fast with handling objects, function delegation and dealing with closures. Your win is separation of concerns, code reuse, still sticking to ES3 language core (no need for 3rd party library), being able of introducing additional state whilst having control over how to expose or hide such additional state.

    The refactored example provided with the original 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
    

    Still taking the OP's example as starting point, the next approach might be cleaner for a Points prototypal print method does not make any assumptions about methods that are neither part of the constructor nor of the 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