一、先下结论
Proxy 能做到 Object.defineProperty 做不到的事,根本原因不是"API 更高级",而是:
👉 Proxy 是"拦截整个对象的行为"
👉 defineProperty 是"劫持某一个已经存在的属性"
二、从语言底层:两者"拦截的是什么?"
1️⃣ Object.defineProperty ------「属性级劫持」
javascript
Object.defineProperty(obj, 'a', {
get() {},
set() {}
})
| 能力 | 是否支持 |
|---|---|
| 拦截 get/set | ✅ |
| 拦截新增属性 | ❌ |
| 拦截 delete | ❌ |
| 拦截 for...in / Object.keys | ❌ |
| 拦截数组索引变化 | ❌(非常别扭) |
👉 它只能拦截"已存在的 key 的读写"
本质上:
JS 引擎在访问 obj.a 时,会走你定义的 getter / setter
但如果是 obj.b = 1,引擎根本不会"通知你",因为你根本没劫持过 b
2️⃣ Proxy ------「对象级劫持(行为拦截)」⭐
javascript
const proxy = new Proxy(obj, {
get(target, key, receiver) {},
set(target, key, value, receiver) {},
deleteProperty(target, key) {},
ownKeys(target) {}
})
Proxy 拦截的是:
"对这个对象做了什么操作"
而不是"访问了哪个属性"。
| 操作 | 是否能拦截 |
|---|---|
| 访问属性 | ✅ |
| 修改属性 | ✅ |
| 新增属性 | ✅ |
| 删除属性 | ✅ |
| in 运算符 | ✅ |
| for...in / Object.keys | ✅ |
| 数组索引变化 | ✅ |
| length 变化 | ✅ |
二、深层原理:为什么会有这种差异?
1. 设计哲学的差异
javascript
// Object.defineProperty 的哲学:装饰模式
// 给现有属性添加额外的行为
function decorateProperty(obj, key) {
let value = obj[key]
Object.defineProperty(obj, key, {
get() { return value },
set(newVal) {
value = newVal
// 触发更新...
}
})
return obj
}
// 问题:必须先有属性,才能装饰
const obj = {}
decorateProperty(obj, 'name') // 必须先装饰
obj.name = '小明' // 现在才会被监听
// Proxy 的哲学:代理模式
// 创建一个代理对象,拦截所有操作
function createProxy(obj) {
return new Proxy(obj, {
get(target, key) {
// 在读取时才进行依赖收集
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
// 触发更新
trigger(target, key)
return true
}
})
}
// 优势:拦截先行,操作后置
const obj = {}
const proxy = createProxy(obj) // 先创建代理
proxy.name = '小明' // 操作时自动被拦截
三、为什么 defineProperty 监听不了「新增属性」?
obj.b = 1
JS 引擎内部做了什么?
-
检查 obj 是否已有 b
-
如果没有 → 直接创建属性
-
赋值
👉 整个过程没有任何"属性级回调钩子"
Object.defineProperty 的 getter/setter 只在:
obj.b // 读取已定义属性
obj.b = // 写入已定义属性
才生效。
Vue2 是怎么"硬补"的?
Vue.set(obj, 'b', 1)
本质:defineReactive(obj, 'b', 1)
👉 手动补一个 defineProperty
⚠️ 这也是 Vue2 最大的心智负担之一。
四、Proxy 为什么天然支持「新增属性监听」?
proxy.b = 1
流程变成:
-
触发 Proxy 的 set trap
-
set trap 里你可以判断:
const hadKey = Object.hasOwn(target, key)
-
决定是:
-
新增 → trigger ADD
-
修改 → trigger SET
因为 Proxy 拦截的是"赋值行为"本身
而不是属性。
-
五、核心难点二:什么叫「按需响应式」?
Vue2 的问题:全量递归劫持
javascript
data() {
return {
a: {
b: {
c: 1
}
}
}
}
Vue2 在初始化时会:
walk(a)
walk(a.b)
walk(a.b.c)
👉 不管你用不用,全部 defineProperty 一遍
Vue3 的思路:用的时候再劫持
javascript
const state = reactive({
a: {
b: {
c: 1
}
}
})
只有当你:state.a.b.c
执行流程是:
get(state, 'a') → 返回 proxy(a)
get(proxy(a), 'b') → 返回 proxy(b)
get(proxy(b), 'c') → track
六、从"响应系统设计"角度对比
Vue2(defineProperty)
初始化阶段:
全量 walk
每个 key 都有一个 Dep
特点:
-
依赖关系 = 属性级
-
初始化成本极高
-
无法精确拦截结构变化
Vue3(Proxy)
运行阶段:
用到哪个 key
才 track 哪个 key
特点:
-
依赖关系 = target + key
-
没访问 → 没依赖
-
没依赖 → 不触发更新
Object.defineProperty 是"属性级劫持",只能劫持已存在的 key,因此无法监听新增、删除和结构性变化,也无法按需响应;而 Proxy 是"对象级行为拦截",可以拦截所有对对象的操作,使得 Vue3 能实现新增属性监听、懒递归、精准依赖收集和更好的性能优化。