打字稿装饰器和 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
对于 propA
是 true
。
现在,如果我将 get
和 set
替换为
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
在装饰时不能正常工作。如果您在 运行 时间执行此操作,事情就会按预期进行。那么,如何在装饰器中定义 属性,但在 运行 时间?
这里是诀窍:因为重写装饰器内部的 属性 是在装饰时起作用的(只有可枚举的行为似乎被破坏了),你可以定义 属性 但使用初始化函数代替 getter
和 setter
。该函数将在 属性 首次分配 (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"]
我正在尝试实现一个装饰器来覆盖 属性 (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
对于 propA
是 true
。
现在,如果我将 get
和 set
替换为
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
在装饰时不能正常工作。如果您在 运行 时间执行此操作,事情就会按预期进行。那么,如何在装饰器中定义 属性,但在 运行 时间?
这里是诀窍:因为重写装饰器内部的 属性 是在装饰时起作用的(只有可枚举的行为似乎被破坏了),你可以定义 属性 但使用初始化函数代替 getter
和 setter
。该函数将在 属性 首次分配 (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"]