这篇《从 Proxy 到 Vue 源码,深入理解 Vue 3.0 响应系统》提到了vue中通过Object.defineProperty
的,递归遍历 data 对象上的所有属性,将其转换为 getter/sette
如果用proxy代理对象来替代target对象的话,如果不想对外暴露target对象,该对象不要申明,而是以对象字面量的形式写在new Proxy()中。
Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。
接收两个参数:被代理者和处理代理的对象。
Proxy称为代理,当通过代理来操作被代理者时,会触发代理相关的代码;如果直接操作被代理者,不会触发代理相关代码。
const handler = {
get(target, prop, receiver){
console.log(target); 被代理者obj对象 {name: "zhu", age: 24}
console.log(prop); 被代理者的属性名 name
console.log(receiver); Proxy或者继承Proxy的对象 Proxy {name: "zhu", age: 24}
return "proxy调用了该方法"
}
}
let obj = {
name: "zhu",
age: 24
}
let proxy = new Proxy(obj, handler);
console.log(obj.name); 打印zhu,无法触发代理代码
console.log(proxy.name); 打印 proxy调用了该方法,触发代理代码中赋值相关的代码
console.log(proxy); Proxy {name: "zhu", age: 24}
Reflect:需要在Proxy
内部调用对象的默认行为,即执行Proxy时,将方法转到目标对象上,让目标对象也生效。
Reflect是内置静态对象,不是构造函数,不能进行new的操作,Reflect对象上有和Proxy对应的静态方法,可直接使用。就行Math对象上的方法一样。
基本特点:引用自:https://zhuanlan.zhihu.com/p/60126477
Proxy
对象具有的代理方法,Reflect
对象全部具有,以静态方法的形式存在。这些方法能够执行默认行为,无论Proxy
怎么修改默认行为,总是可以通过Reflect
对应的方法获取默认行为。Object
方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)
在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)
则会返回false
。Object
操作都变成函数行为。某些Object
操作是命令式,比如name in obj
和delete obj[name]
,而Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
让它们变成了函数行为。比如上面的get方法,我们通过Reflect反射到目标对象上,获得目标对象的对应属性的值:
const handler = {
get(target, prop, receiver){
return Reflect.get(target,prop);
}
}
let obj = {
name: "zhu",
age: 24
}
let proxy = new Proxy(obj, handler);
console.log(proxy.name); zhu
也可以单独调用Reflect方法,对对象进行操作设置,比如:
var obj = {};
Reflect.set(obj, "name", "zhu"); // true
obj.name; // "zhu"
// Array
var arr = ["duck", "duck", "duck"];
Reflect.set(arr, 2, "goose"); // true
arr[2]; // "goose"
// It can truncate an array. 截断数组
Reflect.set(arr, "length", 1); // true
arr; // ["duck"];
1、Proxy对象的get方法和Reflect.get()
(1)Proxy对象的get方法 用于代理处理对象读取属性的操作。
以下是传递给get方法的参数,this上下文绑定在
handler对象上.
target
目标对象。property
被获取的属性名。receiver
Proxy或者继承Proxy的对象。返回值:get方法可以返回任何值。
该方法会处理目标对象的以下操作:
proxy[foo]和
proxy.bar
Object.create(proxy)[foo]
Reflect.get()
const handler = {
get(target, prop, receiver){
console.log(target); 被代理者obj对象 {name: "zhu", age: 24}
console.log(prop); 被代理者的属性名 name
console.log(receiver); Proxy或者继承Proxy的对象 Proxy {name: "zhu", age: 24}
return Reflect.get(target,prop);
}
}
let obj = {
name: "zhu",
age: 24
}
let proxy = new Proxy(obj, handler);
console.log(proxy.name); get方法的返回值:zhu
如果违背了以下的约束,proxy会抛出 TypeError
:
关于第一条的异常:只有当configurable和writable同时为false的时候,get方法的返回值不能修改该属性的值,不然会报错。
var obj = {};
Object.defineProperty(obj, "a", {
configurable: false,
enumerable: false,
value: 10,
writable: false
});
var p = new Proxy(obj, {
get: function(target, prop) {
return 20;
}
});
console.log(p.a); 会抛出TypeError
(2) Reflect
.get()
方法与从 对象 (target[propertyKey]
) 中读取属性类似,但它是通过一个函数执行来操作的。
target
需要取值的目标对象propertyKey
需要获取的值的键值target
对象中指定了getter
,receiver
则为getter
调用时的this
值。返回属性的值
2、Proxy对象的set
handler.set()
方法用于代理处理对象设置属性值的操作。
以下是传递给 set()
方法的参数。this
绑定在 handler 对象上。
target
目标对象。property
将被设置的属性名或 Symbol
。value
新属性值。receiver
最初被调用的对象。通常是 proxy 本身,但 handler 的 set 方法也有可能在原型链上,或以其他方式被间接地调用(因此不一定是 proxy 本身)。const handler = {
set(target, prop, value, receiver) {
console.log(this); handler对象
console.log(target); 目标对象 {name: "zhu", age: 24}
console.log(prop); 将被设置的属性名或 Symbol name
console.log(value); 设置的属性值 zhuyuzhu
console.log(receiver); 最初被调用的对象 Proxy {name: "zhu", age: 24}
}
}
let obj = {
name: "zhu",
age: 24
}
let proxy = new Proxy(obj, handler);
proxy.name = "zhuyuzhu";
比如:假设有一段代码执行 obj.name = "jen"
, obj
不是一个 proxy,且自身不含 name
属性,但是它的原型链上有一个 proxy,那么,那个 proxy 的 set()
处理器会被调用,而此时,obj
会作为 receiver 参数传进来。
let proxyObj = new Proxy({name:"fa"},{
set(target, prop, value, receiver){
console.log(receiver); 谁调用的该方法,receiver就是谁。此处是proxyObj的子对象obj
console.log(target); target指向Proxy传入的第一个参数,目标对象
return Reflect.set(target,prop,value);
}
})
let obj = Object.create(proxyObj);
obj.name = "child";
(2)Reflect.set()方法
target
设置属性的目标对象。propertyKey
设置的属性的名称。value
设置的值。receiver
如果遇到 setter
,receiver
则为setter
调用时的this
值。(1)handler.apply()
方法用于拦截函数的调用。
以下是传递给apply方法的参数,this上下文绑定在
handler对象上.
target
目标对象(函数)。thisArg
被调用时的上下文对象。argumentsList
被调用时的参数数组apply方法可以返回任何值。但是target必须是一个函数。
该方法会拦截目标对象的以下操作:
proxy(...args)
Function.prototype.apply()
和 Function.prototype.call()
Reflect.apply()
实例:注意也可以操作target
function sum(a, b) {
return a + b;
}
const handler = {
//target目标函数,thisArg执行上下文,argumentsList参数列表数组
apply: function(target, thisArg, argumentsList) {
console.log(`Calculate sum: ${argumentsList}`);
// return target(argumentsList[0], argumentsList[1]) * 10;
return Reflect.apply(target,thisArg,argumentsList) * 10
}
};
const proxy1 = new Proxy(sum, handler);
console.log(sum(1, 2));
// expected output: 3
console.log(proxy1(1, 2));
// expected output: 30
(2)handler.has()
方法是针对 in
操作符的代理方法。
下面是传递给 has
方法的参数. this
is bound to the handler.
target
目标对象. prop
需要检查是否存在的属性. has
方法返回一个 boolean 属性的值.
handler.has
可以拦截下面这些操作:
foo in proxy
foo in Object.create(proxy)
with
检查: with(proxy) { (foo); }
Reflect.has()
注意:在方法中最后也是可以返回操作的target的,但是用Reflect反射到目标对象上会更好
const handler1 = {
has(target, key) {
// return key in target;
return Reflect.has(target,key);
}
};
const monster1 = {
_secret: 'easily scared',
eyeCount: 4
};
const proxy1 = new Proxy(monster1, handler1);
console.log('eyeCount' in proxy1);
// expected output: true
console.log('_secret' in proxy1);
// expected output: false
console.log('_secret' in monster1);
// expected output: true
(3)handler.getPrototypeOf()
是一个代理(Proxy)方法,当读取代理对象的原型时,该方法就会被调用。
以下这几个操作都会触发代理的getPrototypeOf方法:
Object.getPrototypeOf()
Reflect.getPrototypeOf()
__proto__
Object.prototype.isPrototypeOf()
instanceof
(4)handler.setPrototypeOf()
方法主要用来拦截Object.setPrototypeOf()和
Reflect.setPrototypeOf()的操作。
(5)
handler.ownKeys()
方法用于拦截以下这个几种操作:
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
Reflect.ownKeys()
Reflect.ownKeys
方法返回一个由目标对象自身的属性键组成的数组。(对象自身的所有属性,包括不可遍历属性和Symbol属性)它的返回值等同于Object.getOwnPropertyNames
(target).concat(Object.getOwnPropertySymbols
(target))。
(6)
handler.defineProperty()
用于拦截对对象的Object.defineProperty()和
eflect.defineProperty()操作
(7)handler.deleteProperty()
方法用于拦截对对象属性的 delete
操作。包括:删除属性: delete proxy[foo]
和 delete proxy.foo和
Reflect.deleteProperty()
(8)
handler.getOwnPropertyDescriptor()
方法是 Object.getOwnPropertyDescriptor()
的钩子。
陷阱可以拦截这些操作:Object.getOwnPropertyDescriptor()和
Reflect.getOwnPropertyDescriptor()
(9)handler.isExtensible() 方法用于拦截对对象的Object.isExtensible()。
(10)handler.preventExtensions()
方法用于设置对Object.preventExtensions()
的拦截
总结:
(1)代理的每个方法除了拦截之前了解的一些操作外,肯定会拦截Reflect对象对应的方法。
(2)对对象属性的获取、设置、删除,遍历,判断对象是否有某个属性的方法:get方法、set方法、deleteProperty方法、ownKeys方法、has方法。
(3)对象获取原型,设置原型,配置对象的属性,获取对象属性的描述信息,判断是否可拓展,阻止对象拓展
getPrototypeOf方法,setPrototypeOf方法、defineProperty方法,getOwnPropertyDescriptor方法,isExtensible方法,preventExtensions方法
(4)函数的调用和拦截new 操作符:apply方法和handler.construct()
(5)Proxy.revocable()方法:该方法的返回值是一个对象,其结构为: {"proxy": proxy, "revoke": revoke}
proxy
:表示新生成的代理对象本身,和用一般方式 new Proxy(target, handler)
创建的代理对象没什么不同,只是它可以被撤销掉。
revoke
:撤销方法,调用的时候不需要加任何参数,就可以撤销掉和它一起生成的那个代理对象。
(1)校验数据
let numberObj = {};
let proxyNumObj = new Proxy(numberObj, {
set(target, prop, value) {
if(typeof value !=="number") {
throw Error("numberObj的属性值必须是数字"); //错误类型
}
return Reflect.set(target, prop, value);
}
})
proxyNumObj.age = 24;
proxyNumObj.name = "zhu";
(2)实现对象私有方法和私有属性
只有该对象内部可以调用,禁止外部调用该对象的私有属性和方法。在 JavaScript 或其他语言中,大家会约定俗成地在变量名之前添加下划线 _
来表明这是一个私有属性(并不是真正的私有),但我们无法保证真的没人会去访问或修改它。在下面的代码中,我们声明了一个私有的属性和方法,便于这个对象内部的方法调用,但不希望从外部也能够访问到他们。
该实例中使用到可销毁的Proxy对象:
let obj = {
name: "zhu",
age: 24,
func1: function(){
let that = obj;//防止修改this指向
//内部访问的时候,关闭再开启Proxy---感觉有点麻烦,但是没有想到好的解决办法
proxyObj.revoke();
console.log("可以通过func1访问到_love属性和_func2方法");
that._func2();
console.log(that._love);
proxyObj = Proxy.revocable(that, handler);//开启
proxy = proxyObj.proxy;
},
_func2(){
console.log("_func2方法在内部执行了");
},
_love: "null"
};
let handler = {
get(target, prop, receiver){
if(prop === "_love" || prop === "_func2"){ //是对象中带有_私有属性,允许其他开发者在该对象上添加_这样的属性
if(typeof prop === "string"){
return undefined;
}else {
throw Error(`${prop} is not function`)
}
}
return Reflect.get(target,prop,receiver);
},
set(target, prop, value, receiver){
}
}
let proxyObj = Proxy.revocable(obj, handler); //proxyObj才可以调用revoke方法
let proxy = proxyObj.proxy; //proxy是Proxy对象,对外proxy就是obj
proxy.func1();
console.log(proxy._love); //undefined
console.log(proxy.name); //zhu
proxy._func2(); //报错!
创建可销毁的Proxy对象
创建proxy对象的父对象:
let proxyObj = Proxy.revocable(obj, handler);
该对象有一个属性proxy对象和一个销毁方法revoke
{proxy: Proxy, revoke: ƒ}
可以用解构赋值进行赋值:
let {proxy, revoke} = Proxy.revocable(obj, {});
销毁可执行revoke方法,那么父对象内的proxy对象就是空:
proxyObj.revoke();
父对象内,空的proxy对象:
重新开启,再重新赋值即可:
proxyObj = Proxy.revocable(that, handler);//开启
proxy = proxyObj.proxy;
(3)单例模式
保证整个系统中一个类只有一个对象的实例,实现这种功能的方式就叫单例模式。
handler.construct()
方法用于拦截new
操作符. 为了使new操作符在生成的Proxy对象上生效,用于初始化代理的目标对象自身必须具有[[Construct]]内部方法(即 new target
必须是有效的,说明目标对象是一个函数)。
下面的参数将会传递给construct
方法,this
绑定在handler上。
target
目标对象。argumentsList
constructor的参数列表。newTarget
最初被调用的构造函数,就上面的例子而言是p。该拦截器可以拦截以下操作:
new proxy(...args)
Reflect.construct()
必须返回一个对象,不然代理将会抛出错误 TypeError。
实例:创建一个函数的Proxy对象,通过构造函数创建
注意target就代表了目标函数Monster,所以return的函数可以通过new target的形式得到一个对象;也可以通过Reflect.construct的形式,反射到目标对象上。
function Monster(disposition) { Monster巨人 disposition性情
this.disposition = disposition;
}
const handler1 = {
construct(target, args) { 拦截构造函数或class类的new操作,target就是构造函数或class类
return new target(...args) || Reflect.construct(target, args);
}
};
const proxy1 = new Proxy(Monster, handler1);
console.log(new proxy1('fierce').disposition); fierce凶猛的
实例:单例模式,通过类创建
保证整个系统中一个类只有一个对象的实例,实现这种功能的方式就叫单例模式。在修改创建的单例对象时,也需要修改对应的存储它的变量flag。
class Monsters {
constructor(disposition, name){
this.disposition = disposition;
this.name = name;
}
}
let handler = {
flag: null,
construct(target, args, newTarget){
//
if(!this.flag){
let obj = Reflect.construct(target, args) || new target(...args);
this.flag = obj;
return obj;
}
throw Error(`only can construct one Object!`);
}
}
let proxy = new Proxy(Monsters, handler)
let monster1 = new proxy("fierce", "lizhi");
monster1 = null; handler.flag = null;
let monster2 = new proxy("aa","zhu");
console.log(monster2);
实例:实现对象多继承(重点实例)
该实例转自《深入实践 ES6 Proxy & Reflect》
const people = {
name: 'people',
run() {
console.log('people.run:', this.name);
}
};
const powerMan = {
name: 'powerMan',
run() {
console.log('powerMan.run:', this.name);
},
fight() {
console.log('powerMan.fight:', this.name);
}
};
const handler = {
get(target, prop, receiver) {
if (Reflect.has(target, prop)) {//实际上是for in。此处也巧妙应用了Reflect对象。
return Reflect.get(target, prop, receiver);
}
else {
for (let P of target[Symbol.for('[[Prototype]]')]) {
if (Reflect.has(P, prop)) {
return Reflect.get(P, prop, receiver);
}
}
}
}
};
//对象字面量,不对外暴露目标对象
Object.prototype.a = 1;
const hero = new Proxy({
name: 'hero',
strike() {
this.run();
this.fight();
}
}, handler);
//在全局Symbol注册表上注册一个属性Prototype,该属性存储两个
hero[Symbol.for('[[Prototype]]')] = [people, powerMan];
hero.strike();
console.log(Reflect.has({},"a"));
上面代码解读:
(1)Reflect.has({},"a")判断对象及其原型上是否有“a”属性,对Reflect的理解
(2)Reflect的方法不仅仅是非要用在Proxy代理对应的方法上,而是可以灵活的使用。
(3)Symbol.for全局注册一个值为[[Prototype]]的Symbol值。该Symbol值可以作为对象的属性去使用,注意:是将该Symbol值作为对象的属性,而用在每个对象上时,作为属性,它的值是要每个对象去设置的。
代码执行的流程:
1)当执行hero.strike()时,要去hero代理上看有没有strike方法(get方法得到执行);
2)然后执行run方法和fight方法时,同样要去hero代理上去看对象及原型上有没有该方法,没有这两个方法,去hero的Symbol.for('[[Prototype]]')属性上看有没有这两个方法,有才会执行,
3)这个两个方法执行的时候,this是hero,因为hero是执行上下文this。