JavaScript 高级程序设计第九章阅读笔记

102 阅读7分钟

第九章 代理和反射

3个要点

1、代理基础

2、代码捕获器与反射方法

3、代理模式

9.1 代理基础

代理是目标对象的抽象,类似于C++中的指针,因为它可以用作目标对象的替身,但又完全独立于目标对象。

9.1.1 创建空代理

最简单的代理就是空代理,即抽象一个目标对象,什么也不做。默认情况下,在代理对象上执行的素有操作都会五拽的传播到目标对象上。因此,在任何可以使用目标对象的地方,都可以通过同样的方式来使用与之关联的代理对象。

代理是通过Proxy构造函数创建的。这个构造函数接收两个参数:目标对象和处理程序对象,缺少一个都会抛出TypeError的异常

sample 1 改变对象,指针也改变

	const target={
		id:'thisISID'
	};
	const handler={};
	const proxy=new Proxy(target,handler);
	
	//id属性会访问同一个值
	console.log(target.id);			//返回	'thisISID'
	console.log(proxy.id);			//返回	'thisISID'
	
	//改变目标对象的属性,指针也会改变
	target.id="我要变了";
	console.log(target.id);			//返回	'我要变了'
	console.log(proxy.id);			//返回	'我要变了'

hasOwnProperty()

	console.log(target.hasOwnProperty('id'));	//返回true
	conosle.log(proxy.hasOwnProperty('id'));	//返回true

不能用 instanceof

	console.log(target instanceof Proxy);	//TypeError
	console.log(proxy instacneof Proxy);	//TypeError

9.1.2 定义捕获器

使用代理的目的就是可以定义**捕获器(trap)**捕获器就是在处理程序对象中定义的“基本操作的拦截器”。

sample

	const target={
		foo:"bar"
	};
	const handler={
		//捕获器在处理程序对象中以方法名键
		get(){
			return '不允许读';
		}
	};
	const proxy=new Proxy(target,handler);
	console.log(proxy.foo);		//被get 拦截,不允许读
	console.log(target.foo);	//返回 bar 在目标对象上不受影响

9.1.3 捕获器参数和反射API

所有捕获器都可以访问相应的参数,基于这些参数可以被重建捕获方法的原始行为。get()捕获器会接收目标对象,查询的属性和代理对象三个参数

	const target={
		name:"JackMa",
		age:59,
		job:"福报厂创始人"
	};
	const handler={
		get(trapTarget,property,receiver){
			console.log(trapTarget===target);
			console.log(property);
			console.log(receiver===proxy);
		}
	};
	const proxy=new Proxy(target,handler);
	proxy.name;		//返回true 	name	true

通过上面的捕获器就可以重建被捕获方法的原始行为

	const target={
		name:"JackMa",
		age:59,
		job:"福报厂创始人"
	};
	const handler={
		get(trapTarget,property,receiver){
			console.log(trapTarget===target);
			console.log(trapTarget[property]);
			console.log(receiver==proxy);
			return trapTarget[property];
		}
	}
	console.log(proxy.name);	//返回 true JackMa true JackMa
	conosle.log(target.name);	//返回 	JackMa

所有捕获器都可以基于自己的参数重建原始操作,但并非所有的捕获器行为都像get()那么简单。因此通过手动写代码时不现实的。实际上可以通过调用全局Reflect对象上(封装了原始行为)的同名方法来轻松重建

处理程序对象中所有可以被捕获的方法都有相应的反射(Reflect) API方法。这些方法与捕获器的方法加油相同的名称和函数签名,而且也具有与被拦截的方法相同的行为。

反射的例子

	let person ={
		name:"JackMA",
		age:59,
		job:'福报厂CEO'
	};
	let handler={
		get(){
			return Reflect.get(...arguments);
		}
	};
	const proxy=new Proxy(target,handler);
	console.log(proxy.name);		//返回JackMa
	console.log(person.name);		//返回JackMa

