Playdome.io
Playdome.io

Reputation: 3245

Javascript Decorators: How to store function with this scope?

I would like to create a decorator to store a function with this scope in an array for every instance created of a class I can't find the right words to explain so here let's say i have the following code:

class Foo
{
    constructor()
    {
        this.MyValue = "Foo";
    }

    @StoreFunction()
    TestA()
    {
        console.log("Foo MyValue:", this.MyValue);
    }
}
class Bar extends Foo
{
    constructor()
    {
        this.MyValue = "Bar";
    }

    @StoreFunction()
    TestB()
    {
        console.log("Bar MyValue:", this.MyValue);
    }
}

function StoreFunction()
{
    return function(target, key, descriptor)
    {
        // How would i go about saving the function there so i can call it later??
        return descriptor;
    }
}

I know that decorators are not processed during class instantiation. So i tried doing the following in StoreFunction.

var StoredFunctions = [];

function StoreFunction()
{
    return function(target, key, descriptor)
    {
        if(target._StoredFunctions)
        {
            target._StoredFunctions = [];
        }
        // Save the function's name
        target._StoredFunctions.push(key);

        return descriptor;
    }
}

Then binding them in the constructor.

class Foo
{
    constructor()
    {
        this.MyValue = "Foo";
        this.BindFunctions()
    }

    BindFunctions()
    {
        if(this._StoredFunctions)
        {
            this._StoredFunctions.forEach( method => {
                StoredFunctions.push(this[method].bind(this));
            });
        }
    }
}

But again the this._StoredFunctions does not store the StoredFunctions properly since they are static. I am not quite sure how to make it work properly.

Please let me know if the question is not clear i am having a hard time explaining the issue.

Upvotes: 0

Views: 483

Answers (2)

Playdome.io
Playdome.io

Reputation: 3245

T.J. Crowder provided a correct answer, i am providing a clean and readable solution that i came up with based on T.J. Crowder.

var StoredFunctions = {};
class Foo
{
    constructor()
    {
        this.MyValue = "Foo";
        this.SetupStore();
    }

    SetupStore()
    {
        let Methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
        for(let Method of Methods)
        {
            if(typeof(this[Method]) == "function" && this[Method]._StoredFunction)
            {
                StoredFunctions[Method] = this[Method].bind(this);
            }

        }
    }

    @StoreFunction()
    TestA()
    {
        console.log("Foo MyValue:", this.MyValue);
    }
}

class Bar extends Foo
{
    constructor()
    {
        this.MyValue = "Bar";
    }

    @StoreFunction()
    TestB()
    {
        console.log("Bar MyValue:", this.MyValue);
    }
}



function StoreFunction(StoreInfo)
{
    return function(target, key, descriptor)
    {
        descriptor.value._StoredFunction = true; // Mark the function to be stored on initialization
        return descriptor;
    }
}

Upvotes: 0

T.J. Crowder
T.J. Crowder

Reputation: 1075915

You can insert an automatic subclass that can handle doing the necessary work of "exposing" (or otherwise doing things with) the marked functions, see comments:

// `exposable` sets the class up for `exposed` on methods
function exposable() {
    return function decorator(target, name) {
        // Create our subclass with the same name
        const o = {[name]: class extends target {
            constructor(...args) {
                super(...args);
                // "Expose" the bound methods
                this.exposed = {};
                Object.getOwnPropertyNames(target.prototype).forEach(name => {
                    const method = target.prototype[name];
                    if (method.exposed) {
                        this.exposed[name] = method.bind(this);
                    }
                });
            }
        }};
        return o[name];
    };
}

// `expose` marks a method to be exposed in the constructor
function exposed(state) {
    return function decorator(target, name, config) {
        config.value.exposed = true;
        return config;
    };
}

// Example
@exposable()
class Foo {
    constructor(name) {
        this.name = name;
    }

    @exposed()
    testA() {
        console.log("testA says " + this.name);
    }

    testB() {
        console.log("testB says " + this.name);
    }
}

const f = new Foo("Fred");
f.exposed.testA(); // Says "testA says Fred" because it's bound

You've said we can rely on having a common base class (Foo). If so, we can move the logic into Foo itself:

// `expose` marks a method to be exposed in the constructor
function exposed(state) {
    return function decorator(target, name, config) {
        config.value.exposed = true;
        return config;
    };
}

// Example
class Foo {
    constructor(name) {
        this.name = name;
        this.exposed = Object.create(null);
        let proto = Object.getPrototypeOf(this);
        while (proto && proto !== Object.prototype) {
            Object.getOwnPropertyNames(proto).forEach(name => {
                if (!this.exposed[name]) {
                    const method = this[name];
                    if (typeof method === "function" && method.exposed) {
                        // Expose it
                        this.exposed[name] = method.bind(this);
                    }
                }
            });
            proto = Object.getPrototypeOf(proto);
        }
    }

    @exposed()
    testA() {
        console.log("testA says " + this.name);
    }
}

class Bar extends Foo {

    @exposed()
    testB() {
        console.log("testB says " + this.name);
    }
}

const f = new Foo("Fred");
f.exposed.testA(); // Says "testA says Fred" because it's bound
const b = new Bar("Barney");
b.exposed.testA(); // Says "testA says Barney" because it's bound
b.exposed.testB(); // Says "testB says Barney" because it's bound

Upvotes: 1

Related Questions