2

I'd like to dual-purpose an ES6 class constructor as a mixin function.

I have a simple class with a few methods. A simplified example:

class foo {
    constructor() {}

    hi() { console.log('hello'); }
}

In most cases I create instances of this class:

let bar = new foo();
bar.hi(); // hello

Sometimes it's used as a superclass:

class baz extends foo {
    constructor() { super(); }
}

let bar = new baz();
bar.hi(); // hello

But, as the methods can work independent of anything else, it would be nice if I could use the constructor as a mixin, like so:

class foo {
    constructor( mixin ) {
        if ( !new.target ) { // not called with `new`
            if ( !mixin ) throw new TypeError( 'Must supply mixin' );
            mixin.hi = foo.prototype.hi;
            return mixin;
        }
    }

    // ...
}

let bar = {}; // this could be instance of another class, whatever.
foo( bar ); // Class constructor foo cannot be invoked without 'new'
bar.hi();

That's the problem I run in to. Constructors can't be invoked as if they were just a normal function.

Is there any way to stop the constructor from throwing an error when invoked without new, without reverting to an ES5 approach of building classes?

I tried wrapping the class in a normal function, and using new.target (my env is ES6) to detect when new is being used:

function Goo( mixin ) {
   if ( new.target ) return new foo();
   if ( !mixin ) throw new TypeError( 'Must supply mixin' );
   mixin.hi = foo.prototype.hi;
   return mixin;
}

let goo = new Goo(); // I get a new instance of foo
goo.hi(); // hello

let bar = {};
Goo( bar );
bar.hi(); // hello

...but quickly realised that:

class oof extends Goo { // ugh

Also, I'd have to clone static stuff from foo on to Goo, which is meh.

As a fallback I currently use a static mixin() method on the foo class:

class foo {
   static mixin( target ) {
      if ( !target ) throw new TypeError( 'Must supply target' );
      target.hi = foo.prototype.hi;
      return target;
   }
   // ...
}

let bar = {};
foo.mixin( bar ); // Well, it's semantic at least
bar.hi(); // hello

But I'm keen, even just to see if it can be done, to have something that works in these three scenarios:

let ifoo = new foo();
ifoo.hi();

class woo extends foo { /* ... */ }
let iwoo = new woo();
iwoo.hi();

let bar = {};
let ibar = foo( bar );
ibar.hi();

Anyone up for seeing if it's do-able, even if it probably shouldn't be done?

cabbageforall
  • 620
  • 1
  • 5
  • 12
  • 2
    No, it's not possible, but you can [make the `Goo` wrapper approach work with subclassing](https://stackoverflow.com/a/31789308/1048572). – Bergi Jun 16 '19 at 18:27
  • Sounds like a reverse [`Reflect.construct()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/construct) – zer00ne Jun 16 '19 at 19:52

1 Answers1

0

Based on snippet posted by Bergi, I came up with the following function: altEgo().

It creates a mask function, behind which sit an alt (used for normal invocation) and ego (used for new instances). It checks new.target to decide whether to use alt() or new ego(). You can also extends the mask.

// to add an alter ego to fooFn:
// fooFn = altEgo( altFn, fooFn );

function altEgo( alt, ego ) {

    if ( typeof alt !== 'function' ) throw new TypeError( `alt must be a function` );
    if ( typeof ego !== 'function' ) throw new TypeError( `ego must be a function` );

    const mask = function( ...args ) {
        return new.target ? Reflect.construct( ego, args, new.target ) : alt( ego, mask, ...args );
    }

    for ( const property of Object.getOwnPropertyNames( ego ) )
        if ( altEgo.always.has( property ) || !mask.hasOwnProperty( property ) )
            Object.defineProperty( mask, property, Object.getOwnPropertyDescriptor( ego, property ) );

    return mask;
}
altEgo.always = new Set([ 'name', 'length', 'prototype' ]); // properties to copy to mask

This works successfully with all three scenarios mentioned at end of OP.

Here's a testbed in which the ego function is class foo. As you can see, the masked foo works just like the original when it's used as a class and can even be extended by the woo class. Instances of foo and woo behave as expected, and statics also work. If you invoke foo like a normal function, the alt kicks in.

'use strict';

class foo {
    static chk() {
        console.log(`static ${this.name}.chk()`);
    }

    static get getchk() {
        console.log(`static ${this.name}.getchk`);
    }

    constructor( a, b, c ) {
        this.con = true;
        console.log(`new ${new.target.name}()`);
    }

    check() {
        console.log(`${this.constructor.name} inst.check(); inst.con = `+this.con);
    }

    get getcheck() {
        console.log(`${this.constructor.name} inst.getcheck`);
    }
}

console.dir( foo );

function altEgo( alt, ego ) {

    if ( typeof alt !== 'function' ) throw new TypeError( `alt must be a function` );
    if ( typeof ego !== 'function' ) throw new TypeError( `ego must be a function` );

    const mask = function( ...args ) {
        return new.target ? Reflect.construct( ego, args, new.target ) : alt( ego, mask, ...args );
    }

    for ( const property of Object.getOwnPropertyNames( ego ) )
        if ( altEgo.always.has( property ) || !mask.hasOwnProperty( property ) )
            Object.defineProperty( mask, property, Object.getOwnPropertyDescriptor( ego, property ) );

    return mask;
}
altEgo.always = new Set([ 'name', 'length', 'prototype' ]); // properties to copy to mask

let alt = function( ego, mask, target ) {
    console.log( 'alt function' );
    for ( const property of alt.clone )
        Object.defineProperty( target, property, Object.getOwnPropertyDescriptor( ego.prototype, property ) );
    return target;
}
alt.clone = new Set([ 'check', 'getcheck' ]);

foo = altEgo( alt, foo );

console.dir( foo );

console.log('foo =====================================');

foo.chk();
void foo.getchk;
let ifoo = new foo;
ifoo.check();
void ifoo.getcheck;

console.log('woo =====================================');

class woo extends foo {
    constructor() {
        super();
    }
}

woo.chk();
void woo.getchk;
let iwoo = new woo;
iwoo.check();
void iwoo.getcheck;

console.log('bar =====================================');

let ibar = foo( {} );
ibar.check();
void ibar.getcheck;

Note: I also investigated the potential for using Proxy with a construct handler but wasn't able to get it working due to lack of new.target.

cabbageforall
  • 620
  • 1
  • 5
  • 12