Object.defineProperty 和 Proxy
把下面的代码补全,要求:实现 reactive 函数(用 Proxy),修改属性时打印"更新了",支持嵌套对象(可选,进阶)。Deepseek前端进阶计划第二条:搞懂 Object.defineProperty 和 Proxy 的区别。这些都是 Proxy 内置的,自动识别的。✍️ 动手:实现一个 reactive(obj),让修改属性时打印“更新了”。get/set 不能直接操作 obj.nam
搞懂 Object.defineProperty 和 Proxy 的区别
Deepseek前端进阶计划第二条:搞懂 Object.defineProperty 和 Proxy 的区别。
✍️ 动手:实现一个 reactive(obj),让修改属性时打印“更新了”。
一、先搞懂 Object.defineProperty vs Proxy
| 对比维度 | Object.defineProperty (Vue 2) | Proxy (Vue 3) |
|---|---|---|
| 监听对象 | 需要遍历每个属性 | 代理整个对象 |
| 监听新增属性 | ❌ 不行(需要 Vue.set) | ✅ 可以 |
| 监听删除属性 | ❌ 不行 | ✅ 可以 |
| 监听数组 | ❌ 需要 hack | (重写7个方法) |
| 性能 | 初始化时需要遍历所有属性 | 懒代理,访问时才拦截 |
二、动手实现 reactive(obj)
- 基础版:用 Object.defineProperty(Vue 2 风格)
function reactive(obj) {
const result = {}
Object.keys(obj).forEach(key => {
let value = obj[key]
Object.defineProperty(result, key, {
get() {
console.log(`读取了 ${key}:`, value)
return value
},
set(newValue) {
if (value !== newValue) {
console.log(`修改了 ${key}: ${value} -> ${newValue}`)
value = newValue
}
},
enumerable: true,
configurable: true
})
})
return result
}
// 测试
const state = reactive({ name: '张三', age: 18 })
console.log(state.name) // 读取了 name: 张三
state.age = 20 // 修改了 age: 18 -> 20
state.gender = '男' // ❌ 没有打印(新增属性监听不到)
- 进阶版:用 Proxy(Vue 3 风格)
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
console.log(`读取了 ${String(key)}:`, target[key])
return target[key]
},
set(target, key, value) {
if (target[key] !== value) {
console.log(`修改了 ${String(key)}: ${target[key]} -> ${value}`)
target[key] = value
}
return true // 表示设置成功
},
deleteProperty(target, key) {
console.log(`删除了 ${String(key)}`)
delete target[key]
return true
}
})
}
// 测试
const state = reactive({ name: '张三', age: 18 })
console.log(state.name) // 读取了 name: 张三
state.age = 20 // 修改了 age: 18 -> 20
state.gender = '男' // ✅ 修改了 gender: undefined -> 男(新增也能监听)
delete state.age // ✅ 删除了 age
三、你的任务(今天的动手练习)
把下面的代码补全,要求:实现 reactive 函数(用 Proxy),修改属性时打印"更新了",支持嵌套对象(可选,进阶)。
function reactive(obj) {
// TODO: 实现 Proxy 拦截
// 1. get 中打印读取
// 2. set 中打印"更新了"
}
// 测试代码
const data = reactive({ count: 0 })
data.count = 1 // 应该打印:更新了
四、Object.defineProperty和Proxy的用法
在写这个任务之前我还让Deepseek解释一下Object.defineProperty和Proxy的用法以及包含的全部属性,果然像我这样的虚假前端连这个都不会,以下是它的回答:
1. Object.defineProperty
基本语法
Object.defineProperty(对象, '属性名', {
get() { ... }, // 读取属性时触发
set(val) { ... }, // 设置属性时触发
enumerable: true, // 是否可枚举(for...in)
configurable: true // 是否可删除/修改配置
})
具体例子
const obj = {}
let storedValue = '初始值'
Object.defineProperty(obj, 'name', {
get() {
console.log('get触发了:读取name')
return storedValue
},
set(val) {
console.log('set触发了:修改name为', val)
storedValue = val
},
enumerable: true, // 可以被遍历
configurable: true // 可以被删除或重新配置
})
// 使用
console.log(obj.name) // get触发了:读取name → '初始值'
obj.name = '张三' // set触发了:修改name为 张三
console.log(obj.name) // get触发了:读取name → '张三'
delete obj.name // configurable: true 允许删除
注意事项
get/set 不能直接操作 obj.name,会造成死循环
需要用中间变量(如上面的 storedValue)来存储真实值
Object.defineProperty 全部属性
Object.defineProperty(obj, 'key', {
// ========== 数据描述符(二选一)==========
value: 123, // 属性值(默认 undefined)
writable: true, // 是否可修改(默认 false)
// ========== 访问器描述符(二选一)==========
get() { ... }, // 读取时触发,不能与 value/writable 共存
set(val) { ... }, // 写入时触发,不能与 value/writable 共存
// ========== 公共描述符(两者都有)==========
enumerable: true, // 是否可枚举(for...in / Object.keys),默认 false
configurable: true // 是否可删除/重新配置,默认 false
})
数据描述符 vs 访问器描述符
// 数据描述符:直接存值
Object.defineProperty(obj, 'name', {
value: '张三',
writable: true,
enumerable: true,
configurable: true
})
// 访问器描述符:通过 get/set 拦截
Object.defineProperty(obj, 'age', {
get() { return this._age },
set(val) { this._age = val },
enumerable: true,
configurable: true
})
// ❌ 不能混用
Object.defineProperty(obj, 'both', {
value: 1,
get() { return 1 } // ❌ 报错:不能同时指定 value 和 get
})
2. Proxy
基本语法
const proxy = new Proxy(原对象, {
get(target, key) { ... }, // 读取
set(target, key, val) { ... }, // 写入
deleteProperty(target, key) { ... } // 删除
})
具体例子
const obj = { name: '初始值' }
const proxy = new Proxy(obj, {
get(target, key) {
console.log(`读取了 ${String(key)}`)
return target[key] // 直接返回原对象的值
},
set(target, key, val) {
console.log(`修改了 ${String(key)}: ${target[key]} → ${val}`)
target[key] = val // 直接修改原对象
return true // 表示设置成功
},
deleteProperty(target, key) {
console.log(`删除了 ${String(key)}`)
delete target[key]
return true
}
})
// 使用
console.log(proxy.name) // 读取了 name → '初始值'
proxy.name = '李四' // 修改了 name: 初始值 → 李四
delete proxy.name // 删除了 name
console.log(obj.name) // undefined(原对象也被删了)
get、set、deleteProperty 是内置的"陷阱方法"
这些都是 Proxy 内置的,自动识别的。JavaScript 内部会识别这些特殊名字的方法:
- 当你读取 proxy.xxx → 自动调用 get
- 当你写入 proxy.xxx = yy → 自动调用 set
- 当你删除 delete proxy.xxx → 自动调用 deleteProperty
完整的 Proxy 陷阱方法列表
const proxy = new Proxy(target, {
// 1. 读取属性: proxy.name
get(target, key, receiver) {
return target[key]
},
// 2. 设置属性: proxy.name = '张三'
set(target, key, value, receiver) {
target[key] = value
return true // 必须返回布尔值,表示设置成功
},
// 3. 检查属性是否存在: key in proxy
has(target, key) {
return key in target
},
// 4. 删除属性: delete proxy.name
deleteProperty(target, key) {
delete target[key]
return true // 必须返回布尔值
},
// 5. 获取所有属性名: Object.keys(proxy) / Object.getOwnPropertyNames
ownKeys(target) {
return Reflect.ownKeys(target) // 返回数组
},
// 6. 拦截 Object.getOwnPropertyDescriptor
getOwnPropertyDescriptor(target, key) {
return Reflect.getOwnPropertyDescriptor(target, key)
},
// 7. 拦截 Object.defineProperty
defineProperty(target, key, descriptor) {
return Reflect.defineProperty(target, key, descriptor)
},
// 8. 拦截 Object.preventExtensions
preventExtensions(target) {
return Reflect.preventExtensions(target)
},
// 9. 拦截 Object.getPrototypeOf
getPrototypeOf(target) {
return Reflect.getPrototypeOf(target)
},
// 10. 拦截 Object.setPrototypeOf
setPrototypeOf(target, proto) {
return Reflect.setPrototypeOf(target, proto)
},
// 11. 拦截函数调用: proxy()
apply(target, thisArg, args) {
return target.apply(thisArg, args)
},
// 12. 拦截 new 操作: new proxy()
construct(target, args, newTarget) {
return new target(...args)
},
// 13. 拦截 Object.isExtensible
isExtensible(target) {
return Reflect.isExtensible(target)
}
})
速查表
| 场景 | 触发哪个陷阱 |
|---|---|
| obj.name | get |
| obj.name = ‘a’ | set |
| ‘name’ in obj | has |
| delete obj.name | deleteProperty |
| Object.keys(obj) | ownKeys |
| obj() (函数调用) | apply |
| new obj() | construct |
五、对比总结
// Object.defineProperty
const obj = {}
let _name = 'a'
Object.defineProperty(obj, 'name', {
get() { return _name }, // 需要闭包变量
set(v) { _name = v }
})
// Proxy
const obj = { name: 'a' }
const proxy = new Proxy(obj, {
get(t, k) { return t[k] }, // 直接用原对象
set(t, k, v) { t[k] = v }
})
| 特性 | Object.defineProperty | Proxy |
|---|---|---|
| 是否需要中间变量 | ✅ 需要 | ❌ 不需要 |
| 监听新增属性 | ❌ 不能 | ✅ 能 |
| 监听删除 | ❌ 不能 | ✅ 能 |
| 监听数组 | ❌ 需要 hack | ✅ 能 |
| 性能 | 初始化时慢 | 访问时拦截 |
六、自学代码成果
实现 reactive 函数
又是进步的一天 耶
更多推荐
所有评论(0)