TypeScript装饰器实现Vue响应式时属性重定义报错解决方案
问题根源
你遇到的继承场景属性失效问题,来自三个核心逻辑错误,和JS类字段初始化的规范特性直接相关:
- 显性代码笔误
你在UseVueReactivity实现中写死了从Extendable.prototype读取标记的属性键,这个Extendable对象在代码中从未定义,直接导致子类装饰时读取不到自身标记的响应式属性列表。 - 属性标记的数组污染问题
你在@Ref这类属性装饰器中,判断标记数组是否存在时如果直接用真值判断、没有用hasOwnProperty校验属性是否属于当前类原型,会通过原型链拿到父类原型上的标记数组,把子类的属性键直接push到父类的数组中,既污染了父类的属性列表,也导致子类自身原型上没有专属的标记数组,装饰时无法正确读取子类的响应式属性。 - 存取器定义时机晚于类字段初始化(核心原因)
根据ES2022类字段规范,类的实例字段(比如bar = 20)会在super()调用返回后、子类构造器剩余逻辑执行前,直接在实例上执行赋值操作。你当前的逻辑是在super()执行完成后,才通过defineProperty给实例逐个添加响应式存取器,此时类字段已经完成了第一轮赋值:- 如果存取器设置
configurable: false,后续类字段赋值尝试重定义属性就会抛出Cannot redefine property错误 - 如果设置
configurable: true,你后续添加的存取器虽然能覆盖实例上的自有属性,但如果属性键收集逻辑出错(比如前两个笔误),就会出现属性初始值丢失、变成undefined的问题
- 如果存取器设置
修复方案
按照以下逻辑调整代码即可解决所有继承场景的问题:
- 修正属性装饰器逻辑:给每个类的原型初始化专属的标记数组,必须用
hasOwnProperty判断数组是否存在于当前原型自身,避免原型链污染 - 修正类装饰器的属性收集逻辑:沿着被装饰类的原型链向上遍历,收集所有层级标记的响应式属性,去重后得到完整的处理列表
- 把存取器定义从实例构造阶段移到类装饰阶段:直接在装饰返回的新类的原型上定义响应式属性的get/set,不要等实例化时再给实例打补丁。这样类字段初始化执行赋值时,会直接触发原型上的setter,把初始值写入响应式对象,不会出现属性覆盖问题
- 用Symbol作为响应式实例的存储键,不要用
Ref.xxx这类字符串键,避免和用户自定义属性重名
修正后的核心代码示例
import { ref } from 'vue' // 存储各类响应式属性标记的Symbol,以及存储实例上响应式对象的Symbol const refKeysSymbol = Symbol('Ref keys') const refStoreSymbol = Symbol('Ref store') // 属性装饰器:标记属性为ref类型 const Ref = (target: object, key: string) => { // 必须判断当前类自身原型上有没有标记数组,避免拿到父类的数组造成污染 if (!Object.prototype.hasOwnProperty.call(target, refKeysSymbol)) { // 给当前类原型新建专属的标记数组,不要修改父类的数组 Object.defineProperty(target, refKeysSymbol, { value: [], enumerable: false, configurable: false }) } (target as any)[refKeysSymbol].push(key) } // 类装饰器:包装类为响应式类 const UseVueReactivity = <T extends new (...args: any[]) => any>(Original: T) => { // 沿原型链收集所有层级标记的ref属性,去重 const allRefKeys = new Set<string>() let proto = Original.prototype while (proto) { if (Object.prototype.hasOwnProperty.call(proto, refKeysSymbol)) { ;(proto as any)[refKeysSymbol].forEach((key: string) => allRefKeys.add(key)) } proto = Object.getPrototypeOf(proto) } // 创建装饰后的新类 class ReactifiedClass extends Original { constructor(...args: any[]) { super(...args) // 初始化当前实例的ref存储 Object.defineProperty(this, refStoreSymbol, { value: Object.create(null), enumerable: false, configurable: false }) } } // 在原型上为每个ref属性定义存取器,所有实例共享 for (const key of allRefKeys) { Object.defineProperty(ReactifiedClass.prototype, key, { get() { // 如果ref还没初始化(第一次访问,还没赋值过),先创建初始ref if (!this[refStoreSymbol][key]) { this[refStoreSymbol][key] = ref(undefined) } return this[refStoreSymbol][key].value }, set(newVal) { if (!this[refStoreSymbol][key]) { this[refStoreSymbol][key] = ref(newVal) } else { this[refStoreSymbol][key].value = newVal } }, enumerable: true, configurable: true }) } return ReactifiedClass as T }
验证效果
用你之前的继承场景测试:
@UseVueReactivity class A { @Ref foo = 10 } @UseVueReactivity class B extends A { @Ref bar = 20 } const a = new A() const b = new B() console.log(a.foo) // 10 console.log(b.foo, b.bar) // 10 20 b.bar++ console.log(b.bar) // 21,无NaN问题
不会再出现属性重定义报错、初始值丢失的问题,多层继承也能正常工作。相关的知识点可以参考MDN上的类字段初始化顺序、Object.defineProperty属性描述符规则、原型链继承相关章节。
内容的提问来源于stack exchange,提问作者vivilor
相关产品推荐
相关产品推荐

