「JavaScript深入」理解 Object.defineProperty 与 Proxy


在 JavaScript 中,Object.definePropertyProxy 是两种强大的机制,用于拦截和处理对象属性的读写操作。Vue.js 的响应式系统正是基于它们实现的。下面将详细介绍这两者的基础用法、区别以及它们在 Vue 响应式系统中的应用。

Object.defineProperty

基本用法

Object.defineProperty 允许在对象上定义新的属性或修改现有属性,并控制属性的行为。

👇语法如下:

Object.defineProperty(obj, prop, descriptor);

其中,descriptor 是一个描述符对象,包含以下两类属性:

数据描述符(Data Descriptor):
  • value:属性的值,默认为 undefined
  • writable:属性是否可修改,默认为 false
  • enumerable:属性是否可枚举(可被 for...inObject.keys() 访问),默认为 false
  • configurable:属性描述符是否可被修改,且属性是否可删除,默认为 false
访问器描述符(Accessor Descriptor):
  • get:属性的 getter 函数
  • set:属性的 setter 函数

注意:两类描述符不能同时存在。

示例代码

let obj = {};
Object.defineProperty(obj, 'name', {
  value: 'Simon',
  writable: false,    // 不能修改
  enumerable: true,   // 可枚举
  configurable: false // 不能删除或重新配置
});

console.log(obj.name); // Simon
obj.name = 'Rita';
console.log(obj.name); // 仍然是 Simon(因为 `writable: false`)

在 Vue2 响应式系统中的应用

Vue 2 通过 Object.defineProperty 实现响应式数据绑定,即在 gettersetter 中拦截数据读取和修改,并触发视图更新。

function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log(`Get ${key}: ${val}`);
      return val;
    },
    set(newVal) {
      console.log(`Set ${key}: ${newVal}`);
      if (val !== newVal) {
        val = newVal;
        // 这里可以添加视图更新逻辑
      }
    }
  });
}

let data = { message: 'Hello, Simon!' };
defineReactive(data, 'message', data.message);

data.message = 'Hello, Vue!'; // Set message: Hello, Vue!
console.log(data.message);    // Get message: Hello, Vue!

Proxy

基本介绍

Proxy 是 ES6 引入的功能,它允许创建一个代理对象来拦截对目标对象的操作,从而增强或修改对象的行为。

const proxy = new Proxy(target, handler);
  • target:被代理的目标对象
  • handler:定义拦截行为的对象

常用拦截方法

  • get(target, property, receiver):拦截读取属性
  • set(target, property, value, receiver):拦截设置属性
  • has(target, property):拦截 in 操作符
  • deleteProperty(target, property):拦截 delete 操作
  • apply(target, thisArg, argumentsList):拦截函数调用
  • construct(target, argumentsList, newTarget):拦截 new 操作符

示例代码

const handler = {
  get(target, prop) {
    console.log(`Get ${prop}`);
    return target[prop];
  },
  set(target, prop, value) {
    console.log(`Set ${prop} to ${value}`);
    target[prop] = value;
    return true;
  }
};

let obj = { name: 'John' };
let proxyObj = new Proxy(obj, handler);

console.log(proxyObj.name); // Get name
proxyObj.name = 'Byron';   // Set name to Byron

Reflect

Reflect 是 ES6 引入的一个全局对象,提供了与对象操作相关的静态方法,主要用于搭配 Proxy 处理默认行为。

const handler = {
  get(target, prop, receiver) {
    console.log(`Get ${prop}`);
    return Reflect.get(target, prop, receiver);
  },
  set(target, prop, value, receiver) {
    console.log(`Set ${prop} to ${value}`);
    return Reflect.set(target, prop, value, receiver);
  }
};

Vue2 vs Vue3 响应式的变化

Vue 2 使用 Object.defineProperty,但它有以下局限性:

  • 只能拦截已存在的属性,无法监听新增属性
  • 不能检测数组索引变化
  • 需要对每个属性单独定义 gettersetter

Vue 3 改用 Proxy,带来了以下优化:

  • 直接监听整个对象,包括属性的新增和删除
  • 数组索引和 length 变化可以被检测
  • 更灵活强大的拦截系统

