【透彻讲解】Proxy 和 Object.defineProperty 的区别:数据代理 vs 数据劫持

前言

在 Vue2 和 Vue3 的响应式原理中,我们经常会听到"数据劫持"和"数据代理"这两个概念。它们分别对应着 Object.definePropertyProxy 这两种技术实现。本文将彻底解析它们的区别,通过代码示例、参数说明和使用场景对比,让你完全掌握这两种技术。

一、基本概念对比

特性 Object.defineProperty Proxy
ES版本 ES5 ES6
拦截维度 属性级别 对象级别
数组处理 需要特殊处理 原生支持
新增属性 需要手动监听 自动拦截
性能 较好 更优

二、Object.defineProperty(数据劫持)

1. 基本用法

js 复制代码
const obj = {};
let value = '初始值';

Object.defineProperty(obj, 'property', {
  get: function() {
    console.log('获取值');
    return value;
  },
  set: function(newValue) {
    console.log('设置值', newValue);
    value = newValue;
  }
});

obj.property; // 控制台输出"获取值"
obj.property = '新值'; // 控制台输出"设置值 新值"

2. 参数详解

js 复制代码
Object.defineProperty(obj, prop, descriptor)
  • obj:目标对象

  • prop:要定义或修改的属性名

  • descriptor:属性描述符,包含:

    • configurable:是否可删除,默认为 false
    • enumerable:是否可枚举,默认为 false
    • value:属性值
    • writable:是否可写
    • get:getter 函数
    • set:setter 函数

3. 实现简单响应式

js 复制代码
function observe(data) {
  if (!data || typeof data !== 'object') return;
  
  Object.keys(data).forEach(key => {
    let value = data[key];
    observe(value); // 递归子属性
    
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get() {
        console.log(`获取 ${key}: ${value}`);
        return value;
      },
      set(newVal) {
        console.log(`设置 ${key}: ${newVal}`);
        if (value !== newVal) {
          value = newVal;
          observe(newVal); // 新值是对象时继续监听
        }
      }
    });
  });
}

const data = { name: '张三', info: { age: 20 } };
observe(data);

data.name; // 获取 name: 张三
data.info.age = 21; // 获取 info: [object Object] (注意:无法直接监听嵌套属性变化)

4. 局限性

  1. 无法检测对象属性的添加或删除
js 复制代码
 const obj = { a: 1 };
    observe(obj);
    obj.b = 2; // 无法监听新增属性
  1. 数组方法需要特殊处理
js 复制代码
    const arr = [1, 2, 3];
    observe(arr);
    arr.push(4); // 无法监听数组push操作
  1. 需要递归监听属性造成额外的性能开销

三、Proxy(数据代理)

1. 基本用法

js 复制代码
const target = {};
const handler = {
  get(target, prop, receiver) {
    console.log(`获取属性 ${prop}`);
    return Reflect.get(target, prop, receiver);
  },
  set(target, prop, value, receiver) {
    console.log(`设置属性 ${prop} 为 ${value}`);
    return Reflect.set(target, prop, value, receiver);
  }
};

const proxy = new Proxy(target, handler);
proxy.name = '李四'; // 设置属性 name 为 李四
console.log(proxy.name); // 获取属性 name → 李四

2. 参数详解

js 复制代码
const proxy = new Proxy(target, handler)
  • target:目标对象

  • handler:处理器对象,包含"陷阱"方法:

    • get(target, prop, receiver):拦截属性读取
    • set(target, prop, value, receiver):拦截属性设置
    • has(target, prop):拦截in操作符
    • deleteProperty(target, prop):拦截delete操作
    • 13种拦截操作

3. 实现完整响应式

js 复制代码
function reactive(data) {
  if (!data || typeof data !== 'object') return data;
  
  const handler = {
    get(target, key, receiver) {
      console.log(`获取 ${key}`);
      const result = Reflect.get(target, key, receiver);
      return reactive(result); // 递归代理嵌套对象
    },
    set(target, key, value, receiver) {
      console.log(`设置 ${key} 为 ${value}`);
      return Reflect.set(target, key, reactive(value), receiver);
    },
    deleteProperty(target, key) {
      console.log(`删除 ${key}`);
      return Reflect.deleteProperty(target, key);
    }
  };
  
  return new Proxy(data, handler);
}

