Object.defineProperty封装函数引发属性值重复及对象属性绑定问题
咱们一步步把你遇到的问题讲透,再给出靠谱的实现方案:
为什么你的getSet函数会出问题?
1. 箭头函数的this坑了你
你在getSet里用箭头函数写getter/setter,箭头函数的this是定义时就固定死的——这里在全局作用域下定义的函数,this直接指向全局对象(浏览器里是window,Node里是global)。所以不管你操作a.name还是a.age,本质上都是在读写全局变量的同名属性,结果就是所有属性互相覆盖,初始值自然也丢了。
2. 没给属性留“存值的地方”,还差点搞出无限递归
当你用Object.defineProperty重新定义属性时,原来的属性值被直接覆盖了,而getter里又直接去读this[key]——如果换成普通函数让this指向当前对象,那读取this[key]又会触发getter,直接无限循环到栈溢出,这就是你遇到Maximum call stack size exceeded的原因。
第一个示例能工作纯属巧合
你第一个示例里的代码其实也有this指向的隐患(箭头函数的this还是全局),但因为setter里直接给b.val硬编码赋值,所以同步效果碰巧实现了。但本质上a.val的实际值存在了全局变量里,并不是存在a对象自身,这写法其实很不严谨。
正确实现对象属性绑定的方案
核心思路就是:给每个属性搞个“私有小仓库”存实际值,避免getter/setter递归调用;同时确保this指向正确,同步目标对象的属性。
方案1:单个属性的单向绑定
先从最简单的单向绑定开始,确保逻辑清晰:
// 把sourceObj的sourceProp绑定到targetObj的targetProp,source变则target同步 function bindSingleProp(sourceObj, sourceProp, targetObj, targetProp) { // 用Symbol做存储键,不会和对象原有属性冲突 const storageKey = Symbol(`_${sourceProp}`); // 先把初始值存到私有仓库 sourceObj[storageKey] = sourceObj[sourceProp]; Object.defineProperty(sourceObj, sourceProp, { configurable: true, enumerable: true, get() { return this[storageKey]; }, set(newValue) { this[storageKey] = newValue; // 同步到目标对象 targetObj[targetProp] = newValue; } }); } // 测试一下 const a = { val: 10 }; const b = {}; bindSingleProp(a, 'val', b, 'val'); console.log(a.val); // 10 console.log(b.val); // 10 a.val = 20; console.log(a.val); // 20 console.log(b.val); // 20
方案2:多属性的双向绑定
如果需要两个对象的属性互相同步(双向绑定),可以扩展成批量处理的函数,注意要避免循环触发setter:
function bindObjectProps(sourceObj, targetObj, propList) { propList.forEach(prop => { // 给sourceObj的属性加私有存储和绑定逻辑 const sourceStorage = Symbol(`_${prop}`); sourceObj[sourceStorage] = sourceObj[prop]; Object.defineProperty(sourceObj, prop, { configurable: true, enumerable: true, get() { return this[sourceStorage]; }, set(newValue) { // 只有值变化时才同步,避免循环触发 if (newValue !== targetObj[prop]) { this[sourceStorage] = newValue; targetObj[prop] = newValue; } } }); // 给targetObj的属性加同样的逻辑,实现双向绑定 const targetStorage = Symbol(`_${prop}`); targetObj[targetStorage] = targetObj[prop] ?? sourceObj[prop]; Object.defineProperty(targetObj, prop, { configurable: true, enumerable: true, get() { return this[targetStorage]; }, set(newValue) { if (newValue !== sourceObj[prop]) { this[targetStorage] = newValue; sourceObj[prop] = newValue; } } }); }); } // 测试双向绑定 const a = { name: 'the letter a', age: 12 }; const b = {}; bindObjectProps(a, b, ['name', 'age']); console.log(a.name); // 'the letter a' console.log(b.name); // 'the letter a' b.age = 18; console.log(a.age); // 18 console.log(b.age); // 18
顺便修复你的getSet函数
如果你的getSet只是想给单个对象的属性加安全的getter/setter(不需要绑定其他对象),修复后的版本如下:
function getSet(props) { for (const key in props) { // 用Symbol存原始值,避免递归 const storageKey = Symbol(`_${key}`); props[storageKey] = props[key]; Object.defineProperty(props, key, { configurable: true, enumerable: true, get() { return this[storageKey]; }, set(newValue) { this[storageKey] = newValue; } }); } return props; } const a = getSet({name: 'the letter a', age: 12}); console.log(a.name); // 'the letter a' a.name = 'updated a'; console.log(a.age); // 12
内容的提问来源于stack exchange,提问作者Jamie S

