Search code examples
javascriptes6-proxy

Why do ES6's Proxy doesn't work when used inside a method of class?


The logic doesn't work when used inside method of a class but works out side if I use it in functional style.

class Hook {
    constructor(object) {
        this.object = object;
    }

    toStringProperty() {
        const handler = {
            apply: function (target, thisArg, args){
                if (thisArg === Function.prototype.toString) {
                    return 'function toString() { [native code] }'
                }

                if (thisArg === this.object) {
                    return "Hooked String"
                }

                return target.apply(thisArg, args)
            }
        }

        Function.prototype.toString = new Proxy(Function.prototype.toString, handler)
    }
}

let hook = new Hook(HTMLAudioElement);

hook.toStringProperty();

// Interesting enough this when called (I use Devtools) logs Proxy Object itself but only happen if I use a Class
console.log(Function.prototype.toString)

console.log(HTMLAudioElement.toString())

What should I do to make it works inside a class?


Solution

  • The "problem" is not with the proxy - all calls already go through it. The issue is the age old specifics of how the this keyword works. In short, it's determined at call time, so this.object will have a different meaning depending on when and how the function is called. In this case, the value of this is "lost" not unlike how you lose it in a callback.

    If you need to refer to something concretely, you have a few choices

    Lexically bind this using an arrow function () => {}

    An arrow function uses the this value of the enclosing context at creation time, so it doesn't vary at call time:

    class Hook {
        constructor(object) {
            this.object = object;
        }
    
        toStringProperty() {
            const handler = {
                apply: (target, thisArg, args) => { //<--- arrow function
                    if (thisArg === Function.prototype.toString) {
                        return 'function toString() { [native code] }'
                    }
    
                    if (thisArg === this.object) {
                        return "Hooked String"
                    }
    
                    return target.apply(thisArg, args)
                }
            }
    
            Function.prototype.toString = new Proxy(Function.prototype.toString, handler)
        }
    }
    
    let hook = new Hook(HTMLAudioElement);
    
    hook.toStringProperty();
    
    // Interesting enough this when called (I use Devtools) logs Proxy Object itself but only happen if I use a Class
    console.log(Function.prototype.toString)
    
    console.log(HTMLAudioElement.toString())

    Manually bind this using Function#bind

    This is basically redundant with arrow functions, but still an option:

    class Hook {
        constructor(object) {
            this.object = object;
        }
    
        toStringProperty() {
            const handler = {
                apply: function (target, thisArg, args){
                    if (thisArg === Function.prototype.toString) {
                        return 'function toString() { [native code] }'
                    }
    
                    if (thisArg === this.object) {
                        return "Hooked String"
                    }
    
                    return target.apply(thisArg, args)
                }.bind(this) //<--- bind `this` from creation time
            }
    
            Function.prototype.toString = new Proxy(Function.prototype.toString, handler)
        }
    }
    
    let hook = new Hook(HTMLAudioElement);
    
    hook.toStringProperty();
    
    // Interesting enough this when called (I use Devtools) logs Proxy Object itself but only happen if I use a Class
    console.log(Function.prototype.toString)
    
    console.log(HTMLAudioElement.toString())

    Capture the value in a variable

    This avoids the usage of this by capturing the value of this.object at creation time with const obj = this.object and just using obj later which will always have the same value:

    class Hook {
        constructor(object) {
            this.object = object;
        }
    
        toStringProperty() {
            const obj = this.object; //<--- capture 
            const handler = {
                apply: function (target, thisArg, args){
                    if (thisArg === Function.prototype.toString) {
                        return 'function toString() { [native code] }'
                    }
    
                    if (thisArg === obj) { //<--- use
                        return "Hooked String"
                    }
    
                    return target.apply(thisArg, args)
                }
            }
    
            Function.prototype.toString = new Proxy(Function.prototype.toString, handler)
        }
    }
    
    let hook = new Hook(HTMLAudioElement);
    
    hook.toStringProperty();
    
    // Interesting enough this when called (I use Devtools) logs Proxy Object itself but only happen if I use a Class
    console.log(Function.prototype.toString)
    
    console.log(HTMLAudioElement.toString())