defineProperty 与 Proxy 的区别

本文详细介绍了JavaScript中的Object.defineProperty和Proxy对象的用法,比较了两者在存储位置、属性拦截和数组操作上的区别,以及在Vue中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Object.defineProperty 用法

此方法可以直接在对象上定义一个新属性,也可以修改对象的现有属性。
语法:Object.defineProperty(obj, prop, descriptor)

  • obj:要定义或修改属性的对象
  • prop:要定义或修改的属性的名称或 Symbol
  • descriptor:该属性的描述符
  • 返回值:传入的对象obj

该方法的核心在于属性描述符(descriptor 参数),存在两种写法,分别为数据描述符与存取描述符
这两种描述符都是对象。它们共享以下键值:

  • configurable 表示该属性的描述符能否被改变以及该属性能否从对象上删除,默认为 false

  • enumerable 表示该属性是否会出现在对象的枚举属性中,默认为 false

数据描述符还具有以下键值:

  • value 表示该属性的值,默认为 undefined

  • writable 表示该属性是否可写,默认为 false

存取描述符还具有以下键值:

  • get 表示该属性的取函数,在读取改属性时会执行此函数,函数返回值作为属性的值

  • set 表示该属性的存函数,修改属性值时会调用此函数,并接受一个参数(被赋予的新值)

两种描述符不可同时配置,如果一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常
用一个简单的示例展示其用法:

let obj = {}

Object.defineProperty(obj, '_num', {
  value: 1,
  writable: true,
  enumerable: false, // 不被枚举
  configurable: false,
})
Object.defineProperty(obj, 'num', {
  get() {
    console.log('num属性被访问')
    return this._num
  },
  set(value) {
    console.log('num属性被修改')
    this._num = value
  },
  enumerable: true,
  configurable: true,
})

console.log(obj._num) // 1
console.log(obj.num) // num属性被访问 1
console.log(Object.keys(obj)) // ['num'] (_num属性未被枚举)
obj.num = 10 // num属性被修改
delete obj._num // (configurable为false,无法删除)
delete obj.num
console.log(obj.num) // undefined
console.log(obj._num) // 1


Proxy 用法

Proxy 是一个类,可以为对象创建一个代理对象,从而实现对基本操作的拦截和自定义。
语法:const p = new Proxy(target, handler)

  • target:要代理的对象
  • handler:拦截器对象
  • 返回值:新的代理对象

该方法的核心在于拦截器对象,其包含许多的函数属性,用于定义代理行为。
Proxy 可以拦截对象的 14 种操作,不过我们一般拦截的只有读写操作,语法如下:

const hander = {
  get(target, property, receiver) {},
  set(target, property, value, receiver) {},
}
  • target:被代理的原对象
  • property:要访问的属性名或 Symbol
  • value:要设置的新属性值
  • receiver:代理对象
  • 返回值:get 返回要获取的属性值,而 set 返回一个布尔值,表示赋值是否成功

用一个简单的示例展示其用法:

const obj = {
  _num: 1,
}

const hander = {
  get(target, property) {
    if (property === 'num') return target['_num']
    else return undefined
  },
  set(target, property, value) {
    if (property === 'num') {
      console.log('num属性赋值成功')
      target['_num'] = value
      return true
    } else {
      console.log(`${property}属性赋值失败`)
      return false
    }
  },
}

const p = new Proxy(obj, hander)

console.log(p._num) // undefined
console.log(p.num) // 1
p.num = 2 // num属性赋值成功
p._num = 3 // _num属性赋值失败
console.log(p.num) // 2
console.log(obj._num) // 2

两者区别

看完了 Obejct.defineProperty 与 Proxy 的用法,接下来结合 Vue 讲解一下它们的区别

属性值的存储位置

在 Vue2 中,是通过闭包来存储属性值的

// 属性值存储在 val 变量中
const def = (obj, prop, val = obj[prop]) => {
  Object.defineProperty(obj, prop, {
    get() {
      console.log(`${prop}属性被访问`)
      return val
    },
    set(value) {
      console.log(`${prop}属性被修改为${value}`)
      val = value
    },
    enumerable: true,
    configurable: true,
  })
}

而在 Vue3 中,属性值一致存储在原对象中

const hander = {
  get(target, prop) {
    console.log(`${prop}属性被访问`)
    return target[prop]
  },
  set(target, prop, value) {
    console.log(`${prop}属性被修改为${value}`)
    target[prop] = value
    return true
  },
}

拦截多个属性

当使用 Obejct.defineProperty 拦截对象的多个属性时,需要遍历对象的所有属性名,依次设置描述符

而使用 Proxy 拦截对象的多个属性时,拦截器会直接作用于所有属性

Obejct.defineProperty

const def = (obj, prop) => {...}

const observe = (obj) => {
  // 循环所有属性名,Vue2为了兼容 IE,源码中使用的是 for in
  for (const prop of Object.keys(obj)) {
    def(obj, prop)
  }
  return obj
}

const obj = observe({
  a: 1,
  b: 2,
  c: 3,
})

obj.a // a属性被访问
obj.b // b属性被访问

Proxy

const hander = {...}

const reactive = (obj) => {
  return new Proxy(obj, hander)
}

const obj = reactive({
  a: 1,
  b: 2,
  c: 3,
})

obj.a // a属性被访问
obj.b // b属性被访问

多层对象

二者在拦截多层对象的操作,也就是深度监听时,都需要递归地操作对象,区别在于 Vue2 在定义时就要完成所有层属性的拦截,而 Vue3 则是到真正使用时,才生成对应的 Proxy 对象。

Vue2

const def = (obj, prop, val = obj[prop]) => {
  // 值是对象,递归监听
  if (typeof val === 'object') observe(val)

  Object.defineProperty(obj, prop, {
    get() {
      console.log(`${prop}属性被访问`)
      return val
    },
    set(value) {
      console.log(`${prop}属性被修改为${value}`)
      val = value
    },
    enumerable: true,
    configurable: true,
  })
}

const observe = (obj) => {
  for (const prop of Object.keys(obj)) {
    def(obj, prop)
  }
  return obj
}

let obj = observe({
  a: {
    b: 1,
  },
})
obj.a.b
// a属性被访问
// b属性被访问

Vue3

const hander = {
  get(target, key) {
    let val = target[key]
    // 值是对象,则进行包装后返回
    return typeof val === 'object' ? reactive(val) : val
  }