Vue3 响应式示例

const reactiveHandler = {
  get(target, prop) {
    console.log(`Get ${prop}`);
    return Reflect.get(target, prop);
  },
  set(target, prop, value) {
    console.log(`Set ${prop} to ${value}`);
    return Reflect.set(target, prop, value);
  }
};

const reactiveData = new Proxy({ message: 'Hello' }, reactiveHandler);
reactiveData.message = 'Hello, Vue 3!'; // Set message to Hello, Vue 3!
console.log(reactiveData.message); // Get message

结论

  • Object.defineProperty 适用于简单的数据拦截,但有一定的局限性。
  • Proxy 提供更强大的拦截能力,能监听对象的新增、删除及数组索引变化。
  • Vue 2 使用 Object.defineProperty 作为响应式系统的基础,而 Vue 3 改用 Proxy 以解决 Vue 2 的限制。

希望这篇文章能帮助你理解 Object.definePropertyProxy 的核心概念及它们在 Vue 响应式系统中的应用!

### 使用 `Object.defineProperty` 实现监听数组变化 由于 JavaScript 中的对象属性可以通过 `Object.defineProperty()` 方法进行拦截,但对于数组来说,默认情况下其长度和内部元素的变化不会触发这些拦截器。为了实现对数组的监听,可以重写数组原型上的变更方法。 #### 重写数组变异方法 JavaScript 数组有多种改变自身状态的方法,比如 `push`, `pop`, `shift`, `unshift`, `splice`, `sort`, 和 `reverse` 等。要使这些操作能够被侦测到并作出反应,可以在创建新数组之前先保存原始方法,再覆盖它们以便在调用时通知观察者[^1]。 下面是一个简单的例子展示如何利用 `Object.defineProperty` 对象定义来监控数组的变化: ```javascript function observeArray(arr) { const methodsToOverride = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']; // 创建一个函数用来替换原有的数组方法 function makeObservable(methodName) { let originalMethod = Array.prototype[methodName]; return function(...args) { console.log(`Calling ${methodName} with arguments `, args); // 调用原来的数组方法 let result = originalMethod.apply(this, args); // 当 push 或 unshift 添加了新的项,则需要遍历新增加的内容继续观测 if (['push', 'unshift'].includes(methodName)) { for(let i=0; i<args.length;i++){ observe(args[i]); } } // 如果是 splice 操作,那么可能既会删除也会增加项目 if ('splice' === methodName){ for(let i=0; i<args.length && i%2===1 ;i+=2){ observe(args[i]); } } // 发布更新事件给订阅者或其他逻辑处理... notifyChange(); return result; }; } // 遍历所有需要替代的方法名列表,并将其替换成我们自定义版本 methodsToOverride.forEach(function(methodName) { arr[methodName] = makeObservable(methodName).bind(arr); }); } // 定义一个辅助函数用于递归地监测复杂结构内的简单对象或数组 function observe(value) { if (!value || typeof value !== 'object') {return;} Object.keys(value).forEach(key => { defineReactiveProperty(value, key, value[key]); }); if(Array.isArray(value)){ observeArray(value); } } // 定义响应式的 getter/setter 属性描述符 function defineReactiveProperty(obj, key, val) { observe(val); // 递归观测嵌套的对象 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { console.log(`${key} was accessed`); return val; }, set: function reactiveSetter(newVal) { if (newVal === val) return; console.log(`Setting new value to ${key}:`, newVal); val = newVal; observe(newVal); // 继续监视新设置的值 notifyChange(); // 告知视图或者其他组件发生了更改 } }); } ``` 上述代码片段展示了怎样通过 `Object.defineProperty` 来追踪数组及其成员的变化。每当执行像 `push` 这样的动作时,不仅会记录下这个行为本身,还会进一步检查是否有任何新加入的数据也需要受到同样的监听机制的影响[^4]。 请注意,在 Vue.js 版本升级至 Vue 3 后,官方推荐使用更强大的 Proxy API 替代 `Object.defineProperty`,因为后者存在一些局限性,特别是在处理动态添加属性以及深层路径绑定方面表现不佳[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

八了个戒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值