egenskaper - Typescript-dekoration och Object.defineProperty konstigt beteende


Jag försöker implementera en dekoratör som åsidosätter en egenskap (1) och definierar en dold egenskap (2). Anta följande exempel:

function f() {
    return (target: any, key: string) => {

        let pKey = '_' + key;

        // 1. Define hidden property
        Object.defineProperty(target, pKey, {
            value: 0,
            enumerable: false,
            configurable: true,
            writable: true
        });

        // 2. Override property get/set
        return Object.defineProperty(target, key, {
            enumerable: true,
            configurable: true,
            get: () => target[pKey],
            set: (val) => {
                target[pKey] = target[pKey] + 1;
            }
        });
    };
}

class A {
    @f()
    propA = null;
    propB = null;
}

let a = new A();

console.log(Object.keys(a), a.propA, a._propA, a);

Vilka resultat:

[ 'propB' ] 1 1 A { propB: null }

Men jag skulle snarare förvänta mig:

[ 'propA', 'propB' ] 1 1 A { propA: 1, propB: null }

eftersom enumerable är true för propA.

Nu, om jag ersätter get och set med

get: function () {
    return this[pKey]
},
set: function (val) {
    this[pKey] = this[pKey] + 1;
}

utdata är nu:

[ '_propA', 'propB' ] 1 1 A { _propA: 1, propB: null }

Även om enumerable uttryckligen är satt till false för _propA i f.

Så, hur konstiga dessa beteenden än kan vara, skulle jag vilja förstå vad som pågår här, och hur jag skulle implementera det jag försöker få till?

Svar #1

Okej, det tog mig ett tag, men jag hittade en lösning. Problemet verkar vara att Object.defineProperty inte fungerar korrekt vid dekorationstid. Om du gör det vid körningstid går allt som förväntat. Så, hur definierar man egenskapen inne i dekoratören, men vid körningstid?

Här är tricket: eftersom att skriva över egenskapen inom dekoratorn fungerar vid dekorationstid (det verkar bara vara det uppräkningsbara beteendet som är brutet), kan du definiera egenskapen men använda en initialiseringsfunktion istället för getter och setter. Den funktionen kommer att köras första gången egenskapen tilldelas (set) eller nås (get). När det händer refererar ordet this till körningsinstansen av ditt objekt, vilket betyder att du korrekt kan initialisera det du avsåg att göra vid dekorationstid.

Här är lösningen:

function f() {
    return (target: any, key: string) => {
        let pKey = `_${key}`;

        let init = function (isGet: boolean) {
            return function (newVal?) {
                /*
                 * This is called at runtime, so "this" is the instance.
                 */

                // Define hidden property
                Object.defineProperty(this, pKey, {value: 0, enumerable: false, configurable: true, writable: true});
                // Define public property
                Object.defineProperty(this, key, {
                    get: () => {
                        return this[pKey];
                    },
                    set: (val) => {
                        this[pKey] = this[pKey] + 1;
                    },
                    enumerable: true,
                    configurable: true
                });

                // Perform original action
                if (isGet) {
                    return this[key]; // get
                } else {
                    this[key] = newVal; // set
                }
            };
        };

        // Override property to let init occur on first get/set
        return Object.defineProperty(target, key, {
            get: init(true),
            set: init(false),
            enumerable: true,
            configurable: true
        });
    };
}

Vilka resultat:

[ 'propA', 'propB' ] 1 1 A { propA: [Getter/Setter], propB: null }

Denna lösning stöder standardvärden, eftersom de tilldelas efter att rätt get/set har initialiserats.

Det stöder också enumerable korrekt: sätt enumerable till true för egenskapen pKey och utdata blir:

[ '_propA', 'propA', 'propB' ] 1 1 A { _propA: 1, propA: [Getter/Setter], propB: null }

Det är inte snyggt, det vet jag, men det fungerar och minskar inte prestandan så vitt jag vet.

Svar #2

Jag granskade din kod och ser att den kommer att definiera egenskapen två gånger. Jag ändrade din kod.

 class A {
    @dec
    public value: number = 5
 }

 function dec(target, key) {
    function f(isGet: boolean) {
      return function (newValue?: number) {
          if (!Object.getOwnPropertyDescriptor(this, key)) {
             let value: number;
             const getter = function () {
               return value
             }

             const setter = function (val) {
                value = 2 * val
             }
              Object.defineProperty(this, key, {
                     get: getter,
                     set: setter,
                     enumerable: true,
                     configurable: true
                     })
          }  
          if (isGet) {
              return this[key]
          } else {
              this[key] = newValue
          }
      }
  }

  Object.defineProperty(target, key, {
      get: f(true),
      set: f(false),
      enumerable: false,
      configurable: false
  })
}

const a = new A()
console.log(Object.keys(a))

Och vi kommer att komma in i konsolen

["value"]