一、Object.defineProperty(obj, prop, descriptor)
- 该方法会【直接】在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
- obj:要定义属性的对象。
- prop:要定义或修改的属性的名称或 Symbol(被劫持的属性)。
- descriptor:要定义或修改的属性描述符。
- Object.defineProperty在劫持对象和数组时的缺陷:
- 无法检测到对象属性的添加或删除
- 无法检测数组元素的变化,需要进行数组方法的重写
- 无法检测数组的长度的修改
let user = {
age: 16,
name: "啊橙",
}
/*
如果直接在get函数中return user.name的话,这里的user.name同时也会
调用一次get函数,这样的话会陷入一个死循环;
set函数也是同样的道理,因此我们通过一个第三方的变量initName来防止死循环。
*/
var initName = '啊橙'
// 通过defineProperty定义的属性会在user上定义一个name属性,并为其绑定setter和getter
Object.defineProperty(user, 'name', {
get() {
console.log('触发--> get')
return initName
},
set(val) {
console.log('触发--> set')
initName = val
}
})
// user.name // 触发get
二、Porxy
2.1 Porxy简介
-
Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写
-
相较于Object.defineProperty劫持某个属性,Proxy直接代理了target整个对象,并且【返回了一个新的对象】,通过监听代理对象上属性的变化来获取目标对象属性的变化;Proxy还能监听到属性的增加和删除,比Object.defineProperty的功能更为强大。
-
不管是数组下标或者数组长度的变化,还是通过函数调用,Proxy都能很好的监听到变化;而且除了我们常用的get、set,Proxy更是支持13种拦截操作。
-
Proxy是一个构造函数,通过new Proxy生成拦截的实例对象,让外界进行访问;
构造函数中的:
target:所要拦截的目标对象,可以是对象或者数组;
handler:用来定制拦截规则/配置对象,只能是对象,和Object.defineProperty中的descriptor描述符有些类似
var proxy = new Proxy(target, handler)
handler = {
// 目标(拦截)对象、拦截对象的属性名、proxy实例本身 其中最后一个参数可选
get:function(target, key, receiver){
},
// 目标(拦截)对象、拦截对象的属性名、属性值、Proxy实例本身 其中最后一个参数可选
set:function(target, key, value, receiver){
}
}
Reflect: 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上
5.示例1
let target = { name: "啦啦啦啦" } // 监听对象
// let target = [10,21,13,41] // 监听数组
var proxy = new Proxy(target, {
// 拦截对象 访问的拦截对象属性 指向proxy对象
get(target, key, receiver) {
console.log(`触发get--> 属性为:${key}!`);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log(`触发set -------> 属性为:${key}!`);
// Reflect.set 如果不传入receiver 那么就不会触发defineProperty函数
return Reflect.set(target, key, value, receiver);
},
deleteProperty(target, key) {
console.log(`触发删除函数 删除的属性为:${key}!`);
delete target[key];
return true;
},
defineProperty(target, key, attribute) {
console.log('触发defineProperty');
Reflect.defineProperty(target, key, attribute);
}
})
console.log(proxy)
proxy的结构如下↓:
proxy:{
Handler:{} 就是new Proxy时传递的配置对象或者叫代理规则
Target:{} 就是new Proxy时传递的所要拦截的目标对象
}
// 取值
proxy.name; // 触发get--> 属性为:name! 返回'啦啦啦啦'
// 赋值
proxy.count = 1; // 触发set -------> 属性为:ads! 触发defineProperty 返回1
// 删除
delete proxy.count // 触发删除函数 删除的属性为:count! 返回true
如果 Proxy对象和Reflect对象联合使用,前者拦截赋值操作,后者完成赋值的默认行为,而且传入了receiver,那么Reflect.set会触发Proxy.defineProperty拦截。
上面代码中,Proxy.set拦截里面使用了Reflect.set,而且传入了receiver,导致触发Proxy.defineProperty拦截。这是因为Proxy.set的receiver参数总是指向当前的Proxy实例,而Reflect.set一旦传入receiver,就会将属性赋值到receiver上面(即obj),导致触发defineProperty拦截。如果Reflect.set没有传入receiver,那么就不会触发defineProperty拦截。
6.示例2
let target_1 = {} // 目标对象
let handler_1 = {}
// handler_1 是一个空对象,没有任何拦截效果,访问proxy就等同于访问target
let proxy_1 = new Proxy(target_1, handler_1)
proxy_1.a = 'b' // 'b'
target_1.a // "b"
2.2. Proxy 实例的方法
2.2.1 get
// 例1:访问代理对象上不具有的属性就报错
let userInfo = { name : 'wuwei' }
let userInfoProxy = new Proxy(userInfo,{
get(target,property){
if(property in target){ // in 操作符用来判断某个属性属于某个对象
console.log(`你访问了${property}属性`)
return target[property]; // proxy可以这么写,但是Object.defineProperty 这么写会死循环
}else{
throw new ReferenceError(`${property}属性不在此对对象上`)
}
}
})
// 例2:下面的例子使用get拦截,实现数组读取负数的索引。
function createArray(...elements) { // 将传递给函数的所有实参打包到一个数组中
let target = []
target.push(...elements)
return new Proxy(target, {
get(target, key, receiver) {
if (Number(key) < 0) {
key = String(target.length + Number(key))
}
return Reflect.get(target, key, receiver)
// return target[key] 两种都可以
}
})
}
let arr = createArray('a', 'v', 'f')
console.log(arr);
// arr[-1] // 'f'
// 例3:
// 利用Proxy,可以将读取属性的操作(get),转变为执行某个函数,从而实现属性的链式操作。
// 人话:利用proxy的get属性,每次调用都返回proxy 每次调用proxy的get属性都会触发一个函数
let pipe = function (value) {
let arr = []; // 存放函数的
let target = {}
let oproxy = new Proxy(target, {
get(target, key ,receiver) {
console.log(target,key,receiver)
// 如果调用了get 转变为执行某个函数
if (key === 'get') {
return arr.reduce((prev, current)=>{ // current就是函数 prev就是要计算的数据
console.log(prev,'*---*',current)
return current(prev);
}, value);
}
// 将函数push进arr数组中
arr.push(window[key]);
console.log(arr)
// 触发get函数 不再返回对应的值 return target[key] 或者 return Reflect.get(target,key,receiver)
return oproxy; // 返回的是proxy
}
});
console.log(999)
return oproxy;
}
// 务必用var 变量提升 这个一个函数
var double = n => n * 2;
var pow = n => n * n;
var reverseInt = n => n.toString().split("").reverse().join("") | 0;
var add = n => n
/*
pipe函数默认返回Proxy的实例化对象
当打点调用时(非调用get)就会触发get函数,把被调用的函数key保存到数组中,此时默认也返回一个Proxy的实例化对象,
当最后打点调用get时,就会把数组中的函数执行一遍。最终返回结果
最后遍历把arr的三个函数执行一遍
*/
// pipe(3).double.pow.get; // 36
pipe(3).double.pow.reverseInt.get; // 63
2.2.2 set
// 例1:假定Person对象有一个age属性,该属性应该是一个不大于 200 的整数,
// 那么可以使用Proxy保证age的属性值符合要求。
let target1 = {}
let proxy1 = new Proxy(target1,{
set(target1, key, value, receiver){
if(key === 'age'){
// Number 对象中的 isInteger() 方法用来判断传入的参数是否为整型
if(!Number.isInteger(value)){
throw new TypeError('类型错误!')
}
if(value>200){
throw new RangeError('范围错误 参数超范围');
}
}
// 对于满足条件的 age 属性以及其他属性,直接保存
// Reflect.set(target1, key, value, receiver)
target1[key] = value;
return true; // 严格模式下必须要返回一个true
}
})
/*
上面代码中,由于设置了存值函数set,任何不符合要求的age属性赋值,都会抛出一个错误,
这是数据验证的一种实现方法。
*/
// 例2:利用set方法,还可以数据绑定,即每当对象发生变化时,会自动更新 DOM。
let data = { count:1 }
let dataProxy = new Proxy(data,{
set(data, key, value, receiver){
data[key] = value
document.querySelector('#h3').innerText = receiver.count
console.log(data);
}
})
const handleInput = (event) => {
let value = event.target.value
dataProxy.count = value
}
2.2.3 其他方法
let obj = {
a:100,
b:200
}
let objProxy = new Proxy(obj, {
deleteProperty(target, key) {
console.log('delete' + ' -> ' + key);
delete target[key];
return true;
// return Reflect.deleteProperty(target, name); // proxy配合Reflect使用
},
has(target, key) {
console.log('has' + ' -> ' + key);
return key in target;
// return Reflect.has(target, key); // proxy配合Reflect使用
}
});
'b' in objProxy // 查看objProxy中是否有这个属性
delete objProxy.b // 删除objProxy中的b属性
结果: