31

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!

Thomas
  • 735
  • 1
  • 9
  • 15
  • 6
    In a typical OOP language you achieve what you want with things like [composition](https://en.wikipedia.org/wiki/Object_composition) and [inheritance](https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming)). – Marty Oct 18 '16 at 09:07
  • 1
    So you want to assign these methods on the class prototype instead? If so, make it global if it's not – Klaider Oct 18 '16 at 09:10
  • 1
    It's not a good idea to define class members (methodB, methodC) in a different file than the class definition itself. But you can put separate functionality in its own class file. For example, if methodC contains a lot of calculator code, you could create a separate calculator class and call its methods from methodC. – Kokodoko Oct 18 '16 at 15:28
  • 3
    Possible duplicate of [Splitting up class definition in ES 6 / Harmony](http://stackoverflow.com/questions/27956779/splitting-up-class-definition-in-es-6-harmony) – JBCP Jan 29 '17 at 04:31

6 Answers6

27

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.

Frxstrem
  • 34,562
  • 9
  • 73
  • 106
  • Inspired by your answer: https://stackoverflow.com/a/62142995/1599699 – Andrew Jun 02 '20 at 00:27
  • 6
    this is great, but for Typescript you can't make methodA be protected/private on the main class (has to be public), and 'this' becomes any in your implementation so you loose code hint. fine for ES6, but any suggestions for TS users ? – Alain Dumesny Nov 22 '20 at 15:44
4

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));
      }
    }
  }
}
Erez
  • 71
  • 4
3

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.

Andrew
  • 4,757
  • 1
  • 43
  • 62
1

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]
    })
  }
}
Daniel Garmoshka
  • 4,963
  • 36
  • 38
1

My solution is similar to the one by Erez (declare methods in files and then assign methods to this in the constructor), but

  • it uses class syntax instead of declaring constructor as a function
  • no option for truly private fields - but this was not a concern for this question anyway
  • it does not have the layer with the .apply() call - functions are inserted into the instance directly
  • one method per file: this is what works for me, but the solution can be modified
  • results in more concise class declaration

1. Assign methods in constructor

C.js

class C {
  constructor() {
    this.x = 1;
    this.addToX = require('./addToX');
    this.incX = require('./incX');
  }
}

addToX.js

function addToX(val) {
  this.x += val;
  return this.x;
}

module.exports = addToX;

incX.js

function incX() {
  return this.addToX(1);
}

module.exports = incX;

2. Same, but with instance fields syntax

Note that this syntax is a Stage 3 proposal as of now.
But it works in Node.js 14 - the platform I care about.

C.js

class C {
  x = 1;
  addToX = require('./addToX');
  incX = require('./incX');
}

Test

const c = new C();
console.log('c.incX()', c.incX());
console.log('c.incX()', c.incX());

Danylo Fedorov
  • 413
  • 5
  • 8
-2

Other answers show the class can be normally defined once, made visible to other files and supplied with methods later. It is made visible either in the global object or through the export object (in CommonJS).

You may also opt to the ECMAScript 3 model instead of the ECMAScript 2015 class definition.

fooimpl1.js

function Bar() {}

function Foo(a, b) {}

// Foo extends Bar
Foo.prototype = Object.create(Bar.prototype)

Foo.prototype.mA = function(a, b) {}

exports.Foo = Foo

fooimpl2.js

var {Foo} = require('./fooimpl1')

Foo.prototype.mB = function() {a, b}

As a special case, invoke super constructor with SuperClass.call(this) and define virtual properties using Object.defineProperty():

Object.defineProperty(Foo.prototype, 'x', {
    get: function() {
        // Compute value
    },

    set: function(value) {
        // Compute value
    },
})
Klaider
  • 3,565
  • 3
  • 23
  • 52