ES6代理与反射
什么是代理(Proxy)?
代理(proxy)是一种可以拦截并且改变底层JavaScript引擎操作的包装器,在新语言中通过它暴露内部运作的对象。
简单来说代理允许你拦截目标对象上的底层操作,而这本来是JS引擎的内部能力,拦截行为适用了一个能响应特定操作的函数(被称之为陷阱)。
如何设置代理(Proxy)?
- 语法:new Proxy(target, handler)
- target 代理目标对象
- handler 处理对象的操作程序
let person = {};
let proxy = new Proxy(person, {});
proxy.name = 'bob';
console.log(proxy.name); // bob
console.log(person.name); // bob
person.name = 'lynn';
console.log(proxy.name); // lynn
console.log(person.name); // lynn
以上的代码是通过代理proxy的方式给person
对象进行赋值,proxy.name
和 person.name
的引用地址是一样的,所以最后修改了perosn.name
,proxy.name
和 person.name
都会同时改变。
什么是反射接口?
以Reflect对象的形式出现,对象中方法的默认特性与相同的底层操作一致,而代理可以覆写这些操作,每个代理陷阱对于一个命名和参数都相同的Reflect方法。
代理陷阱的特性
代理方法 | 覆写的特性 | 默认特性 |
---|---|---|
get() | 读取一个属性值 | Reflect.get() |
set() | 写入一个属性 | Reflect.set() |
has() | in操作符 | Reflect.has() |
deleteProperty() | delete操作符 | Reflect.deleteProperty() |
getPrototyOf() | Object.getPrototypeOf() | Reflect.getPrototypeOf() |
setPrototyOf() | Object.setPrototypeOf() | Reflect.setPrototypeOf() |
isExtensible() | Object.isExtensible() | Reflect.isExtensible() |
preventExtensions() | Object.preventExtensions() | Reflect.preventExtensions() |
getOwnPropertyDescriptor() | Object.getOwnPropertyDescriptor() | Reflect.getOwnPropertyDescriptor() |
defineProperty() | Object.defineProperty() | Reflect.defineProperty() |
apply() | 调用一个函数 | Reflect.apply() |
construct() | 用new调用一个函数 | Reflect.construct() |
ownkeys() | Object.keys()、 Object.getOwnPropertyNames()、 Object.getOwnPropertySymbols() | Reflect.ownKeys() |
set方法
- 作用:在对象给属性添加属性值时,可以添加一些判断验证,比如添加的属性值不少数值就抛出错误
- 参数:
- trapTargt 代理对象
- key 要写入的属性键
- value 被写入的属性值
- receiver 操作发生的对象
let person = {
name: 'bob'
}
let proxy = new Proxy(person, {
set(trapTarget, key, value, receiver) {
// 防止自身属性受到影响
if (!trapTarget.hasOwnProperty(key)) {
// 传入的值必须要是数值
if (isNaN(value)) {
throw new TypeError("传入的属性值必须是数字")
}
}
// 如果传入的键值为age,那么为它检测传入的值是否为数值,且代理加上100
if (key === 'age') {
// 传入的值必须要是数值
if (isNaN(value)) {
throw new TypeError("传入的属性值必须是数字")
} else {
value += 100;
}
}
// 通过上面的检测,就添加属性
return Reflect.set(trapTarget, key, value, receiver);
}
});
// 添加一个新属性
proxy.count = 1;
console.log(proxy.count); // 1
console.log(person.count); // 1
// 为已有的属性赋值
proxy.name = "lynn";
console.log(proxy.name); // lynn
console.log(person.name); // lynn
// 给不存在的属性赋值就会进入验证传入的值是否是数值,
// 如果否则抛出错误
// proxy.likes = "coding"; // TypeError: 传入的属性值必须是数字 ...
proxy.likes = 123; // 传入数值后正确
proxy.age = 10;
console.log(proxy.age); // 110 传入10,代理自动加上了100
console.log(person.age); // 110 传入10,代理自动加上了100
get方法
- 作用:在读取属性时候,可以添加一些验证或者操作,比如属性不存在时,可以抛出错误,或者给读取的属性添加一些增改的操作
- 参数
- trapTarget 操作的对象
- key 读取的属性
- receiver 操作发生的对象
let person = {
name: 'bob'
}
let proxy = new Proxy(person, {
get(trapTarget, key, receiver) {
// 检测属性是否存在
if (!(key in receiver)) {
throw new TypeError(`属性 ${key} 不存在`);
}
return Reflect.get(trapTarget, key, receiver);
}
})
console.log(proxy.name); // bob
console.log(proxy.age); // TypeError: 属性 age 不存在
上面的例子,在读取属性时候,判断属性是否存在对象中,如果不存在直接抛出属性不存在错误,如果存在则直接读取。
has方法
- 作用:使用in操作符可以检测属性是否在对象中,在代理has中可以拦截in操作符返回值
- 参数
- trapTarget 读取的对象
- key 检测的属性键
let person = {
name: 'bob',
likes: 'coding'
}
let proxy = new Proxy(person, {
/**
* 作用:使用in操作符检测对象是否包含该值,使用has可以拦截in操作的返回值
* @param trapTarget 读取属性的对象
* @param key 检测的属性键
*/
has(trapTarget, key) {
if (key === 'name') {
return false;
} else {
return Reflect.has(trapTarget, key)
}
}
});
console.log("name" in proxy); // true
console.log("likes" in proxy); // false
上面的例子,使用has方法拦截了对in操作符检测 “name” 键值返回false,如果检测其他的键值返回默认值。
deleteProperty方法
- 作用:使用delete操作符可以删除对象属性,使用代理的deleteProperty方法可以进行对使用delete操作符删除属性操作一些验证,比如强制不允许删除
- 参数
- trapTarget 需要删除属性的对象
- key 需要删除的属性键
let person = {
name: 'bob',
likes: 'coding'
}
let proxy = new Proxy(person, {
deleteProperty(trapTarget, key) {
if (key === 'name') {
return false;
} else {
return Reflect.deleteProperty(trapTarget, key);
}
}
})
// 上面的代码限制了不允许删除 name 属性
// 测试一下删除 name 属性是否成功
delete proxy.name;
console.log("name" in proxy); // true // 测试是不能删除name属性的
// 试图删除一下 likes 属性
delete proxy.likes;
console.log("likes" in proxy); // false // 测试可以删除 likes 属性
getPrototypeOf方法
- Object.getPrototypeOf(object)返回指定对象的原型属性的值
- 作用:拦截Object.getPrototypeOf(proxy),返回一个对象
- 返回的结果必须是对象或者null
let person = {
name: 'bob',
}
let proxy = new Proxy(person, {
getPrototypeOf(trapTarget) {
return null;
}
})
console.log(Object.getPrototypeOf(person));
/**
* 输出:
* constructor: ƒ Object()
* hasOwnProperty: ƒ hasOwnProperty()
* isPrototypeOf: ƒ isPrototypeOf()
* propertyIsEnumerable: ƒ propertyIsEnumerable()
* toLocaleString: ƒ toLocaleString()
* toString: ƒ toString()
* valueOf: ƒ valueOf()
*/
console.log(Object.getPrototypeOf(proxy));
/**
* 输出:null
*/
上面方法通过代理的getPrototypeOf()方法拦截了Object.prototypeOf()返回的原型对象,且设置返回对象为null
setPrototypeOf方法
- Object.setPrototypeOf(obj, proto) 设置一个指定的对象的原型到另一个对象或 null
- 拦截Object.setPrototypeOf(obj, proto)的结果且返回一个布尔值,如果操作失败返回的必须是false
复习一下Object.setPrototypeOf()方法:
// 构造函数
function Person(name) {
this.name = name;
}
var p = new Person("bob");
//等同于将构造函数的原型对象赋给实例对象p的属性__proto__
p.__proto__ = Object.setPrototypeOf({}, Person.prototype);
Person.call(p,"bob");
代理setPrototypeOf方法:
let person = {
name: 'bob'
}
let proxy = new Proxy(person, {
setPrototypeOf(trapTarget, proto) {
return false;
}
})
console.log(Object.setPrototypeOf(person, {num: 123}));
console.log(person.name); // bob
console.log(person.num); // 123
console.log(Object.setPrototypeOf(proxy, {num: 456}));
// 输出:Uncaught SyntaxError: Identifier 'person' has already been declared
console.log(proxy.name); // 由于上面报错会下面代码会停止输出:实际上打印是"bob"
console.log(proxy.num);
// 由于上面报错会下面代码会停止输出:实际上打印是"123",没有设置成功的,
// 原因是因为代理的setPrototypeOf设置返回false
上面的方法代理使用了setPrototypeOf方法设置返回false,这时候回抛出错误。
isExtensible方法和preventExtensions方法
- Object.isExtensible() 方法判断一个对象是否可以在它上面添加新的属性
- 代理isExtensible方法
- 作用:拦截Object.isExtensible()返回的结果
- Object.preventExtensions()方法让一个对象变不能再添加新的属性
- 代理preventExtensions方法
- 作用:拦截Object.preventExtensions()返回的结果
let person = {}
let proxy = new Proxy(person, {
isExtensible(trapTarget) {
return Reflect.isExtensible(trapTarget);
},
preventExtensions(trapTarget) {
return false;
}
})
console.log(Object.isExtensible(person)); // true
console.log(Object.isExtensible(proxy)); // true
Object.preventExtensions(proxy);
// 代理拦截了设置Object.preventExtensions的结果且返回false,
// 所以下面结果返回仍然是true
console.log(Object.isExtensible(person)); // true
console.log(Object.isExtensible(proxy)); // true
defineProperty方法
- 代理definedProperty()方法
- 参数
- trapTarget 要定义属性的对象
- key 属性的键
- descriptor 属性的描述符对象
- 作用:拦截Object.definedProperty()方法的调用
- 参数
// 示例:给Object.defineProperty()添加限制
let target = {}
let proxy = new Proxy(target, {
defineProperty(trapTarget, key, descriptor) {
if (key === 'name') {
return false;
}
return Reflect.defineProperty(trapTarget, key, descriptor);
}
})
Object.defineProperty(proxy, "num", {
value: '16'
})
console.log(proxy.num); // 16
// 会报错,因为代理陷阱设置了 name 属性设置false失败状态了
Object.defineProperty(proxy, 'name', {
value: 'bob'
})
console.log(proxy.name);
上面的例子,使用代理限制了设置 name 属性设置失败状态,所以会设置不了其值
getOwnPropertyDescriptor方法
- 代理getOwnPropertyDescriptor()方法
- 参数
- trapTarget 要定义属性的对象
- key 属性的键
- 参数
// getOwnPropertyDescriptor()
let target = {
name: 'bob'
}
let proxy = new Proxy(target, {
getOwnPropertyDescriptor(trapTarget, key) {
return {
name: 'lynn'
}
}
})
// 报错
let descriptor = Object.getOwnPropertyDescriptor(proxy, 'name');
console.log(descriptor.value);
上面的例子,getOwnPropertyDescriptor()陷阱的限制条件有所不同,它的返回值必须是null,undefined,或是一个对象,如果返回的是对象,那么对象的属性必须是enumerable、configurable,value,wriable,get和set,在返回的对象中使用不被允许的属性会抛出一个错误。
ownKeys陷阱
ownKeys 代理陷阱拦截了内部方法 [[OwnPropertyKeys]] ,并允许你返回一个数组用于重写该行为。返回的这个数组会被用于四个方法: Object.keys() 方法、Object.getOwnPropertyNames() 方法、Object.getOwnPropertySymbols()方法与Object.assign() 方法,其中 Object.assign() 方法会使用该数组来决定哪些属性会被复制。
ownKeys 陷阱函数接受单个参数,即目标对象,同时必须返回一个数组或者一个类数组对象。你可以使用 ownKeys 陷阱函数去过滤特定的属性,以避免这些属性被Object.keys() 方法、Object.getOwnPropertyNames() 方法、Object.getOwnPropertySymbols() 方法或 Object.assign() 方法使用。
apply和construct陷阱
有 apply 与 construct要求代理目标对象必须是一个函数。
-
apply陷阱函数
-
参数
- trapTarget :被执行的函数(即代理的目标对象)
- thisArg :调用过程中函数内部的 this 值
- argumentsList :被传递给函数的参数数组
-
construct陷阱函数
-
参数
- trapTarget :被执行的函数(即代理的目标对象)
- argumentsList :被传递给函数的参数数组
当使用 new 调用函数时,会触发construct陷阱函数,反之触发apply陷阱函数
情景一:验证函数的参数必须是数值,且不允许使用new操作符调用
function sum(...values) {
return values.reduce((previous, current) => previous + current, 0)
}
let proxy = 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(proxy(1, 2, 3, 4, 5, 6)); // 21
console.log(proxy(1, 2, "2", 4, 5, 6)); // 报错:TypeError: 所有参数必须是数字
new proxy(1, 2, 3, 4); // TypeError: 该函数不允许使用new关键字调用
情景二:验证函数的参数必须是数值,且必须使用new操作符调用
function sum(...values) {
return values.reduce((previous, current) => previous + current, 0)
}
let MyProxy = new Proxy(sum, {
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);
}
})
// console.log(MyProxy(1, 2, 3, 4, 5, 6)); // TypeError: 该函数必须使用new关键字调用
// console.log(new MyProxy(1, 2, "2", 4, 5, 6)); // 报错:TypeError: 所有参数必须是数字
console.log(new MyProxy(1, 2, 3, 4));
可撤销代理
- 使用Proxy.revocable(proxy, revoke)
- 参数
- proxy 可被撤销的代理对象
- revoke 撤销代理要调用的函数
let target = {
name: 'bob'
}
let {proxy, revoke} = Proxy.revocable(target, {});
console.log(proxy.name);
revoke(); // 移除代理
console.log(proxy.name); // 移除代理后会输出报错
将代理作为原型
将代理作为原型,始终要记住一个规律:当查找一个属性时,当当前对象查找不到时,会往它的原型对象上面找,这时候才会触发代理陷阱函数,可用的代理陷阱函数有get,set,和has。
1.在原型上面使用get陷阱,访问不存在的属性进行报错
// get
// 在原型上读取一个不存在的属性抛出错误
let target = {}
let thing = Object.create(new Proxy(target, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
}));
thing.name = 'bob';
console.log(thing.name); // bob
// 访问不存在的属性
console.log(thing.age); // ReferenceError: age doesn't exist
2.在原型上面使用set陷阱
let target = {}
let thing = Object.create(new Proxy(target, {
set(trapTarget, key, value, receiver) {
Reflect.set(trapTarget, key, value, receiver);
}
}))
console.log(thing.hasOwnProperty("name")); // false
// 触发set代理陷阱
thing.name = 'bob';
console.log(thing.hasOwnProperty("name")); // true
// 不会触发set代理陷阱
thing.name = 'lynn';
3.在原型上面上使用has代理陷阱
let target = {}
let thing = Object.create(new Proxy(target, {
has(trapTarget, key) {
Reflect.set(trapTarget, key, value, receiver);
}
}))
// 触发has代理陷阱
console.log('name' in thing);
thing.name = 'bob'
// 不会触发has代理陷阱
console.log('name' in thing);
将代理用作类的原型
function NoSuchPrototype() {
// todo
}
let proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
})
NoSuchPrototype.prototype = proxy;
class Square extends NoSuchPrototype {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
}
let area = new Square(2, 4);
console.log(area.length * area.width); // 8
// 当访问不存在的属性时会报错,因为代理陷阱set 设置了false
console.log(area.name); // ReferenceError: name doesn't exist
共同学习,写下你的评论
评论加载中...
作者其他优质文章