什么是 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);
参数说明
- obj - 要定义属性的目标对象
- prop - 要定义或修改的属性名称(String 或 Symbol)
- 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️⃣ 访问器属性(重点!数据劫持的关键)
这才是重头戏!我们可以用 get 和 set 来“拦截”读和写操作。
{
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); // 四
✅ 看!我们通过
get和set,拦截了读写操作,还能在中间加逻辑,比如打印日志、拆分名字等。
这就是“数据劫持”——我们“劫持”了属性的访问过程,让它变得“智能”。
五、用数据劫持实现一个简单的“响应式系统”
想象一下,如果数据变了,页面能自动更新,那该多好?
Vue 2 就是这么干的!
我们来模拟一个极简版的“响应式系统”。
💡 思路:
- 遍历对象的每个属性
- 用
Object.defineProperty给每个属性加上get和set - 在
get中收集“谁用了这个数据” - 在
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 被修改了!从 北京 变成 上海
✅ 成功了!我们实现了对对象属性的“全面监控”!
六、数据劫持的实际用途
-
Vue 2 的响应式原理
Vue 2 就是用Object.defineProperty监听数据变化,一旦数据改了,就自动更新页面。 -
表单验证
比如用户输入邮箱时,自动检查格式是否正确。 -
数据日志/埋点
记录用户修改了哪些字段,用于分析行为。 -
自动计算属性
比如fullName自动由firstName和lastName拼接。
七、它的局限性(为什么 Vue 3 改用 Proxy)
虽然 Object.defineProperty 很强大,但它也有“短板”:
| 问题 | 说明 |
|---|---|
| ❌ 无法监听新增属性 | 比如 data.newProp = 'xxx',不会被监控到 |
| ❌ 无法监听数组下标修改 | arr[0] = 'new' 不会触发 set |
| ❌ 必须递归遍历对象 | 对深层对象性能有影响 |
所以 Vue 3 改用了更强大的
Proxy,它能解决这些问题。
但理解 Object.defineProperty 依然非常重要,它是理解“响应式原理”的第一步!
小练习:
试着用 Object.defineProperty 实现一个“自动计算总价”的购物车,当商品数量变化时,自动更新总价并打印日志。