标准答案

  1. Object.defineProperty 是属性级拦截。它可以给指定属性定义 getter/setter,从而拦截这个属性的读取和设置,但通常需要对每个已有属性逐个处理。
  2. defineProperty 的边界是新增属性、删除属性、数组索引变化、数组 length 变化等场景需要额外补丁。Vue 2 响应式就因为这些边界,需要 Vue.set、数组方法重写等辅助机制。
  3. Proxy 是对象级代理。它代理整个目标对象,可以拦截 get、set、has、deleteProperty、ownKeys、getPrototypeOf 等更多操作,对新增属性、删除属性和数组操作更自然。
  4. 现代响应式更偏向 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 后继续操作原对象,导致拦截不生效。

作者信息