1. 语法
Object.defineProperty(obj, prop, descriptor)
-
obj:要定义或修改属性的目标对象。 -
prop:要定义或修改的属性名称(String 或 Symbol)。 -
descriptor:属性描述符对象,用于控制属性的行为。 Object.defineProperty()是 JavaScript 中一个非常强大的工具,它允许你精确地控制对象属性的行为。下面我将详细解释其用法,并通过丰富的示例帮助你透彻理解。
属性描述符分为两种主要类型,它们决定了属性的核心工作方式:
一个属性描述符只能是其中一种,不能同时包含 value/writable和 get/set。
| 描述符类型 | 核心作用 | 可选键 |
|---|---|---|
数据描述符 | 定义包含值的属性,并控制该值是否可被修改 | value, writable, enumerable, configurable |
访问器描述符 | 通过 getter/setter 函数来控制属性的读取和赋值行为 | get, set, enumerable, (configurable可配置性) |
2. 缺陷
Object.defineProperty在实现精细属性控制时,存在一些天然的拦截盲区。下面我们通过具体示例来探讨这些问题。
🔍 对象新增/删除属性的拦截问题
Object.defineProperty只能对对象上已经存在的属性设置 getter/setter 进行拦截。对于初始化后动态新增或删除的属性,它无法自动感知。
const person = { name: '小明' };
// 仅为初始属性 'name' 设置拦截
Object.defineProperty(person, 'name', {
get() {
console.log('读取 name 属性');
return this._name;
},
set(newVal) {
console.log('设置 name 属性为:', newVal);
this._name = newVal;
}
});
// 测试初始属性
person.name = '小红'; // 控制台输出: "设置 name 属性为: 小红"
console.log(person.name); // 控制台输出: "读取 name 属性", 然后输出: "小红"
// 问题1: 新增属性无法被拦截
person.age = 18; // 控制台没有任何输出,新增属性未被拦截
console.log(person.age); // 输出: 18,读取新增属性时也无任何输出
// 问题2: 删除属性无法被拦截
delete person.name; // 控制台没有任何输出,属性删除操作未被拦截
console.log(person.name); // 输出: undefined
原因分析:Object.defineProperty的作用范围仅限于在调用它时指定的属性。当新的属性被添加到对象上时,如果没有再次调用 Object.defineProperty来显式定义该属性的描述符,引擎就不会为其建立拦截层。删除操作亦是如此。
📊 数组索引与方法的拦截局限
对于数组,Object.defineProperty的局限性更加明显,主要体现在通过索引设置值以及操作数组长度上。
const arr = [1, 2, 3];
// 尝试为数组的索引 0 设置拦截
Object.defineProperty(arr, '0', {
get() {
console.log('读取索引 0');
return this._0;
},
set(newVal) {
console.log('设置索引 0 为:', newVal);
this._0 = newVal;
}
});
// 测试1: 直接通过索引设置已存在的元素
arr[0] = 99; // 控制台输出: "设置索引 0 为: 99" (本次有效)
// 测试2: 通过数组方法(如 push)添加新元素
arr.push(4); // 控制台没有任何输出
// push 操作相当于执行了 `arr[3] = 4`,但新索引 3 未被拦截。
// 测试3: 直接通过索引设置新元素
arr[3] = 100; // 控制台没有任何输出,新增的索引 3 未被拦截
// 测试4: 直接修改数组长度
arr.length = 0; // 控制台没有任何输出,无法拦截对 length 属性的修改
console.log(arr); // 输出: [],数组已被清空,但拦截器未触发
Vue 2 基于 Object.defineProperty的实现方式,它也带来了一些固有的限制:
-
对象新增/删除属性:由于 Object.defineProperty
只能劫持初始化时存在的属性,因此直接给对象设置新属性(this.obj.newProp = value) 或删除属性(delete this.obj.prop) 是无法被侦测到的。为了解决这个问题,Vue 2 提供了 Vue.set(或 this.$set) 和 Vue.delete 方法 。 -
数组的局限性:
Object.defineProperty无法有效侦测通过数组索引直接设置值 (this.items[index] = newValue) 或直接修改数组长度 (this.items.length = 0)。Vue 2 的选择是重写数组的 7 个变更方法(如push,pop,splice等),让这些方法在执行变更操作的同时,能够手动触发更新通知