第十二章、代理(Proxy)和反射(Reflection)API
代理Proxy是一种可以拦截并改变底层JavaScript引擎的包装器,在新语言中通过它暴露内部运作的对象。
1、数组问题
ES6出现以前,开发者不能通过自己定义的对象模仿JavaScript数组对象的行为方式。
当给数组的特定元素赋值时,影响到数组的length属性;也可以通过length属性修改数组元素。
ES6出现以后,通过代理Proxy就可以实现。
备注:数值属性和length属性具有这种非标准行为,因此在ES6中,数组被认为是奇异对象。
2、代理和反射
调用new Proxy()可创建代替其他目标对象的代理,代理可以拦截JavaScript引擎内部目标的底层对象操作,这些底层操作被拦截后会触发响应特定操作的陷阱函数。
反射API以Reflect对象的形式出现,对象中方法的默认特性与相同的底层操作一致,而代理可以覆写这些操作,每个代理陷阱对应一个命名和参数都相同的Reflect方法。
代理陷阱 | 覆写的特性 | 默认特性 |
---|---|---|
get | 读取一个属性值 | Reflect.get() |
set | 写入一个属性 | Reflect.set() |
has | in操作符 | Reflect.has() |
deleteProperty | delete操作符 | Reflect.deleteProperty() |
getPrototypeOf | Object.getPrototyOf() | Reflect.getPrototypeOf() |
setPrototypeOf | Object.setPrototypeOf() | Reflect.setPrototypeOf() |
isExtensible | Object.isExtensible() | Reflect.isExtensible() |
preventExtensions | Object.preventExtensions() | Reflect.preventExtensions() |
getOwnPropertyDescripter | Object.getOwnPropertyDescripter() | Reflect.getOwnPropertyDescripter() |
defineProperty | Object.defineProperty() | Reflect.defineProperty() |
ownKeys | Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols() | Reflect.ownKeys() |
apply | 调用一个函数 | Reflect.apply() |
constructor | 用new调用一个函数 | Reflect.constructor() |
每个陷阱覆写JavaScript对象的一些内建特性,可以用它们拦截并修改这些特性。
如果仍需使用内建特性,则可以使用相应的发射API方法。
创建代理会让代理和反射API的方法变得清楚。
3、创建一个简单的代理
用Proxy构造函数创建代理需要两个参数:目标target和处理程序Handler。
处理程序是定义一个或多个陷阱的对象,在代理中,除了专门为操作定义的陷阱外,其余操作均使用默认特性。
不使用任何陷阱的处理程序等价于简单的转发代理。
let target = {} ;
let proxy = new Proxy(target,{});
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
target.name = "target";
console.log(proxy.name); // "target"
console.log(target.name); // "target"
该实例中代理只是简单地将所有操作转发给目标,代理自身不存储该属性。
4、使用set陷阱验证属性
场景:创建一个属性值是数字的对象,对象中没新增一个属性都要加以验证,如果不是数字必须抛出错误。
方案:定义一个set陷阱来覆写设置值的默认特性。
set陷阱接收4个参数:
trapTarget:用于接受属性(代理的目标)的对象。
key:要写入的属性键(字符串或Symbol类型)。
value:被写入属性的值。
receive:操作发生的对象(通常是代理)。
Reflect.set()是set陷阱对应的反射方法和默认属性,它和set代理陷阱一样接收相同的4个参数。
如果属性已经设置陷阱返回true;否则返回false。
reflect.set()方法基于操作是否成功返回恰当的值。
let target = {
name: "target"
};
let proxy = new Proxy(target,{
set(trapTarget,key,value,receive){
// 忽略不希望受到影响的已有属性
if(!trapTarget.hasOwnProperty(key)){
if(isNaN(value)){
throw new TypeError("属性必须是数字");
}
}
// 添加属性
return Reflect.set(trapTarget,key,value,receive);
}
});
// 添加一个新属性
proxy.count = 1;
console.log(proxy.count); // 1
console.log(target.count); // 1
// 给目标已有的name属性赋值
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
// 给不存在的属性赋值
proxy.anothername = "proxy"; // 报错
set代理陷阱可以拦截写入属性的操作,get代理陷阱可以拦截读取属性的操作。
5、用get属性验证对象结构(Object Shape)
场景:JavaScript有一个令人费解的特殊行为,即读取不存在的属性时不报错,而是以undefined代替输出。
方案:定义一个get陷阱检查对象结构
get陷阱接收3个参数:
trapTarget:被读取属性的源对象(代理的目标)
key:要读取的属性键(字符串或Symbol)
receiver:操作发生的对象(通常指代理)
Reflect.get()也接收同样的3个参数并返回属性的默认值。
let proxy = new Proxy({},{
get(trapTarget,key,receiver){
if(!(key in receiver)){
throw new TypeError(`属性${key}不存在`);
}
return Reflect.get(trapTarget,key,receiver);
}
});
// 添加一个属性
proxy.name = "proxy";
console.log(proxy.name);
// 如果属性不存在
console.log(proxy.lname); // 抛出错误:属性lname不存在
备注:此处用in操作符检查receiver而不检查trapTarget是防止receiver上有has陷阱。否则可能看不到报错。
6、使用has陷阱隐藏已有属性
可以用in操作符来检测给定对象中是否有某个属性,如果自由属性或原型属性匹配这个名称或Symbol就返回true。
在代理中使用has陷阱可以拦截这些in操作并返回一个不同的值。
每当使用in操作符都会调用has陷阱,并传入2个参数:
trapTarget:被读取属性的源对象(代理的目标)
key:要检查的属性键(字符串或Symbol)
Reflect.has()方法也接收这些参数并返回in操作符的默认响应,同时使用has陷阱和Reflect.has()可以改变一部分属性被in检测是的行为,
并恢复另外一些属性的默认行为。
let target = {
name: "target",
value: 42
};
let proxy = new Proxy(target,{
has(trapTarget,key){
if(key === "value"){
return false;
}else{
return Reflect.has(trapTarget,key);
}
}
});
console.log("name" in proxy); // true
console.log("value" in proxy); // false
console.log("toString" in proxy); // true
7、用deleteProperty陷阱防止删除属性
delete操作符可以从对象中移除属性,如果成功则返回true,不成功则返回false。
严格模式下,如果尝试删除一个不可配置的属性则会导致程序抛出错误;而在非严格模式下只是返回false。
在代理中可以通过deleteProperty陷阱来改变这个行为。
每当通过delete操作符删除对象属性时,deleteProperty陷阱都会被调用,它接收2个参数:
trapTarget:要删除属性的源对象(代理的目标)
key:要删除的属性键(字符串或Symbol)
Reflect.deleteProperty()方法为deleteProperty陷阱提供默认实现,并接收同样的2个参数,结合二者可以改变delete的具体行为。
let target = {
name: "target",
value: 42
};
let proxy = new Proxy(target,{
deleteProperty(trapTarget,key){
if(key === "value"){
return false;
}else{
return Reflect.deleteProperty(trapTarget,key);
}
}
});
// 尝试删除proxy.value
console.log("value" in proxy); // true
let r1 = delete proxy.value;
console.log(r1); // false
console.log("value" in proxy); // true
// 尝试删除proxy.name
let r2 = delete proxy.name;
console.log(r2); // true
console.log("name" in proxy); // false
8、原型代理陷阱
Object.setPrototypeOf()方法和Object.getPrototypeOf()方法
通过代理中的setPrototypeOf陷阱getPrototypeOf陷阱可以拦截这两个方法的执行过程。
Object上的方法会调用代理中的同名陷阱来改变方法的行为。
setPrototypeOf陷阱接收2个参数:
trapTarget:接收原型设置的对象(代理的目标)
proto:作为原型使用的对象
Reflect.setPrototypeOf()方法同样接收2个相同的参数。
getPrototypeOf陷阱接收1个参数:
trapTarget:接收原型设置的对象(代理的目标)
Reflect.getPrototypeOf()方法同样接收1个相同的参数
1、原型代理陷阱的运行机制
原型代理陷阱存在一些限制:
①、getPrototypeOf陷阱必须返回对象或null;
②、setPrototypeOf陷阱中,操作失败返回的一定是false,如果返回的不是false,则表示操作成功了。
let target = {};
let proxy = new Proxy(target,{
getPrototypeOf(trapTarget){
return null;
},
setPrototypeOf(trapTarget,proto){
return false;
}
});
let targetProto = target.getPrototypeOf(target);
let proxyProto = proxy.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // false
console.log(proxyProto); // null
Object.setPrototypeOf(target,{}); // 设置成功,程序正常
Object.setPrototypeOf(proxy,{}); // 报错
当然,也可以使用这两个陷阱的默认行为:
let target = {};
let proxy = new Proxy(target,{
getPrototypeOf(trapTarget){
return Reflect.getPrototypeOf(trapTarget);
},
setPrototypeOf(trapTarget,proto){
return Reflect.setPrototypeOf(trapTarget,proto);
}
});
let targetProto = target.getPrototypeOf(target);
let proxyProto = proxy.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // true
Object.setPrototypeOf(target,{}); // 设置成功,程序正常
Object.setPrototypeOf(proxy,{}); // 设置成功,程序正常
2、为什么有两组方法
Objec.getPrototypeOf()和Object.setPrototypeOf()是高级操作,创建以来就是给开发者使用的;
Reflect.getPrototypeOf()和Reflect.setPrototypeOf()是底层操作,其赋予开发者可以访问之前只能在内部操作的[[GetPrototypeOf]]和[[SetPrototypeOf]]的权限。这两个方法是其对应内部操作的包裹器。
如果传入的参数不是对象,则Reflect.getPrototypeOf()会抛出错误,然Object.getPrototypeOf()在操作前会将参数强制类型转换为一个对象。
let r1 = Objec.getPrototypeOf(1);
console.log(r1 === Number.prototype); // true
Reflect.getPrototypeOf(1); // 抛出错误
Reflect.setPrototypeOf()返回一个布尔值表示操作是否成功,成功为true,失败为false;
Object.setPrototypeOf()一旦失败会抛出错误。
Object.setPrototypeOf()返回第一个参数作为它的值,因此不适合用来实现setPrototypeOf代理陷阱的默认行为。
在所有代理陷阱中一定要使用Reflect上的方法。
备注:当Object.getPrototypeOf()/Reflect.getPrototypeOf()和Object.setPrototypeOf()/Reflect.setPrototypeOf()被用于一个代理时将调用代理陷阱getPrototypeOf和setPrototypeOf
9、对象可扩展陷阱
对象的可扩展性方法:Object.preventExtensions()方法和Object.isExtensible()方法
通过代理中的preventExtensions和isExtensible陷阱拦截这两个方法并调用底层对象。
两个陷阱都接受一个参数:trapTarget对象
isExtensible陷阱返回的是一个布尔值,表示对象是否可扩展;preventExtensions陷阱返回的也是一个布尔值,表示操作是否成功。
Reflect.preventExtensions()方法和Reflect.isExtensible()方法实现了相应陷阱中的默认行为,二者都返回布尔值。
1、两个基础实例
isExtensible和preventExtensions陷阱的默认行为:
let target = {};
let proxy = new Proxy(target,{
isExtensible(trapTarget){
return Reflect.isExtensible(trapTarget);
},
preventExtensions(trapTarget){
return Reflect.preventExtensions(trapTarget);
}
});
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
// 禁止对象可扩展
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // false
console.log(Object.isExtensible(proxy)); // false
改变这种默认行为:让禁止对象可扩展失效
let target = {};
let proxy = new Proxy(target,{
isExtensible(trapTarget){
return Reflect.isExtensible(trapTarget);
},
preventExtensions(trapTarget){
return false;
}
});
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
// 禁止对象可扩展
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
2、重复的可扩展性方法
Object.isExtensible()/Object.preventExtensions()和Reflect.isExtensible()/Reflect.preventExtensions()的区别:
当传入非对象值时,Object.isExtensible()返回false,Reflect.isExtensible()则抛出错误;
无论传入Object.preventExtensions()方法的参数是否是一个对象,它总是返回该参数;
如果Reflect.preventExtensions()方法的参数不是对象就会抛出错误;如果参数是对象,操作成功返回true,否则返回false。
let r1 = Object.isExtensible(2);
console.log(r1); // false
let r2 = Reflect.isExtensible(2); // 抛出错误
let r3 = Object.preventExtensions(2);
console.log(r3); // 2
let target = {};
let r4 = Reflect.preventExtensions(target);
console.log(r4); // true
let r5 = Reflect.preventExtensions(2); // 抛出错误
10、属性描述符陷阱
Object.defineProperty()方法可以定义属性特性:定义访问器属性、将属性设置为可读或不可配置等。
Object.getOwnPropertyDescriptor()方法可以获取这些属性。
在代理中可以通过defineProperty陷阱和getOwnPropertyDesciptor陷阱拦截这两个方法的调用。
defineProperty陷阱接收3个参数:
trapTarget:要定义属性的对象(代理的目标)
key:属性的键(字符串和Symbol)
descriptor:属性的描述符对象
defineProperty陷阱在操作成功后返回true,否则返回false
getOwnPropertyDescriptor陷阱只接受2个参数:trapTarget和key,最终返回描述符。
Reflect.defineProperty()方法和Reflect.getOwnPropertyDescriptor()方法与对应的陷阱接收相同的参数。
let proxy = new Proxy({},{
defineProperty(trapTarget,key,descriptor){
return Reflect.defineProperty(trapTarget,key,descriptor);
},
getOwnPropertyDescriptor(trapTarget,key){
return Reflect.getOwnPropertyDescriptor(trapTarget,key);
}
});
Object.defineProperty(proxy,"name",{
value:"proxy"
});
console.log(proxy.name); // "proxy"
let descriptor = Object.getOwnPropertyDescriptor(proxy,"name");
console.log(descriptor.name); // "proxy"
1、给Object.defineProperty()添加限制
defineProperty陷阱返回true表示Object.defineProperty()方法执行成功;返回false时,Object.defineProperty()抛出错误。
该功能可以用来限制Object.defineProperty()方法可定义的属性类型。
场景:阻止Symbol类型的属性
let proxy = new Proxy({},{
defineProperty(trapTarget,key,descriptor){
if(typeof key === "symbol"){
return false;
}
return Reflect.defineProperty(trapTarget,key,descriptor);
}
});
Object.defineProperty(proxy,"name",{
value:"proxy"
});
console.log(proxy.name); // "proxy"
let symbolName = new Symbol("name");
// 抛出错误
Object.defineProperty(proxy,symbolName,{
value:"name"
});
备注:如果让陷阱返回true并且不调用Reflect.defineProperty()方法,则可以让Object.defineProperty()方法静默失效,既消除了错误又不会真正定义属性,
2、描述符对象限制
为确保Object.defineProperty()方法和Object.getOwnPropertyDescriptor()方法的行为保持一致,传入defineProperty陷阱的描述符对象已经规范化。
从getOwnPropertyDesciptor陷阱返回的对象总是优于相同的原因被验证。
无论将什么对象作为第三个参数传递给Object.defineProperty()方法,在传递给defineProperty陷阱的描述符对爱选哪个中都只有以下属性:
enumberable、configurable、value、writable、get、set属性
let proxy = new Proxy({},{
defineProperty(trapTarget,key,descriptor){
console.log(descriptor.value); // "proxy"
console.log(descriptor.name); // undefined
return Reflect.defineProperty(trapTarget,key,descriptor);
}
});
Object.defineProperty(proxy,"name",{
value:"proxy",
name:"custom"
});
Reflect.defineProperty()方法同样也忽略了描述符对象上的所有非标准属性。
getOwnPropertyDescriptor陷阱的限制条件:返回值必须是null、undefined或一个对象。
如果返回对象,则对象自己的属性只能是enumberable、configurable、value、writable、get、set,在返回的对象中使用不被允许的属性会抛出错误。
let proxy = new Proxy({},{
getOwnPropertyDescriptor(trapTarget,key){
return {
name:"proxy"
};
}
});
let descriptor = Object.getOwnPropertyDescriptor(proxy,"name"); // 抛出错误
该限制可以保证无论代理中使用了什么方法,Object.getOwnPropertyDescriptor()返回值的结构总是可靠的。
3、重复的描述符方法
Object.defineProperty()方法和Reflect.defineProperty()方法只有返回值不同:
Object.defineProperty()方法返回第一个参数;Reflect.defineProperty()方法的返回值与操作无关,成功返回true,失败返回false。
由于defineProperty代理陷阱需要返回一个布尔值,因此必要时最好用Reflect.defineProperty()来实现默认行为。
let target = {};
let r1 = Object.defineProperty(target,"name",{
value:"target"
});
console.log(r1 === target); // true
let r2 = Reflect.defineProperty(target,"name",{
value:"reflect"
});
console.log(r2); // true
调用Object.getOwnPropertyDescriptor()方法时传入原始值作为第一个参数,内部将这个原始值强制类型转换为一个对象;
调用Reflect.getOwnPropertyDescriptor()方法时传入原始值最为第一个参数会抛出错误。
let r3 = Object.getOwnPropertyDescriptor(2,"name");
console.log(r3); // undefined
let r4 = Reflect.getOwnPropertyDescriptor(2,"name"); // 抛出错误
11、ownKeys陷阱
onKeys代理陷阱可以拦截内部方法[[OwnPropertyKeys]],通过返回一个数组的值可以覆写其行为。
这个数组被用于Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()、Object.assign()方法。
Object.assign()方法用数组来确定需要复制的属性。
ownKeys陷阱通过Reflect.ownKeys()方法实现默认行为,返回的数组中包含所有自有属性的键名,字符串和Symbol类型的都包含在内。
Object.getOwnPropertyNames()方法和Object.keys()方法返回的结果不包含Symbol类型的属性名;
Object.getOwnPropertySymbols()方法返回的结果不包含字符串类型的属性名;
Object.assign()方法支持字符串和Symbol两种类型。
ownKeys陷阱接收的唯一参数是操作的目标,返回值必须是一个数组或类数组的对象,否则就抛出错误。
当调用Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()、Object.assign()方法时,可以用onKeys陷阱来过滤不想使用的属性键。
场景:假设你不想引入任何以下划线开头的属性名称,则可以用onKeys陷阱过滤掉它们
let proxy = new Proxy({},{
ownKeys(trapTarget){
return Reflect.onKeys(trapTarget).filter(key => {
return typeof key !== "string" || key[0] !== "_";
});
}
});
let nameSymbol = new Symbol("name");
proxy.name = "proxy";
proxy._name = "private";
proxy[nameSymbol] = "symbol";
let names = Object.getOwnPropertyNames(proxy);
let keys = Object.keys(proxy);
let symbols = Object.getOwnPropertySymbols(proxy);
console.log(names.length); // 1
console.log(names[0]); // name
console.log(keys.length); // 1
console.log(keys[0]); // name
console.log(symbols.length); // 1
console.log(symbols[0]); // "Symbol(name)"
ownKeys陷阱还会影响for-in循环,当确定循环内部使用的键时会调用陷阱。
12、函数代理中的apply和construct陷阱
所有代理陷阱中,只有apply和construct的代理目标是一个函数。
函数有两个内部方法[[call]]和[[construct]],apply陷阱和construct陷阱可以覆写这些内部方法。
若使用new操作符调用函数,则执行[[construct]]方法;
若不用,则执行[[call]]方法,此时会执行apply陷阱,它和Reflect.apply()都接受3个相同的参数:
trapTarget:被执行的函数(代理的目标)
thisArg:函数被调用时内部this的值
argumentsList:传递给函数的参数数组
当使用new调用函数是调用的construct陷阱接受2个参数:
trapTarget:被执行的函数(代理的目标)
argumentsList:传递给函数的参数数组
Reflect.construct()方法也接收相同的参数,它还有一个可选的第三个参数newTarget,该参数用于指定函数内部new.target的值。
apply陷阱和construct陷阱可以完全控制任何代理目标函数的行为。
模拟函数的默认行为:
let target = function(){
return 42;
};
let proxy = new Proxy(target,{
apply(trapTarget,thisArg,argumentsList){
return Reflect.apply(trapTarget,thisArg,argumentsList);
},
construct(trapTarget,argumentsList){
return Reflect.construct(trapTarget,argumentsList);
}
});
console.log(typeof proxy); // "function"
// 不用new调用则进入apply陷阱
console.log(proxy); // 42
// new调用则进入construct陷阱
let instance = new proxy();
console.log(instance instanceof proxy); // true
console.log(instance instanceof target); // true
1、验证函数参数
apply陷阱和construct陷阱增加了一些可能改变函数执行方式的可能性。
场景:假设想验证所有参数都属于特定类型,则可以在apply陷阱中检查参数:
function sum(...values){
return values.reduce((previous,current)=>previous+current,0);
}
let sumProxy = new Proxy(sum,{
apply(trapTarget,thisArg,argumentsList){
argumentsList.forEach(arg=>{
if(typeof arg !== "number"){
throw new TypeError("所有参数必须是数字");
}
});
return Reflect.apply(trapTarget,thisArg,argumentsList);
},
construct(trapTarget,argumentsList){
throw new TypeError("该函数不可通过new来调用");
}
});
console.log(sumProxy(1,2,3,4)); // 10
console.log(sumProxy(1,"2",3,4)); // 抛出错误:所有参数必须是数字
let result = new sumProxy(); // 抛出错误:该函数不可通过new来调用
当然也可以执行相反的操作,确保必须用new来调用函数并验证其参数为数字。
function Numbers(...values){
this.values = values;
}
let NumbersProxy = new Proxy(Numbers,{
apply(trapTarget,thisArg,argumentsList){
throw new TypeError("该函数必须通过new来调用");
},
construct(trapTarget,argumentsList){
argumentsList.forEach(arg=>{
if(typeof arg !== "number"){
throw new TypeError("所有参数必须是数字");
}
});
return Reflect.construct(trapTarget,argumentsList);
}
});
let instance = new NumbersProxy(1,2,3,4);
console.log(instance.values); // [1,2,3,4]
NumbersProxy(1,2,3,4); // 抛出错误:该函数必须通过new来调用
2、不用new调用构造函数
new.target元属性是用new调用函数时对该函数的引用,所以可以通过检查new.target的值来确定函数是否是通过new来调用的。
function Numbers(...values){
if(typeof new.target === "undefined"){
throw new TypeError("该函数必须通过new来调用");
}
this.values = values;
}
let instance = new Numbers(1,2,3,4);
console.log(instance.values); // [1,2,3,4]
Numbers(1,2,3,4); // 抛出错误:该函数必须通过new来调用
有一种情况是当你无法修改Numbers函数,即用new调用Numbers的行为已被锁定,所以这时候可以使用apply陷阱:
function Numbers(...values){
if(typeof new.target === "undefined"){
throw new TypeError("该函数必须通过new来调用");
}
this.values = values;
}
let NumbersProxy = new Proxy(Numbers,{
apply(trapTarget,thisArg,argumentsList){
return Reflect.construct(trapTarget,argumentsList);
}
});
let instance = NumbersProxy(1,2,3,4);
console.log(instance.values); // [1,2,3,4]
注意此处为什么没有抛出错误呢?是因为这里apply陷阱中调用了Reflect.construct()
3、覆写抽象基类构造函数
可以对Reflect.construct()方法指定第三个参数作为new.target的特定值。
场景:
class AbstractNumbers{
construct(...values){
if(new.target === AbstractNumbers){
throw new TypeError("该函数必须被继承");
}
this.values = values;
}
}
class Numbers extends AbstractNumbers{}
let instance = new Numbers(1,2,3,4);
console.log(instance.values); // [1,2,3,4]
new AbstractNumbers(1,2,3,4); // 抛出错误
这种情况下,我们可以手动用代理给new.target赋值绕过构造函数限制:
class AbstractNumbers{
construct(...values){
if(new.target === AbstractNumbers){
throw new TypeError("该函数必须被继承");
}
this.values = values;
}
}
let AbstractNumbersProxy = new Proxy(AbstractNumbers,{
construct(trapTarget,argumentsList){
return Reflect.construct(trapTarget,argumentsList,function(){}); // 给new.target赋值为空函数
}
});
let instance = new AbstractNumbersProxy(1,2,3,4);
console.log(instance.values); // [1,2,3,4]
4、可调用的类构造函数
代理可以拦截对[[call]]方法的调用,也就意味着可以通过代理来有效的创建可调用类构造函数。
场景:类构造函数不用new调用就可以运行
解决:使用apply陷阱来创建一个新实例。
class Person(){
construct(name){
this.name = name;
}
}
let PersonProxy = new Proxy(Person,{
apply(trapTarget,thisArg,argumentsList){
return new trapTarget(...argumentsList);
}
});
let me = PersonProxy("zhangsan");
console.log(me.name); // "zhangsan"
console.log(me instanceof Person); // true
console.log(me instanceof PersonProxy); // true
创建可调用类构造函数只能通过代理来进行。
13、可撤销代理
通常,在创建代理后,代理不能脱离其目标。但是也可能存在需要撤销代理的情况。
可以使用Proxy.revocable()方法创建可撤销的代理,该方法采用与Proxy构造函数相同的参数:目标对象和代理处理程序。
返回值是具有以下属性的对象:
proxy:可被撤销的代理对象
revoke:撤销代理要调用的函数
当调用revoke()函数时,不能通过Proxy执行进一步的操作。任何与代理对象交互的尝试都会触发代理陷阱抛出错误。
let target = {
name:"target"
};
let {proxy,revoke} = new Proxy.revocable(target,{});
console.log(proxy.name); // "target"
revoke();
console.log(proxy.name); // 抛出错误
14、解决数组问题
ES6中的代理和反射API可以用来创建一个对象,该对象的行为与添加和删除属性时内建数组类型的行为相同。
主要模拟以下两个行为:
①、给数组不存在的索引赋值,length的属性自动增加;
②、当length属性被设置时,数组中的元素自动发生变化。
1、检测数组索引
要判断一个属性是否是一个数组索引,可以根据以下算法:
当且仅当ToString(ToUnit32(P))等于P,并且ToUnit32(P)不等于2^32-1时,字符串属性名称P才是一个数组索引。即:
function toUnit32(value){
return Math.floor(Math.abs(Number(value)))%Math.pow(2,32);
}
function isArrayIndex(key){
let numerickey = toUnit32(key);
return String(numerickey) == key && numerickey < (Math.pow(2,32)-1);
}
2、添加新元素时增加length的值
只需要用set代理陷阱即可实现之前提到的两个行为。
function toUnit32(value){
return Math.floor(Math.abs(Number(value)))%Math.pow(2,32);
}
function isArrayIndex(key){
let numerickey = toUnit32(key);
return String(numerickey) == key && numerickey < (Math.pow(2,32)-1);
}
function createMyArray(length = 0){
return new Proxy({length},{
set(trapTartget,key,value){
let currentLength = Reflect.get(trapTartget,"length");
if(isArrayIndex(key)){
let numerickey = Number(key);
if(numerickey >= currentLength){
Reflect.set(trapTartget,"length",numerickey+1);
}
}
}
return Reflect.set(trapTartget,key,value);
});
}
let colors = createMyArray(3);
colors.log(colors.length); // 3
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"
3、减少length的值来删除元素
function toUnit32(value){
return Math.floor(Math.abs(Number(value)))%Math.pow(2,32);
}
function isArrayIndex(key){
let numerickey = toUnit32(key);
return String(numerickey) == key && numerickey < (Math.pow(2,32)-1);
}
function createMyArray(length = 0){
return new Proxy({length},{
set(trapTartget,key,value){
let currentLength = Reflect.get(trapTartget,"length");
if(isArrayIndex(key)){
let numerickey = Number(key);
if(numerickey >= currentLength){
Reflect.set(trapTartget,"length",numerickey+1);
}
}else if(key === "length"){
if(value < currentLength){
for(let index = currentLength-1;index >= value;index--){
Reflect.deleteProxy(trapTartget,index);
}
}
}
}
return Reflect.set(trapTartget,key,value);
});
}
let colors = createMyArray(3);
colors.log(colors.length); // 3
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";
console.log(colors.length); // 4
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
console.log(colors[0]); // "red"
4、实现MyArray类
function toUnit32(value){
return Math.floor(Math.abs(Number(value)))%Math.pow(2,32);
}
function isArrayIndex(key){
let numerickey = toUnit32(key);
return String(numerickey) == key && numerickey < (Math.pow(2,32)-1);
}
class MyArray{
constructor(length = 0){
this.length = length;
return new Proxy(this,{
set(trapTartget,key,value){
let currentLength = Reflect.get(trapTartget,"length");
if(isArrayIndex(key)){
let numerickey = Number(key);
if(numerickey >= currentLength){
Reflect.set(trapTartget,"length",numerickey+1);
}
}else if(key === "length"){
if(value < currentLength){
for(let index = currentLength-1;index >= value;index--){
Reflect.deleteProxy(trapTartget,index);
}
}
}
}
return Reflect.set(trapTartget,key,value);
});
}
}
let colors = new MyArray(3);
console.log(colors instanceof MyArray); // true
colors.log(colors.length); // 3
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";
console.log(colors.length); // 4
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
console.log(colors[0]); // "red"
这种情况下每创建一个实例就需要创建一个代理。
15、将代理用作原型
1、在原型上使用get陷阱
调用内部方法[[Get]]读取属性的时候先查找自有属性,如果未找到,则继续在原型中查找,知道原型结束。
当访问我们不能保证存在的属性时,可以用get陷阱来预防意外的发生。
let target = {};
let thing = Object.create(new Proxy(target,{
get(trapTarget,key,receiver){
throw new ReferenceError(`${key} doesn't exist`);
}
}));
thing.name = "thing";
console.log(thing.name); // "thing"
let unknown = thing.unknown; // 抛出错误
当代理被用作原型时,trapTarget时原型对象,receiver时实例对象。
2、在原型上使用set陷阱
调用内部方法[[Set]]同样会检查目标对象中是否含有某个自有属性,如果没有则继续在原型上查找。
当给对象赋值时,如果存在该自有属性,则赋值给它;如果不存在,则继续在原型上查找。
但问题是,不管原型上是否有同名属性,给该属性赋值时都将默认在实例上创建该属性。
let target = {};
let thing = Object.create(new Proxy(target,{
set(trapTarget,key,value,receiver){
return Reflect.set(trapTarget,key,value,receiver);
}
}));
console.log(thing.hasOwnProperty("name")); // false
// 触发set代理陷阱
thing.name = "thing";
console.log(thing.name); // "thing"
console.log(thing.hasOwnProperty("name")); // true
// 不出发set代理陷阱
thing.name = "boo";
console.log(thing.name); // "boo"
当代理被用作原型时,trapTarget时原型对象,receiver时实例对象。
一旦在thing上创建了name属性,那么在thing.name被设置为其他值时便不再调用set陷阱。
3、在原型上使用has陷阱
has陷阱,可以拦截对象中的in操作符。
只有在搜索原型链上的代理对象时才会调用has陷阱,当用代理作为原型时,只有当指定名称没有对应的自有属性时才会调用has陷阱。
let target = {};
let thing = Object.create(new Proxy(target,{
has(trapTarget,key){
return Reflect.has(trapTarget,key);
}
}));
// 触发has陷阱
console.log("name" in thing); // false
thing.name = "thing";
// 不触发has陷阱
console.log("name" in thing); // true
4、将代理用作类的原型
由于类的prototype是不可写的,因此不能直接修改类来使用代理作为类的原型。
但可以通过集成的方式让类误认为自己可以将代理用作自己的原型。
function NoSuchProperty(){
}
let proxy = new Proxy({},{
get(trapTarget,key,receiver){
throw new ReferenceError(`${key} doesn't exist`);
}
});
NoSuchProperty.prototype = proxy;
class Square extends NoSuchProperty{
constructor(length,width){
super();
this.length = length;
this.width = width;
}
getArea(){
return this.length * this.width;
}
}
let shape = new Square(6,2);
let shapeProto = Object.getPrototype(shape);
console.log(shapePhoto === proxy); // false
let secondLevelProto = Object.getPrototype(shapeProto);
console.log(secondLevelProto === proxy); // true
let area1 = shape.length * shape.width;
console.log(area1); // 12
let area2 = shape.getArea();
console.log(area2); // 12
console.log(shape.wdth); // 抛出错误,wdth属性不存在