大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
1. 什么是 Object.defineProperty
1.1 Object.defineProperty 基本用法
Object.defineProperty() 允许精确添加或修改对象属性。通过赋值添加的普通属性会在枚举属性时(例如 for...in、Object.keys() 等)出现,值可以被更改,也可以被删除。
defineProperty() 方法允许更改额外细节,以使其不同于默认值。默认情况下,使用 Object.defineProperty() 添加的属性是不可写、不可枚举和不可配置的。此外,Object.defineProperty() 使用 [[DefineOwnProperty]] 内部方法,而不是 [[Set]],因此即使属性已经存在也不会调用 setter。
Object.defineProperty(obj, prop, descriptor)
方法每一个参数定义如下:
- obj:要定义属性的对象。
- prop:一个字符串或 Symbol,指定了要定义或修改的属性键。
- descriptor:要定义或修改的属性的描述符,包括:configurable(如是否可删除)、enumerable、writable、get、set 等等。
下面示例使用 Object.defineProperty 进行对象属性定义:
const obj = {};
// 1. 使用 null 原型:没有继承的属性
const descriptor = Object.create(null);
descriptor.value = "static";
// 默认情况下,不可枚举、不可配置、不可写
// obj.key ="static modified" 赋值后依然是 "static"
Object.defineProperty(obj, "key", descriptor);
// 2. 使用一个包含所有属性的临时对象字面量来明确其属性
Object.defineProperty(obj, "key2", {
enumerable: false,
configurable: false,
writable: false,
value: "static",
});
// 3. 重复利用同一对象
function withValue(value) {
const d =
withValue.d ||
(withValue.d = {
enumerable: false,
writable: false,
configurable: false,
value,
});
// 避免重复赋值
if (d.value !== value) d.value = value;
return d;
}
// 然后
Object.defineProperty(obj, "key", withValue("static"));
// 如果 freeze 可用,防止添加或删除对象原型属性
// (value、get、set、enumerable、writable、configurable)
(Object.freeze || Object)(Object.prototype);
1.2 Object.defineProperty 优缺点
Object.defineProperty 的主要优点包括:
- Object.defineProperty() 方法可以对属性的行为方式进行细粒度的控制
- 允许设置只读属性,防止意外修改
- 开发者可以决定某个属性是否在枚举期间出现,从而实现特定的功能
- 允许开发者使属性不可删除,从而保证核心属性的安全。
当然,Object.defineProperty 也有不足之处,主要体现在:
- Object.defineProperty() 不能很好地处理数组,因为无法捕获修改索引值或长度属性以及动态属性(动态 getter 是一种没有为 property 显式定义 getter,而是在访问属性时动态创建的),详情可以看这篇文章(https://vue3js.cn/interview/vue3/proxy.html#一、object-defineproperty)。同时,也不支持嵌套对象,这意味着不会观察到嵌套对象的任何更改
下面是对象示例:
const obj = {
foo: "foo",
bar: "bar"
}
observe(obj)
delete obj.foo // no ok
obj.jar = 'xxx' // no ok
下面是数组示例:
const arrData = [1,2,3,4,5];
arrData.forEach((val,index)=>{
defineProperty(arrData,index,val)
})
arrData.push() // no ok
arrData.pop() // no ok
arrDate[0] = 99 // ok
基于对象和数组的以上局限性,Vue2 增加了 set、delete API,并且对数组 api 方法进行一个重写。
- Object.defineProperty() 的语法很冗长,可能会增加可读性,影响代码的模块化和可重用性。
而掌握 Object.defineProperty 的关键在于透彻理解属性描述符的属性。 例如,正确地将 writable 属性设置为 false 可以确保属性值在整个程序中保持不变,从而减少出现错误的机会。
比如下面的示例将 π 置为常量后将无法修改:
let constantObj = {};
Object.defineProperty(constantObj, 'pi', {
value: 3.14159,
writable: false
});
console.log(constantObj.pi);
// Outputs 3.14159
constantObj.pi = 3;
// Attempting to change the value
console.log(constantObj.pi);
// Still outputs 3.14159
注意:Vue 3 改用了 Proxy 。Proxy 可以拦截对象属性读取、赋值和删除操作,从而能够在属性发生变化时触发相应的更新。对于数组,Proxy 可以拦截数组的修改操作,比如: push、pop、splice 等,从而能够在数组发生变化时触发相应的更新。
此外,Proxy 还可以拦截对象的原型方法和构造函数调用,从而可以对对象的所有操作进行拦截和处理。
2. 什么是 Proxy
JavaScript 的 Proxy 对象是一项强大的功能,使开发者能够拦截和自定义对对象执行的操作,例如:属性查找、赋值、枚举和函数调用。 这种多功能工具允许开发人员创建更高效、更灵活的代码,同时还提高代码的可维护性。
Proxy 遵循以下语法规范:
const p = new Proxy(target, handler)
- target:要使用 Proxy 包装的目标对象,可以是任何类型的对象,包括原生数组,函数,甚至另一个代理。
- handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
值得一提的是,handler 对象是一个容纳一批特定属性的占位符对象,包含有 Proxy 的各个捕获器(trap)。而且所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。
常见的捕获器包括:
- getPrototypeOf()
- setPrototypeOf()
- isExtensible()
- preventExtensions()
- getOwnPropertyDescriptor()
- defineProperty()
- has()
- get()
- set()
- deleteProperty()
- ownKeys()
- apply()
- construct()
在以下例子中,使用了一个原生 JavaScript 对象,Proxy 会将所有应用到它的操作转发到这个对象上。
let target = {};
let p = new Proxy(target, {});
p.a = 37;
// 操作转发到目标
console.log(target.a);
// 37. 操作已经被正确地转发
3.Proxy 与 Object.defineProperty 主要区别
Proxy 与 Object.defineProperty 一个主要区别在于抽象级别。 Proxy 在对象周围创建一个新层,可以 Hook 任何属性,而无需预先显式定义, 而 Object.defineProperty 直接修改对象并要求相关属性在定义时就存在。
此外,Proxies 涵盖了广泛的 property 操作,而 Object.defineProperty 则专注于 attribute 属性操作。
关于 Proxy 和 Object.defineProperty 还需要弄清楚一个常见错误,即将 Object.defineProperty 用于复杂的动态对象,期望新属性的反应行为可能会导致意外结果,因为 Object.defineProperty 只影响现有属性。
比如下面的示例:
let object = {};
Object.defineProperty(object, 'property', {
value: 42,
writable: false
});
object.newProperty = 100;
console.log(object.newProperty);
// 输出: 100
// newProperty 的行为不受 Object.defineProperty 控制
实现新属性反应性(Reactivity)的正确方法是使用 Proxy,或者为每个新属性动态实现 Object.defineProperty。比如下面的 Proxy 示例:
let targetObject = {message: 'Hello, world'};
let handler = {
set: function(target, prop, value) {
if (prop === 'newProperty') {
target[prop] = value * 2;
} else {
target[prop] = value;
}
}
};
let proxy = new Proxy(targetObject, handler);
proxy.newProperty = 100;
console.log(proxy.newProperty);
// 输入: 200
4. 使用 Proxy 的场景
4.1 验证对象属性
考虑创建一个需要具有某些有条件所需属性的严格架构的对象,可以通过使用代理包装对象并在 set 中实施验证检查来管理,从而确保只有有效数据进入对象。
比如下面的代码示例使用 Proxy 实现 set 方法,验证对象的属性是否在指定的 schema 中:
let schema = {
id: {
type: 'number',
required: true
},
comment: {
type: 'string',
required: false
}
};
let handler = {
set: function (target, key, value) {
if (schema[key] && typeof value !== schema[key].type) {
throw new Error(`Type ${typeof value} is not assignable to type ${schema[key].type}`);
} else if (schema[key] && schema[key].required && value === undefined) {
throw new Error(`${key} is required.`);
}
target[key] = value;
return true;
}
};
let movie = new Proxy({}, handler);
4.2 对象级访问控制
Proxy 对象可以有效控制对象属性的访问,通常可用于提供对象的只读视图或限制可访问的对象属性的范围。
比如下面的代码示例表示访问 password 属性后则会抛出错误:
let personDetails = {
firstName: 'John',
lastName: 'Doe',
password: '12345!'
};
let handler = {
get: function (target, prop) {
if (prop === 'password') {
throw new Error('Access to password is denied');
}
return target[prop];
}
};
let proxy = new Proxy(personDetails, handler);
console.log(proxy.password);
// 抛出错误
console.log(proxy.firstName);
// 输出: 'John'
又或者下面的代码示例在修改元素属性之前做精确的控制,从而属性相互覆盖:
function assignIfNotExists(target, source){
for (let prop in source) {
if (!target.hasOwnProperty(prop)) {
target[prop] = source[prop];
}
}
}
let data = {username: 'Zach'};
let userInput = {username: 'JohnDoe', password: 'secret'};
// Avoid overwriting 'username' in data object
assignIfNotExist(data, userInput);
4.3 数据绑定和观察者
Proxy 可以帮助构建数据绑定解决方案,比如:当应用程序的状态发生变化时,开发者可能希望跟踪变化并做出响应,比如: Vue.js 就是一个很好的示例。
let state = {
count: 0
};
let handler = {
set: function (target, property, value) {
target[property] = value;
console.log(`State has changed. New ${property}: ${value}`);
return true;
}
};
let proxy = new Proxy(state, handler);
proxy.count = 2;
// 输出: State has changed. New count: 2
以上代码示例,JavaScript Proxy 提供了对对象交互的精确控制,从而实现复杂行为、验证、访问控制等等。 然而,由于 Proxy 的复杂性,考虑使用 Proxy 的开销也同样重要。 因此,Proxy 的使用应该针对特定的挑战,其独特的功能可以显著提高系统操作和可读性。
5.Proxy 与 Object.defineProperty 深入比较
5.1 性能
JavaScript Proxy 比 Object.defineProperty 消耗的时间稍多, Proxy 本质上应用了一个额外的抽象层(处理程序),从而可能会使操作比 Object.defineProperty 更慢。
比如下面的代码示例:
let object = {};
Object.defineProperty(object, 'property', {
value: 42,
writable: false
});
object 中的 property 属性值必须是常量,直接访问 property 非常简单快捷。 而对于 Proxy 来说,在获取对象值之前有一个额外的检查和验证过程:
let targetObject = {property: 42};
let handler = {
get: function(target, prop) {
return target[prop];
}
};
let proxy = new Proxy(targetObject, handler);
5.2 代码复杂性和可读性
Object.defineProperty 重点关注属性级别, 当需要控制属性是否可以修改、配置甚至枚举时则是理想选择,同时 Object.defineProperty 的用法直接且有针对性的,使代码更容易阅读和理解。
然而,Proxy 在提供更高级别的抽象方面表现出色。 Proxy 对象可以针对整个对象,而不仅仅是单个属性,从而允许开发人员以更高级的方式拦截和重新定义对象的默认行为。
然而,Proxy 中的处理程序可能会造成复杂性,因为总是需要通过一个额外的中间层。 其他开发者也需要对 Proxy 概念有更多的了解才能轻松阅读 Proxy 代码。
5.3 模块化和可重用性
在模块化和可重用性方面,当想要为更大范围甚至整个应用程序定义全局处理程序行为时,Proxy 通常会发挥作用。 Proxy 通常提供一种极好的方法来将特定的控制行为封装在单独的处理程序中。 这样,同一个处理程序可以与多个目标对象重复使用。
相反,Object.defineProperty 允许模块化和保护单个对象属性,对于以模块化方式定义、保护或控制对象的属性非常重要。
Proxy 提供了更多的可能性,捕获更多的动作,并提供对对象的更多控制。 然而,它们也会带来性能成本,需要了解它们的用法,并且可能会使调试变得复杂。
另一方面,Object.defineProperty 虽然不如代理那么强大和灵活,但提供了一种简单、直接且易于调试的方法。
6.Proxy 常见方法
6.1 Proxy 转为普通对象
const proxy = {"name":"高级前端进阶"}
const proxyObj = new Proxy(proxy, {
get: (target, prop) => prop in target ? target[prop] : 37
});
console.log(proxyObj.a)
// 输出 37
console.log(proxyObj.name)
// 输出 ` 高级前端进阶 `
console.log(JSON.stringify(proxyObj))
// 输出 {"name":"晴天"}
值得注意的是,使用 JSON.parse(JSON.stringify(proxyObj)) 方法会删除任何不能字符串化的内容,比如:类、函数、回调等。
如果确实需要,可以考虑使用 Lodash 的 cloneDeep 函数,该方法在将 Proxy 对象转换为 POJO(The Plain Old JavaScript Object) 的同时保持对象结构方面确实做得很好。
convertProxyObjectToPojo(proxyObj) {
return _.cloneDeep(proxyObj);
}
6.2 Proxy 监听数组元素变化
以下示例表示 Proxy 确实能监听到数组元素的变更,这与 defineProperty 是有差别的,至于监听嵌套对象属性变化可以自行验证。
function get(target, prop, receiver) {
console.log('target:' + target);
console.log('property:' + prop);
return Reflect.get(target, prop, receiver);
}
var handler = {
'get': get
};
// 为数组添加 Proxy
var proxy = new Proxy([1,2,3,4,5], handler );
console.log('Result => beep:' + proxy.beep );
// target: 1,2,3,4,5
// property: beep
// Result => beep: undefined
console.log('Result => -123:' + proxy[ -123] );
// target: 1,2,3,4,5
// property: -123
// proxy:16 Result => -123: undefined
console.log(proxy.fill( 1) );
// target: 1,2,3,4,5
// property: fill
// target: 1,2,3,4,5
// property: length
// Proxy(Array) {0: 1, 1: 1, 2: 1, 3: 1, 4: 1}
console.log('Result => 0:' + proxy[ 0 ] );
// target: 1,1,1,1,1
// property: 0
// Result => 0: 1
var arr1 = [10, 20, 30, 40, 50];
Object.setPrototypeOf(arr1, proxy);
console.log('Result => beep:' + arr1.beep );
console.log('Result => -123:' + arr1[ -123 ] );
console.log(arr1.fill( 100) );
// 输出 (5) [100, 100, 100, 100, 100]
console.log('Result => 0:' + arr1[ 0 ] );
6.3 Proxy 监听嵌套对象
var validator = {
get(target, key) {
if (typeof target[key] === 'object' && target[key] !== null) {
// 如果是对象则继续创建 Proxy
return new Proxy(target[key], validator)
} else {
return target[key];
}
},
set (target, key, value) {
console.log(target);
// 输出 {salary: 8250, Proffesion: '.NET Developer'}
console.log(key);
// 输出 salary
console.log(value);
// 输出 foo
return true
}
}
var person = {
firstName: "alfred",
lastName: "john",
inner: {
salary: 8250,
Proffesion: ".NET Developer"
}
}
var proxy = new Proxy(person, validator)
proxy.inner.salary = 'foo'
// 这一句代码会先访问 proxy.inner 属性,发现是 Object
// 然后会继续访问 salary 属性
参考资料
https://borstch.com/blog/objects-in-javascript-properties-methods-and-prototypes
https://borstch.com/blog/proxies-vs-objectdefineproperty-when-to-use-which
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
https://blog.javascripttoday.com/blog/deep-dive-proxies-in-javascript/
https://vue3js.cn/interview/vue3/proxy.html#二、proxy
https://vue3js.cn/interview/vue3/proxy.html#一、object-defineproperty
https://juejin.cn/post/7306783965532717108
https://www.youtube.com/watch?app=desktop&v=_k3WiANNB4U
https://segmentfault.com/q/1010000043053833
https://www.30secondsofcode.org/js/s/dynamic-getter-setter-proxy/
https://gist.github.com/kgryte/713ab40f36c128bc1d52
https://stackoverflow.com/questions/41299642/how-to-use-javascript-proxy-for-nested-objects