搞懂 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 函数

又是进步的一天 耶

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