9.1.4 捕获器不变式

使用捕获器几乎可以改变所有基本方法的行为,但也不是没有限制。根据ECMAScipt规范,每个捕获的方法都知道目标对象的上下文、捕获函数签名。而捕获处理程序的行为必须遵守"捕获器不变式(trap invariant)"

比如,如果一个对象又一个补课配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,就抛出TypeError

	let person={};
	Object.defineProperty(person,'name',{
		configurable:false,
		writable:false,
		value:"JackMa"
	});
	let handler={
		get(){
			return "XXX";
		}
	};
	let handler2={
		get(){
			return "JackMa";
		}
	}
	
	let proxy=new Proxy(person,handler);
	console.log(proxy.name);	//返回TypeError
	
	let proxy2=new Proxy(person,handler2);
	console.log(proxy2.name);	//返回	JackMa

9.1.5 可撤销代理 revoke()函数

有时候可能需要终端代理对象与目标对象之间的联系。使用new Proxy()创建的普通代理来说,这种联系会在代理对象的生命周期内一直存在。

Proxy也暴露了revocable()方法,这个方法支持撤销代理与目标对象的关系。 revoke()时幂等的,调用多少次的结果都一样。撤销代理之后再调用代理会抛出TypeError

	const person={
		name:'JackMa'
	};
	const handler={
		get(){
			return 'this is a ret';
		}
	};
	const {proxy,revoke}=Proxy.revocable(person,handler);
	cosole.log(proxy.name);		//返回 this is a ret
	cosole.log(person.name);	//返回 JackMa
	revoke();
	console.log(proxy.name);	//返回TypeError

9.1.6 实用反射API

某些情况下应该优先使用反射API ,这是由一定的理由的。

1、反射API与对象API

在使用反射API时,要记住:

(1)、反射API并不限于捕获处理程度。

(2)、大多数反射API方法在Object类型上有对应的方法。

通常,Object上的方法适用于通用程序,而反射方法适用于细粒度的对象控件与操作。

2、状态标记

很多反射方法返回称作"状态标记"的布尔值,表示意图执行的操作是否成功。有时候,状态标记比那些返回修改后的对象或者抛出错误(取决于方法)的反射API方法更有用。

例如,可以使用反射API对下面的代码进行重构

	//初始化代码
	const o={};
	try{
		Object.defineProperty(o,'name','JackMa');
		console.log('sucess');
	}catch(e){
		console.log('failure');
	}	
	//这里返回		failure

在定义新属性时如果发生问题,Reflect.defineProperty()会返回false,而不是抛出错误。因此使用这个反射方法可以重构上面代码

	//重构后的代码
	let person={};
	
	if(Reflect.defineProperty(person,'name','JackMa')){
		console.log('sucess!');
	}else{
		console.log('failure');
	}
	//返回	sucess
	console.log(person.name);		//返回JackMa

以下反射方法都会提供状态标记:

1、类方法 Reflect.preventExtensions(target)

此方法是阻止新属性添加到对象上去

	let person={};
	Reflect.preventExtension(person);
	if(Reflect.defineProperty(person,'name','JackMa')){
		console.log('Sucess!');
	}else{
		console.log('failure!');
	}
	//返回 failure!

2、类方法 Reflect.deleteProperty(target,propertyKey);

9.1.7 代理另一个代理

代理可以拦截反射API的操作,而着以为着完全可以创建一个代理,通过它去代理另一个代理,这样可以在一个目标上构建多个拦截网。

	let person={
		name:"JackMa",
		age:59
	};
	let firstProxy=new Proxy(target,{
		get(){
			console.log('first Proxy');
			return Reflect.get(...arguments);
		}
	});
	
	const secondProxy=new Proxy(firstProxy,{
		get(){
			console.log('第二次代理');
			return Reflect.get(...arguments);
		}
	})
	console.log(secondProxy.name);
	//返回	第二次代理	first Proxy	JackMa

