一、Object.defineProperty
这里只是简单描述,具体请看另一篇文章:Object.defineProperty。
Object.defineProperty
是 JavaScript 中用于定义或修改对象属性的功能强大的方法。它可以精确地控制属性的行为,如是否可枚举、可配置、可写等。
基本用法
Object.defineProperty(obj, prop, descriptor)
Object.defineProperty
方法接受三个参数:
- 『目标对象』:要在其上定义属性的对象。
- 『属性名称』:要定义或修改的属性名称。
- 『描述符对象』:属性描述符对象,用于描述该属性的行为。
属性描述符对象可以包含以下键:
value:属性的值。默认为 undefined。
writable:属性是否可写。默认为 false。
configurable:属性是否可配置。默认为 false。
enumerable:属性是否可枚举。默认为 false。
get:属性的 getter 函数。如果没有 getter,值为 undefined。
set:属性的 setter 函数。如果没有 setter,值为 undefined。
用例
1. 定义一个只读属性
let obj = {};
Object.defineProperty(obj, 'message', {
value: 'Hello, world!',
writable: false
});
console.log(obj.message); // 输出 "Hello, world!"
obj.message = 'Hi!'; // 无效,因为属性是只读的
console.log(obj.message); // 仍然输出 "Hello, world!"
2. 定义一个不可枚举属性
let obj = {};
Object.defineProperty(obj, 'message', {
value: 'Hello, world!',
enumerable: false
});
console.log(obj.message); // 输出 "Hello, world!"
console.log(Object.keys(obj)); // 输出 [], 因为属性不可枚举
3. getter与setter
let obj = {};
let value = 'Hello, world!';
Object.defineProperty(obj, 'message', {
get() {
return value;
},
set(newValue) {
value = newValue;
},
enumerable: true,
configurable: true
});
console.log(obj.message); // 输出 "Hello, world!"
obj.message = 'Hi!';
console.log(obj.message); // 输出 "Hi!"
4. 定义不可配置属性
let obj = {};
Object.defineProperty(obj, 'message', {
value: 'Hello, world!',
configurable: false
});
console.log(obj.message); // 输出 "Hello, world!"
Object.defineProperty(obj, 'message', {
value: 'Hi!'
}); // 抛出错误,因为属性不可配置
二、Proxy
Proxy
是 ES6 引入的一项功能,用于定义自定义行为来拦截并改变对某个对象的基本操作(例如属性读取、赋值、枚举、函数调用等)。
基本语法
let proxy = new Proxy(target, handler);
Proxy 构造函数接受两个参数:
- target:要包装的目标对象(可以是任何类型的对象,包括数组、函数等)。
- handler:一个对象,其中包含一组捕捉器(
traps
)。这些捕捉器定义了在执行各种操作时,代理对象如何处理这些操作。
捕捉器(Traps)包含:
get(target, prop, receiver):拦截对象属性的读取。
set(target, prop, value, receiver):拦截对象属性的设置。
has(target, prop):拦截 in 操作符。
deleteProperty(target, prop):拦截 delete 操作符。
ownKeys(target):拦截 Object.getOwnPropertyNames 和 Object.getOwnPropertySymbols 方法。
apply(target, thisArg, argumentsList):拦截函数调用。
construct(target, args):拦截 new 操作符。
用例
1. 基本使用
let target = {
message: "Hello, world!"
};
let handler = {
get(target, prop) {
return prop in target ? target[prop] : `Property ${prop} does not exist.`;
},
set(target, prop, value) {
console.log(`Setting ${prop} to ${value}`);
target[prop] = value;
return true;
}
};
let proxy = new Proxy(target, handler);
console.log(proxy.message); // 输出 "Hello, world!"
console.log(proxy.nonExistent); // 输出 "Property nonExistent does not exist."
proxy.message = "Hi!"; // 输出 "Setting message to Hi!"
2. 拦截函数调用
let target = function() {
return "I am the target";
};
let handler = {
apply(target, thisArg, argumentsList) {
return "I am the proxy";
}
};
let proxy = new Proxy(target, handler);
console.log(proxy()); // 输出 "I am the proxy"
3. 拦截属性删除
let target = {
message: "Hello, world!"
};
let handler = {
deleteProperty(target, prop) {
if (prop in target) {
delete target[prop];
console.log(`Property ${prop} deleted`);
return true;
} else {
console.log(`Property ${prop} does not exist`);
return false;
}
}
};
let proxy = new Proxy(target, handler);
delete proxy.message; // 输出 "Property message deleted"
delete proxy.nonExistent; // 输出 "Property nonExistent does not exist"
三、与Vue2、Vue3的关系
vue2响应式数据原理是Object.defineProperty
。
Vue3响应式数据原理是Proxy
。
四、为什么Proxy性能优于Object.defineProperty
Object.defineProperty
只能监听Object的某个属性,所以只能通过遍历才可以深度监听整个对象。
只监听obj对象的a属性:
const obj = {
a: 1,
b: 2,
c: {
aa: 11,
bb: 11,
cc: {
aaa: 111
}
}
}
let val = obj.a
Object.defineProperty(obj, 'a', {
get: () => {
console.log("读值:", val);
return val
},
set: (newVal) => {
if (newVal !== val) {
console.log("改值:", newVal);
val = newVal
}
}
})
obj.a // 读
obj.a = 10 // 改
但若想深度监听整个obj,则需要遍历对象的全部属性,这里封装函数observe:
const obj = {
a: 1,
b: 2,
c: {
aa: 11,
bb: 11,
cc: {
aaa: 111
}
}
}
function _isObject(val) {
return typeof val === 'object' && val !== null
}
function observe(obj) {
for (const k in obj) {
let val = obj[k]
if (_isObject(val)) {
observe(val)
}
Object.defineProperty(obj, k, {
get: () => {
console.log(`读${k}的值:`, val);
return val
},
set: (newVal) => {
if (newVal !== val) {
console.log("改值:", newVal);
val = newVal
}
}
})
}
}
observe(obj) // 启动监听
obj.c.aa
obj.a = 10
obj.c.aa = "aaa"
vue2通过该种方式实现响应式数据,由于getter和setter中监听不到属性的新增与删除,所以导致vue2的响应式数据也无法直接监听到属性的新增与删除,需要通过$set
和$delete
。
但Proxy不会改变原始对象,而是生成一个代理对象,所以性能由于Object.defineProperty
。
const obj = {
a: 1,
b: 2,
c: {
aa: 11,
bb: 11,
cc: {
aaa: 111
}
}
}
function createReactiveObject(target) {
return new Proxy(target, {
get: (target, k) => {
let v = target[k]
console.log(`读${k}的值:`, v);
if (typeof v === 'object' && v !== null) {
return createReactiveObject(v)
}
return v
},
set: (target, k, val) => {
let v = target[k]
if (v !== val) {
console.log("改值:", val);
target[k] = val
}
return true
}
})
}
const proxy = createReactiveObject(obj)
// 测试读取和修改属性
proxy.c.aa
proxy.a = 10
proxy.c.aa = "aaa"
vue2使用Object.defineProperty小结
- Object.defineProperty只针对对象的某个属性,所以vue2中想实现深度监听只能遍历。
- Object.defineProperty会改动原始对象。
- 由于新增与删除属性不会进入getter和setter,所以vue2中响应式数据新增与删除属性时不会被监听,需要使用
$set
和$delete
。
vue3使用Proxy小结
- Proxy针对整个对象,所以无需遍历。
- Proxy生成一个代理对象,所以不会改动原始对象。
综上,vue3使用Proxy性能优于vue2的Object.defineProperty。
五、结合文档理解Proxy与Object.defineProperty
1. Proxy
拦截和重新定义对象的基本操作
什么是对象的基本操作
访问/修改属性值、in、for in本质上会被转为内部f函数调用
,如GET、SET、HAS、ownKeys、DELETE。通过官方文档查看详细内容。
函数对象额外多两个内部方法,函数调用时触发Call,new生成实例调用Construct方法,至此也能理解为什么函数也是对象。
Proxy 会将这些对象基本操作暴露出来,开发者可以更改基本操作行为。
2. Object.defineProperty
观察官方文档中对象基本操作的[[DefineOwnProperty]],Object.defineProperty就是它,所以 Object.defineProperty
只是对象中的某个基本操作。
六、二者对比
代理的粒度不同
defineProperty
只能代理属性,Proxy
代理的是对象。
defineProperty
如果想代理对象的所有属性,需要遍历并为每个属性添加 setter 和 getter。
Proxy
只需要配置一个可以获取属性名参数的函数即可。
是否破坏原对象
defineProperty
的代理行为会破坏原对象,它会将原本的 value 变成了 setter 和 getter。
Proxy
则不会破坏原对象,只是在原对象上覆盖了一层。当新增属性时,希望属性被代理,defineProperty
需要显式调用该 API,而 Proxy
则可以直接用 obj.key = val
的形式
Object.defineProperty | Proxy | |
---|---|---|
拦截范围 | 只能拦截对象的单个属性 操作,即只能定义特定属性的getter和setter | 可以拦截对对象的所有操作 ,包括属性访问、赋值、删除、函数调用等,可以使用get、set、deleteProperty等捕捉器来拦截这些操作 |
动态属性 | 不可以处理动态属性 | 可以处理对象的动态添加和删除属性 |
是否破坏原对象 | 是 | 否 |
性能 | 性能逊于Proxy | 对于处理嵌套对象和大量属性的情况,性能好 |
兼容性 | 兼容性更好 | 由于ES6中才引入Proxy,所以兼容性略差 |
本质 | 对象基本操作之一,[[DefineOwnProperty]] | 针对整个对象所有基本方法的拦截器 |
属性不存在时的行为 | 无法拦截不存在的属性 | 可以拦截并处理 |