typescript 属性 装饰器可以修改实例成员而不是整个 class 吗?

Can typescript property decorators modify instance members instead of the entire class?

我想写一个消毒装饰器,我可以把它放在所有用户输入的字符串字段上。这只是将标准 .set(newValue) 替换为 .set( sanitize(newValue) )。但是我发现下面的代码只适用于一个实例。相同 class 的第二个实例最终共享 currentValue。进一步阅读后,这实际上是预期的,但我不知道如何针对每个实例进行设置。

import "reflect-metadata";

export const Sanitize = () => {
    return (target: any, propertyKey: string | symbol) => {
        let currentValue: any = sanitiseString(options, `${target[propertyKey] || ''}`);

        Reflect.deleteProperty(target, propertyKey);

        Reflect.defineProperty(target, propertyKey, {
            get: () => currentValue,
            set: (newValue: string) => {
                currentValue = sanitiseString(newValue);
            },
        });
    }
}

编辑 1: 最小可重现示例:

import "reflect-metadata";

const sanitiseString = (valToSanitise: string) => {
  // do some stuff, return clean value
  return valToSanitise;
}

const Sanitize = () => {
  return (target: any, propertyKey: string | symbol) => {
    let currentValue: any = sanitiseString(`${target[propertyKey] || ''}`);

    Reflect.deleteProperty(target, propertyKey);

    Reflect.defineProperty(target, propertyKey, {
      get: () => currentValue,
      set: (newValue: string) => {
        currentValue = sanitiseString(newValue);
      },
    });
  }
}

class UserInput {
  constructor(propOne: string, propTwo: string, propThree: number) {
    this.propOne = propOne;
    this.propTwo = propTwo;
    this.propThree = propThree;
  }

  @Sanitize() propOne: string
  @Sanitize() propTwo: string
  propThree: number
}

const inputOne = new UserInput('input 1, prop 1', 'input 1, prop 2', 1)
const inputTwo = new UserInput('input 2, prop 1', 'input 2, prop 2', 2)

console.log(inputOne)
console.log(inputTwo)

// expected output: 
// [LOG]: UserInput: {
//    "propOne": "input 1, prop 1",
//    "propTwo": "input 1, prop 2",
//    "propThree": 1
// } 
// [LOG]: UserInput: {
//    "propOne": "input 2, prop 1",
//    "propTwo": "input 2, prop 2",
//    "propThree": 2
// } 
//  
// actual output: 
//
// [LOG]: UserInput: {
//    "propThree": 1
// } 
// [LOG]: UserInput: {
//    "propThree": 2
// } 
// When you remove @Sanitize() the fields appear in console.log. When you add @Sanitize() the fields disappear.
// Further, forcing console.log(inputOne.propOne) returns [LOG]: "input 2, prop 1" 
// indicating that the property is being written for the class proto and not per instance

console.log(inputOne.propOne)

这里的主要问题是 Sanitize() 在每个修饰的 class 属性 声明中被调用一次,因此对于任何给定的 class 属性 都会只有一个 currentValue。这意味着 class 的两个实例将共享相同的 currentValue。如果你想为每个 class 属性 每个 class 实例 存储一个值,那么你需要访问 class 个实例,并且您必须将值存储在这些实例中(通过不会干扰任何其他属性的 属性 键),或者存储在以这些实例为键的某些映射中。在下文中,我将展示如何在 class 实例中存储值,并且为了避免担心 属性 名称冲突,我将使用 the Symbol functionsymbol 输出,它保证是唯一的。

另请注意,Sanitize() 被传递给 class 原型 作为 target 参数,因此您对 [=20 执行的任何操作=] 将影响原型而不是 class 的任何实例。当您编写 target[propertyKey] 时,您正在查找 class 原型中的 属性,而 string 值的属性几乎肯定不会在原型中设置。所以这可能没有必要或没有用,我们应该摆脱它。

因此,如果您只能直接访问 class 原型,您如何使用 class 实例做任何事情?那么,为此,您应该使用 get method and set method of the accessor property descriptor you pass to defineProperty(). And that means get and set need to be methods or at least function expressions, and not arrow function expressionsthis 上下文,它没有明显的 this 上下文。


好的,解释够多了,这是代码:

const Sanitize = () => {
  return (target: any, propertyKey: string | symbol) => {
    const privatePropKey = Symbol();
    Reflect.defineProperty(target, propertyKey, {
      get(this: any) {
        return this[privatePropKey]
      },
      set(this: any, newValue: string) {
        this[privatePropKey] = sanitiseString(newValue);
      },
    });
  }
}

让我们确保它按您预期的那样工作。让我们 sanitiseString 实际做点什么:

const sanitiseString = (valToSanitise: string) => {
  return valToSanitise+"!"; 
}

现在我们的 class:

class UserInput {
  constructor(propOne: string, propTwo: string, propThree: number) {
    this.propOne = propOne;
    this.propTwo = propTwo;
    this.propThree = propThree;
  }
  @Sanitize() propOne: string
  @Sanitize() propTwo: string
  propThree: number
}

最后让我们看看它是否有效:

const inputOne = new UserInput('input 1, prop 1', 'input 1, prop 2', 1)
const inputTwo = new UserInput('input 2, prop 1', 'input 2, prop 2', 2)

console.log(inputOne.propOne, inputOne.propTwo, inputOne.propThree)
console.log(inputTwo.propOne, inputTwo.propTwo, inputTwo.propThree);
// GOOD OUTPUT
// [LOG]: "input 1, prop 1!",  "input 1, prop 2!",  1 
// [LOG]: "input 2, prop 1!",  "input 2, prop 2!",  2 

看起来不错。 UserInput 的每个实例都有自己经过清理的 propOnepropTwo 属性。

Playground link to code