How to add methods from a class to an existing object of another class?

I had one big class that I split in two and load them separately on a page to improve performance. Core part is loaded first synchronously as it contains critical functionality while Extension (non-critical functionality) loads later in the page asynchronously.

I want only one object which contains functionality of both classes. But by the time Extension loads, there’s already an object of Core. How do I add functionality from Extension onto the object of Core?

I’m using a Gulp based asset pipeline with

  1. Rollup - to bundle the JS from different files into one file
  2. Babel - to transpile ES6 to ES5
  3. Uglify - to minimize the JS

Here’s my directory structure:

js
|_ _classes
|   |_ ab.js
|   |_ cd.js
|_ core.js
|_ ext.js

I have set the gulp build task to ignore files in _classes directory. Rollup parses the import statements to bundle the code.

This is what I have in core.js

//core.js
import AB from './_classes/ab.js';

window.oab = new AB();

and this is ext.js

//ext.js
import CD from './_classes/cd.js';

window.oab.prototype = CD;

This is the Core class

// file ab.js
class AB {

    constructor() {
        this.car = 'McLaren';
        this.fruit = 'Mango';
    }

    getCar() {
        return this.car;
    }

    getFruit() {
        return this.fruit;
    }

}

and this is the Extension class

//file cd.js
class CD {

    constructor() {
        this.plane = 'Gulfstream';
    }

    getPlane() {
        return this.plane;
    }

}

I’m trying to get this to work:

console.log( window.oab.getCar() );  // prints McLaren
console.log( window.oab.getFruit() );  // prints Mango
console.log( window.oab.getPlane() );  // prints Gulfstream

Now I can very well import the AB class in CD class, set CD class to extend AB and that will give me what I want. But with my current gulp pipeline setup, that would mean that Rollup would bundle a copy of class AB with class CD as well and class AB has already loaded earlier.

Due to Rollup, Babel & Uglify, the class names AB & CD etc don’t persist, so I cannot assume AB being available in CD for me to extend without importing it first and importing it would mean it being bundled with CD .

Can anyone suggest a solution?

I am a bit confused with the idea that some how splitting the classes improves performance (in most cases it doesn’t. You really should only split if the functionality truly lends itself to distinct objects). In most cases a class should stand on its own. That is the whole idea of classes. Now once they are stand alone and self contained, you have a few options…

  1. You extend a base class with your derived class if they second class is indeed a specialized version of the base.

  2. You can compose one class in the other. Here one class contains the reference to the other. Of course this leads to some coupling between the two, but that is often alright if it makes true sense.This could be injected too which might be a nice solution.

  3. You import them both and declare them both and use them together (sounds like this might not be what you are after)

  4. You can create an instance of the first one and just dynamically assign the methods of the second to the first. Remember you can go

let example1 = new AB();
let example2 = new CD();
example1.getPlane() = example2.getPlane();

This option is not exactly something you should do, but you can if you really wanted. I would focus on just making sure you have stand alone classes. Notice here that I have not made any reference to your build pipeline because that should be somewhat separate and a non factor in how you are organizing your classes from a code perspective. :slight_smile:

Its an existing library with a collection of utility functions. Its the first JS that loads first thing on the page and all subsequent JS assumes availability of that object. But over time this library has become a bit bloated with functions that really don’t need to load in page head and can load in the footer asynchronously. So yes, that will help in page load performance a fair amount.

Also, its in use for years in quite a lot of code, I can’t create two objects for both these classes as that will break the existing code.

Hi @amitgupta, technically you might monkey patch your main class like so:

import { AB } from './classes/ab.js'

const ab = new AB()

import('./classes/cd.js').then(({ CD }) => {
  const { constructor, ...prototypePatch } = Object.getOwnPropertyDescriptors(CD.prototype)

  // Define all property descriptors (except for the constructor) of the BC prototype
  // on the AB prototype, and assign the own properties of a newly created CD instance
  Object.defineProperties(AB.prototype, prototypePatch)
  Object.assign(ab, new CD())

  console.log(ab.getPlane()) // -> Gulfstream
})

However I agree with @Martyr2 that splitting classes this way sounds like premature optimisation and probably creates more problems than it solves – for one, you’d always have to check if a given method already exists on the class instance or not. IMHO a better solution would be to keep the classes separate, and implement a mediator class that provides a common, reliable interface.

Yes this would also be a better option; you might look into code splitting so you don’t have to load the same asset twice.

@m3g4p0p Your suggested monkey patch wouldn’t work for the reasons I mentioned in my original post.

However, I applied the following monkey patch in ext.js and this gives me what I’m looking for.

import CD from './_classes/cd.js';

( function() {

	let i;
	let ocd = new CD();
	let ocd_methods = Object.getOwnPropertyNames( ocd.__proto__ ).filter( p => {
		return ( 'function' === typeof ocd.__proto__[ p ] );
    });

	Object.assign( window.oab, ocd );	// this assigns only properties of ocd into window.oab and not the methods

	for ( i in ocd_methods ) {

		let method = ocd_methods[ i ];

		if ( '__proto__' === method || 'constructor' === method ) {
			continue;
		}

		window.oab.__proto__[ method ] = ocd[ method ];

	}

} () );

console.log( window.oab.getCar() );  // prints McLaren
console.log( window.oab.getFruit() );  // prints Mango
console.log( window.oab.getPlane() );  // prints Gulfstream

Glad you got it solved! Just out of interest though, which reason would that be? FWICT the main difference is that you’re assigning all methods (and only methods) in the prototype chain, whereas I was defining all properties as described in the direct prototype. So as it is you’d be missing getters and setters, I’d be missing inherited prototypes.

This reason:

Your solution has

Object.defineProperties(AB.prototype, prototypePatch)

which is referencing class prototype and due to use of Uglify, the class name AB is not available for use when it is time to load class CD.

Perhaps using object of AB would work here just as same, I’m not sure. TBH I did not try your approach as I got my monkey-patch to work by the time I saw your reply. :slight_smile:

My solution copies the whole object - both properties and methods. It uses Object.assign() to copy over all properties and then runs the loop to copy over methods. :slight_smile:

This sounds interesting, I’ll look it up.

Yes, this is something I would like to end up with finally. The current approach is just a stop gap measure for the moment.

Ah ok, thx for the clarification… but no, property names (to which babel transpiles named exports) are not getting renamed.

What I was getting at are getters and setters on the prototype, such as e.g.

class Person {
  constructor (firstname, lastname) {
    this.firstname = firstname
    this.lastname = lastname
  }

  get fullname () {
    return `${this.firstname} ${this.lastname}`
  }
}

const john = new Person('John', 'Doe')
const copy = Object.assign({}, john)

console.log(john.fullname) // -> "John Doe"
console.log(copy.fullname) // -> undefined

Not meaning to split hairs, but maybe something to consider. :-)

I haven’t used rollup myself, but it seems it does that automatically as soon as you’re using dynamic imports…

Ah ok, good point. Right now that’s not a concern in the library I’m splitting, for which I needed this approach, since its just a collection of static methods - a utility library. But it is a good point to consider.

Yeah, I haven’t had time to look more in-depth on Rollup. I’ll look into it for sure.

1 Like

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.