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()
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属性不存在
发布了247 篇原创文章 · 获赞 23 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/LiyangBai/article/details/103338753