标准答案
- Object.defineProperty 是属性级拦截。它可以给指定属性定义 getter/setter,从而拦截这个属性的读取和设置,但通常需要对每个已有属性逐个处理。
- defineProperty 的边界是新增属性、删除属性、数组索引变化、数组 length 变化等场景需要额外补丁。Vue 2 响应式就因为这些边界,需要 Vue.set、数组方法重写等辅助机制。
- Proxy 是对象级代理。它代理整个目标对象,可以拦截 get、set、has、deleteProperty、ownKeys、getPrototypeOf 等更多操作,对新增属性、删除属性和数组操作更自然。
- 现代响应式更偏向 Proxy,是因为它覆盖面更完整,能以更统一的方式描述对象操作。它的主要边界是不能被完整 polyfill,并且只有通过代理对象访问才会触发拦截,直接操作原对象不会触发代理逻辑。
题目解析
这两个 API 都能做响应式基础,但抽象层级不同。defineProperty 的模型是“给属性装 getter/setter”,Proxy 的模型是“给对象套一层代理”。理解这点后,新增属性和删除属性的差异就很清楚。
defineProperty 并不是不能实现响应式,它只是需要预先遍历对象属性,并对一些操作做额外处理。Vue 2 的响应式系统就是基于 defineProperty 做出来的。
Proxy 的优势是覆盖对象操作更完整,代码模型更统一。比如判断属性是否存在、删除属性、枚举 key,都能通过对应 trap 处理。
Proxy 的限制也要记住:代理对象和原对象不是同一个引用;只有访问代理对象才会触发 trap;旧环境无法通过 polyfill 完整模拟 Proxy。
代码示例
下面例子展示 defineProperty 只拦截单个属性,而 Proxy 可以拦截新增属性和删除属性。
JavaScript
const user = {}
Object.defineProperty(user, 'name', {
get() {
return 'Alice'
},
set(value) {
console.log('set name:', value)
}
})
user.name = 'Bob'
user.age = 18 // 这个新增属性不会被上面的 setter 拦截
const proxy = new Proxy({}, {
set(target, key, value) {
console.log('set', key, value)
target[key] = value
return true
},
deleteProperty(target, key) {
console.log('delete', key)
return delete target[key]
}
})
proxy.age = 18
delete proxy.age常见误区
- 误以为 defineProperty 不能实现响应式。它可以,只是边界更多。
- 忘记 Proxy 不能兼容所有旧环境。
- 创建 Proxy 后继续操作原对象,导致拦截不生效。