Uczymy się o proxy tego, czego jeszcze nie poznaliśmy, czyli klasę, która w konstruktorze zwraca proxy do siebie samej (sam byłem zdziwiony, że tak można). Do dzieła!

Ok, oto przykład:

class Person {
    constructor(name, age){
        this.name = name;
        this.age = age;
        return new Proxy(this, {
            set(obj, prop, value) {
                obj.setterCallback();
                return Reflect.set(obj, prop, value);
              },

            get(target, prop, receiver) {
                target.getterCallback();
                return Reflect.get(target, prop, receiver);
              },
        })

    }
    getterCallback(){
        console.log("getter called");
    }

    setterCallback(){
        console.log("setter called");
    }
}

let john = new Person("john", 33);

Pobawmy się tym. Gettercallback i settercallback będą wywoływane za każdym razem, gdy próbujemy się dostać do get/set danej klasy. Reflect api, to też warto zwrócić uwagę, nie licząc tych metod, które coś zwracają (np. ownKeys) zawsze zwraca za nas true/false i dlatego w proxy zawsze powinno się reflect api używać.

Zresztą, gdybyśmy chcieli używać TS zamiast JS to musimy wszystko zrobić „po Bożemu”, wszystko musi się zgadzać (w JS teoretycznie uszłoby zrobienie return new proxy this this i wrzucenie pułapek handlera jako metody klasy).

Ok, oto inny przykład tego proxy:

class PersonWithAttrs {
    constructor(name, age){
        this.name = name;
        this.age = age;
        this.attrs = new Map();
        return new Proxy(this, {
            set(obj, prop, value) {
                if(obj.attrs.has(prop)){
                    return obj.setAttribute(prop, value);
                }
                Reflect.set(obj, prop, value);
              },

            get(target, prop, receiver) {
                if(target.attrs.has(prop)){
                    return target.getAttribute(prop);
                }
                return Reflect.get(target, prop);
              },
        })

    }
    
    setAttribute(key, val){
        this.attrs.set(key, val);
    }

    getAttribute(key){
        return this.attrs.get(key);
    }
}

let jane = new PersonWithAttrs("Jane", 20);
jane.setAttribute("sex", "female");
console.log(jane.sex);

Tu jeszcze wypadałoby się zastanowić, czy nie chcemy metody createAttribute albo czy chcemy na pewno pozwalać użytkownikowi ustawiać wszystko, czego chce. Np.:

jane.foo = 42;

Foo nie jest ani w properties ani w attrs… Ok tu mamy lekko poprawioną wersję:

class PersonWithAttrs {
    constructor(name, age){
        this.name = name;
        this.age = age;
        this.attrs = new Map();
        return new Proxy(this, {
            set(obj, prop, value) {
                if(obj.attrs.has(prop)){
                    return obj.setAttribute(prop, value);
                }
                if(prop === 'attrs')
                    return false;
                if(!Object.getOwnPropertyNames(obj).includes(prop))
                    return false;
                Reflect.set(obj, prop, value);
              },

            get(target, prop, receiver) {
                if(target.attrs.has(prop)){
                    return target.getAttribute(prop);
                }
                return Reflect.get(target, prop);
              },
        })

    }

    createAttribute(key, val=null){
        this.attrs.set(key, val);
    }
    
    setAttribute(key, val){
        if(this.attrs.has(key))
            this.attrs.set(key, val);
    }

    getAttribute(key){
        return this.attrs.get(key);
    }
}

let jane = new PersonWithAttrs("Jane", 20);
jane.setAttribute("sex", "female");
console.log(jane.sex);

I tak się tym bawimy, warto dodać, że powinniśmy zwracać Reflect (w pułapkach handlera) bo reflect zwraca za nas true/false, w JS to nie problem takie niedbalstwo, w TS byłoby niedopuszczalne.

Inna sprawa, że w TS mamy dekoratory i inne rzeczy, nie wiem, czy tego Proxy tak często będziemy tam używać. Tym niemniej warto znać.