12、代理(Proxy)和反射(Reflection)API

第十二章、代理(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()
hasin操作符Reflect.has()
deletePropertydelete操作符Reflect.deleteProperty()
getPrototypeOfObject.getPrototyOf()Reflect.getPrototypeOf()
setPrototypeOfObject.setPrototypeOf()Reflect.setPrototypeOf()
isExtensibleObject.isExtensible()Reflect.isExtensible()
preventExtensionsObject.preventExtensions()Reflect.preventExtensions()
getOwnPropertyDescripterObject.getOwnPropertyDescripter()Reflect.getOwnPropertyDescripter()
definePropertyObject.defineProperty()Reflect.defineProperty()
ownKeysObject.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属性不存在

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

明致成

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值