Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,
可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
var proxy = new Proxy(target, handler);
Proxy 对象的所有用法,都是上面这种形式,不同的只是handler
参数的写法。
其中,new Proxy()
表示生成一个Proxy
实例,target
参数表示所要拦截的目标对象,handler
参数也是一个对象,用来定制拦截行为。
var proxy = new Proxy({}, { get: function(target, property) { return 35; } }); proxy.time // 35 proxy.name // 35 proxy.title // 35
第一个参数是所要代理的目标对象(上例是一个空对象),即如果没有Proxy
的介入,操作原来要访问的就是这个对象;
第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。
一个技巧是将 Proxy 对象,设置到object.proxy
属性,从而可以在object
对象上调用。
利用 Proxy,可以将读取属性的操作(get
),转变为执行某个函数,从而实现属性的链式操作。
var pipe = (function () { return function (value) { var funcStack = []; var oproxy = new Proxy({} , { get : function (pipeObject, fnName) { if (fnName === 'get') { return funcStack.reduce(function (val, fn) { return fn(val); },value); } funcStack.push(window[fnName]); return oproxy; } }); return oproxy; } }()); var double = n => n * 2; var pow = n => n * n; var reverseInt = n => n.toString().split("").reverse().join("") | 0; pipe(3).double.pow.reverseInt.get; // 63
var object = { proxy: new Proxy(target, handler) };
Proxy 实例也可以作为其他对象的原型对象。
var proxy = new Proxy({}, { get: function(target, property) { return 35; } }); let obj = Object.create(proxy); obj.time // 35
对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。
(1)get(target, propKey, receiver)
拦截对象属性的读取,比如proxy.foo
和proxy['foo']
。
最后一个参数receiver
是一个对象,可选,参见下面Reflect.get
的部分。
(2)set(target, propKey, value, receiver)
拦截对象属性的设置,比如proxy.foo = v
或proxy['foo'] = v
,返回一个布尔值。
(3)has(target, propKey)
拦截propKey in proxy
的操作,返回一个布尔值。
(4)deleteProperty(target, propKey)
拦截delete proxy[propKey]
的操作,返回一个布尔值。
(5)ownKeys(target)
拦截Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。
(6)getOwnPropertyDescriptor(target, propKey)
拦截Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。
(7)defineProperty(target, propKey, propDesc)
拦截Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一个布尔值。
(8)preventExtensions(target)
拦截Object.preventExtensions(proxy)
,返回一个布尔值。
(9)getPrototypeOf(target)
拦截Object.getPrototypeOf(proxy)
,返回一个对象。
(10)isExtensible(target)
拦截Object.isExtensible(proxy)
,返回一个布尔值。
(11)setPrototypeOf(target, proto)
拦截Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。
如果目标对象是函数,那么还有两种额外操作可以拦截。
(12)apply(target, object, args)
拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。
(13)construct(target, args)
拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)
。
set()
set
方法用来拦截某个属性的赋值操作。
假定Person
对象有一个age
属性,该属性应该是一个不大于200的整数,那么可以使用Proxy
保证age
的属性值符合要求。
let validator = { set: function(obj, prop, value) { if (prop === 'age') { if (!Number.isInteger(value)) { throw new TypeError('The age is not an integer'); } if (value > 200) { throw new RangeError('The age seems invalid'); } } // 对于age以外的属性,直接保存 obj[prop] = value; } }; let person = new Proxy({}, validator); person.age = 100; person.age // 100 person.age = 'young' // 报错 person.age = 300 // 报错
上面代码中,由于设置了存值函数set
,任何不符合要求的age
属性赋值,都会抛出一个错误,这是数据验证的一种实现方法。
利用set
方法,还可以数据绑定,即每当对象发生变化时,会自动更新 DOM。
有时,我们会在对象上面设置内部属性,属性名的第一个字符使用下划线开头,表示这些属性不应该被外部使用。
结合get
和set
方法,就可以做到防止这些内部属性被外部读写。
var handler = { get (target, key) { invariant(key, 'get'); return target[key]; }, set (target, key, value) { invariant(key, 'set'); target[key] = value; return true; } }; function invariant (key, action) { if (key[0] === '_') { throw new Error(`Invalid attempt to ${action} private "${key}" property`); } } var target = {}; var proxy = new Proxy(target, handler); proxy._prop // Error: Invalid attempt to get private "_prop" property proxy._prop = 'c' // Error: Invalid attempt to set private "_prop" property