pery mimon
pery mimon

Reputation: 8335

Inherit from two (or more) build in Objects in JS (Map, EventTarget)

I'm not Noob in JS and I know that technically there is no right way to inherit from multiple classes. so my question is very straightforward

Is there any idea how can I create a class or just an object that acts as it inherits from two builds in JS Native object. especially EventTarget and another object.

I try :

var map = new Map();
var eventtarget = new EventTarget();
mix = Object.create({...Map.prototype, ...EventTarget.prototype});
Object.assign(mix, et , map);

It seems not working because the methods inMap.prototype are not itrables also using Object.assign({}, Map.prototype, ...EventTarget.prototype) as the same effect.

Another try:

class Base5 extends Map{
 constructor(){
    super();
    var eventTarget = new EventTarget();
    Object.assign(this,eventTarget);
 }
}

Base5.prototype = Object.create(Base5.prototype)
Object.assign(Base5.prototype,EventTarget.prototype); 

//    that seem to work
const b5 = new Base5();
b5.set('foo','bar');
//    but... 
b4.addEventListener('fire', _=>_ )
// throw Uncaught TypeError: Illegal invocation at <anonymous>:1:4

This one worked but it not generic

const wm = new WeakMap();

class Base6 extends Map{
 constructor(){
    super();
    wm.set(this, new EventTarget() )
 }
 addEventListener(){ 
    wm.get(this).addEventListener(...arguments)
 }
 dispatchEvent(){
    wm.get(this).dispatchEvent(...arguments)
 }
 removeEventListener(){
   wm.get(this).removeEventListener(...arguments)
 }
}

const b6 = new Base6();
b6.set('foo','bar'); // Map(1) {"foo" => "bar"}
b6.addEventListener('foo', e=>console.log(e) );
b6.dispatchEvent( new Event('foo') ) 

So anyone can come with a better approach?

Maybe Reflect.construct can help here somehow

Upvotes: 0

Views: 227

Answers (3)

pery mimon
pery mimon

Reputation: 8335

@trincot inspired me to research a little bit more and come up with this solution, which improves some aspects of his solution:

  1. Weak bind for inherited function.
  2. Ability to use the Mixed Class as a prototype to extend another custom class.

If you like my answer, don't forget to thumb up his answer too.

Anyway, here is a factory function that extends multiple native classes. This is a start and it probably isn't perfect, so feel free to participate in the improvement of this snippet:

function CreateMixedClasess (...classes) {
  return function Mixed () {
    const instances = [this, ...classes.map(cls => new cls(...arguments))]

    const weakbind = (proxy, fn, obj) => new Proxy(fn, {
      apply (fn, thisArg, props) {
        return fn.apply(proxy === thisArg ? obj : thisArg, props)
      }
    })

    return new Proxy(this, {
      get (_, prop, proxy) {
        obj = instances.find(obj => prop in obj)
        const val = Object(obj)[prop]
        return typeof val === 'function' ? weakbind(proxy, val, obj) : val
      },
      has (_, prop) { // Optional: if you care about the `in` operator
        return instances.some(obj => prop in obj)
      }
    })
  }
}


// class Mixed extends CreateMixedClasess (Map, EventTarget) {}
const Mixed = CreateMixedClasess(Map, EventTarget)
const obj12 = new Mixed()
obj12.set('foo', 'bar')
console.log('Map contains: ', Object.fromEntries(obj12))
obj12.addEventListener('foo', e => console.log('Event object type: ', e.type))
obj12.dispatchEvent(new Event('foo'))
obj12.a = 3
obj12.myfn = function () { console.log(this.a) }
obj12.myfn() // 3
obj12.myfn.call({a: 4}) // 4

class MyAwsomeClass extends CreateMixedClasess (Map, EventTarget) {
  constructor () {
    super(...arguments)
    this.a = 33
  }
  logA () {
    console.log(this.a)
  }
}

const myAwsomeInstance = new MyAwsomeClass(obj12)
myAwsomeInstance.has('foo') // true

Upvotes: 1

pery mimon
pery mimon

Reputation: 8335

For now, and for other that seek solution to that problem I come with this soulution

const wm = new WeakMap();

function Emitter(Base) {
   return class extends Base {
    constructor() {
        super(...arguments);
        wm.set(this, new EventTarget())
    }

    addEventListener() {
        wm.get(this).addEventListener(...arguments)
    }

    dispatchEvent() {
        wm.get(this).dispatchEvent(...arguments)
    }

    removeEventListener() {
        wm.get(this).removeEventListener(...arguments)
    }
  }
}

// how to use
const EmitterableMap = Emitter(Map);

Upvotes: 1

trincot
trincot

Reputation: 350776

You could create a function, that creates private instances of the base classes, and returns a proxy that dispatches property retrievals to one of those objects. The base classes could be passed to the constructor to keep it generic:

createMix(Map, EventTarget)

Several things will remain problematic. One blocking issue is that method calls often need this to be set to the base object for them to work. A work around may be to return a bound method, knowing that this in itself can have undesirable effects (e.g. the customer could not take the method and bind it themselves to something else -- if that made sense at all).

For sure this does not solve all potential issues, but it seems to work in very basic usage:

function createMix(...classes) {
    const obj = {};
    const instances = [obj, ...classes.map(cls => new cls)];
    return new Proxy(obj, {
        get(obj, prop) {
            obj = instances.find(obj => prop in obj);
            const val = Object(obj)[prop];
            return typeof val === "function" ? val.bind(obj) : val;
        },
        has(obj, prop) { // Optional: if you care about the `in` operator
            return instances.some(obj => prop in obj);
        }
    });
}

// Tiny test
const obj = createMix(Map, EventTarget);
obj.set('foo','bar'); 
console.log("Map contains: ", Object.fromEntries(obj));
obj.addEventListener('foo', e => console.log("Event object type: ", e.type) );
obj.dispatchEvent( new Event('foo') );

As this function returns a container object, it will not be instanceof any of the base classes passed to the function.

Upvotes: 1

Related Questions