JavaScript 的“读心术”:如何监听对象的一举一动?

73 阅读5分钟

什么是 Object.defineProperty?—— JavaScript 中的“监控神器”

你有没有想过,JavaScript 是怎么知道你“读”或“改”了一个对象的属性?
在 Vue 这样的前端框架里,只要数据变了,页面就会自动更新。这背后其实有一个关键技术:数据劫持(Data Hijacking)
而实现这个技术的核心方法,就是我们今天要讲的主角:Object.defineProperty()

别被名字吓到,它其实就像给对象的属性装上了一个“监控摄像头”。下面我们一步步来揭开它的神秘面纱。


一、JavaScript 对象的默认行为

我们平时这样定义一个对象:

const person = {
  name: '小明',
  age: 12
};

你可以读取:

console.log(person.name); // 输出:小明

也可以修改:

person.age = 13;

这些操作都很自然,但问题是:JavaScript 不会告诉你“谁读了”或者“谁改了”这个值。

如果我们想在“读取”或“修改”时做点事情(比如打印日志、更新页面),怎么办?

这时候,Object.defineProperty() 就派上用场了!


二、Object.defineProperty 是什么?

Object.defineProperty() 是 JavaScript 中用于直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象的函数。它是 Vue2 响应式系统的核心实现原理。

📌 基本语法

Object.defineProperty(obj, prop, descriptor);
参数说明
  1. obj - 要定义属性的目标对象
  2. prop - 要定义或修改的属性名称(String 或 Symbol)
  3. descriptor - 属性的描述符对象(核心配置)

🛠️ 三、描述符的几种配置方式

描述符可以设置很多选项,我们重点看两个核心类型:

1️⃣ 数据属性(最常见)

特点:直接定义属性的值和特性

{
  value: '小明',         // 属性的值
  writable: true,        // 是否允许修改(true 表示能改)
  enumerable: true,      // 是否能被 for...in 遍历
  configurable: true     // 是否能被删除或再次修改描述符
}

👉 例子:

const person = {};

Object.defineProperty(person, 'name', {
  value: '小明',
  writable: false,     // 属性值能否被重新赋值
  enumerable: true,    // 属性是否可以出现在 `for...in` 循环和 `Object.keys()` 中
  configurable: true   // 属性能否被删除,能否更改访问器属性
});

console.log(person.name); // "小明"
person.name = '小红';    // 静默失败(严格模式下会报错)
console.log(person.name); // 仍然是"小明"

// 测试枚举
for(let key in person) {
  console.log(key); // 输出 "name"(因为 enumerable: true)
}

✅ 这样我们就“锁定”了这个属性,不能被随意修改。


2️⃣ 访问器属性(重点!数据劫持的关键)

这才是重头戏!我们可以用 getset 来“拦截”读和写操作。

{
  get() {
    // 当读取属性时,自动执行这个函数
  },
  set(新值) {
    // 当修改属性时,自动执行这个函数
  },
  enumerable: true,
  configurable: true
}

四、数据劫持:让属性“有感觉”

什么叫“数据劫持”?
简单说就是:当别人读或改某个属性时,我能第一时间知道,并做出反应。

这就像你家门上装了摄像头和报警器:

  • 有人进来(读取)?我知道了!
  • 有人改动东西(修改)?我立刻报警!

✅ 示例:监控一个人的名字

const person = {
  firstName: '张',
  lastName: '三'
};

// 我们想让 fullName 自动组合姓和名
let fullName = '张 三';

Object.defineProperty(person, 'fullName', {
  get() {
    console.log('有人读取了 fullName!');
    return `${this.firstName} ${this.lastName}`;
  },
  set(value) {
    console.log('有人想修改 fullName 为:', value);
    const parts = value.split(' ');
    this.firstName = parts[0];
    this.lastName = parts[1] || '';
  },
  enumerable: true
});

🧪 测试一下:

console.log(person.fullName);
// 输出:
// 有人读取了 fullName!
// 张 三

person.fullName = '李 四';
// 输出:
// 有人想修改 fullName 为:李 四

console.log(person.firstName); // 李
console.log(person.lastName);  // 四

✅ 看!我们通过 getset拦截了读写操作,还能在中间加逻辑,比如打印日志、拆分名字等。

这就是“数据劫持”——我们“劫持”了属性的访问过程,让它变得“智能”。


五、用数据劫持实现一个简单的“响应式系统”

想象一下,如果数据变了,页面能自动更新,那该多好?
Vue 2 就是这么干的!

我们来模拟一个极简版的“响应式系统”。

💡 思路:

  1. 遍历对象的每个属性
  2. Object.defineProperty 给每个属性加上 getset
  3. get 中收集“谁用了这个数据”
  4. set 中通知“数据变了,快更新!”

先不考虑“通知谁”,我们先实现监控:

// 定义一个函数,让某个属性变成“响应式”
function defineReactive(obj, key, value) {
  // 如果 value 是对象,也要递归监听(比如嵌套对象)
  observe(value);

  Object.defineProperty(obj, key, {
    get() {
      console.log(`访问了 ${key},值是:`, value);
      return value;
    },
    set(newVal) {
      if (newVal === value) return; // 没变就不处理
      console.log(`${key} 被修改了!从`, value, '变成', newVal);
      value = newVal;
      // 新值如果是对象,也要监听
      observe(newVal);
    },
    enumerable: true,
    configurable: true
  });
}

// 遍历对象,让所有属性都变成响应式
function observe(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return; // 只处理对象
  }

  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
  });
}

🧪 测试我们的响应式系统

const data = {
  name: '小明',
  age: 18,
  address: {
    city: '北京'
  }
};

// 让 data 变成响应式
observe(data);

// 开始测试
console.log(data.name);
// 输出: 访问了 name,值是: 小明

data.age = 20;
// 输出: age 被修改了!从 18 变成 20

console.log(data.address.city);
// 输出: 访问了 city,值是: 北京

data.address.city = '上海';
// 输出: city 被修改了!从 北京 变成 上海

✅ 成功了!我们实现了对对象属性的“全面监控”!


六、数据劫持的实际用途

  1. Vue 2 的响应式原理
    Vue 2 就是用 Object.defineProperty 监听数据变化,一旦数据改了,就自动更新页面。

  2. 表单验证
    比如用户输入邮箱时,自动检查格式是否正确。

  3. 数据日志/埋点
    记录用户修改了哪些字段,用于分析行为。

  4. 自动计算属性
    比如 fullName 自动由 firstNamelastName 拼接。


七、它的局限性(为什么 Vue 3 改用 Proxy)

虽然 Object.defineProperty 很强大,但它也有“短板”:

问题说明
❌ 无法监听新增属性比如 data.newProp = 'xxx',不会被监控到
❌ 无法监听数组下标修改arr[0] = 'new' 不会触发 set
❌ 必须递归遍历对象对深层对象性能有影响

所以 Vue 3 改用了更强大的 Proxy,它能解决这些问题。

但理解 Object.defineProperty 依然非常重要,它是理解“响应式原理”的第一步!


小练习
试着用 Object.defineProperty 实现一个“自动计算总价”的购物车,当商品数量变化时,自动更新总价并打印日志。