vendredi 13 mars 2020

Wrapping a constructor with Proxy to dynamically observe and work with instance methods

Just started learning about Proxies and Reflection, and would appreciate some guidance in the right direction since I got kinda lost here. My task is to create a class Wand which accepts an object of functions (spells). Each spell invocation should add the spell name to history array, which holds logs for most recent calls (up-to 3).

The new instance should also allow usage of prioriIncantatem() and deletrius() methods (but they should not be present on the instance/class itself), the former will return current history array (before adding prioriIncantatem to it), the latter will clear the history. These methods should also be logged to the history array (prioriIncantatem will only be be shown on next invocation, but will still be logged). The instance should inherit from Object.prototype so stuff like .toString() work as usual, resulting in [object Object] (they will also be logged to history).

The instance should also allow to dynamically add new methods (spells), being able to call existing methods using this which will log both calls to history array, and work with other methods as if they were all instantiated on construction.

It should work like this:

const w = new Wand({ alohomora: function() { console.log('unlocked!') },});

w.alohomora(); // logs 'unlocked!'
w.prioriIncantatem(); // => ['alohomora']

w.myNewSpell = function() { console.log('magic!'); }
w.myNewSpell(); // logs 'magic!'

w.prioriIncantatem(); // => ['myNewSpell', 'prioriIncantatem', 'alohomora']

w.deletrius();
w.prioriIncantatem(); // => ['deletrius']

Object.getOwnPropertyNames(w) == ["alohomora", "_history", "myNewSpell"]

Didn't really understand how to wrap the instance with a proxy, so after a bit of research this is what I came up with, which seems to work with the above example

class WandClass {
    constructor(opts = {}) {
        Object.assign(this, opts);
        this._history = [];
    }
}

const Wand = new Proxy(WandClass, {
    construct(target, args) {
        return new Proxy(new target(...args), {
            get(target, prop, receiver) {
                const value = target[prop];
                const history = target._history;
                if (typeof value == 'function') {
                    return (...args) => {
                        history.unshift(prop);
                        if (history.length > 3) history.pop();
                        return Reflect.apply(value, target, args);
                    };
                }
                else if (['prioriIncantatem', 'deletrius'].includes(prop)) {
                    return () => {
                        switch (prop) {
                            case 'prioriIncantatem':
                                console.log(history);
                                history.unshift(prop);
                                if (history.length > 3) history.pop();
                                break;
                            case 'deletrius':
                                history.length = 0;
                                history.unshift(prop);
                                break;
                        }
                    };
                }
                else { return Reflect.get(target, prop); }
            }
        });
    }
});

However, when trying the following instantiation, the code doesn't work as expected:

const w = new Wand({
    alohomora: function () { console.log('unlocked!'); },
    expelliarmus: function () { console.log('disarmed!'); }
});

w.unlockThenDisarm = function () {
    this.alohomora();
    this.expelliarmus();
};

w.unlockThenDisarm(); // EXPECTED: logs 'unlocked!' then 'disarmed!', NO ISSUE HERE
w.prioriIncantatem(); // EXPECTED:  => ['expelliarmus', 'alohomora', 'unlockThenDisarm']
                      // instead got ['unlockThenDisarm']

Initially I though there is an issue with this binding, but since console.log works as expected, i assumed it's not it. From what I understand, not the call itself is what logs to history field, but the action of getting the property. Can't figure out how to trap the invocations themselves, and add the logic of logging the calls on apply trap instead of get... I tried adding the trap directly like so

get(target, prop, receiver){
// ...
},
apply(target, thisArg, args) { debugger; }

but the debugger never executes...





Aucun commentaire:

Enregistrer un commentaire