本文共 27535 字,大约阅读时间需要 91 分钟。
本文简单阐述一点元编程的知识,然后较为详细的给出 Proxy
的有关用法(起码比 MDN 详细,补充了各种错误情况的具体示例,且比上面的机翻准确),再用一些例子讲述 Proxy
适合在什么场景下使用
首先,在编程中有以下两个级别:
元( meta ) 这个词缀在这里的意思是:关于某事自身的某事,因此元编程( metaprogramming )这个词意味着 关于编程的编程,可以在两种同的语言进行元编程,编写元程序的语言称之为元语言。被操纵的程序的语言称之为“目标语言”,在下面这段代码中 JavaScript
为元语言,而 Java
为目标语言:
const str = 'Hello' + '!'.repeat(3);console.log('System.out.println("'+str+'")');复制代码
一门编程语言同时也是自身的元语言的能力称之为反射(Reflection),用于发现和调整你的应用程序结构和语义。
元编程有三种形式:
Object.keys(obj)
等,ES6 中新出了个 Reflect
对许多获取内部状态的接口进行了整合与统一delete
或 property descriptors
等Proxy
ES6 中也新增了一个全局对象 ,其中的大多数方法都早已以其他形式存在,这次将其接口统一的目的在于:
Object.prototype
上,有的挂在 Function.prototype
上,有的是一个操作符(如 delete
/ in
等 )obj.hasOwnProperty
时对象上可能没有这个方法(比如这个对象是通过 Object.create(null)
创建的),因此这个时候使用 Object.prototype.hasOwnProperty.call
才是最安全的,但是这样过于复杂call
和 apply
也有上述问题Object.defineProperty
,如果成功返回一个对象,否则抛出一个 TypeError
,因此不得不使用 try...catch
来捕获定义属性时发生的任何错误。而 Reflect.defineProperty
返回一个布尔值表示的成功状态,所以在这里可以只用 if...else
这里可以参考来了解 Reflect
做出了哪些优化
终于来到了我们的主角,首先我们来看看 Proxy
的构造函数:
Proxy(target, handler)复制代码
target
: 用 Proxy
包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。handler
: 处理器对象( proxy's handler
)用来自定义代理对象的各种可代理操作。其中包括众多 traps
在进一步探究 Proxy
有什么之前,先回顾一下如何通保护对象:
'use strict' const obj = Object.preventExtensions({}); console.log(Object.isExtensible(obj)); // false obj.foo = 123; // Cannot add property foo, object is not extensible Object.setPrototypeOf(obj, null) // #
value
不能被赋值运算符改变writable
改为 false
)使用代理以后,很容易违反上述约束(因为上述约束作用在被 Proxy
代理的对象中, Proxy
对象并不受其约束),因此在调用/返回的时候 Proxy
会帮我们检查或者强制做类型转换等(比如预期是 Boolean
时会把 truish
和 falsish
强制转换成 Boolean
等)。后文中的约束部分有进一步的解释与示例。
这里有一份关于
然后来看看 Proxy
的 handler
提供了哪些东西供我们使用
handler.get()
拦截对象的读取属性操作。
get: function(target, property, receiver) {}复制代码
target
:目标对象。property
:被获取的属性名。receiver
:最初被调用的对象。通常是 proxy
本身,但 handler
的 get
方法也有可能在原型链上或以其他方式被间接地调用(因此不一定是 proxy
本身)。 target
和 property
都很好理解,但是 receiver
需要额外注意,下面使用一个例子帮助理解:var obj = { myObj: 1 }; obj.__proto__ = new Proxy({ test: 123 },{ get:function(target, property, receiver) { console.log(target, property, receiver); return 1; } }); console.log(obj.test); // {test: 123}, "test" ,{myObj: 1} // 可以看见 receiver 是最初被调用的对象复制代码
该方法会拦截目标对象的以下操作:
proxy[foo]
和 proxy.bar
Object.create(proxy)[foo]
约束(违反约束会抛出 Type Error
):
const obj = {}; // 不可写以及不可配置 Object.defineProperty(obj, "a", { configurable: false, enumerable: true, value: 10, writable: false }); const p = new Proxy(obj, { get: function(target, prop) { return 20; } }); console.log(p.a); // 'get' on proxy: property 'a' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '10' but got '20')复制代码
get
方法,则其返回值必须为 undefined
const obj = { a: 10 }; // 不可配置 且 没有定义 get Object.defineProperty(obj, "a", { configurable: false, get: undefined, }); const p = new Proxy(obj, { get: function(target, prop) { return 20; } }); console.log(p.a) // 'get' on proxy: property 'a' is a non-configurable accessor property on the proxy target and does not have a getter function, but the trap did not return 'undefined' (got '20')复制代码
handler.set()
拦截设置属性值的操作
set: function(target, property, value, receiver) {}复制代码
target
:目标对象。property
:被设置的属性名。value
:被设置的新值receiver
:最初被调用的对象。同上文 get
中的 receiver
返回值:
set
方法应该返回一个 Boolean
:
true
代表此次设置属性成功了false
且设置属性操作发生在严格模式下,那么会抛出一个 TypeError
注意: Proyx
中大多数方法要求返回 Boolean
时本质上是会帮你把返回值转换成 Boolean
,因此可以在里面随便返回啥,到了外面拿到的都是 Boolean
;这也是为什么报错的时候用词为: truish
和 falsish
该方法会拦截目标对象的以下操作:
proxy[foo] = bar
和 proxy.foo = bar
Object.create(proxy)[foo] = bar
约束:
const obj = {}; // 不可写以及不可配置 Object.defineProperty(obj, "a", { configurable: false, enumerable: true, value: 10, writable: false }); const p = new Proxy(obj, { set: function(target, prop, value, receiver) { console.log("called: " + prop + " = " + value); return true; } }); p.a = 20; // trap returned truish for property 'a' which exists in the proxy target as a non-configurable and non-writable data property with a different value // 注意这里我们并没有真正改变 'a' 的值,该错误由 return true 引起 复制代码
set
方法,则不能设置它的值。const obj = {}; // 不可写 且 没有定义 set Object.defineProperty(obj, "a", { configurable: false, set: undefined }); const p = new Proxy(obj, { set: function(target, prop, value, receiver) { console.log("called: " + prop + " = " + value); return true; } }); p.a = 20; // trap returned truish for property 'a' which exists in the proxy target as a non-configurable and non-writable accessor property without a setter // 注意这里我们并没有真正改变 'a' 的值,该错误由 return true 引起复制代码
set
方法返回 false
,则会抛出一个 TypeError
异常。'use strict' const obj = {}; const p = new Proxy(obj, { set: function(target, prop, value, receiver) { console.log("called: " + prop + " = " + value); return false; } }); p.a = 20; // trap returned falsish for property 'a'复制代码
handler.apply()
拦截函数的调用
apply: function(target, thisArg, argumentsList) {}复制代码
target
:目标对象(函数)。thisArg
:被调用时的上下文对象。argumentsList
:被调用时的参数数组。该方法会拦截目标对象的以下操作:
proxy(...args)
Function.prototype.apply()
和 Function.prototype.call()
Reflect.apply()
约束:
target
本身必须是可被调用的。也就是说,它必须是一个函数对象。handler.construct()
用于拦截 new
操作符
construct: function(target, argumentsList, newTarget) {}复制代码
target
:目标对象。argumentsList
:constructor
的参数列表。newTarget
:最初被调用的构造函数。该方法会拦截目标对象的以下操作:
new proxy(...args)
注意:
new
操作符在生成的 Proxy
对象上生效,用于初始化代理的目标对象自身必须具有 [[Construct]]
内部方法,即 new target
必须是有效的。比如说 target
是一个 function
约束:
construct
方法必须返回一个对象,否则将会抛出错误 TypeError
const p = new Proxy(function () {}, { construct: function (target, argumentsList, newTarget) { return 1; } }); new p(); // 'construct' on proxy: trap returned non-object ('1')复制代码
handler.defineProperty()
用于拦截 Object.defineProperty()
操作
defineProperty: function(target, property, descriptor) {}复制代码
target
:目标对象。property
:待检索其描述的属性名。descriptor
:待定义或修改的属性的描述符。注意:
defineProperty
方法也必须返回一个布尔值,表示定义该属性的操作是否成功。(严格模式下返回 false
会抛 TypeError
)defineProperty
方法只能接受如下标准属性,其余的将直接无法获取(示例代码如下): enumerable
configurable
writable
value
get
set
var p = new Proxy({}, { defineProperty(target, prop, descriptor) { console.log(descriptor); return Reflect.defineProperty(target, prop, descriptor); }});Object.defineProperty(p, 'name', { value: 'proxy', type: 'custom'}); // { value: 'proxy' }复制代码
该方法会拦截目标对象的以下操作 :
Object.defineProperty()
Reflect.defineProperty()
约束:
const obj = { a: 10 }; Object.preventExtensions(obj); const p = new Proxy(obj, { defineProperty(target, prop, descriptor) { return true; } }); Object.defineProperty(p, 'name', { value: 'proxy' }); // 'defineProperty' on proxy: trap returned truish for adding property 'name' to the non-extensible proxy target复制代码
const obj = { a: 10 }; const p = new Proxy(obj, { defineProperty(target, prop, descriptor) { return true; } }); Object.defineProperty(p, 'a', { value: 'proxy', configurable: false, }); // trap returned truish for defining non-configurable property 'a' which is either non-existant or configurable in the proxy target复制代码
Object.defineProperty(target, prop, descriptor)
将不会抛出异常。false
作为 handler.defineProperty
方法的返回值的话将会抛出 TypeError
异常.const obj = { a: 10 }; const p = new Proxy(obj, { defineProperty(target, prop, descriptor) { return false } }); Object.defineProperty(p, 'a', { value: 'proxy', }); // 'defineProperty' on proxy: trap returned falsish for property 'a'复制代码
handler.deleteProperty()
用于拦截对对象属性的 delete
操作
deleteProperty: function(target, property) {}复制代码
target
: 目标对象。property
: 待删除的属性名。返回值: 必须返回一个 Boolean
类型的值,表示了该属性是否被成功删除。(这次返回 false
不会报错了)
该方法会拦截以下操作:
delete proxy[foo]
和 delete proxy.foo
Reflect.deleteProperty()
约束:
TypeError
const obj = {}; Object.defineProperty(obj, 'a', { value: 'proxy', }); const p = new Proxy(obj, { deleteProperty: function (target, prop) { return true; } }); delete p.a; // trap returned truish for property 'a' which is non-configurable in the proxy target复制代码
handler.getOwnPropertyDescriptor()
用于拦截对对象属性的 getOwnPropertyDescriptor()
方法
getOwnPropertyDescriptor: function(target, prop) {}复制代码
target
:目标对象。prop
:属性名。返回值: 必须返回一个 object
或 undefined。
该方法会拦截以下操作:
Object.getOwnPropertyDescriptor()
Reflect.getOwnPropertyDescriptor()
约束:
getOwnPropertyDescriptor
必须返回一个 object
或 undefined
const obj = { a: 10 }; const p = new Proxy(obj, { getOwnPropertyDescriptor: function(target, prop) { return ''; } }); Object.getOwnPropertyDescriptor(p, 'a'); // trap returned neither object nor undefined for property 'a'复制代码
const obj = { a: 10 }; Object.defineProperty(obj, 'b', { value: 20 }); const p = new Proxy(obj, { getOwnPropertyDescriptor: function(target, prop) { return undefined; } }); Object.getOwnPropertyDescriptor(p, 'b'); // trap returned undefined for property 'b' which is non-configurable in the proxy target复制代码
const obj = { a: 10 }; Object.preventExtensions(obj); const p = new Proxy(obj, { getOwnPropertyDescriptor: function(target, prop) { return undefined; } }); Object.getOwnPropertyDescriptor(p, 'a'); // trap returned undefined for property 'a' which exists in the non-extensible proxy target复制代码
const obj = { a: 10 }; Object.preventExtensions(obj); const p = new Proxy(obj, { getOwnPropertyDescriptor: function(target, prop) { return Object.getOwnPropertyDescriptor(obj, prop) || {}; } }); console.log(Object.getOwnPropertyDescriptor(p, 'a')) Object.getOwnPropertyDescriptor(p, 'b'); // trap returned descriptor for property 'b' that is incompatible with the existing property in the proxy target复制代码
const obj = { a: 10 }; const p = new Proxy(obj, { getOwnPropertyDescriptor: function(target, prop) { return { configurable: false }; } }); Object.getOwnPropertyDescriptor(p, 'a'); // trap reported non-configurability for property 'a' which is either non-existant or configurable in the proxy target复制代码
Object.getOwnPropertyDescriptor(target)
的结果可以使用 Object.defineProperty
应用于目标对象,也不会抛出异常。handler.getPrototypeOf()
用于拦截读取代理对象的原型的方法
getPrototypeOf(target) {}复制代码
target
: 被代理的目标对象。返回值: 必须返回一个对象值或者返回 null
,不能返回其它类型的原始值。
该方法会拦截以下操作:
Object.getPrototypeOf()
Reflect.getPrototypeOf()
__proto__
Object.prototype.isPrototypeOf()
instanceof
举例如下:
const obj = {};const 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);复制代码
约束:
getPrototypeOf()
方法返回的不是对象也不是 null
。const obj = {}; const p = new Proxy(obj, { getPrototypeOf(target) { return "foo"; } }); Object.getPrototypeOf(p); // TypeError: trap returned neither object nor null复制代码
getPrototypeOf()
方法返回的原型不是目标对象本身的原型。const obj = {}; Object.preventExtensions(obj); const p = new Proxy(obj, { getPrototypeOf(target) { return {}; } }); Object.getPrototypeOf(p); // proxy target is non-extensible but the trap did not return its actual prototype复制代码
handler.has()
主要用于拦截 in
和 with
操作
has: function(target, prop) {}复制代码
target
: 目标对象prop
: 需要检查是否存在的属性返回值: Boolean
(返回一个可以转化为 Boolean
的也没什么问题)
该方法会拦截以下操作:
foo in proxy
foo in Object.create(proxy)
with
检查: with(proxy) { (foo); }
Reflect.has()
约束:
const obj = {}; Object.defineProperty(obj, 'a', { value: 10 }) const p = new Proxy(obj, { has: function (target, prop) { return false; } }); 'a' in p; // trap returned falsish for property 'a' which exists in the proxy target as non-configurable复制代码
const obj = { a: 10 }; Object.preventExtensions(obj); const p = new Proxy(obj, { has: function(target, prop) { return false; } }); 'a' in p; // trap returned falsish for property 'a' but the proxy target is not extensible复制代码
handler.isExtensible()
用于拦截对对象的 Object.isExtensible()
操作
isExtensible: function(target) {}复制代码
target
: 目标对象。该方法会拦截目标对象的以下操作:
Object.isExtensible()
Reflect.isExtensible()
返回值: Boolean
值或可转换成 Boolean
的值。
约束:
Object.isExtensible(proxy)
必须同 Object.isExtensible(target)
返回相同值。 Object.isExtensible(target)
返回 ture
,则 Object.isExtensible(proxy)
必须返回 true
或者为 true
的值Object.isExtensible(target)
返回 false
,则 Object.isExtensible(proxy)
必须返回 false
或者为 false
的值const p = new Proxy({}, { isExtensible: function(target) { return false; } }); Object.isExtensible(p); // trap result does not reflect extensibility of proxy target (which is 'true')复制代码
handler.ownKeys()
用于拦截 Reflect.ownKeys()
ownKeys: function(target) {}复制代码
target
: 目标对象返回值: 一个可枚举对象
该方法会拦截目标对象的以下操作(同时有一些额外的限制):
Object.getOwnPropertyNames()
String
的,Symbol
类型的将被忽视Object.keys()
for...in
循环遍历该对象时返回的顺序一致。 可枚举的属性可以通过 for...in
循环进行遍历(除非该属性名是一个Symbol)String
数组Object.getOwnPropertySymbols()
Symbol
, String
类型的将被忽视Reflect.ownKeys()
const mySymbel = Symbol('juanni'); const obj = { a: 10 }; Object.defineProperty(obj, 'b', { configurable: false, enumerable: false, value: 10 } ); Object.defineProperty(obj, mySymbel, { configurable: true, enumerable: true, value: 10 } ); const p = new Proxy(obj, { ownKeys: function (target) { return ['a', 'b', mySymbel]; } }); console.log(Object.getOwnPropertySymbols(p)); // [Symbol(juanni)] console.log(Object.getOwnPropertyNames(p)); // ["a", "b"] console.log(Object.keys(p)); // ["a"] console.log(Reflect.ownKeys(p)); // ["a", "b", Symbol(juanni)]复制代码
约束:
ownKeys
的结果必须是一个数组const obj = { a: 10 }; const p = new Proxy(obj, { ownKeys: function (target) { return 123; } }); Object.getOwnPropertyNames(p); // CreateListFromArrayLike called on non-object复制代码
String
,要么是一个 Symbol
const obj = { a: 10 }; const p = new Proxy(obj, { ownKeys: function (target) { return [123]; } }); Object.getOwnPropertyNames(p); // 123 is not a valid property name复制代码
non-configurable
)、自有( own
)属性的 key
const obj = { a: 10 }; Object.defineProperty(obj, 'b', { configurable: false, enumerable: true, value: 10 } ); const p = new Proxy(obj, { ownKeys: function (target) { return []; } }); Object.getOwnPropertyNames(p); // trap result did not include 'b'复制代码
own
)属性的 key
,不能有其它值const obj = { a: 10 }; Object.preventExtensions(obj); const p = new Proxy(obj, { ownKeys: function (target) { return ['a', 'd']; } }); Object.getOwnPropertyNames(p); // trap returned extra keys but proxy target is non-extensible复制代码
handler.preventExtensions()
用于拦截对对象的 Object.preventExtensions()
操作
preventExtensions: function(target) {}复制代码
target
: 所要拦截的目标对象该方法会拦截目标对象的以下操作:
Object.preventExtensions()
Reflect.preventExtensions()
返回值: Boolean
约束:
Object.isExtensible(proxy)
是 false
时 Object.preventExtensions(proxy)
才能 true
const p = new Proxy({}, { preventExtensions: function (target) { return true; } }); Object.preventExtensions(p); // trap returned truish but the proxy target is extensible复制代码
handler.setPrototypeOf()
用于拦截对对象的 Object.setPrototypeOf()
操作
setPrototypeOf: function(target, prototype) {}复制代码
target
: 被拦截目标对象prototype
: 对象新原型或为 null
该方法会拦截目标对象的以下操作:
Object.setPrototypeOf()
Reflect.setPrototypeOf()
返回值: Boolean
约束:
target
不可扩展, 原型参数必须与 Object.getPrototypeOf(target)
的值相const obj = { a: 10 }; Object.preventExtensions(obj); const p = new Proxy(obj, { setPrototypeOf(target, prototype) { Object.setPrototypeOf(target, prototype) return true; } }); Object.setPrototypeOf(obj, null); // #
Proxy
Proxy.revocable()
方法被用来创建可撤销的 Proxy
对象。此种代理可以通过revoke函数来撤销并且关闭代理。关闭代理后,在代理上的任意的操作都会导致 TypeError
const revocable = Proxy.revocable({}, { get: function (target, name) { return "[[" + name + "]]"; }});const proxy = revocable.proxy;console.log(proxy.foo); // "[[foo]]"revocable.revoke();console.log(proxy.foo); // Cannot perform 'get' on a proxy that has been revokedproxy.foo = 1 // Cannot perform 'set' on a proxy that has been revokeddelete proxy.foo; // Cannot perform 'deleteProperty' on a proxy that has been revokedtypeof proxy // "object", typeof doesn't trigger any trap复制代码
在这里打算用 Vue 和 Mobx 来体现出 Proxy
的优势
首先看看 Vue2.x 因为使用 defineProperty
带来的限制:
vm.items[indexOfItem] = newValue
vm.items.length = newLength
隔壁 Mobx4 也是用的 defineProperty
,但是通过一系列 hack 来绕过一些限制:
Array.isArray
返回 false
array.slice()
创建一份浅拷贝的真正数组sort
和 reverse
不会改变数组本身,而只是返回一个排序过/反转过的拷贝因为使用了类数组对象,所以 length
变成了对象上的属性而不是数组的 length
,因此可以被劫持。更多技巧可以查看
Object.defineProperty(ObservableArray.prototype, "length", { enumerable: false, configurable: true, get: function(): number { return this.$mobx.getArrayLength() }, set: function(newLength: number) { this.$mobx.setArrayLength(newLength) } })复制代码
Mobx5 在今年使用 Prxoy
重写后正式发布,成功解决了上述问题,接下来:
const arrayTraps = { get(target, name) { if (name === $mobx) return target[$mobx] // 成功拦截 length if (name === "length") return target[$mobx].getArrayLength() if (typeof name === "number") { return arrayExtensions.get.call(target, name) } if (typeof name === "string" && !isNaN(name as any)) { return arrayExtensions.get.call(target, parseInt(name)) } if (arrayExtensions.hasOwnProperty(name)) { return arrayExtensions[name] } return target[name] }, set(target, name, value): boolean { // 成功拦截 length if (name === "length") { target[$mobx].setArrayLength(value) return true } // 直接设置数组值 if (typeof name === "number") { arrayExtensions.set.call(target, name, value) return true } // 直接设置数组值 if (!isNaN(name)) { arrayExtensions.set.call(target, parseInt(name), value) return true } return false }, preventExtensions(target) { fail(`Observable arrays cannot be frozen`) return false }}复制代码
defineProperty
是劫持的对象上的属性引起的,没有办法劫持对象上不存在的属性,而 Prxoy
劫持整个对象自然没有了这个问题这个东西因为语言本身限制所以 polyfill 并不好搞,但是部分实现还是可以的:
是谷歌基于 defineProperty
撸的,只支持 get
, set
, apply
, construct
, 也支持 revocable
,代码只有一百多行非常简单,所以就不多做讲解
基础知识了解了这么多,接下来该看看实际运用了
正好有个设计模式叫代理模式:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
优点有二:
Proxy
在原对象的基础上进行了功能的衍生而又不影响原对象,符合松耦合高内聚的设计理念。this
在热身之前有一个需要注意的小点 - this
:
const target = { foo() { return { thisIsTarget: this === target, thisIsProxy: this === proxy, }; } }; const handler = {}; const proxy = new Proxy(target, handler); console.log(target.foo()); // {thisIsTarget: true, thisIsProxy: false} console.log(proxy.foo()); // {thisIsTarget: false, thisIsProxy: true}复制代码
通常情况下,通过 Proxy
中的 this
来调用方法或者获取/设置属性没什么问题,因为最终还是会被拦截到走到原始对象上,但是如果是本身用 this
进行骚操作或是有些内置方法需要 this
指向正确就需要额外注意了
this
使用骚操作需要额外注意const _name = new WeakMap(); class Person { constructor(name) { _name.set(this, name); } get name() { return _name.get(this); } } const juanni = new Person('Juanni'); const proxy = new Proxy(juanni, {}); console.log(juanni.name); // 'juanni' console.log(proxy.name); // undefined复制代码
this
const target = new Date(); const handler = {}; const proxy = new Proxy(target, handler); // 依赖 this 导致报错 proxy.getDate(); // this is not a Date object. // 修正方案 const handler = { get(target, propKey, receiver) { if (propKey === 'getDate') { return target.getDate.bind(target); } return Reflect.get(target, propKey, receiver); }, }; const proxy = new Proxy(new Date('2020-12-24'), handler); proxy.getDate(); // 24复制代码
首先让我们简单热身一下,看一个简单的:假设我们有一个函数tracePropAccess(obj, propKeys)
,只要设置或获得了 obj
的在 propKeys
的属性,就会被记录下来。
由于这个是简单的热身 demo,因此就直接给出使用 defineProperty
和 Proxy
完成的代码来供对比
// ES5 function tracePropAccess(obj, propKeys) { const propData = Object.create(null); propKeys.forEach(function (propKey) { propData[propKey] = obj[propKey]; Object.defineProperty(obj, propKey, { get: function () { console.log(`GET ${propKey}`); return propData[propKey]; }, set: function (value) { console.log(`SET ${propKey} = ${value}`); propData[propKey] = value; }, }); }); return obj; } class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `Point( ${this.x} , ${this.y} )`; } } p = tracePropAccess(new Point(7), ['x', 'y']); p.x // GET x p.x = 666 // SET x = 666 p.toString() // GET x // GET y复制代码
// ES6 with Proxy function tracePropAccess(obj, propKeys) { const propKeySet = new Set(propKeys); return new Proxy(obj, { get(target, propKey, receiver) { if (propKeySet.has(propKey)) { console.log(`GET ${propKey}`); } return Reflect.get(target, propKey, receiver); }, set(target, propKey, value, receiver) { if (propKeySet.has(propKey)) { console.log(`SET ${propKey} = ${value}`); } return Reflect.set(target, propKey, value, receiver); }, }); } class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `Point( ${this.x} , ${this.y} )`; } } p = tracePropAccess(new Point(7), ['x', 'y']); p.x // GET x p.x = 666 // SET x = 666 p.toString() // GET x // GET y复制代码
隔壁 python 等都可以通过负数索引访问到数组倒数第 N 个元素,现在我们有了一种新方法直接实现这一特性:
function createArray(array) { if(!Array.isArray(array)) { throw Error('must be an array'); } const handler = { get(target, propKey, receiver) { const index = Number(propKey); if (index < 0) { propKey = String(target.length + index); } return Reflect.get(target, propKey, receiver); } }; return new Proxy(array, handler); } const arr = createArray(['a', 'b', 'c']); console.log(arr[-1]); // c复制代码
对于方法调用没有单一操作可以进行拦截,因为方法调用被视为两个独立的操作:首先使用 get
检索函数,然后调用该函数。
const obj = { multiply(x, y) { return x * y; }, squared(x) { return this.multiply(x, x); },};function traceMethodCalls(obj) { const handler = { get(target, propKey, receiver) { const origMethod = target[propKey]; return function (...args) { const result = origMethod.apply(this, args); console.log(propKey + JSON.stringify(args) + ' -> ' + JSON.stringify(result)); return result; }; } }; return new Proxy(obj, handler);}const tracedObj = traceMethodCalls(obj);console.log(tracedObj.multiply(2,7));// multiply[2,7] -> 14// test.js:25 14console.log(tracedObj.squared(9));// multiply[9,9] -> 81// test.js:16 squared[9] -> 81// test.js:26 81复制代码
我们可以看见即使 this
指向了 Proxy
在原始对象内部的方法调用(如 this.multiply(x, x)
)也能被拦截到
function singleton(func) { let instance, handler = { construct: function (target, args) { if (!instance) { instance = new func(); } return instance; } }; return new Proxy(func, handler);}复制代码
Reflect.has
、Object.hasOwnProperty
、Object.prototype.hasOwnProperty
、 in
运算符全部使用了 [[HasProperty]]
,可以通过 has
拦截。Object.keys
、 Object.getOwnPropertyNames
, Object.entrie
都使用了 [[OwnPropertyKeys]]
,可以通过 ownKeys
拦截。Object.getOwnPropertyDescriptor
使用了 [[GetOwnProperty]]
可以通过 getOwnPropertyDescriptor
拦截。因此我们可以写出如下代码彻底让某个属性彻底消失掉
function hideProperty(object, ...propertiesToHide) { const proxyObject = new Proxy(object, { has(object, property) { if (propertiesToHide.indexOf(property) != -1) { return false; } return Reflect.has(object, property); }, ownKeys(object) { return Reflect.ownKeys(object).filter( (property) => propertiesToHide.indexOf(property) == -1 ); }, getOwnPropertyDescriptor(object, property) { if (propertiesToHide.indexOf(property) != -1) { return undefined; } return Reflect.getOwnPropertyDescriptor(object, property); } }); return proxyObject;}复制代码
这里还有很多可以用 Proxy
来实现的,比如:
has
, ownKeys
, getOwnPropertyDescriptor
和 get
, set
来让属性变成私有属性