原创文章&经验总结&从校招到A厂一路阳光一路沧桑
详情请戳www.codercc.com
主要知识点:代理和反射的定义、常用的陷阱函数、可被撤销的代理、将代理对象作为原型使用、将代理作为类的原型
1. 代理和反射
代理是什么?
通过调用 new Proxy()
,你可以创建一个代理用来替代另一个对象(被称之为目目标对象) ,这个代理对目标对象进行了虚拟,因此该代理与该目标对象表面上可以被当作同一个对象来对待。
代理允许你拦截目标对象上的底层操作,而这本来是JS引擎的内部能力,拦截行为适用了一个能响应特定操作的函数(被称之为陷阱);
反射是什么?
被Reflect
对象所代表的反射接口,是给底层操作提供默认行为的方法的集合,这些操作是能够被代理重写的。每个代理陷阱都有一个对应的反射方法,每个方法都与对应的陷阱函数同名,并且接收的参数也与之一致。
创建一个简单的代理
使用Proxy构建可以创建一个简单的代理对象,需要传递两个参数:目标对象以及一个处理器,后者是定义一个或多个陷阱函数的对象。如果不定义陷阱函数,则依然使用目标对象的默认行为。
2. 常用陷阱函数
2.1 基本陷阱函数
1.使用Set陷阱函数验证属性值
假如有这样一个场景,必须要求对象的属性值必须只能是数值,这就意味着该对象每个新增属性时都要被验证,并且在属性不为数值属性时就应该抛出错误。因此就需要使用set
陷阱函数来重写set
函数的默认行为,set
陷阱函数接收四个参数:
- trapTarget:代理的目标对象;
- key:需要写入的属性的键;
- value:被写入属性的值;
- receiver:操作发生的对象(通常是代理对象)
Reflect.set()
是set
陷阱函数对应的反射方法,同时也是set
操作的默认行为,Reflect.set()
方法与set
陷阱函数一样,能够接受四个参数。
针对上述场景,示例代码:
//set陷阱函数
let target = {
name:'target'
}
let proxy = new Proxy(target,{
set(tarpTarget,key,value,receiver){
if(!tarpTarget.hasOwnProperty(key)){
if(isNaN(value)){
throw new Error('property must be number');
}
}
return Reflect.set(tarpTarget,key,value,receiver);
}
});
proxy.msg='hello proxy'; //Uncaught Error: property must be number
通过set陷阱函数就可以检测设置属性时属性值的类型,当属性值不是数字时,就会抛出错误。
2.使用get陷阱函数进行对象外形验证
对象外形(Object Shape)指的是对象已有的属性与方法的集合。能够使用代理很方便进行对象外形验证。由于使用属性验证只需要在读取属性时被触发,因此只需要使用get陷阱函数
。该函数接受三个参数:
- trapTarget:代理的目标对象;
- key:需要读取的属性的键;
- receiver:操作发生的对象(通常是代理对象);
相应的Reflect.get()
方法同样拥有这三个参数。进行对象外形验证的示例代码:
//get陷阱函数
let target={
name:'hello world'
}
let proxy = new Proxy(target,{
get(tarpTarget,key,receiver){
if(!(key in tarpTarget)){
throw new Error('不存在该对象');
}
return Reflect.get(tarpTarget,key,receiver);
}
});
console.log(proxy.name); //hello world
console.log(proxy.age); // Uncaught Error: 不存在该对象
使用get陷阱函数
进行对象外形验证,由于target
对象存在name
属性,所以可以正常返回,当获取age
属性时,由于该属性并不存在,所以会抛出错误。
3.使用has陷阱函数隐藏属性
in
运算符用于判断指定对象中是否存在某个属性,如果对象的属性名与指定的字符串或符号值相匹配,那么in
运算符就会返回true
。无论该属性是对象自身的属性还是其原型的属性。
has陷阱函数
会在使用in
运算符的情况下被调用,控制in运算符返回不同的结果,has陷阱函数
会传入两个参数:
- trapTarget:代理的目标对象;
- key:属性键;
Reflect.has()
方法接收相同的参数,并向in
运算符返回默认的响应结果,用于返回默认响应结果。
例如想要隐藏value属性:
//has陷阱函数
let target = {
value:'hello world'
}
let proxy = new Proxy(target,{
has(tarpTarget,key){
if(Object.is(key,'value')){
return false;
}
Reflect.has(tarpTarget,key);
}
})
console.log('value' in proxy); //false
使用has陷阱函数
,能够控制in
运算符的结果,value
属性在target对象
中存在,通过代理的has陷阱函数
使得在检查value
属性时返回false
,达到隐藏属性的效果。
4.使用deleteProperty陷阱函数避免属性被删除
deleteProperty
陷阱函数会在使用delete
运算符删除对象属性时被调用,该方法接收两个参数:
- trapTarget:代理的目标对象;
- key:需要删除的键;
Reflect.deleteProperty()
方法也接受这两个参数,并提供了 deleteProperty
陷阱函数的默认实现。你可以结合 Reflect.deleteProperty()
方法以及 deleteProperty
陷阱函数,来修改 delete
运算符的行为。例如,能确保 value 属性不被删除:
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 result1 = delete proxy.value;
console.log(result1); // false
2.2 原型代理上的陷阱函数
在调用Object.setPrototypeOf()和getPrototypeOf()方法时,可以使用setPrototypeOf
和getPrototypeOf
陷阱函数来影响Object上相应的两个方法的效果。setPrototypeOf陷阱函数接收两个参数:
- trapTarget:代理的目标对象;
- proto:需要被用作原型的对象;
setPrototypeOf()
方法与Reflect.setPrototypeOf()
传入相同的参数。另外,getPrototypeOf陷阱函数只接收trapTarget
参数,Reflect.getPrototype
也只接收一个参数。
例如,通过返回 null 隐藏了代理对象的原型,并且使得该原型不可被修改:
//原型代理上的陷阱函数
let target = {};
let proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return null;
},
setPrototypeOf(trapTarget, proto) {
return false;
}
});
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // false
console.log(proxyProto); // null
// 成功
Object.setPrototypeOf(target, {});
// 抛出错误
Object.setPrototypeOf(proxy, {});
使用 target
对象作为参数调用Object.getPrototypeOf()
会返回一个对象值;而使用 proxy 对象调用该方法则会返回null
,因为 getPrototypeOf
陷阱函数被调用了。类似的,使用 target 去调用Object.setPrototypeOf()
会成功;而由于 setPrototypeOf
陷阱函数的存在,使用 proxy
则会引发错误。
2.3 对象可扩展性的陷阱函数
ES5 通过Object.preventExtensions()
与 Object.isExtensible()
方法给对象增加了可扩展性。而 ES6 则通过 preventExtensions
与 isExtensible
陷阱函数允许代理拦截对于底层对象的方法调用。这两个陷阱函数都接受名为 trapTarget
的单个参数,此参数代表代理的目标对象。 isExtensible
陷阱函数必须返回一个布尔值用于表明目标对象是否可被扩展,而 preventExtensions
陷阱函数也需要返回一个布尔值,用于表明操作是否已成功。同时也存在Reflect.preventExtensions()
与Reflect.isExtensible()
方法,用于实现默认的行为。这两个方法都返回布尔值,因此它们可以在对应的陷阱函数内直接使用。
例如,不想让代理对象的Object.preventExtensios()
操作成功,可以强制preventExtensions
陷阱函数返回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.4 属性描述符的陷阱函数
ES5 最重要的特征之一就是引入了Object.defineProperty()
方法用于定义属性的特性。在JS 之前的版本中,没有方法可以定义一个访问器属性,也不能让属性变成只读或是不可枚举。而这些特性都能够利用 Object.defineProperty()
方法来实现,并且你还可以利用Object.getOwnPropertyDescriptor()
方法来检索这些特性。代理允许你使用 defineProperty
与 getOwnPropertyDescriptor
陷阱函数,来分别拦截对于Object.defineProperty()
与 Object.getOwnPropertyDescriptor()
的调用。 defineProperty
陷阱函数接受下列三个参数:
- trapTarget :需要被定义属性的对象(即代理的目标对象) ;
- key :属性的键(字符串类型或符号类型) ;
- descriptor :为该属性准备的描述符对象。
defineProperty
陷阱函数要求你在操作成功时返回 true
,否则返回 false
。getOwnPropertyDescriptor
陷阱函数则只接受 trapTarget
与 key
这两个参数,并会返回对应的描述符。 Reflect.defineProperty()
与 Reflect.getOwnPropertyDescriptor()
方法作为上述陷阱函数的对应方法,接受与之相同的参数。
defineProperty
陷阱函数要求你返回一个布尔值用于表示操作是否已成功。当它返回 true
时, Object.defineProperty()
会正常执行;而如果它返回了 false
,则Object.defineProperty()
会抛出错误。 你可以使用该功能来限制哪些属性可以被Object.defineProperty()
方法定义。
etOwnPropertyDescriptor
陷阱函数有一个微小差异,要求返回值必须是 null
、
undefined
,或者是一个对象。如果返回值是一个对象,则只允许该对象拥有 enumerable
、 configurable
、 value
、 writable
、 get
或 set
这些自有属性
2.5 ownKeys陷阱函数
ownKeys
代理陷阱拦截了内部方法 [[OwnPropertyKeys]]
,并允许你返回一个数组用于重写该行为。返回的这个数组会被用于四个方法: Object.keys()
方法、Object.getOwnPropertyNames()
方法、Object.getOwnPropertySymbols()
方法与Object.assign()
方法,其中 Object.assign()
方法会使用该数组来决定哪些属性会被复制。
ownKeys
陷阱函数接受单个参数,即目标对象,同时必须返回一个数组或者一个类数组对象。你可以使用 ownKeys
陷阱函数去过滤特定的属性,以避免这些属性被Object.keys()
方法、Object.getOwnPropertyNames()
方法、Object.getOwnPropertySymbols()
方法或 Object.assign()
方法使用。
2.6 apply与construct陷阱函数
只有 apply
与 construct
要求代理目标对象必须是一个函数。函数拥有两个内部方法:[[Call]]
与 [[Construct]]
,前者会在函数被直接调用时执行,而后者会在函数被使用 new
运算符调用时执行。 apply
与 construct
陷阱函数对应着这两个内部方法,并允许你对其进行重写。apply
陷阱函数会接收到下列三个参数( Reflect.apply()
也会接收这些参数) :
- trapTarget :被执行的函数(即代理的目标对象) ;
- thisArg :调用过程中函数内部的 this 值;
- argumentsList :被传递给函数的参数数组。
当使用 new
去执行函数时, construct
陷阱函数会被调用并接收到下列两个参数:
- trapTarget :被执行的函数(即代理的目标对象) ;
- argumentsList :被传递给函数的参数数组。
Reflect.construct()
方法同样会接收到这两个参数,还会收到可选的第三参数 newTarget
,如果提供了此参数,则它就指定了函数内部的 new.target
值。
使用apply和construct陷阱函数有这样一些应用场景:
验证函数的参数
假如需要保证所有参数都是某个特定类型,可使用 apply
陷阱函数进行验证:
//apply和construct陷阱函数
let sum = function (arr=[]) {
return arr.reduce((previous,current)=>previous+current);
}
let proxy = new Proxy(sum,{
apply(trapTarget,thisArg,argumentList){
argumentList[0].forEach((item)=>{
if(typeof item != 'number'){
throw new Error('不是数字类型');
}
})
return Reflect.apply(trapTarget,thisArg,argumentList);
},
construct(trapTarget,argumentList){
throw new Error('不能使用new');
}
});
console.log(proxy([1,2,3,4])); // 10
console.log(proxy([1, "2", 3, 4]));//Uncaught Error: 不是数字类型Uncaught Error: 不是数字类型
let result = new proxy(); //Uncaught Error: 不能使用new
3. 可被撤销的代理
在被创建之后,代理通常就不能再从目标对象上被解绑。有的情况下你可能想撤销一个代理以便让它不能再被使用。当你想通过公共接口向外提供一个安全的对象,并且要求要随时都能切断对某些功能的访问,这种情况下可被撤销的代理就会非常有用。
你可以使用Proxy.revocable()
方法来创建一个可被撤销的代理,该方法接受的参数与Proxy
构造器的相同:一个目标对象、一个代理处理器,而返回值是包含下列属性的一个对象:
- proxy :可被撤销的代理对象;
- revoke :用于撤销代理的函数;
当 revoke()
函数被调用后,就不能再对该 proxy
对象进行更多操作。例如:
let target = {
name: "target"
};
let { proxy, revoke } = Proxy.revocable(target, {});
console.log(proxy.name); // "target"
revoke();
// 抛出错误
console.log(proxy.name);
这个例子创建了一个可被撤销的代理,它对Proxy.revocable()
方法返回的对象进行了解构
赋值,把同名属性的值赋给了 proxy
与 revoke
变量。此时 proxy
对象和代理的目标对象一样,于是 proxy.name
属性的值就是 "target"
,因为它直接传递了
target.name
的值。然而一旦revoke()
函数被调用, 代理对象就和所代理的目标对象就解除“绑定”关系,之后试图访问 proxy.name
会抛出错误。
4. 将代理对象作为原型使用
代理对象可以被作为原型使用,在把代理对象作为原型时,仅当操作的默认行为会按惯例追踪原型时,代理陷阱才会被调用。因此,将代理对象作为原型时,常见的应用场景有:
1.在原型上使用get陷阱函数
当内部方法 [[Get]]
被调用以读取属性时,该操作首先会查找对象的自有属性;如果指定名称的属性没有找到,则会继续在对象的原型上进行属性查找;这个流程会一直持续到没有原型可供查找为止。得益于这个流程,若你设置了一个 get
代理陷阱,则只有在对象不存在指定名称的自有属性时,该陷阱函数才会在对象的原型上被调用。当所访问的属性无法保证存在时,你可以使用 get
陷阱函数来阻止预期外的行为。例如,创建了一个对象,当你尝试去访问一个不存在的属性时,它会抛出错误:
//原型上使用get陷阱函数
let target = {};
let newTarget = Object.create(new Proxy(
target,{
get(trapTarget,key,receiver){
throw new Error('不存在该属性');
}
}
));
newTarget.name = 'hello world';
console.log(newTarget.name); //hello world
console.log(newTarget.age); //Uncaught Error: 不存在该属性
由于 name
属性存在,所以不会调用 get
陷阱函数,而 age
属性在对象上并不存在,所以,会从原型上去找该属性,因此,会触发 get
陷阱函数,从而抛出错误。
2.在原型上使用 set
陷阱函数
内部方法 [[Set]]
同样会查找对象的自有属性,并在必要时继续对该对象的原型进行查找。当你对一个对象属性进行赋值时,如果指定名称的自有属性存在,值就会被赋在该属性上;而若该自有属性不存在,则会继续检查对象的原型,但默认情况下它会在对象实例(而非原型) 上创建一个新的属性用于赋值,无论同名属性是否存在于原型上。
3.在原型上使用has陷阱函数
has
陷阱函数会拦截对象上 in
运算符的使用。 in
运算符首先查找对象上指定名称的自有属性;如果不存在同名自有属性,则会继续查找对象的原型;如果原型上也不存在同名自有属性,那么就会沿着原型链一直查找下去,直到找到该属性、或者没有更
多原型可供查找时为止。has
陷阱函数只在原型链查找触及原型对象的时候才会被调用。
当使用代理作为原型时,这只会在指定名称的自有属性不存在时,才会触发 has
陷阱函数。
5.将代理作为类的原型
代理对象不能直接作为类的原型,因为类的 prototype
属性是不可写入的。但是,可以使用继承来实现:
//代理对象作为类的原型
function NoSuchProperty() {
// empty
}
NoSuchProperty.prototype = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
class Square extends NoSuchProperty {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
}
let shape = new Square(2, 6);
let area1 = shape.length * shape.width;
console.log(area1); // 12
// 由于 "wdth" 不存在而抛出了错误
let area2 = shape.length * shape.wdth;
Square
类继承了 NoSuchProperty
类,因此该代理就被加入了 Square
类的原型链。随后shape
对象被创建为 Square
类的一个实例,让它拥有两个属性: length
与 width
。由于 get
陷阱函数永远不会被调用,因此能够成功读取这两个属性的值。只有访问 shape
上不存在的属性时(例如这里的 shape.wdth
拼写错误) ,才触发了 get
陷阱函数并导致错误被抛出。
6. 总结
在 ES6 之前,特定对象(例如数组) 会显示出一些非常规的、无法被开发者复制的行为,而代理的出现改变了这种情况。代理允许你为一些 JS 底层操作自行定义非常规行为,因此你就可以通过代理陷阱来复制 JS 内置对象的所有行为。在各种不同操作发生时(例如对于
in
运算符的使用) ,这些代理陷阱会在后台被调用。反射接口也是在 ES6 中引入的,允许开发者为每个代理陷阱实现默认的行为。每个代理陷阱在
Reflect
对象(ES6 的另一个新特性) 上都有一个同名的对应方法。将代理陷阱与反射接口方法结合使用,就可以在特定条件下让一些操作有不同的表现,有别于默认的内置行为。可被撤销的代理是一种特殊的代理,可以使用
revoke()
函数去有效禁用。revoke()
函数终结了代理的所有功能,因此在它被调用之后,所有与代理属性交互的意图都会导致抛出错误。尽管直接使用代理是最有力的使用方式,但你也可以把代理用作另一个对象的原型。但只有很少的代理陷阱能在作为原型的代理上被有效使用,包括
get
、set
与has
这几个,这让这方面的用例变得十分有限