Reputation: 957
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
}
Upvotes: 47
Views: 25978
Reputation: 169
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)
}
}
Upvotes: 0
Reputation: 13788
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
Upvotes: 0
Reputation: 1
Here's my solution. It:
.bind()
ing, no prototype
. (EDIT: Actually, see the comments for more on this, it may not be desirable.)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.
Upvotes: 6
Reputation: 46
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;
};
Upvotes: 5
Reputation: 1091
My solution is similar to the one by Erez (declare methods in files and then assign methods to this
in the constructor), but
.apply()
call - functions are inserted into the instance directlyC.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;
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');
}
const c = new C();
console.log('c.incX()', c.incX());
console.log('c.incX()', c.incX());
Upvotes: 1
Reputation: 6352
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]
})
}
}
Upvotes: 3
Reputation: 91
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));
}
}
}
}
Upvotes: 6
Reputation: 40804
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.
Upvotes: 39