打字稿装饰器和 Object.defineProperty 奇怪的行为

Typescript decorator and Object.defineProperty weird behavior

我正在尝试实现一个装饰器来覆盖 属性 (1) 并定义一个隐藏的 属性 (2)。假设以下示例:

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);

输出:

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

不过,我更希望:

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

因为 enumerable 对于 propAtrue

现在,如果我将 getset 替换为

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

现在的输出是:

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

虽然 enumerable 明确设置为 false for _propA in f.

所以,尽管这些行为可能很奇怪,但我想了解这里发生了什么,以及我将如何实现我想要得到的东西?

好吧,我花了一些时间,但我找到了解决方法。问题似乎是 Object.defineProperty 在装饰时不能正常工作。如果您在 运行 时间执行此操作,事情就会按预期进行。那么,如何在装饰器中定义 属性,但在 运行 时间?

这里是诀窍:因为重写装饰器内部的 属性 是在装饰时起作用的(只有可枚举的行为似乎被破坏了),你可以定义 属性 但使用初始化函数代替 gettersetter。该函数将在 属性 首次分配 (set) 或访问 (get) 时为 运行。发生这种情况时,this 一词会引用对象的 运行time 实例,这意味着您可以正确地初始化您打算在装饰时执行的操作。

解决方法如下:

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
        });
    };
}

输出:

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

此解决方案支持默认值,因为它们是在正确的 get/set 初始化后分配的。

它也支持 enumerable:将 enumerable 设置为 true for 属性 pKey 输出将是:

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

这不是很漂亮,我知道,但它有效并且据我所知不会降低性能。

我检查了您的代码,发现它会定义 属性 两次。我修改了你的代码。

 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))

我们将进入控制台

["value"]