前言
今天我们要来讲一下Javascript中一个很有趣的类 -- Proxy类,类名翻译成中文是代理的意思。在真正开始之前还必须说明一点,本篇文章并非原创,主要借鉴了先行者贾顺名的文章,在此非常感谢他(她)的分享,原文地址是https://segmentfault.com/a/1190000015009255
什么叫代理?
我就举个生活中的小例子吧,对于很多出门在外的小伙伴来说,肯定有过一个非常刺激的经历---租房子。咱们在租房子的时候很少能直接接触到房东,接触到的都是房产中介,房产中介就是房东的代理,帮忙处理租房的相关事宜。
Javascript的代理(Proxy)
Proxy是ES6中提供的新的API,用来自定义对象的各项基本操作,说白了就是在我们访问对象前添加了一层拦截器(可能过滤数据,数据验证等过程)。有些小伙伴就会有疑问了,比如说简单的赋值操作加个拦截器有啥意义吗?大家想一个简单的问题,如果一个人的年龄被设置为500合理吗?代理的设置就可以帮我们规避这种不合理的数据更改。
语法
let p = new Proxy(target, handler);
创建一个Proxy的实例需要传入两个参数:
1、target 要被代理的对象,可以是一个object或者function
2、handlers对代理对象p的各种操作的自定义处理函数
注意:在第二个参数为空对象的情况下,基本可以理解为是对第一个参数做的一次浅拷贝(Proxy必须是浅拷贝,如果是深拷贝则会失去了代理的意义)
Traps(各种行为的代理)
其实Javascript早就定义了两种trap:getter 和 setter(存取器属性),具体代码如下:
let obj = {
_age: 18,
get age () {
return `I'm ${this._age} years old`
},
set age (val) {
this._age = Number(val)
}
}
console.log(obj.age) // I'm 18 years old
obj.age = 19
console.log(obj.age) // I'm 19 years old
以上这方式存在两个缺点:
1、针对每一个要代理的属性都要编写对应的getter、setter。
2、必须还要存在一个存储真实值的key(上面就是_age),试想如果没有_age的话,直接在age的getter里面返回this.age,这个动作又会触发新一轮的getter,导致死循环的产生。
那么Proxy如果来解决着两个问题呢?
1、首先通过创建get、set两个trap来统一管理所有的操作;
2、trap内部操作的是target对象,而不是proxy对象,无需额外的用一个key来存储真实的值
let target = {
age: 18,
name: 'Niko Bellic'
}
let handlers = {
get (target, property) {
return `${property}: ${target[property]}`
},
set (target, property, value) {
target[property] = value
}
}
let proxy = new Proxy(target, handlers)
proxy.age = 19
console.log(target.age, proxy.age) // 19, age : 19
console.log(target.name, proxy.name) // Niko Bellic, name: Niko Bellic
除了get和set,还有其他trap:
getPrototypeOf:当读取代理对象的原型时,该方法就会被调用;方法内部this指向handler(处理器对象);方法的返回值必须是一个对象或者 null。
在 JavaScript 中,有下面这五种操作(方法/属性/运算符)可以触发 JS 引擎读取一个对象的原型,也就是可以触发 getPrototypeOf() 代理方法的运行:
1、Object.getPrototypeOf()
2、Reflect.getPrototypeOf()
3、__proto__
4、Object.prototype.isPrototypeOf()
5、instanceOf
var obj = {};var p = new Proxy(obj, {
getPrototypeOf(target) {
return Array.prototype;
}
});
console.log(
Object.getPrototypeOf(p) === Array.prototype, // true
Reflect.getPrototypeOf(p) === Array.prototype, // true
p.__proto__ === Array.prototype, // true
Array.prototype.isPrototypeOf(p), // true
p instanceof Array // true
);
如果遇到了下面两种情况,JS 引擎会抛出TypeError 异常:
1、getPrototypeOf() 方法返回的不是对象也不是 null。
2、目标对象是不可扩展的,且 getPrototypeOf() 方法返回的原型不是目标对象本身的原型
var obj = {};var p = new Proxy(obj, {
getPrototypeOf(target) {
return "foo";
}
});
Object.getPrototypeOf(p); // TypeError: "foo" is not an object or null
var obj = Object.preventExtensions({});
var p = new Proxy(obj, {
getPrototypeOf(target) {
return {};
}
});
Object.getPrototypeOf(p); // TypeError: expected same prototype value
defineProperty:用于拦截对对象的Object.defineProperty()操作; this绑定在 handler 对象上; 方法必须以一个 Boolean 返回,表示定义该属性的操作成功与否。
该方法会拦截目标对象的以下操作 :
1、Object.defineProperty()
2、Reflect.defineProperty()
3、proxy.property = 'value'
如果违背了以下的不变量,proxy会抛出 TypeError:
1、如果目标对象不可扩展, 将不能添加属性。
2、不能添加或者修改一个属性为不可配置的,如果它不作为一个目标对象的不可配置的属性存在的话。
3、如果目标对象存在一个对应的可配置属性,这个属性可能不会是不可配置的。
4、如果一个属性在目标对象中存在对应的属性,那么 Object.defineProperty(target, prop, descriptor) 将不会抛出异常。
5、在严格模式下, false 作为 handler.defineProperty 方法的返回值的话将会抛出 TypeError 异常.
除此之外,当调用 Object.defineProperty() 或者 Reflect.defineProperty(),传递给 defineProperty 的 descriptor 有一个限制 - 只有以下属性才有用,非标准的属性将会被无视 : enumerable、configurable、writable、value、get、set
var p = new Proxy({}, {
defineProperty: function(target, prop, descriptor) {
console.log('called: ' + prop); return true;
}
});
var desc = { configurable: true, enumerable: true, value: 10 };
Object.defineProperty(p, 'a', desc); // "called: a"
has:针对 in 操作符的代理方法; this绑定在 handler 对象上; 方法返回一个 boolean 属性的值。
该方法会拦截目标对象的以下操作 :
1、属性查询: foo in proxy
2、继承属性查询: foo in Object.create(proxy)
3、with检查: with(proxy) { (foo); }
4、Reflect.has()
如果违反了下面这些规则, proxy 将会抛出 TypeError:
1、如果目标对象的某一属性本身不可被配置,则该属性不能够被代理隐藏.
2、如果目标对象为不可扩展对象,则该对象的属性不能够被代理隐藏
var p = new Proxy({}, {
has: function(target, prop) {
console.log('called: ' + prop); return true;
}
});
console.log('a' in p);
deleteProperty:用于拦截对对象属性的 delete 操作; this绑定在 handler 对象上; 方法必须返回一个 Boolean 类型的值,表示了该属性是否被成功删除。
该方法会拦截以下操作:
1、删除属性: delete proxy[foo] 和 delete proxy.foo
2、Reflect.deleteProperty()
如果违背了以下不变量,proxy 将会抛出一个 TypeError:
1、如果目标对象的属性是不可配置的,那么该属性不能被删除。
var p = new Proxy({}, {
deleteProperty: function(target, prop) {
console.log('called: ' + prop);
return true;
}
});
delete p.a;
ownKeys: 用于拦截获取自有属性操作; this绑定在 handler 对象上; 方法必须返回一个可枚举对象。
该拦截器可以拦截以下操作:
1、Object.getOwnPropertyNames()
2、Object.getOwnPropertySymbols()
3、Object.keys()
4、Reflect.ownKeys()
如果违反了下面的约定,proxy将抛出错误 TypeError:
1、ownKeys的结果必须是一个数组
2、数组的元素类型要么是一个 String ,要么是一个Symbol
3、结果列表必须包含目标对象的所有不可配置(non-configurable )、自有(own)属性的key
4、如果目标对象不可扩展,那么结果列表必须包含目标对象的所有自有(own)属性的key,不能有其它值
var p = new Proxy({}, {
ownKeys: function(target) {
console.log('called');
return ['a', 'b', 'c'];
}
});
console.log(Object.getOwnPropertyNames(p)); // "called" // [ 'a', 'b', 'c' ]
apply: 用于拦截函数的调用;
接受三个参数:target -- 目标对象(函数);thisArg -- 被调用时的上下文对象;argumentsList -- 被调用时的参数数组
返回值:可返回任何值
该方法会拦截目标对象的以下操作:
1、proxy(...args)
2、Function.prototype.apply() 和 Function.prototype.call()
3、Reflect.apply()
如果违反了以下约束,代理将抛出一个TypeError:
1、target必须是可被调用的。也就是说,它必须是一个函数对象。
var p = new Proxy(function() {}, {
apply: function(target, thisArg, argumentsList) {
console.log('called: ' + argumentsList.join(', '));
return argumentsList[0] + argumentsList[1] + argumentsList[2];
}
});
console.log(p(1, 2, 3)); // "called: 1, 2, 3" // 6
construct: 用于拦截 new 操作符. 为了使new操作符在生成的Proxy对象上生效,用于初始化代理的目标对象自身必须具有[[Construct]]内部方法(即 new target 必须是有效的)。
接受三个参数:target -- 目标对象;argumentsList -- constructor的参数列表;newTarget -- 最初被调用的构造函数
返回值:必须返回一个对象
该方法会拦截以下操作:
1、new proxy(...args)
2、Reflect.construct()
至此,我们已经把比较常用的trap学习了一下,如果想要学习更详细的内容,请前往https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler查看