Split a Javascript class (ES6) over multiple files?
Asked Answered
P

8

43

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
}
Polio answered 18/10, 2016 at 9:6 Comment(4)
In a typical OOP language you achieve what you want with things like composition and inheritance.Distemper
So you want to assign these methods on the class prototype instead? If so, make it global if it's notPotentate
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.Mascon
Possible duplicate of Splitting up class definition in ES 6 / HarmonyStarinsky
S
37

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.

Swansdown answered 18/10, 2016 at 9:18 Comment(2)
Inspired by your answer: https://mcmap.net/q/24619/-split-a-javascript-class-es6-over-multiple-filesLatvina
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 ?Fussy
L
6

Here's my solution. It:

  • uses regular modern classes and .bind()ing, no prototype. (EDIT: Actually, see the comments for more on this, it may not be desirable.)
  • 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.

Latvina answered 2/6, 2020 at 0:27 Comment(5)
Thanks to Frxstrem's answer below for inspiration.Latvina
Using regular modules, you'll need to export default an object in the file containing the functionsNephelinite
After coming back to this solution of mine fresh, I've come to realize it may actually not be very desirable. This would especially be the case if one were to be creating many instances of these classes, if there were many functions to be bound, or if the code is performance-critical. In such cases, one would likely want to find a way to modify the .prototype of the class, like in Frxstream's answer above.Latvina
The reason for this is that, as the MDN documentation says, "Each object has a private property which holds a link to another object called its prototype." If, then, like in my answer, you're modifying each class instance (per function), that would be slower than simply modifying the prototype one time (per function).Latvina
For more on this, see here: https://mcmap.net/q/24618/-how-to-add-an-method-to-a-class-in-javascript-es6 developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… developer.mozilla.org/en-US/docs/Web/JavaScript/… developer.chrome.com/blog/smooshgateLatvina
A
5

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));
      }
    }
  }
}
Arrhythmia answered 7/11, 2019 at 14:37 Comment(0)
C
3

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]
    })
  }
}
Counterinsurgency answered 7/12, 2020 at 14:32 Comment(0)
S
3

TypeScript Solution

foo-methods.ts

import { MyClass } from './class.js'

export function foo(this: MyClass) {
  return 'foo'
}

bar-methods.ts

import { MyClass } from './class.js'

export function bar(this: MyClass) {
  return 'bar'
}

class.ts

import * as barMethods from './bar-methods.js'
import * as fooMethods from './foo-methods.js'

const myClassMethods = { ...barMethods, ...fooMethods }

class _MyClass {
    baz: string

    constructor(baz: string) {
        this.baz = baz
        Object.assign(this, myClassMethods);
    }
}

export type MyClass = InstanceType<typeof _MyClass> &
    typeof myClassMethods;

export const MyClass = _MyClass as unknown as {
    new (
        ...args: ConstructorParameters<typeof _MyClass>
    ): MyClass;
};
Savior answered 25/10, 2022 at 19:4 Comment(0)
C
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());

Carrigan answered 4/7, 2021 at 11:18 Comment(0)
E
0

You can use Object.defineProperty to add methods/properties manually to your class prototype anywhere in your code. But a more convenient way is making class "extensions" and then defining all their methods/properties descriptors on your class automatically.

The following function enumerates all properties descriptors from a list of classes and defines them on a target class:

function extendClass(target, classes) {
  classes.forEach((item) => {
    Object.getOwnPropertyNames(item.prototype).forEach((name) => {
      if (name !== 'constructor') {
        const descriptor = Object.getOwnPropertyDescriptor(item.prototype, name);
        Object.defineProperty(target.prototype, name, descriptor);
      }
    });
  });
}

Example:

class A {
  valueA = 11;
  a() {
    console.log('A:' + this.valueA);
  }
}

class B {
  b() {
    console.log('B:' + this.valueA);
  }
  get valueB() {
    return 22;
  }
  set valueB(value) {
    console.log('set valueB ' + value);
  }
}

class C {
  c() {
    console.log('C:' + this.valueA);
  }
}

extendClass(A, [B, C]);

const a = new A();
a.a(); // Prints: A:11
a.b(); // Prints: B:11
a.c(); // Prints: C:11
a.valueB; // 22
a.valueB = 33; // Prints: set valueB 33

Eckstein answered 3/4, 2023 at 13:11 Comment(0)
B
0

I think the intent of this question is to discover a coding pattern that does not involve modifying the code inside the class's methods when you reach the point where you discover that it would be a good idea to split the class into multiple files.

The technique used in this example achieves this.

This is the file where we instantiate and use the multi-file class (nothing unusual here).

import { MyMultiFileClass } from './MyMultiFileClass.js'
const mmfc = new MyMultiFileClass()
mmfc.PrintMethod(true)

In this file, called "MyClassPart1.js", we define the class and then extend it by referring to code in another file.

import * as MyClassPart2 from './MyClassPart2.js'

export class MyMultiFileClass {
    constructor() {
        this.myMultiFileClassProperty = [
            "I", "love", "small", "files", "and", "hate", "semi-colons"]
    }
}

MyMultiFileClass.prototype.PrintMethod = MyClassPart2.definePrintMethod()

This file, called "MyClassPart2.js", extends the multi-file class we previously defined. Note that we can use this to access and modify the members of the class.

export function definePrintMethod() {
    return function (upperCaseLoveHate = false) {
        let myString = ""
        this.myMultiFileClassProperty.forEach((word, index) => {
            const separator = (index === this.myMultiFileClassProperty.length - 1) ? "!" : " "
            if (upperCaseLoveHate && (word === "love" || word === "hate")) {
                myString += word.toUpperCase() + separator
            }
            else {
                myString += word + separator
            }
        })
        console.log(myString)
    }
}
Bicolor answered 16/8, 2023 at 19:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.