Jump to content
DoubleX

OTHER [RPG Maker MZ]Using ES6 Class Inheritance With Extensions For Aliasing Without Direct Prototyping

Recommended Posts

With ES6 classes, trying to extend the parent class in plugins can lead to its children still using the old parent codes.

For instance, you can place this in a JavaScript sandbox and verify the console output yourselves:

Spoiler

// Default RMMZ codebase
class DefaultMZParentClass {

    method() { return "Parent method"; }

}

class DefaultMZChildClass extends DefaultMZParentClass {

    method() { return `${super.method()} Child method`; }

}
//

// Plugin codebase
const PluginAParentClass = DefaultMZParentClass;
DefaultMZParentClass = class extends DefaultMZParentClass {

    method() { return `Plugin ${super.method()}`; }

};
//

// It's "Parent method Child method" but should be "Plugin Parent method Child method"
console.info("new DefaultMZChildClass().method()", new DefaultMZChildClass().method());
//

 

So normally, you'll still have to directly type prototypes:

Spoiler

// Default RMMZ codebase
class DefaultMZParentClass {

    method() { return "Parent method"; }

}

class DefaultMZChildClass extends DefaultMZParentClass {

    method() { return `${super.method()} Child method`; }

}
//

// Plugin codebase
const parentProto = DefaultMZParentClass.prototype, pluginAMethod = parentProto.method;
parentProto.method = function() { return `Plugin ${pluginAMethod.call(this)}`; };
//

// It's "Plugin Parent method Child method" which is the intended output
console.info("new DefaultMZChildClass().method()", new DefaultMZChildClass().method());
//

 

 

But I wanted to offer an alternative for those not being familiar with ES5 or avoiding prototypes like a plague despite the fact that many RMMZ codes are written that way, and this is what I've come up with:

Spoiler

/*
 * Do these 2 additional things when using ES6 class inheritance aliasing
 * without directly typing prototypes:
 * 1. Add the following code right below a new class inheriting another one:
 *    - ExtendedClassAlias.inherit(Klass);
 *    Where Klass is the new class inheriting another one
 * 2. Add the following code right below extending an existing class as a way
 *    to alias its methods:
 *    - ExtendedClassAlias.updateClass(Klass);
 *    Where Klass is the existing class being extended as a way to alias its
 *    methods
 * Right now it doesn't work well with inheriting static functions in classes,
 * so those in children classes should use ParentClass.staticFunc.call(this)
 * instead of super.staticFunc()
 */

// Codes allowing ES6 class inheritance aliasing without direct prototyping
class ExtendedClassAlias {

    static inherit(Child) {
        const childProto = Child.prototype;
        const parentName = Object.getPrototypeOf(childProto).constructor.name;
        this._inherit(Child, parentName);
    }

    static updateClass(Parent) {
        const parentName = Parent.prototype.constructor.name;
        // There's no need to update anything if the passed class's no children
        if (!this._inheritances.has(parentName)) return;
        this._updateClass(this._inheritances.get(parentName), Parent);
        //
    }

    static _inherit(Child, parentName) {
        // So the parent class will know which classes are its children
        if (this._inheritances.has(parentName)) {
            const oldChildProtos = this._inheritances.get(parentName);
            const newChildProtos = oldChildProtos.concat([Child]);
            this._inheritances.set(parentName, newChildProtos);
        } else {
            this._inheritances.set(parentName, [Child]);
        }
        //
    }

    static _updateClass(children, Parent) {
        this._updateProtoMethods(children, Parent.prototype);
        this._updateStaticFuncs(children, Parent);
    }

    static _updateProtoMethods(children, parentProto) {
        // So all the children will inherit the new rather than the old parent
        children.forEach(Child => Child.prototype.__proto__ = parentProto);
        //
    }

    static _updateStaticFuncs(children, Parent) {
        // So all children will inherit all new static functions from new parent
        Object.getOwnPropertyNames(Parent).forEach(name => {
            const desc = Object.getOwnPropertyDescriptor(Parent, name);
            if (!desc || typeof desc.value !== "function") return;
            children.forEach(Child => {
                Child[name] = Child[name] || Parent[name];
            });
        });
        //
    }

}
ExtendedClassAlias._inheritances = new Map();
//

 

I've tested that it works for adding new instance variables and prototype methods in base classes, and extending and overriding existing prototype methods there(you can place the linked snippet into a JavaScript sandbox and verify the console output yourselves).
While I failed to inherit the changes in the static functions of the base classes from plugins as well, this can be easily mitigated by using BaseClass.staticFunc.call(this) instead of super.staticFunc().

Basically, the essence of the issue when aliasing ES6 class inheritance without direct prototyping is this:
1. The original child class inherits the original base class
2. A plugin extends the original base class to alias some of its prototype methods
3. The child class still inherits the original base class
So to solve this, simply store the linkage between the child class and the base class right after creating that child class, then points the parent of the child class to the extended base class right after extending it.

As for the static functions in classes, while I tried to use the linkage to let the exiting children class use the new static functions from the extended parent class, I failed to cover the case for aliasing existing parent class static functions, because it's just impossible:
1. The super in the static functions of the child class always points to the original parent class
2. The super in the static functions of the extened parent class always points to the original parent class
3. The super in the static functions of the child class's supposed to always point to the extended parent class
Clearly, combining 1 and 2 will contradict with 3, which is the goal I've trying to achieve.

For those not being familiar with ES5 or avoiding prototypes like a plague, I hope using ExtendedClassAlias won't be too demanding for you, as all you need to do is sticking to these:

/*
 * Do these 2 additional things when using ES6 class inheritance aliasing
 * without directly typing prototypes:
 * 1. Add the following code right below a new class inheriting another one:
 *    - ExtendedClassAlias.inherit(Klass);
 *    Where Klass is the new class inheriting another one
 * 2. Add the following code right below extending an existing class as a way
 *    to alias its methods:
 *    - ExtendedClassAlias.updateClass(Klass);
 *    Where Klass is the existing class being extended as a way to alias its
 *    methods
 * Right now it doesn't work well with inheriting static functions in classes,
 * so those in children classes should use ParentClass.staticFunc.call(this)
 * instead of super.staticFunc()
 */

P.S.: I've spent almost 24 hours on this and I enjoyed the process a lot, even though this might not be practical enough to be used in MZ :)

Edited by DoubleX

Share this post


Link to post
Share on other sites

ES is like JS after it runs out of coffee. :P

 

... Okay really it's more like JS after it finally admits it's never actually liked coffee and decides to change it's name to avoid being associated with it. Close enough.

Edited by Kayzee

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.

×