javascriptprototype-pattern

Implementing extends/super manually


I'm building classes dynamically based on descriptive data pulled from a database. For instance, if the data reads as this:

ClassName = ExampleParent
Method1.name = "greet"
Method1.code = "console.log(\"Parent hello\");"

and building it like this works just fine:

classList[ClassName] = function(){}
classList[ClassName].prototype[Method1.name] = Function(Method1.code);

var Bob = new classList["ExampleParent"]();
Bob.greet();

But I'm stumped on how to dynamically create inheritance:

ClassName = ExampleChild
ClassExtends = ExampleParent
Method1.name = "greet"
Method1.code = "super.greet(); console.log(\"Child hello\");"

I don't see how I could use ExampleChild.prototype to point to both ExampleParent AND contain ExampleChild's custom methods, and even when I tried, it said that super was unknown. I don't need to support anything fancy (private, static, etc)... I just need this and super to work. Hints?


Solution

  • Unfortunately, you can't without using eval (or effectively using eval by having Function return a function that does the work), but that's still in line with what you're doing as Function allows arbitrary code execution just like eval does. Still, it's ugly.

    It's important that you absolutely trust the source of the data. Because eval (and Function) allow executing arbitrary code, you must not (for instance) allow User A to write the code that will be stored in the database for User B to retrieve and use without absolutely trusting User A. It puts User B at risk, potentially exposing them to malicious code from User A.

    With that caveat in place, you can do it with eval, e.g.:

    const className = "ExampleChild";
    const parentName = "ExampleParent";
    const methodName = "greet";
    const methodCode = "super.greet();";
    const C = eval(`
        class ${className} extends ${parentName} {
            ${methodName}() {
                ${methodCode}
            }
        }`
    );
    

    You may need an object to store the classes on by their runtime names, since that's what you'll need for the extends clause.

    Example:

    const classes = {};
    function buildClass(spec) {
        const extendsClause = spec.parentName ? ` extends classes.${spec.parentName}` : "";
        const methods = spec.methods.map(
            mspec =>  `${mspec.name}() { ${mspec.code} }`
        ).join("\n");
        const cls = classes[spec.className] = eval(`
            class ${spec.className} ${extendsClause} {
                ${methods}
            }`
        );
        return cls;
    }
    
    const specs = [
        {
            className: "ExampleParent",
            methods: [
              {name: "greet", code: "console.log(\"Parent hello\");"}
            ]
        },
        {
            className: "ExampleChild",
            parentName: "ExampleParent",
            methods: [
                {name: "greet", code: "super.greet(); console.log(\"Child hello\");"}
            ]
        }
    ];
    
    specs.forEach(buildClass);
    
    let c = new classes.ExampleChild();
    c.greet();