Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Split a Javascript class (ES6) over multiple files?

I have a Javascript class (in ES6) that is getting quite long. To organize it better I'd like to split it over 2 or 3 different files. How can I do that?

Currently it looks like this in a single file:

class foo extends bar {
   constructor(a, b) {} // Put in file 1
   methodA(a, b) {} // Put in file 1
   methodB(a, b) {} // Put in file 2
   methodC(a, b) {} // Put in file 2
}

Thanks!

like image 610
Thomas Avatar asked Oct 18 '16 09:10

Thomas


4 Answers

When you create a class

class Foo extends Bar {
  constructor(a, b) {
  }
}

you can later add methods to this class by assigning to its prototype:

// methodA(a, b) in class Foo
Foo.prototype.methodA = function(a, b) {
  // do whatever...
}

You can also add static methods similarly by assigning directly to the class:

// static staticMethod(a, b) in class Foo
Foo.staticMethod = function(a, b) {
  // do whatever...
}

You can put these functions in different files, as long as they run after the class has been declared.

However, the constructor must always be part of the class declaration (you cannot move that to another file). Also, you need to make sure that the files where the class methods are defined are run before they are used.

like image 200
Frxstrem Avatar answered Oct 09 '22 00:10

Frxstrem


I choose to have all privte variables/functions in an object called private, and pass it as the first argument to the external functions.

this way they have access to the local variables/functions.

note that they have implicit access to 'this' as well

file: person.js

const { PersonGetAge, PersonSetAge } = require('./person_age_functions.js');

exports.Person = function () {
  // use privates to store all private variables and functions
  let privates={ }

  // delegate getAge to PersonGetAge in an external file
  // pass this,privates,args
  this.getAge=function(...args) {
    return PersonGetAge.apply(this,[privates].concat(args));
  }

  // delegate setAge to PersonSetAge in an external file
  // pass this,privates,args
  this.setAge=function(...args) {
    return PersonSetAge.apply(this,[privates].concat(args));
  }
}

file: person_age_functions.js

exports.PersonGetAge =function(privates)
{
  // note: can use 'this' if requires
  return privates.age;
}


exports.PersonSetAge =function(privates,age)
{
  // note: can use 'this' if requires
  privates.age=age;
}

file: main.js

const { Person } = require('./person.js');

let me = new Person();
me.setAge(17);
console.log(`I'm ${me.getAge()} years old`);

output:

I'm 17 years old

note that in order not to duplicate code on person.js, one can assign all functions in a loop.

e.g.

person.js option 2

const { PersonGetAge, PersonSetAge } = require('./person_age_functions.js');

exports.Person = function () {
  // use privates to store all private variables and functions
  let privates={ }

  { 
    // assign all external functions
    let funcMappings={
      getAge:PersonGetAge,
      setAge:PersonSetAge
    };


    for (const local of Object.keys(funcMappings))
    {
      this[local]=function(...args) {
        return funcMappings[local].apply(this,[privates].concat(args));
      }
    }
  }
}
like image 43
Erez Avatar answered Oct 09 '22 00:10

Erez


Here's my solution. It:

  • uses regular modern classes and .bind()ing, no prototype.
  • works with modules. (I'll show an alternative option if you don't use modules.)
  • supports easy conversion from existing code.
  • yields no concern for function order (if you do it right).
  • yields easy to read code.
  • is low maintenance.
  • unfortunately does not play well with static functions in the same class, you'll need to split those off.

First, place this in a globals file or as the first <script> tag etc.:

BindToClass(functionsObject, thisClass) {
    for (let [ functionKey, functionValue ] of Object.entries(functionsObject)) {
        thisClass[functionKey] = functionValue.bind(thisClass);
    }
}

This loops through an object and assigns and binds each function, in that object, by its name, to the class. It .bind()'s it for the this context, so it's like it was in the class to begin with.

Then extract your function(s) from your class into a separate file like:

//Use this if you're using NodeJS/Webpack. If you're using regular modules,
//use `export` or `export default` instead of `module.exports`.
//If you're not using modules at all, you'll need to map this to some global
//variable or singleton class/object.
module.exports = {
    myFunction: function() {
        //...
    },

    myOtherFunction: function() {
        //...
    }
};

Finally, require the separate file and call BindToClass like this in the constructor() {} function of the class, before any other code that might rely upon these split off functions:

//If not using modules, use your global variable or singleton class/object instead.
let splitFunctions = require('./SplitFunctions');

class MySplitClass {
    constructor() {
        BindToClass(splitFunctions, this);
    }
}

Then the rest of your code remains the same as it would if those functions were in the class to begin with:

let msc = new MySplitClass();
msc.myFunction();
msc.myOtherFunction();

Likewise, since nothing happens until the functions are actually called, as long as BindToClass() is called first, there's no need to worry about function order. Each function, inside and outside of the class file, can still access any property or function within the class, as usual.

like image 5
Andrew Avatar answered Oct 08 '22 22:10

Andrew


You can add mixins to YourClass like this:

class YourClass {

  ownProp = 'prop'

}

class Extension {

  extendedMethod() {
    return `extended ${this.ownProp}`
  }

}

addMixins(YourClass, Extension /*, Extension2, Extension3 */)
console.log('Extended method:', (new YourClass()).extendedMethod())

function addMixins() {
  var cls, mixin, arg
  cls = arguments[0].prototype
  for(arg = 1; arg < arguments.length; ++ arg) {
    mixin = arguments[arg].prototype
    Object.getOwnPropertyNames(mixin).forEach(prop => {
      if (prop == 'constructor') return
      if (Object.getOwnPropertyNames(cls).includes(prop))
        throw(`Class ${cls.constructor.name} already has field ${prop}, can't mixin ${mixin.constructor.name}`)
      cls[prop] = mixin[prop]
    })
  }
}
like image 1
Daniel Garmoshka Avatar answered Oct 09 '22 00:10

Daniel Garmoshka