
文章目录
- Proxy与Reflect详解(一):基础篇------告别defineProperty的新一代响应式基石
-
- 概念概述
-
- 为什么需要Proxy?
- [Proxy vs Object.defineProperty 对比](#Proxy vs Object.defineProperty 对比)
- 核心原理:Proxy的13种拦截操作
- 完整可运行代码示例
- 核心特性详解
- 开发坑点
- 工程实战场景
- 面试题总结
Proxy与Reflect详解(一):基础篇------告别defineProperty的新一代响应式基石
概念概述
想象你雇了一个私人管家:
- 没有Proxy:你需要亲自开门、亲自查信箱、亲自记录谁来过(手动拦截)
- 有了Proxy:管家替你打理一切,有人来访他自动登记,有快递他自动签收,你只需要告诉他"帮我盯着这个房子"(自动拦截)
Proxy就是JavaScript中的"全能管家",它能拦截对象的一切操作。Vue3选择它替代
Object.defineProperty,彻底解决了Vue2响应式系统的各种缺陷。
为什么需要Proxy?
Vue2使用Object.defineProperty实现响应式,存在三个致命缺陷:
javascript
// Vue2的三大痛点
// 痛点1:无法检测新增/删除属性
const obj = { name: 'Alice' }
// Vue2中这样添加属性,视图不会更新
obj.age = 25 // ❌ 检测不到
// 需要Vue.set(obj, 'age', 25) 才行
// 痛点2:无法检测数组索引修改
const arr = [1, 2, 3]
arr[0] = 100 // ❌ 检测不到
// Vue2只能重写7个数组方法来变通
// 痛点3:无法检测数组长度修改
arr.length = 0 // ❌ 检测不到
Proxy的解决方案:拦截对象的所有操作(读取、设置、删除、遍历...),从根本上解决上述问题。
Proxy vs Object.defineProperty 对比
| 特性 | Object.defineProperty | Proxy |
|---|---|---|
| 监听属性新增/删除 | 不支持 | 支持 |
| 监听数组索引/长度变化 | 不支持 | 支持 |
| 监听对象遍历(for...in) | 不支持 | 支持 |
| 拦截操作数量 | 只能拦截get/set | 13种拦截操作 |
| 性能 | 初始化时递归遍历所有属性 | 惰性代理,按需拦截 |
| 兼容性 | IE9+ | 不支持IE |
核心原理:Proxy的13种拦截操作
Proxy基本语法
javascript
const proxy = new Proxy(target, handler)
// target:要代理的目标对象
// handler:定义拦截行为的对象(包含"陷阱"方法)
13种拦截操作一览
┌─────────────────────────────────────────────────────────┐
│ Proxy 13种拦截操作 │
├─────────────────────────────────────────────────────────┤
│ │
│ 属性读取类: │
│ get(target, key, receiver) 拦截属性读取 │
│ has(target, key) 拦截 key in obj │
│ ownKeys(target) 拦截遍历操作 │
│ │
│ 属性写入类: │
│ set(target, key, value, receiver) 拦截属性设置 │
│ deleteProperty(target, key) 拦截 delete │
│ defineProperty(target, key, desc) 拦截 defineProperty │
│ │
│ 对象元操作类: │
│ getPrototypeOf(target) 拦截原型读取 │
│ setPrototypeOf(target, proto) 拦截原型设置 │
│ isExtensible(target) 拦截可扩展性检查 │
│ preventExtensions(target) 拦截阻止扩展 │
│ getOwnPropertyDescriptor(target) 拦截属性描述符 │
│ │
│ 函数调用类: │
│ apply(target, thisArg, args) 拦截函数调用 │
│ construct(target, args) 拦截 new 操作符 │
│ │
└─────────────────────────────────────────────────────────┘
完整可运行代码示例
示例1:属性读写拦截(Vue3响应式的基础)
javascript
// ========== 最基础的响应式代理 ==========
const rawObj = {
name: 'Alice',
age: 25,
hobbies: ['reading', 'coding']
}
// 创建响应式代理
const reactiveObj = new Proxy(rawObj, {
// 拦截属性读取
get(target, key, receiver) {
console.log(`[GET] 读取属性: ${String(key)} = ${target[key]}`)
return Reflect.get(target, key, receiver)
},
// 拦截属性设置
set(target, key, value, receiver) {
const oldValue = target[key]
console.log(`[SET] 设置属性: ${String(key)}: ${oldValue} → ${value}`)
const result = Reflect.set(target, key, value, receiver)
// 这里可以触发视图更新!
if (oldValue !== value) {
console.log(`[UPDATE] 属性 ${String(key)} 变化,触发视图更新`)
}
return result
}
})
// 测试
reactiveObj.name // [GET] 读取属性: name = Alice
reactiveObj.name = 'Bob' // [SET] 设置属性: name: Alice → Bob
// [UPDATE] 属性 name 变化,触发视图更新
reactiveObj.age = 30 // [SET] 设置属性: age: 25 → 30
// [UPDATE] 属性 age 变化,触发视图更新
// 数组也能拦截!
reactiveObj.hobbies[0] = 'swimming'
// [GET] 读取属性: hobbies = reading,swimming
// [SET] 设置属性: 0: reading → swimming
// 新增属性也能拦截!(Vue2做不到)
reactiveObj.email = 'alice@example.com'
// [SET] 设置属性: email: undefined → alice@example.com
// [UPDATE] 属性 email 变化,触发视图更新
示例2:属性存在性检查拦截
javascript
const data = { name: 'Alice', age: 25 }
const proxy = new Proxy(data, {
// 拦截 `key in obj` 操作
has(target, key) {
console.log(`[HAS] 检查属性是否存在: ${String(key)}`)
return Reflect.has(target, key)
}
})
// 测试
console.log('name' in proxy) // [HAS] 检查属性是否存在: name → true
console.log('email' in proxy) // [HAS] 检查属性是否存在: email → false
示例3:拦截删除操作
javascript
const data = { name: 'Alice', age: 25, temp: '临时数据' }
const proxy = new Proxy(data, {
// 拦截 delete obj.key 操作
deleteProperty(target, key) {
console.log(`[DELETE] 删除属性: ${String(key)}`)
if (!(key in target)) {
console.warn(`属性 ${String(key)} 不存在`)
return false
}
const result = Reflect.deleteProperty(target, key)
console.log(`[DELETE] 删除成功`)
// 这里可以触发视图更新!
return result
}
})
delete proxy.temp // [DELETE] 删除属性: temp → [DELETE] 删除成功
delete proxy.xxx // [DELETE] 删除属性: xxx → 属性 xxx 不存在
示例4:拦截遍历操作
javascript
const data = { name: 'Alice', age: 25 }
const proxy = new Proxy(data, {
// 拦截 for...in、Object.keys()、Object.getOwnPropertyNames()
ownKeys(target) {
console.log('[OWNKEYS] 拦截遍历操作')
return Reflect.ownKeys(target)
}
})
// 触发 ownKeys 的操作
Object.keys(proxy) // [OWNKEYS] 拦截遍历操作
for (let key in proxy) {} // [OWNKEYS] 拦截遍历操作
Object.getOwnPropertyNames(proxy) // [OWNKEYS] 拦截遍历操作
示例5:函数调用拦截
javascript
// 拦截函数调用
function sum(a, b) {
return a + b
}
const proxySum = new Proxy(sum, {
apply(target, thisArg, args) {
console.log(`[APPLY] 调用函数,参数: ${args}`)
const result = Reflect.apply(target, thisArg, args)
console.log(`[APPLY] 返回结果: ${result}`)
return result
},
// 拦截 new 操作符
construct(target, args) {
console.log(`[CONSTRUCT] new 调用,参数: ${args}`)
return Reflect.construct(target, args)
}
})
proxySum(1, 2) // [APPLY] 调用函数,参数: 1,2 → [APPLY] 返回结果: 3
示例6:深层嵌套对象的深度代理
javascript
// 实际业务中对象通常是嵌套的,需要深度代理
function deepReactive(obj) {
// 如果不是对象或已经是代理,直接返回
if (typeof obj !== 'object' || obj === null) {
return obj
}
return new Proxy(obj, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver)
console.log(`[GET] ${String(key)}`)
// 递归代理:访问到的嵌套对象也变成响应式
if (typeof value === 'object' && value !== null) {
return deepReactive(value)
}
return value
},
set(target, key, value, receiver) {
console.log(`[SET] ${String(key)}: ${JSON.stringify(value)}`)
return Reflect.set(target, key, value, receiver)
}
})
}
// 测试深层嵌套
const state = deepReactive({
user: {
name: 'Alice',
address: {
city: 'Beijing',
zip: '100000'
}
}
})
state.user.name // [GET] user → [GET] name
state.user.address.city // [GET] user → [GET] address → [GET] city
state.user.address.city = 'Shanghai'
// [GET] user → [GET] address → [SET] city: "Shanghai"
核心特性详解
特性1:Proxy的惰性代理特性
javascript
// Object.defineProperty:初始化时就递归遍历所有属性
// Proxy:只有访问到某层属性时才代理那一层
const hugeObj = {
level1: {
level2: {
level3: {
level4: { data: '深层数据' }
}
}
}
}
// defineProperty方式:初始化就要遍历4层,性能差
// Proxy方式:只有访问 level1.level2.level3.level4 时才逐层代理
// → 性能更好,尤其对于大型对象
特性2:handler为空对象时的透明代理
javascript
// 不定义任何陷阱,Proxy是"透明"的
const obj = { a: 1, b: 2 }
const proxy = new Proxy(obj, {})
// 所有操作直接透传到目标对象
proxy.a // 1(直接读取obj.a)
proxy.c = 3 // 直接设置obj.c = 3
delete proxy.b // 直接删除obj.b
特性3:Proxy.revocable可撤销代理
javascript
// 创建可撤销的代理
const { proxy, revoke } = Proxy.revocable(
{ name: 'Alice' },
{
get(target, key) {
return Reflect.get(target, key)
}
}
)
proxy.name // 'Alice'(正常访问)
revoke() // 撤销代理
proxy.name // TypeError: Cannot perform 'get' on a proxy that has been revoked
业务场景:权限控制------用户退出登录后撤销代理,防止继续访问敏感数据。
开发坑点
坑点1:receiver参数搞混
javascript
const parent = new Proxy({ value: 1 }, {
get(target, key, receiver) {
console.log('parent get', key)
// ❌ 错误:直接 return target[key]
// 当子对象通过原型链访问时,this指向错误
return Reflect.get(target, key, receiver)
// ✅ 正确:使用receiver保证this指向正确
}
})
const child = Object.create(parent)
child.value // 正确触发parent的get拦截,this指向child
坑点2:set陷阱必须返回布尔值
javascript
const proxy = new Proxy({}, {
set(target, key, value) {
target[key] = value
// ❌ 忘记return true,在严格模式下会报错
return true // ✅ 必须返回true表示设置成功
}
})
坑点3:this指向问题
javascript
const target = {
_name: 'Alice',
get name() {
return this._name
}
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
// ❌ 直接 return target[key]
// 当通过receiver访问时,getter中的this指向target而非receiver
// 正确做法:
return Reflect.get(target, key, receiver)
}
})
const obj = { __proto__: proxy, _name: 'Bob' }
obj.name // 如果用target[key]返回'Alice',用Reflect返回'Bob'
坑点4:循环引用导致栈溢出
javascript
// 错误示例:无限递归
const obj = {}
const proxy = new Proxy(obj, {
get(target, key) {
// ❌ 访问proxy.xxx → 触发get → 内部访问target[key]
// 如果target本身也是proxy,就会无限递归
return target[key]
}
})
// 正确示例:使用WeakMap缓存
const reactiveMap = new WeakMap()
function reactive(obj) {
if (reactiveMap.has(obj)) {
return reactiveMap.get(obj)
}
const proxy = new Proxy(obj, { /* ... */ })
reactiveMap.set(obj, proxy)
return proxy
}
坑点5:代理数组的特殊行为
javascript
const arr = [1, 2, 3]
const proxyArr = new Proxy(arr, {
get(target, key, receiver) {
console.log(`[GET] ${String(key)}`)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(`[SET] ${String(key)} = ${value}`)
return Reflect.set(target, key, value, receiver)
}
})
// 数组方法内部会触发多次get/set
proxyArr.push(4)
// [GET] push → 获取push方法
// [GET] length → 获取当前长度
// [SET] 3 = 4 → 设置新元素
// [SET] length = 4 → 更新长度
// 注意:push内部会触发get和set各两次!
工程实战场景
场景1:数据变更日志追踪
javascript
// 生产环境数据变更审计
function createAuditProxy(target, entityName) {
return new Proxy(target, {
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 记录变更日志
console.log(`[AUDIT] ${entityName}.${String(key)}: ${JSON.stringify(oldValue)} → ${JSON.stringify(value)}`)
console.log(`[AUDIT] 变更时间: ${new Date().toISOString()}`)
return result
},
deleteProperty(target, key) {
console.log(`[AUDIT] 删除 ${entityName}.${String(key)}`)
return Reflect.deleteProperty(target, key)
}
})
}
// 使用
const user = createAuditProxy({ name: 'Alice', role: 'admin' }, 'User')
user.name = 'Bob' // [AUDIT] User.name: "Alice" → "Bob"
user.role = 'editor' // [AUDIT] User.role: "admin" → "editor"
delete user.role // [AUDIT] 删除 User.role
场景2:接口响应数据自动格式化
javascript
// 后端返回的数据可能是字符串数字、null等,前端需要自动处理
function createAutoFormatProxy(data) {
return new Proxy(data, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver)
// 自动将字符串数字转为数字
if (typeof value === 'string' && /^\d+$/.test(value)) {
return Number(value)
}
// 嵌套对象递归代理
if (typeof value === 'object' && value !== null) {
return createAutoFormatProxy(value)
}
// null转为默认值
if (value === null) {
return undefined
}
return value
}
})
}
// 模拟后端响应
const apiResponse = createAutoFormatProxy({
code: '200', // 字符串数字
data: {
total: '100', // 字符串数字
list: [
{ id: '1', name: 'Alice', avatar: null }
]
}
})
console.log(apiResponse.code) // 200(自动转为数字)
console.log(apiResponse.data.total) // 100(自动转为数字)
console.log(apiResponse.data.list[0].avatar) // undefined(null转为undefined)
面试题总结
Q1:Proxy和Object.defineProperty的区别?
核心答案:
Object.defineProperty只能拦截属性的get/set,无法检测新增/删除属性、数组索引变化Proxy可以拦截13种对象操作,包括读取、设置、删除、遍历、函数调用等Object.defineProperty初始化时需要递归遍历所有属性;Proxy是惰性代理,访问到哪层才代理哪层Proxy的缺点是不兼容IE
Q2:Proxy的handler为空对象时,对target的操作会怎样?
核心答案:变成透明代理,所有操作直接透传到target,等同于操作原对象。
Q3:为什么Vue3选择Proxy替代Object.defineProperty?
核心答案:
- 解决了Vue2无法检测属性新增/删除的问题,去掉了
Vue.set/Vue.deleteAPI - 解决了数组索引/长度变更检测问题,不需要重写数组方法
- 惰性代理性能更好,大型对象初始化更快
- 代码更简洁,不需要hack各种边界情况