Ionel Lupu
Ionel Lupu

Reputation: 2801

Proxies: Calling static methods of target's parent when using a proxy object

Really interesting problem here for Javascripters

In Javascript, is possible to intercept when getting a property using proxies.

Also, with a small hack as shown below, is possible to intercept when getting a static property of a class:

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

    get(target, property){
        if (property === 'all') {
            return () => `selecting data from table: ${this.object.table}` ;
        }
        return target[property];
    }
}
class User{
    static table = 'users'
}

Object.setPrototypeOf(User, new Proxy({}, new Handler(User)));

console.log(User.all()); // prints "selecting data from table: users"

The problem appears when I am trying to extend the User class and then try to call a static method under the User's parent class:

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

    get(target, property){
        if (property === 'all') {
            return () => `selecting data from table: ${this.object.getTable()}` ;
        }
        return target[property];
    }
}

class Model{
    static getTable(){return this.table;}
}

class User extends Model{
    static table = 'users'
}

Object.setPrototypeOf(User, new Proxy({}, new Handler(User)));

console.log(User.all());

Running this will get you TypeError: this.object.getTable is not a function.

Debugging the code, I found out that this.object is not a User class but some sort of a function.

Is it possible, by any workarounds, to call the parent's static method getTable?

The second problem, if you manage to fix the first one, is that I cannot instantiate the User class anymore:

console.log(new User());
TypeError: Super constructor [object Object] of User is not a constructor

I think this is for the fact that the User is not a class anymore: console.log(User) results in ƒ [object Function] not class User extends Model

Does the language allow this kind of functionality?

Upvotes: 1

Views: 1654

Answers (1)

sztyuni
sztyuni

Reputation: 61

In your way you can never call Model's getTable() because Model is not the parent of User anymore.

Let's check the prototype chain after this code:

class Model{
    static getTable(){return this.table;}
}

class User extends Model{
    static table = 'users'
}

console.log(Object.getPrototypeOf(Model));
// f () { [native code] }
// 'parent' of Model is Function (class is a constructor function in JS)
// Model.[[Prototype]] === Function.prototype

console.log(Object.getPrototypeOf(User));
// class Model{
//     static getTable(){return this.table;}
// }
// 'parent' of User is Model
// User.[[Prototype]] === Model.prototype

Up to this point the prototype chain is like you expect. But since the statement Object.setPrototypeOf(User, new Proxy({}, new Handler(User))); the prototype chain is modified, thus the inheritance looks like this:

const userPrototype = Object.getPrototypeOf(User);
console.log(userPrototype);
// Proxy {}
// 'parent' of User is an instance of Proxy({}, new Handler(User)))

const userPrototypePrototype = Object.getPrototypeOf(userPrototype);
console.log(userPrototypePrototype);
// {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
// 'parent' of Proxy instance is an Object instance (because target of your proxy is {})

Object.getPrototypeOf(userPrototypePrototype);
// null
// 'parent' of Object instance is null (this is the end of prototype chain)

As you can see, Model is disappeared from the inheritance chain, so you cannot call its getTable() method from User.

In order to achieve your goals I suggest you a different solution which does not modify the prototype chain directly.

class Handler{
    constructor(object){
        this.object = object;
    }
    
    get(target, property){
        if (property === 'all') {
            return () => `selecting data from table: ${this.object.getTable()}` ;
        }
        return target[property];
    }
}

class Model{
    static getTable(){return this.table;}
}

class User extends Model{
    static table = 'users'
}

class ProxyClass {
    constructor(object) {
        return new Proxy(object, new Handler(object));
    }
}

const UserProxy = new ProxyClass(User);
// this is the same as
// const UserProxy = new Proxy(User, new Handler(User))

console.log(UserProxy.all());
// selecting data from table: users

console.log(new UserProxy());
// User {}

With this you can dynamically wrap your objects in proxy and you are able to use inherited methods, as well as instantiate model class.

Important note! As mentioned on MDN page Inheritance with the prototype chain:

Following the ECMAScript standard, the notation someObject.[[Prototype]] is used to designate the prototype of someObject. Since ECMAScript 2015, the [[Prototype]] is accessed using the accessors Object.getPrototypeOf() and Object.setPrototypeOf(). This is equivalent to the JavaScript property __proto__ which is non-standard but de-facto implemented by many browsers.

It should not be confused with the func.prototype property of functions, which instead specifies the [[Prototype]] to be assigned to all instances of objects created by the given function when used as a constructor. The Object.prototype property represents the Object prototype object.

Upvotes: 2

Related Questions