本文簡單闡述一點元編程的知識,而後較爲詳細的給出 Proxy
的有關用法(起碼比 MDN 詳細,補充了各類錯誤狀況的具體示例,且比上面的機翻準確),再用一些例子講述 Proxy
適合在什麼場景下使用javascript
首先,在編程中有如下兩個級別:html
元( meta ) 這個詞綴在這裏的意思是:關於某事自身的某事,所以元編程( metaprogramming )這個詞意味着 關於編程的編程,能夠在兩種同的語言進行元編程,編寫元程序的語言稱之爲元語言。被操縱的程序的語言稱之爲「目標語言」,在下面這段代碼中 JavaScript
爲元語言,而 Java
爲目標語言:java
const str = 'Hello' + '!'.repeat(3);
console.log('System.out.println("'+str+'")');
複製代碼
一門編程語言同時也是自身的元語言的能力稱之爲反射(Reflection),用於發現和調整你的應用程序結構和語義。python
元編程有三種形式:git
Object.keys(obj)
等,ES6 中新出了個 Reflect
對許多獲取內部狀態的接口進行了整合與統一delete
或 property descriptors
等Proxy
ES6 中也新增了一個全局對象 Reflect
,其中的大多數方法都早已以其餘形式存在,此次將其接口統一的目的在於: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
作出了哪些優化github
終於來到了咱們的主角,首先咱們來看看 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) // #<Object> is not extensible
複製代碼
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]
Reflect.get()
約束(違反約束會拋出 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
Reflect.set()
約束:
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)
Reflect.construct()
注意:
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); // #<Object> is not extensible
複製代碼
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 revoked
proxy.foo = 1 // Cannot perform 'set' on a proxy that has been revoked
delete proxy.foo; // Cannot perform 'deleteProperty' on a proxy that has been revoked
typeof 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
,所以能夠被劫持。更多技巧能夠查看 observablearray.ts
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 並很差搞,可是部分實現仍是能夠的:
proxy-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 14
console.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
來讓屬性變成私有屬性