跳到主要内容

Proxy 与 Object.defineProperty

这题表面上在问 JavaScript API,实际上面试官大多是借它追问:

  • Vue 2 为什么基于 Object.defineProperty
  • Vue 3 为什么切到 Proxy
  • 两者在“响应式能力、性能边界、工程复杂度”上到底差在哪

如果你只答“Proxy 能监听新增删除属性”,通常不够。

面试速答(30 秒版 TL;DR)

  • Object.defineProperty给某个已有属性定义 getter/setter,适合精确劫持单个字段,但天然是“属性级别”的方案。
  • Proxy给整个对象套代理,可以拦截 getsetdeletePropertyhasownKeys 等多种操作,能力更完整。
  • Vue 2 选 Object.defineProperty,主要是历史时期兼容性和浏览器支持决定的;Vue 3 选 Proxy,是因为它更适合现代响应式系统。
  • 核心差异不是“API 更新”,而是:
    • Object.defineProperty 要预先递归遍历已有属性
    • Proxy 可以在对象访问时按需拦截,天然支持新增/删除、数组索引、Map/Set

先按两类方案记:Object.defineProperty = 属性级劫持Proxy = 对象级代理

一、先把两者的本质说清楚

1. Object.defineProperty 是“改属性描述符”

const obj = {}
let value = 1

Object.defineProperty(obj, 'count', {
get() {
return value
},
set(newValue) {
value = newValue
},
})

它的特点是:

  • 目标是“某个属性”
  • 你得先知道属性名
  • 主要拦截这个属性的读和写

所以它更像:给对象上现有字段逐个装监控器

2. Proxy 是“代理整个对象行为”

const target = { count: 1 }

const proxy = new Proxy(target, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver)
},
})

它的特点是:

  • 目标是“整个对象”
  • 某个属性是否已存在,不是前提
  • 你拦截的是对象层面的各种操作

所以它更像:给对象整体套一层代理外壳

二、能力对比:为什么 Vue 3 更偏向 Proxy

对比项Object.definePropertyProxy
拦截粒度单个属性整个对象
能否监听新增属性弱,需要额外处理可以
能否监听删除属性可以
数组索引变更处理麻烦更自然
Map/Set 支持很差更好
初始化成本常要递归遍历可按需代理
浏览器兼容性更老更稳现代浏览器方案

这里最该展开的是三点。

1. 新增、删除属性

Vue 2 里为什么会有 Vue.set / this.$set 这种写法?

因为 Object.defineProperty 只能劫持“已经定义过的属性”。如果某个字段初始化时不存在,后面直接加:

obj.newKey = 1

框架根本没机会提前给它装 getter/setter。

Proxy 拦截的是对象层面的 set,所以新增属性也会经过代理。

2. 数组处理

Vue 2 对数组是经典高频追问。

原因在于:

  • 数组下标很多,不可能逐个预定义
  • length 变化、下标变动、插入删除都比较特殊

所以 Vue 2 只能通过“改写数组变异方法”的方式兜底,比如:

  • push
  • pop
  • shift
  • unshift
  • splice
  • sort
  • reverse

这就是为什么 Vue 2 对数组某些写法不够自然。

Proxy 更容易覆盖这些场景,因为数组本质上也是对象,索引写入、长度变化都能走代理层。

3. 初始化和深层递归成本

Object.defineProperty 想让一个对象“完全响应式”,一般需要在初始化阶段把所有已知属性递归走一遍。

问题是:

  • 对象越深,初始化越重
  • 动态结构越多,维护越麻烦

Proxy 并不代表零成本,但它更适合“访问到哪一层,再代理哪一层”的现代实现思路。

三、Vue 2 为什么当年不用 Proxy

这题常被反问。

核心答案不是“作者没想到”,而是时代约束

  • Vue 2 诞生时,需要兼顾更老的浏览器环境
  • Proxy 无法被完整 polyfill
  • 生态和构建环境对现代特性的接受度也没今天高

所以在当年的工程现实里,Object.defineProperty 是更务实的方案。

四、Vue 3 用 Proxy 后,响应式链路发生了什么变化

Vue 3 不是只把 API 名字换了,而是把响应式核心能力升级了。

典型链路可以概括成:

  1. reactive(obj) 返回一个 Proxy
  2. 渲染或副作用函数读取属性时,触发 get
  3. get 里做依赖收集 track(target, key)
  4. 修改属性时触发 set/delete
  5. 代理里执行 trigger(target, key),通知相关副作用重新运行

最小示意:

const state = reactive({ count: 0 })

effect(() => {
console.log(state.count)
})

state.count++

这里的关键不是 Proxy 本身,而是:

  • 读取时建立依赖
  • 写入时精准触发依赖

五、是不是 Proxy 就一定“完胜”

不能这么答,面试里要有边界意识。

1. Proxy 不是没有成本

  • 代理对象本身有运行期开销
  • 深层大对象、频繁枚举、深度监听仍然要考虑性能
  • 响应式系统真正的成本,不只在“能不能拦截”,还在“依赖收集粒度”和“更新范围”

2. Proxy 代理的是对象,不是原始值

这就是为什么 Vue 3 里基本类型通常还要靠 ref

3. 解构仍然可能丢响应式

即使底层换成了 Proxy,如果你把属性值解构出来:

const state = reactive({ count: 0 })
const { count } = state

此时 count 只是一个普通值副本,不会再经过原代理对象的 get/set

六、典型题 & 标准答法

Q1:Object.definePropertyProxy 最大区别是什么?

Object.defineProperty 是属性级劫持,你得提前知道属性并给它定义 getter/setter;Proxy 是对象级代理,直接从对象操作入口统一拦截,所以对新增删除属性、数组索引、集合类型支持更完整,也更适合现代响应式框架。

Q2:为什么 Vue 2 监听数组这么麻烦?

:因为 Object.defineProperty 不适合天然覆盖数组下标和长度变化,所以 Vue 2 只能通过重写数组变异方法来补齐能力,例如 push/splice 等。它本质上是“在 API 层兜底”,而不是像 Proxy 那样从对象访问层统一拦截。

Q3:Vue 3 用了 Proxy,就完全没有响应式问题了吗?

:没有。Proxy 主要解决的是“拦截能力”问题,但解构丢响应式、深度监听成本、大对象更新范围控制这些问题依然存在。真正的性能和可维护性,还取决于状态设计和依赖粒度。

七、易错点 / 坑

  • 把两者区别只答成“一个老一个新”。
  • 以为 Object.defineProperty 完全不能做响应式。它可以做,只是能力和工程复杂度受限。
  • 以为 Proxy 能代理基本类型。它只能代理对象。
  • 以为 Vue 3 响应式升级后,所有性能问题都会自动消失。

速记要点

  • defineProperty:劫持属性
  • Proxy:代理对象
  • Vue 2 受限于时代兼容性,选了 defineProperty
  • Vue 3 需要更完整的响应式能力,切到 Proxy
  • 关键收益:新增/删除、数组、集合类型、更自然