9.1.8 代理的问题与不足

1、代理中this指向问题

this 指向调用这个方法的对象

	let person={
		thisValEqu(){
			return this===proxy;
		}
	}
	const proxy=new Proxy(person,{});
	
	console.log(person.thisValEqu());	//返回false
	console.log(person.thisValEqu());	//返回true

2、代理与内部槽位

代理和内置引用类型,比如Array的实例可以很好的协同。但有些ECMAScript内置类型可能会依赖代理无法控制的机制,结果导致代理上的某些错误

	let timi=new Date();
	const proxy=new Proxy(timi,{});
	console.log(proxy istance of Date);		//返回true
	proxy.getDate();	//TypeError

9.2 代理捕获器与反射方法

代理可以捕获13种不同的操作方法,这些操作有各自不同的反射API方法。参数,关联ECMAScript操作和不变式

9.2.1 get()

get()捕获器会在获取属性值的操作中被调用。对应的反射API方法为Reflect.get()

	let person={
		name:"JackMa",
	}
	let proxy=new Proxy(person,{
		get(tar,prop,rec){
			console.log(tar);
			console.log(prop);
			console.log(receiver);
			console.log(tar[prop]);
			return Reflect.get(...arguments);
		}
	});
	proxy.name;		//返回 Object name proxy JackMa

1、返回值

返回值无限制

2、拦截的操作

1、proxy.property 2、proxy[property] 3、Object.create(proxy)[property] 4、Reflect.GET(proxy,property,receiver)

3、捕获器处理程序参数

**target:**目标对象 **property:**引用的目标对象上的属性 **receiver:**代理对象或者继承代理对象的对象

4、捕获器不变式

如果target.property不可写且不可配置,则处理程序放回的值必须与target.property匹配

9.2.2 set()

set()捕获器会在设置属性值的操作中被调用。对应的反射API方法为Reflect.set()

	let person={
		name="Timi"
	}
	let proxy=new Proxy(person,{
		set(target,property,value,receiver){
			console.log('set()');
			return Reflect.set(...arguments);
		}
	});
	proxy.name="JackMa";		//返回  set()
	console.log(proxy.name);	//返回  JackMa

1、返回值 返回true表示成功;返回false表示失败,严格模式下会抛出TypeError

2、拦截操作 proxy.property=value

proxy[property]=value

Object.create(proxy)[property]=value

Reflect.set(proxy,property,value,receiver);

3、捕获器处理程序参数

target 目标对象 property 对象上的属性 value 要赋值的值 receiver 接收最初赋值的对象

9.2.3 has()

has()捕获器会在in操作父中被调用。对应的反射API方法为Reflect.has()

	let person={
		name:'JackMa',
	}
	let proxy=new Proxy(person,{
		has(target,property){
			console.log('Has');
			return Reflect.has(...arguments);
		}
	});
	console.log('JackMa' in proxy);	//返回false
	
	console.log('name' in proxy);	//返回true

9.2.4defineProperty()

defineProperty()捕获器会在Object.defineProperty()中被调用。对应的反射API方法为Reflect.defineProperty()

	let person={
		
	};
	const proxy=new Proxy(person,{
		defineProperty(target,property,descriptor){
			console.log('defineProperty()');
			return Reflect.defineProperty(...arguments)
		}
	});
	Object.defineProperty(proxy,'name',{value:'JackMa'});	//返回true
	console.log(proxy.name);	//返回Jack Ma

9.2.5getOwnPropertyDescriptor()

9.3代理模式(暂时略过)

使用代理可以在代码中实现一些有用的编程模式

9.3.1 跟踪属性访问

通过捕获get、set和has等操作,可以知道对象属性什么时候被访问、被查询。把实现相应捕获器的某个对象代理放到应用中,可以监控这个对象合适再何处被访问