const state = reactive({
  name: '王五',
  hobbies: ['篮球', '游泳']
});

state.hobbies.push('跑步'); 
// 获取 hobbies → 设置 2 为 跑步 → 设置 length 为 3

4. 优势体现

  1. 完美监听数组变化
js 复制代码
    const arr = reactive([1, 2, 3]);
    arr.push(4); // 能够正常拦截
  1. 自动监听新增属性
js 复制代码
    const obj = reactive({});
    obj.newProp = '新属性'; // 能够在set中正常拦截
  1. 支持更多拦截操作
js 复制代码
    const proxy = reactive({});
    delete proxy.name; // 可以拦截delete操作
  1. 无需递归遍历自身进行数据劫持

四、核心区别对比

1. 实现原理不同

  • Object.defineProperty:直接修改原对象属性描述符
  • Proxy:创建原对象的代理对象,不修改原对象

2. 拦截粒度不同

js 复制代码
// Object.defineProperty必须指定具体属性
Object.keys(obj).forEach(key => {
  Object.defineProperty(obj, key, {...});
});

// Proxy一次性拦截整个对象
new Proxy(obj, {
  get() {...},
  set() {...}
});

五、Vue2 和 Vue3 响应式原理对比

Vue2 实现

js 复制代码
// 简化版Vue2响应式原理
class Observer {
  constructor(value) {
    this.walk(value);
  }
  
  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key]);
    });
  }
}

function defineReactive(obj, key, val) {
  // 递归处理子属性
  observe(val);
  
  Object.defineProperty(obj, key, {
    get() {
      console.log(`获取 ${key}`);
      return val;
    },
    set(newVal) {
      if (val === newVal) return;
      console.log(`设置 ${key}`);
      val = newVal;
      observe(newVal);
    }
  });
}

Vue3 实现

js 复制代码
// 简化版Vue3响应式原理
function reactive(target) {
  return createReactiveObject(target);
}

function createReactiveObject(target) {
  const handler = {
    get(target, key, receiver) {
      track(target, key); // 依赖收集
      const result = Reflect.get(target, key, receiver);
      return isObject(result) ? reactive(result) : result;
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if (oldValue !== value) {
        trigger(target, key); // 触发更新
      }
      return result;
    }
    // 其他陷阱...
  };
  
  return new Proxy(target, handler);
}

七、总结

Vue3 改用 Proxy 实现响应式,解决了 Vue2 中的诸多限制,降低我们的心智负担

希望这篇博文能帮助你彻底理解 Proxy 和 Object.defineProperty 的区别!如果有任何问题,欢迎在评论区留言讨论。

相关推荐
再学一点就睡3 小时前
手写 Promise 静态方法:从原理到实现
前端·javascript·面试
再学一点就睡4 小时前
前端必会:Promise 全解析,从原理到实战
前端·javascript·面试
前端工作日常4 小时前
我理解的eslint配置
前端·eslint
前端工作日常5 小时前
项目价值判断的核心标准
前端·程序员
90后的晨仔5 小时前
理解 Vue 的列表渲染:从传统 DOM 到响应式世界的演进
前端·vue.js
OEC小胖胖6 小时前
性能优化(一):时间分片(Time Slicing):让你的应用在高负载下“永不卡顿”的秘密
前端·javascript·性能优化·web
烛阴6 小时前
ABS - Rhomb
前端·webgl
植物系青年6 小时前
10+核心功能点!低代码平台实现不完全指南 🧭(下)
前端·低代码
植物系青年6 小时前
10+核心功能点!低代码平台实现不完全指南 🧭(上)
前端·低代码
桑晒.6 小时前
CSRF漏洞原理及利用
前端·web安全·网络安全·csrf