如何利用Object.defineProperty的set方法实现JS双向绑定并避免栈溢出?
嘿,这个坑我当初刚上手原生实现双向绑定时也踩过!你核心思路是对的——视图→模型靠input事件监听,模型→视图靠setter,但问题就出在你在setter里直接给当前属性赋值,这会再次触发setter,无限递归下去直到调用栈炸掉。
别担心,有个很简单的解决思路:把属性的真实值存在一个不会触发setter的“内部容器”里,而不是直接操作目标属性本身。下面给你两种可行的方案,按需选择:
方案一:用Object.defineProperty + 内部存储(兼容旧浏览器)
核心是用一个独立的变量或者对象的隐藏属性来存真实值,这样在setter里更新这个内部值时,不会触发当前属性的setter。
比如用Symbol来做内部存储的键(Symbol是唯一的,不会和其他属性冲突,也不会被遍历到):
// 定义模型对象 const model = {}; // 创建一个唯一的Symbol作为内部存储的键 const _name = Symbol('name'); // 给model添加双向绑定的name属性 Object.defineProperty(model, 'name', { get() { // 读取内部存储的值 return this[_name]; }, set(newVal) { // 先判断值是否变化,避免无意义的更新 if (newVal !== this[_name]) { // 更新内部存储——这一步不会触发setter this[_name] = newVal; // 更新视图元素 document.getElementById('nameInput').value = newVal; } }, enumerable: true, // 让属性可以被遍历 configurable: true // 允许后续修改属性配置 }); // 视图→模型的绑定:监听input事件 const input = document.getElementById('nameInput'); input.addEventListener('input', (e) => { // 给model.name赋值,会触发setter model.name = e.target.value; }); // 初始化测试 model.name = 'Hello World';
为什么不会栈溢出?因为setter里我们是给this[_name]赋值,而不是this.name,所以不会再次触发name属性的setter,自然就不会递归了。
方案二:用Proxy(更简洁,现代浏览器推荐)
如果你不需要兼容IE这类旧浏览器,Proxy是更好的选择——它能监听对象的所有属性操作,写法也更简洁,而且天生不会出现这种递归问题:
// 初始化模型的原始数据 const rawModel = { name: '' }; // 创建Proxy代理 const model = new Proxy(rawModel, { set(target, prop, newVal) { // 判断值是否变化 if (target[prop] !== newVal) { // 直接更新原始对象的属性——不会触发Proxy的set target[prop] = newVal; // 更新对应的视图元素 const input = document.getElementById(`${prop}Input`); if (input) input.value = newVal; } // 必须返回true,表示赋值成功 return true; } }); // 视图→模型绑定 const input = document.getElementById('nameInput'); input.addEventListener('input', (e) => { model.name = e.target.value; }); // 初始化测试 model.name = '初始值';
这里我们直接更新的是原始的rawModel对象的属性,而不是Proxy实例model的属性,所以不会再次触发Proxy的set陷阱,完美避免递归。
额外优化小技巧
- 可以把双向绑定的逻辑封装成一个通用函数,比如
bindModel(model, prop, inputSelector),这样多个属性和输入框可以快速复用。 - 在setter/Proxy里加值相等判断,避免不必要的视图更新(比如用户输入的内容和当前模型值一样时,就不用再更新输入框了)。
内容的提问来源于stack exchange,提问作者neifnei
相关产品推荐
相关产品推荐

