Asken
Asken

Reputation: 8081

ES 6 dynamically work on class after definition

I was previously rolling my own Javascript OOP but now I'm playing with ES6 and want to use the class defined after definition in a generic way.

Note Any answer with new in it is not what I'm after.

Pseudo code:

// base.js
class Base {
    constructor(arg) {
        this.arg = arg;
    }

    // This is the behaviour I'm after
    //afterDefined(cls) {
    afterExtended(cls) {    // probably a better name
        console.log(`Class name ${cls.prototype.name}`);
    }
}

// frombase.js
class FromBase extends Base {
    constructor({ p1='val1', p2='val2'} = {}) {
        super(...arguments);
        this.p1 = p1;
        this.p2 = p2;
    }
}

The output in the console should be:

'Class name FromBase'

So far the only solution I have come up with is to have a static method on Base and call it after the class declaration when I define a new class but I will most likely forget to do this more than once.

Just to be really thorough on why I don't like the static solution; it will force me to import Base in every single file.

Example using a static method (which I don't want) https://jsfiddle.net/nL4atqvm/:

// base.js
class Base {
    constructor(arg) {
        super(...arguments);
        this.arg = arg;
    }

    // This is the behaviour I'm after
    static afterExtended(cls) {
        console.log(`Class name ${cls.name}`);
    }
}

// frombase.js
class FromBase extends Base {
}
// just after defining the FromBase class
Base.afterExtended(FromBase);

Upvotes: 2

Views: 1072

Answers (4)

ninjagecko
ninjagecko

Reputation: 91142

2024 answer:

Original poster seems to want a metaclass/metaprogramming hook for when subclasses are defined, such as Python3's __init_subclass__. There are four ways to go about this, some already mentioned. Decorators are perhaps coming soon to ECMAScript so those would be the way to do it, but if they aren't incorporated or transpilable yet, then see answer #3:

1. Decorator pattern (postprocess with function):

Though not exactly what OP wants, this is the "simple approach".

class MyClass {
    ...
}
postProcess(MyClass, ...)

2. Hook (attached to class)

This is also less versatile than #1, though you can combine both patterns if you need the versatility.

function callOnDefined(cls) {
    cls.callOnDefined(cls);
}

class MyClass {
    static onDefined(cls) {
        ...
    }
}
callOnDefined(MyClass, ...)

3. Decorators, with globals namespace fixing

If OP's main concern is DRY, then they can use the following way to craft decorators, which requires lifting the declaration back into the desired scope (generally the global this, which requires a package or one-liner workaround for es6 modules). This may not play nicely with lexical analysis tools. I'm not sure if it has side-effects, but I'd personally use it, and it is my main recommendation.

I named this postProcess(...) for clarity, but you can come up with some pithy and/or witty name like My(...) or $meta(...) or X(...) or well the sky's the limit.

function postProcess(cls) {
  this[cls.name] = cls;
  ...
}

postProcess(class BaseClass {
})

You can of course extend this approach. Here's a more elaborate version of it, demonstrating a static __init_subclass__-style method, and how you can call the superclass method to ensure inheritance works nicely. (I used __init_subclass__ in case people are familiar, but note that in javascript, the equivalent naming convention is to literally replace __init_subclass__ with [Symbol.for('initSubclass')] in the code below).

function postProcess(cls) {
  this[cls.name] = cls;
  console.log(`${cls.name} = ${cls}`);
  cls.__init_subclass__?.(cls);
}

console.log('------------');

postProcess(class BaseClass {
  static __init_subclass__(cls) {
    console.log(`onDefined hook: ${cls.name} - was defined`);
  }
})

console.log('------------');

postProcess(class SubClass extends BaseClass {
  static __init_subclass__(cls) {
    let parent = Object.getPrototypeOf(cls);
    parent.__init_subclass__(parent, cls);
    console.log(`onDefined hook: ${cls.name} extending ${parent.name} - was defined`);
  }
})

console.log('------------');

postProcess(class SubSubclass extends SubClass {})

Output:

VM588:8 ------------
VM588:4 BaseClass = class BaseClass {
  static __init_subclass__(cls) {
    console.log(`onDefined hook: ${cls.name} - was defined`);
  }
}
VM588:12 onDefined hook: BaseClass - was defined
VM588:16 ------------
VM588:4 SubClass = class SubClass extends BaseClass {
  static __init_subclass__(cls) {
    let parent = Object.getPrototypeOf(cls);
    parent.__init_subclass__(parent, cls);
    console.log(`onDefined hook: ${cls.name} extending ${parent.name} - was defined`);
  }
}
VM588:12 onDefined hook: BaseClass - was defined
VM588:22 onDefined hook: SubClass extending BaseClass - was defined
VM588:26 ------------
VM588:4 SubSubclass = class SubSubclass extends SubClass {}
VM588:12 onDefined hook: BaseClass - was defined
VM588:22 onDefined hook: SubClass extending BaseClass - was defined
VM588:22 onDefined hook: SubSubclass extending SubClass - was defined

4. Official ECMAScript class decorators (status: stage 3 proposal)

The technical committee in charge of ECMAScript has a stage-3 proposal for class decorators, which would allow you to modify classes after definition. Some transpilers support activating this feature, which will shoe-horn it in via some syntactic and/or polyfill transformation. This would allow Python-style class decorators. If you use a transpiler, you can consider this. e.g. see Babel's support and some history for this

5?. thoughts on use of "class ... extends [Proxy]"

zetavolt's answer is especially creative and clever, but it pollutes the prototype chain with a Proxy object. That may be fine, but may cause problems with other metaprogramming tricks. I have not tried Object.getPrototypeOf(Object.getPrototypeOf(...)) for that technique; perhaps one would need to special-case that.

Of course, inheriting from a Proxy would allow one to automatically invoke code without decorating the class. The proper way to do this is thus to, after all class initialization is done, go back and fix the .prototype chain.

Upvotes: 1

zetavolt
zetavolt

Reputation: 3217

Is this what you're looking for?

class Base {
    constructor(arg) { this.arg = arg; }

    static afterDefined(cls) {
        console.log(`Class name ${this.constructor.name}`);
    }
}

Base = new Proxy(Base, {
    get: function(target, key, receiver) {
        if (typeof target == 'function' && key == 'prototype' && target.name == Base.name) {
            Reflect.apply(Base.afterDefined, Reflect.construct(class FromBase {}, []), [])
        }
        return target[key];
    }
});

class FromBase extends Base {}

In order for this to load class names, you will have to embed or forward-declare that information inside of the Proxy receiver prior to extending from it (which means you either need to enumerate ahead of time which classes you'll be inheriting to or to have some function call just prior to that class's declaration).

There are a bunch of other neat total hacks in the same vein such as looking at the source (text) of the file that you just loaded JavaScript from and then parsing that text as if it were JavaScript (people have used this to write Scheme interpreters that can accept new code inside of <script> tags).

If you are a library author intending to target Node, there are even more ways to go about doing this.

Upvotes: 2

Christiaan Westerbeek
Christiaan Westerbeek

Reputation: 11157

There is no javascript built-in trigger that is calling a method on a class when a subclass is defined that extends from it.

Because you're rolling your own library, you could craft some kind of method the creates and returns a new class that extends a given base class. Maybe check out this answer that may help how to define your classes: Instantiate a JavaScript Object Using a String to Define the Class Name

You could also check how other javascript libraries creates (sub)classes. For example, Ext JS has a ClassManager that you could look into.

When this question would be about instantiation and not about defining classes, I would say:

afterDefined(cls) {
    console.log(`Class name ${this.constructor.name}`);
}

Usage:

let x = new FromBase()
x.afterDefined() // --> Class name FromBase

To get the name of the class, use

static afterDefined(cls) {
    console.log(`Class name ${this.name}`);
}

Upvotes: 2

X.zongLiang
X.zongLiang

Reputation: 1

// base.js

class Base {
constructor(arg) {
    this.arg = arg;
}

// This is the behaviour I'm after
afterDefined(cls) {
        console.log(`Class name ${cls}`);
    }
}


// frombase.js
class FromBase extends Base {
    constructor(arg) {
        super(arg)
    }
}


let f = new FromBase();
f.afterDefined('text');//this you text or object

have to be aware of is. file loading order, super is an instance of the parent class. good luck.

Upvotes: -1

Related Questions